📱 Instala Camuy Alerta Accede desde tu pantalla de inicio
CAMUY ALERTA
Código de Orden Público — Camuy
🏘️
Portal Ciudadano
Reporta vehículos, querellas y propiedades en tu comunidad
🏛️
Personal Municipal
Administradores e inspectores del municipio
Portal Ciudadano
Inicia sesión para reportar
Nuevo Registro
Crea tu cuenta en Camuy Alerta
🏘️ PORTAL CIUDADANO
—
📭
No tienes reportes aún
✅
REPORTE ENVIADO
—
El Municipio de Camuy atenderá tu reporte
🚗 VEHÍCULO
📍 UBICACIÓN
💬 COMENTARIOS
📷 FOTOS
📸
Toca para agregar fotos — hasta 5
✅
QUERELLA ENVIADA
—
El Municipio de Camuy atenderá tu querella
📋 TIPO DE VIOLACIÓN
📍 UBICACIÓN
💬 DESCRIPCIÓN
📷 FOTOS
📸
Toca para agregar fotos — hasta 5
✅
PROPIEDAD REPORTADA
—
El Municipio de Camuy evaluará tu reporte
🏚️ PROPIEDAD
💬 DESCRIPCIÓN
📷 FOTOS
📸
Toca para agregar fotos — hasta 5
Personal Municipal
Camuy — Gestión de Casos
CAMUY ALERTA
—
—
0
Total
0
Pendiente
0
En Proceso
0
Resuelto
🚗 VEHÍCULOS
0
total
0pend
0proc
0res
📋 QUERELLAS
0
total
0abierta
0proc
0res
🏚️ PROPIEDADES
0
total
0activo
0proc
0res
ACTIVIDAD RECIENTE
Cargando...
🚗 VEHÍCULO
📍 UBICACIÓN
👤 REPORTANTE
📷 FOTOS
📸
Agregar fotos del vehículo — hasta 5
POR INSPECTOR
POR BARRIO
INSPECTORES
Nombre
Usuario
Email
Rol
Act.
Acciones
📧 EMAILS DE NOTIFICACIÓN (Administradores)
Estos emails reciben todas las notificaciones del sistema.
RESIDENTES
Pendiente
En Proceso
Resuelto
Solo casos con coordenadas GPS · Toca un punto para ver detalle
QUERELLAS GENERALES
0
Total
0
Abiertas
0
En Proceso
0
Resueltas
NUEVA QUERELLA
📌 TIPO DE QUERELLA
📍 UBICACIÓN
👤 QUERELLANTE
📝 DESCRIPCIÓN
INVENTARIO PROPIEDADES
0
Total
0
Activas
0
En Proceso
0
Resueltas
REGISTRAR PROPIEDAD
🏠 DATOS DE LA PROPIEDAD
👤 DATOS DEL DUEÑO
⚠️ CLASIFICACIÓN
🔍 MIS CASOS
—
📅 PRÓXIMA GESTIÓN
🕐 HORA08:00
';
el.innerHTML=h;
var hEl=document.getElementById('pgH_'+cid);
var mEl=document.getElementById('pgM_'+cid);
if(hEl)hEl.textContent=(s.hour<10?'0':'')+s.hour;
if(mEl)mEl.textContent=(s.min<10?'0':'')+s.min;
pgLabel(cid);
}
function pgNav(btn){
var cid=btn.getAttribute('data-cid');
var dir=parseInt(btn.getAttribute('data-dir'));
var s=pgState[cid];if(!s)return;
s.month+=dir;
if(s.month>11){s.month=0;s.year++;}
if(s.month<0){s.month=11;s.year--;}
s.day=0;pgRender(cid);
}
function pgSelDay(btn){
var cid=btn.getAttribute('data-cid');
var s=pgState[cid];if(!s)return;
s.day=parseInt(btn.getAttribute('data-day'));
pgRender(cid);
}
function pgHr(btn){
var cid=btn.getAttribute('data-cid');
var t=btn.getAttribute('data-t');
var d=parseInt(btn.getAttribute('data-d'));
var s=pgState[cid];if(!s)return;
if(t==='h'){s.hour+=d;if(s.hour>23)s.hour=0;if(s.hour<0)s.hour=23;}
else{s.min+=d*15;if(s.min>45)s.min=0;if(s.min<0)s.min=45;}
pgRender(cid);
}
function pgLabel(cid){
var s=pgState[cid];if(!s)return;
var el=document.getElementById('pgSel_'+cid);if(!el)return;
if(!s.day){el.textContent='Sin fecha seleccionada';el.style.color='var(--gray)';el.style.fontWeight='';return;}
var mn=['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
el.textContent='📅 '+s.day+' '+mn[s.month]+' '+s.year+' a las '+(s.hour<10?'0':'')+s.hour+':'+(s.min<10?'0':'')+s.min;
el.style.color='var(--gold)';el.style.fontWeight='700';
}
function pgGetDatetime(cid){
var s=pgState[cid];if(!s||!s.day)return '';
var d=new Date(s.year,s.month,s.day,s.hour,s.min);
return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+
String(d.getDate()).padStart(2,'0')+'T'+
String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
// ── Citizen status email ──────────────────────────────────────
function emailCiudadano(caseObj,gestionKey){
if(!db||!caseObj||!caseObj.residenteUsuario)return;
var labels={
inspeccion:'En Inspección',notificacion:'Notificada',
grua:'Orden de Grúa Emitida',remocion:'Completada',
policia:'Referida a Policía',propietario:'Propietario Contactado',
multa:'Multa Emitida',otro:'En Proceso',programar:'Próxima Acción Agendada'
};
var statusLabel=labels[gestionKey]||'En Proceso';
db.collection('residentes').doc(caseObj.residenteUsuario).get()
.then(function(doc){
if(!doc.exists)return;
var c=doc.data();
if(!c.email)return;
var subject='📋 Actualización de tu Reporte — '+caseObj.id;
var body='Estimado/a '+c.nombre+',\n\n'+
'Tu reporte ha sido actualizado.\n\n'+
'━━━━━━━━━━━━━━━━━━━━━\n'+
'📋 Reporte: '+caseObj.id+'\n'+
'📍 Dirección: '+(caseObj.address||'')+'\n'+
'✅ Estado: '+statusLabel+'\n'+
'━━━━━━━━━━━━━━━━━━━━━\n\n'+
'Gracias por tu reporte. El Municipio de Camuy está trabajando en tu caso.\n\n'+
'Consulta el estado en: https://camuyalerta.pages.dev\n\n'+
'— Depto. de Código de Orden Público\nMunicipio de Camuy, Puerto Rico';
sendEmail(c.email,subject,body);
}).catch(function(){});
}
// ── PDF EXPORT ────────────────────────────────────────────────
function exportPDF(type){
if(typeof window.jspdf==='undefined'&&typeof jsPDF==='undefined'){T('Error','jsPDF no disponible. Recarga.','r');return;}
var J=(typeof window.jspdf!=='undefined')?window.jspdf.jsPDF:jsPDF;
// Filter by type
var pdfCases=(!type||type==='all'||type==='veh')?cases:[];
var pdfQues=(!type||type==='all'||type==='que')?querellas:[];
var pdfProps=(!type||type==='all'||type==='prop')?propiedades:[];
var pdfTitle=type==='veh'?'Vehículos Abandonados':type==='que'?'Querellas Generales':type==='prop'?'Inventario de Propiedades':'Reporte Completo';
var doc=new J({orientation:'portrait',unit:'mm',format:'letter'});
var now=new Date(),ds=now.toLocaleDateString('es-PR',{year:'numeric',month:'long',day:'numeric'});
var W=215.9,M=18,y=0;
function pg(){doc.addPage();y=22;doc.setFillColor(10,22,40);doc.rect(0,0,W,13,'F');doc.setTextColor(232,160,32);doc.setFontSize(9);doc.setFont('helvetica','bold');doc.text('CAMUY ALERTA — Reporte Ejecutivo',M,9);doc.setTextColor(150,150,150);doc.setFontSize(8);doc.text(ds,W-M,9,{align:'right'});}
function cy(n){if(y+n>262)pg();}
// Cover
doc.setFillColor(10,22,40);doc.rect(0,0,W,44,'F');
doc.setTextColor(232,160,32);doc.setFontSize(22);doc.setFont('helvetica','bold');doc.text('CAMUY ALERTA',W/2,18,{align:'center'});
doc.setFontSize(11);doc.setFont('helvetica','normal');doc.setTextColor(255,255,255);
doc.text(pdfTitle+' — Municipio de Camuy, PR',W/2,28,{align:'center'});
doc.text('Generado el '+ds+(cUser?' por '+cUser.name:''),W/2,37,{align:'center'});
y=56;
var tot=pdfCases.length+pdfQues.length+pdfProps.length,p=0,pr=0,r=0;
pdfCases.forEach(function(c){if(c.status==='Pendiente')p++;else if(c.status==='En Proceso')pr++;else if(c.status==='Resuelto')r++;});
pdfQues.forEach(function(q){if(q.status==='Abierta')p++;else if(q.status==='En Proceso')pr++;else if(q.status==='Resuelta')r++;});
pdfProps.forEach(function(p2){if(p2.status==='Activo')p++;else if(p2.status==='En Proceso')pr++;else if(p2.status==='Resuelto')r++;});
var pct=tot>0?Math.round(r/tot*100):0;
// Stats boxes
var boxes=[{l:'Total Casos',v:tot,c:[10,22,40]},{l:'Pendientes',v:p,c:[232,160,32]},{l:'En Proceso',v:pr,c:[26,58,107]},{l:'Resueltos',v:r,c:[39,174,96]},{l:'% Resuelto',v:pct+'%',c:[26,122,74]}];
var bw=(W-M*2-8)/5,bx=M;
boxes.forEach(function(b){
doc.setFillColor(b.c[0],b.c[1],b.c[2]);doc.rect(bx,y,bw,20,'F');
doc.setTextColor(255,255,255);doc.setFontSize(14);doc.setFont('helvetica','bold');doc.text(String(b.v),bx+bw/2,y+10,{align:'center'});
doc.setFontSize(6);doc.setFont('helvetica','normal');doc.text(b.l,bx+bw/2,y+17,{align:'center'});
bx+=bw+2;
});
y+=28;
// Inspectors
cy(30);doc.setTextColor(10,22,40);doc.setFontSize(12);doc.setFont('helvetica','bold');doc.text('RENDIMIENTO POR INSPECTOR',M,y);y+=5;
doc.setDrawColor(232,160,32);doc.setLineWidth(.5);doc.line(M,y,W-M,y);y+=5;
doc.setFillColor(240,236,228);doc.rect(M,y-3,W-M*2,7,'F');
doc.setFont('helvetica','bold');doc.setFontSize(9);doc.setTextColor(26,58,107);
doc.text('Inspector',M,y+1);doc.text('Total',M+60,y+1);doc.text('Activos',M+90,y+1);doc.text('Resueltos',M+120,y+1);doc.text('% Éxito',M+155,y+1);y+=8;
USERS.filter(function(u){return u.role==='inspector';}).forEach(function(u,i){
cy(9);
var ut=pdfCases.filter(function(c){return c.assignedTo===u.name;}).length;
var ua=pdfCases.filter(function(c){return c.assignedTo===u.name&&c.status!=='Resuelto'&&c.status!=='Cancelado';}).length;
var ur=pdfCases.filter(function(c){return c.assignedTo===u.name&&c.status==='Resuelto';}).length;
var up=ut>0?Math.round(ur/ut*100)+'%':'—';
if(i%2===0){doc.setFillColor(250,248,245);doc.rect(M,y-4,W-M*2,8,'F');}
doc.setFont('helvetica','normal');doc.setFontSize(9);doc.setTextColor(40,40,40);
doc.text(u.name,M,y);doc.text(String(ut),M+60,y);doc.text(String(ua),M+90,y);
doc.setTextColor(ur>0?26:150,ur>0?122:150,ur>0?74:150);doc.text(String(ur),M+120,y);
doc.setTextColor(40,40,40);doc.text(up,M+155,y);y+=7;
});
y+=4;
// By barrio
cy(30);doc.setFont('helvetica','bold');doc.setFontSize(12);doc.setTextColor(10,22,40);doc.text('CASOS POR BARRIO',M,y);y+=5;
doc.line(M,y,W-M,y);y+=5;
var bc={};cases.forEach(function(c){if(c.barrio)bc[c.barrio]=(bc[c.barrio]||0)+1;});
Object.keys(bc).sort(function(a,b){return bc[b]-bc[a];}).forEach(function(b,i){
cy(8);
var pct2=tot>0?Math.round(bc[b]/tot*100):0;
if(i%2===0){doc.setFillColor(250,248,245);doc.rect(M,y-4,W-M*2,8,'F');}
doc.setFont('helvetica','normal');doc.setFontSize(9);doc.setTextColor(40,40,40);
doc.text(b,M,y);doc.text(bc[b]+' ('+pct2+'%)',M+80,y);
doc.setFillColor(220,220,220);doc.rect(M+120,y-3,70,5,'F');
doc.setFillColor(232,160,32);doc.rect(M+120,y-3,Math.max(2,70*pct2/100),5,'F');
y+=7;
});
// Footer
var pc2=doc.internal.getNumberOfPages();
for(var i=1;i<=pc2;i++){doc.setPage(i);doc.setFillColor(240,236,228);doc.rect(0,272,W,10,'F');doc.setFont('helvetica','normal');doc.setFontSize(8);doc.setTextColor(100,100,100);doc.text('Municipio de Camuy · camuyalerta.pages.dev',M,278);doc.text('Página '+i+' de '+pc2,W-M,278,{align:'right'});}
doc.save('CamuyAlerta_Dashboard_'+now.toISOString().slice(0,10)+'.pdf');
T('PDF generado','Descargado exitosamente','g');
}
// ── PWA ───────────────────────────────────────────────────────
var dPWA=null;
window.addEventListener('beforeinstallprompt',function(e){
e.preventDefault();dPWA=e;
var d=localStorage.getItem('pwa_d');
if(!d)setTimeout(function(){var b=document.getElementById('pwaBar');if(b&&dPWA)b.classList.add('show');},30000);
});
function installPWA(){var b=document.getElementById('pwaBar');if(b)b.classList.remove('show');if(!dPWA)return;dPWA.prompt();dPWA.userChoice.then(function(){dPWA=null;});}
function dismissPWA(){var b=document.getElementById('pwaBar');if(b)b.classList.remove('show');localStorage.setItem('pwa_d','1');}
if('serviceWorker' in navigator)window.addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').catch(function(e){console.log('[SW]',e);});
});
// ── Calendar Modal ────────────────────────────────────────────
var pgModalCid = null;
function pgModalOpen(cid){
pgModalCid = cid;
if(!pgState[cid]){
var now=new Date();
pgState[cid]={year:now.getFullYear(),month:now.getMonth(),day:0,hour:8,min:0};
}
// Wire up hour/min buttons
document.getElementById('pgMHm').onclick=function(){pgModalAdj('h',-1);};
document.getElementById('pgMHp').onclick=function(){pgModalAdj('h',1);};
document.getElementById('pgMMm').onclick=function(){pgModalAdj('m',-1);};
document.getElementById('pgMMp').onclick=function(){pgModalAdj('m',1);};
pgModalRender();
var m=document.getElementById('pgModal');
if(m){m.style.display='flex';}
}
function pgModalClose(){
var m=document.getElementById('pgModal');
if(m)m.style.display='none';
}
function pgModalAdj(type,dir){
var s=pgState[pgModalCid];if(!s)return;
if(type==='h'){s.hour+=dir;if(s.hour>23)s.hour=0;if(s.hour<0)s.hour=23;}
else{s.min+=dir*15;if(s.min>45)s.min=0;if(s.min<0)s.min=45;}
pgModalRender();
}
function pgModalSelDay(btn){
var d=parseInt(btn.getAttribute('data-day'));
var s=pgState[pgModalCid];if(!s)return;
s.day=d;
pgModalRender();
}
function pgModalNavMonth(dir){
var s=pgState[pgModalCid];if(!s)return;
s.month+=dir;
if(s.month>11){s.month=0;s.year++;}
if(s.month<0){s.month=11;s.year--;}
s.day=0;
pgModalRender();
}
function pgModalRender(){
var cid=pgModalCid;
var s=pgState[cid];if(!s)return;
var el=document.getElementById('pgModalCal');if(!el)return;
var now=new Date();
var MN=['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
var DN=['Do','Lu','Ma','Mi','Ju','Vi','Sá'];
var fd=new Date(s.year,s.month,1).getDay();
var dim=new Date(s.year,s.month+1,0).getDate();
var h='
'+
'◀'+
''+MN[s.month]+' '+s.year+''+
'▶'+
'
';
DN.forEach(function(d){h+='
'+d+'
';});
for(var i=0;i
';
for(var d=1;d<=dim;d++){
var isT=(d===now.getDate()&&s.month===now.getMonth()&&s.year===now.getFullYear());
var isS=(d===s.day);
var cls='pgcal-d'+(isT?' tod':'')+(isS?' sel':'');
h+=''+d+'';
}
h+='
';
el.innerHTML=h;
var hEl=document.getElementById('pgMH');
var mEl=document.getElementById('pgMM');
if(hEl)hEl.textContent=(s.hour<10?'0':'')+s.hour;
if(mEl)mEl.textContent=(s.min<10?'0':'')+s.min;
// Update label
var sel=document.getElementById('pgModalSel');
if(sel){
if(!s.day){sel.textContent='Sin fecha seleccionada';sel.style.color='var(--gray)';sel.style.fontWeight='';}
else{
var mn=['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
sel.textContent='📅 '+s.day+' '+mn[s.month]+' '+s.year+' a las '+(s.hour<10?'0':'')+s.hour+':'+(s.min<10?'0':'')+s.min;
sel.style.color='var(--gold)';sel.style.fontWeight='700';
}
}
}
function pgModalConfirm(){
var s=pgState[pgModalCid];
if(!s||!s.day){T('Atención','Selecciona una fecha','r');return;}
pgModalClose();
// Show confirmation in the action panel
var sel=document.getElementById('pgSel_'+pgModalCid);
if(sel){
var mn=['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
sel.textContent='📅 '+s.day+' '+mn[s.month]+' '+s.year+' '+
(s.hour<10?'0':'')+s.hour+':'+(s.min<10?'0':'')+s.min;
sel.style.color='var(--gold)';sel.style.fontWeight='700';
// Show the pgDiv to show the confirmation label
var pgDiv=document.getElementById('pgDiv_'+pgModalCid);
if(pgDiv)pgDiv.style.display='block';
}
T('Fecha programada',sel?sel.textContent:'','g');
}
// ══════════════════════════════════════════════════════════════
// EMAIL NOTIFICATION SYSTEM
// ══════════════════════════════════════════════════════════════
// ── Load/Save ADMIN_EMAILS from localStorage ──────────────────
function loadAdminEmails(){
try{
var saved=localStorage.getItem('camuy_admin_emails');
if(saved){ADMIN_EMAILS=JSON.parse(saved);}
}catch(e){}
}
function saveAdminEmails(){
try{localStorage.setItem('camuy_admin_emails',JSON.stringify(ADMIN_EMAILS));}catch(e){}
}
// ── Render admin emails list ──────────────────────────────────
function renderAdminEmails(){
var el=document.getElementById('adminEmailsList');
if(!el)return;
if(!ADMIN_EMAILS.length){
el.innerHTML='
';
}).join('');
}
function addAdminEmail(){
var inp=document.getElementById('newAdminEmail');
if(!inp)return;
var email=inp.value.trim().toLowerCase();
if(!email||!/^[^@]+@[^@]+\.[^@]+$/.test(email)){T('Error','Email inválido','r');return;}
if(ADMIN_EMAILS.indexOf(email)>=0){T('Atención','Email ya existe','');return;}
ADMIN_EMAILS.push(email);
saveAdminEmails();
inp.value='';
renderAdminEmails();
T('Email agregado',email,'g');
}
function removeAdminEmail(idx){
ADMIN_EMAILS.splice(idx,1);
saveAdminEmails();
renderAdminEmails();
T('Email eliminado','','');
}
// ── Core send function ────────────────────────────────────────
function sendEmail(toEmail, subject, body, onSuccess){
fetch('https://api.web3forms.com/submit', {
method:'POST',
headers:{'Content-Type':'application/json','Accept':'application/json'},
body:JSON.stringify({
access_key: WEB3FORMS_KEY,
to: toEmail,
subject: subject,
message: body,
from_name: 'Camuy Alerta'
})
})
.then(function(r){return r.json();})
.then(function(d){
if(d.success){
console.log('[Web3Forms] Enviado a',toEmail);
if(onSuccess)onSuccess();
}else{
console.warn('[Web3Forms] Error:',d.message);
}
})
.catch(function(e){console.warn('[Web3Forms] Error:',e);});
}
// ── Send to ALL admins ────────────────────────────────────────
function notifyAdmins(subject, body){
ADMIN_EMAILS.forEach(function(email){
sendEmail(email, subject, body);
});
}
// ── Send to specific inspector ────────────────────────────────
function notifyInspector(inspName, subject, body){
var u=USERS.find(function(x){return x.name===inspName&&x.role==='inspector';});
if(u&&u.email&&u.email.trim()){
sendEmail(u.email.trim(), subject, body);
}
}
// ── TRIGGER: Citizen registers ───────────────────────────────
function emailNuevoResidente(res){
var subject='👤 NUEVO RESIDENTE: '+res.nombre+' '+res.apellido;
var body=
'════════════════════════════════\n'+
'NUEVO RESIDENTE REGISTRADO\n'+
'════════════════════════════════\n\n'+
'👤 Nombre: '+res.nombre+' '+res.apellido+'\n'+
'🔑 Usuario: '+res.usuario+'\n'+
'📞 Teléfono: '+res.telefono+'\n'+
'📧 Email: '+(res.email||'N/E')+'\n'+
'🏘️ Barrio: '+(res.barrio||'N/E')+'\n'+
'📅 Fecha: '+res.fechaRegistro+'\n\n'+
'Ver en: https://camuyalerta.pages.dev';
notifyAdmins(subject, body);
}
// ── TRIGGER: Citizen submits report ──────────────────────────
function emailNuevoReporte(tipo, id, addr, reporter, modulo){
var subject='📨 NUEVO REPORTE: '+id+' ['+modulo+']';
var body=
'════════════════════════════════\n'+
'NUEVO REPORTE CIUDADANO\n'+
'════════════════════════════════\n\n'+
'🆔 ID: '+id+'\n'+
'📁 Módulo: '+(modulo||tipo)+'\n'+
'🏷️ Tipo: '+tipo+'\n'+
'📍 Dirección: '+addr+'\n'+
'👤 Reportado por: '+reporter+'\n'+
'📅 Fecha: '+dStr()+'\n\n'+
'Ver en: https://camuyalerta.pages.dev';
notifyAdmins(subject, body);
}
// ── TRIGGER: Inspector assigned to case ──────────────────────
function emailAsignacion(inspName, caseId, caseType, addr){
var u=USERS.find(function(x){return x.name===inspName;});
var inspEmail=u&&u.email?u.email.trim():'';
var subject='⚡ ASIGNACIÓN: '+caseId+' → '+inspName+(inspEmail?' <'+inspEmail+'>':'');
var body=
'════════════════════════════════\n'+
'CASO ASIGNADO A INSPECTOR\n'+
'════════════════════════════════\n\n'+
'👤 INSPECTOR: '+inspName+'\n'+
'📧 EMAIL INSPECTOR: '+(inspEmail||'no registrado')+'\n\n'+
'📋 Caso: '+caseId+'\n'+
'🏷️ Tipo: '+caseType+'\n'+
'📍 Dirección: '+addr+'\n'+
'📅 Fecha: '+dStr()+'\n\n'+
'Ver caso en: https://camuyalerta.pages.dev';
// Send to each admin email individually with inspector email in replyto
ADMIN_EMAILS.forEach(function(adminEmail){
fetch('https://api.web3forms.com/submit',{
method:'POST',
headers:{'Content-Type':'application/json','Accept':'application/json'},
body:JSON.stringify({
access_key: WEB3FORMS_KEY,
subject: subject,
message: body,
from_name: 'Camuy Alerta',
replyto: inspEmail||adminEmail
})
}).then(function(r){return r.json();})
.then(function(d){console.log('[Web3Forms] Asignación enviada:',d.success);})
.catch(function(e){console.warn('[Web3Forms]',e);});
});
}
// ── TRIGGER: Inspector records a gestión ─────────────────────
function emailGestion(caseId, caseType, inspName, gestionType, notes, newStatus){
if(gestionType==='Proxima Gestion')return; // handled by scheduleNotif
var subject='🔧 GESTIÓN: '+caseId+' ['+newStatus+'] — '+gestionType;
var body=
'════════════════════════════════\n'+
'GESTIÓN REGISTRADA\n'+
'════════════════════════════════\n\n'+
'📋 Caso: '+caseId+'\n'+
'🏷️ Tipo: '+caseType+'\n'+
'🔧 Gestión: '+gestionType+'\n'+
'📊 Nuevo estado: '+newStatus+'\n'+
'👤 Realizado por: '+inspName+'\n'+
'📝 Notas: '+notes+'\n'+
'📅 Fecha: '+dStr()+'\n\n'+
'Ver en: https://camuyalerta.pages.dev';
notifyAdmins(subject, body);
}
// ══════════════════════════════════════════════════════════════
// SCHEDULED NOTIFICATIONS SYSTEM
// ══════════════════════════════════════════════════════════════
// ── Load scheduled notifications ─────────────────────────────
function loadScheduled(){
try{var s=localStorage.getItem(SCHED_KEY);return s?JSON.parse(s):[];}
catch(e){return [];}
}
function saveScheduled(arr){
try{localStorage.setItem(SCHED_KEY,JSON.stringify(arr));}catch(e){}
}
// ── Add a scheduled notification ─────────────────────────────
function scheduleNotif(caseId, caseType, addr, assignedTo, datetime, notes){
var arr=loadScheduled();
// Remove existing for same case
arr=arr.filter(function(x){return x.caseId!==caseId||x.type!=='proxima';});
arr.push({
caseId:caseId, type:'proxima', caseType:caseType,
addr:addr, assignedTo:assignedTo, datetime:datetime,
notes:notes, sent:false
});
saveScheduled(arr);
T('Próxima gestión programada',formatDatetime(datetime),'g');
}
// ── Format datetime for display ──────────────────────────────
function formatDatetime(dt){
if(!dt)return '';
try{
var d=new Date(dt);
return d.toLocaleDateString('es-PR',{weekday:'short',month:'short',day:'numeric'})+
' '+d.toLocaleTimeString('es-PR',{hour:'2-digit',minute:'2-digit'});
}catch(e){return dt;}
}
// ── Check and fire scheduled notifications ────────────────────
function checkScheduled(){
var arr=loadScheduled();
var now=new Date().getTime();
var changed=false;
arr.forEach(function(n){
if(n.sent)return;
var dt=new Date(n.datetime).getTime();
if(now>=dt){
n.sent=true;
changed=true;
// Show in-app alert
T('⏰ Gestión Programada',n.caseId+' — '+n.caseType+' — '+n.addr,'');
// Send emails
var subject='[Camuy Alerta] ⏰ Recordatorio de Gestión — '+n.caseId;
var body='Recordatorio: Tiene una gestión programada.\n\n'+
'ID: '+n.caseId+'\n'+
'Tipo: '+n.caseType+'\n'+
'Dirección: '+n.addr+'\n'+
'Inspector: '+n.assignedTo+'\n'+
'Programada para: '+formatDatetime(n.datetime)+'\n'+
'Notas: '+n.notes+'\n\n'+
'Ver en: https://camuyalerta.pages.dev';
notifyAdmins(subject, body);
if(n.assignedTo&&n.assignedTo!=='Sin asignar'){
notifyInspector(n.assignedTo, subject, body);
}
}
});
if(changed)saveScheduled(arr);
}
// ── Show upcoming notifications badge ─────────────────────────
function renderUpcoming(){
var arr=loadScheduled().filter(function(n){return !n.sent;});
if(!arr.length)return;
var now=new Date().getTime();
var soon=arr.filter(function(n){return new Date(n.datetime).getTime()-now<86400000;});
if(soon.length>0){
T('📅 '+soon.length+' gestión(es) programada(s) hoy','Ver pestaña CASOS','');
}
}
function mTab(t){
ALL_MTABS.forEach(function(x){
var tid='t'+x.charAt(0).toUpperCase()+x.slice(1);
var bid='tab-'+x;
var el=document.getElementById(tid);
var btn=document.getElementById(bid);
if(el)el.style.display='none';
if(btn)btn.classList.remove('on');
});
var tEl=document.getElementById('t'+t.charAt(0).toUpperCase()+t.slice(1));
var bEl=document.getElementById('tab-'+t);
if(tEl)tEl.style.display='block';
// Highlight the parent tab for sub-tabs
var parentTab=t==='queNuevo'?'que':(t==='propNuevo'?'prop':t);
var pBtn=document.getElementById('tab-'+parentTab);
if(pBtn)pBtn.classList.add('on');
// Load data
if(typeof checkScheduled==='function')checkScheduled();
if(t==='casos')renderResumen();
if(t==='veh')loadCases(true);
if(t==='dash')renderDash();
if(t==='insp'){renderIT();if(typeof renderAdminEmails==='function')renderAdminEmails();}
if(t==='res')loadRes();
if(t==='que')loadQue();
if(t==='prop')loadProp();
if(t==='queNuevo'){fillInspSel('QU_insp');}
if(t==='propNuevo'){fillInspSel('PR_insp');}
if(t==='map'){
setTimeout(function(){
if(typeof L!=='undefined'){initMap();if(map){map.invalidateSize();renderMapMkrs();}}
},100);
}
}
// ── SUBMIT MUNI CASE ──────────────────────────────────────────
function muniSubmit(){
var vt=v('MN_vt'),vc=v('MN_vc'),ad=v('MN_ad').trim();
var err=document.getElementById('MN_e');err.style.display='none';
if(!vt){err.textContent='Selecciona el tipo.';err.style.display='block';return;}
if(!vc){err.textContent='Selecciona la condición.';err.style.display='block';return;}
if(!ad){err.textContent='Ingresa la dirección.';err.style.display='block';return;}
if(!db){err.textContent='Sin conexión.';err.style.display='block';return;}
loader(true);
nextID('CAM').then(function(cid){
var co={
id:cid,dateStr:dStr(),timeStr:tStr(),
vehicleType:vt,color:v('MN_cl')||'N/E',plate:v('MN_pl')||'N/D',
make:v('MN_mk'),condition:vc,address:ad,
barrio:v('MN_ba')||'N/E',coords:v('MN_co'),
reporter:v('MN_rn')||'Personal Municipal',phone:v('MN_rt'),
comments:v('MN_cm'),assignedTo:v('MN_as')||'Sin asignar',
status:'Pendiente',createdBy:cUser?cUser.name:'Municipal',
residenteUsuario:'',
history:[{type:'Caso Creado',icon:'🏛️',
notes:'Registrado por '+(cUser?cUser.name:'Municipal')+'.',
status:'Pendiente',ts:nowStr(),by:cUser?cUser.name:'Sistema'}]
};
return saveCase(co).then(function(){
savePhLocal(cid,photos.mnew);
photos.mnew=[];
renderPh('mnew','mNewPrev');
return cid;
});
}).then(function(cid){
loader(false);T('Caso Creado',cid,'g');
['MN_vt','MN_vc','MN_cl','MN_pl','MN_mk','MN_ad','MN_ba','MN_co','MN_rn','MN_rt','MN_cm','MN_as']
.forEach(function(id){sv(id);});
loadCases(false);mTab('casos');
}).catch(function(e){loader(false);T('Error',e.message,'r');});
}
// ── INSPECTOR SELECT ──────────────────────────────────────────
function fillInspSel(selId){
var sel=document.getElementById(selId);if(!sel)return;
sel.innerHTML=''+
USERS.filter(function(u){return u.role==='inspector';})
.map(function(u){return '';}).join('');
}
// ── RENDER RESUMEN ────────────────────────────────────────────
function renderResumen(){
var vt=cases.length,vp=0,vpr=0,vr=0;
cases.forEach(function(c){if(c.status==='Pendiente')vp++;else if(c.status==='En Proceso')vpr++;else if(c.status==='Resuelto')vr++;});
var qt=querellas.length,qab=0,qpr=0,qr=0;
querellas.forEach(function(q){if(q.status==='Abierta')qab++;else if(q.status==='En Proceso')qpr++;else if(q.status==='Resuelta')qr++;});
var pt=propiedades.length,pact=0,ppr=0,pr2=0;
propiedades.forEach(function(p){if(p.status==='Activo')pact++;else if(p.status==='En Proceso')ppr++;else if(p.status==='Resuelto')pr2++;});
var total=vt+qt+pt,pend=vp+qab+pact,proc=vpr+qpr+ppr,res=vr+qr+pr2;
var el;
var rg=document.getElementById('resGrid');
if(rg)rg.innerHTML=
'
'+total+'
Total General
'+
'
'+pend+'
Pendientes
'+
'
'+proc+'
En Proceso
'+
'
'+res+'
Resueltos
';
function set(id,val){var e=document.getElementById(id);if(e)e.textContent=val;}
set('resVehN',vt);set('resVehP',vp);set('resVehPr',vpr);set('resVehR',vr);
set('resQueN',qt);set('resQueAb',qab);set('resQuePr',qpr);set('resQueR',qr);
set('resPropN',pt);set('resPropAct',pact);set('resPropPr',ppr);set('resPropR',pr2);
var recent=[];
cases.slice(0,5).forEach(function(c){recent.push({id:c.id,tipo:'🚗 '+c.vehicleType,addr:c.address,status:c.status,date:c.dateStr||''});});
querellas.slice(0,5).forEach(function(q){recent.push({id:q.id,tipo:'📋 '+q.tipo,addr:q.address,status:q.status,date:q.dateStr||''});});
propiedades.slice(0,5).forEach(function(p){recent.push({id:p.id,tipo:'🏚️ '+p.tipo,addr:p.address,status:p.status,date:p.dateStr||''});});
recent.sort(function(a,b){return b.date.localeCompare(a.date);});
var rr=document.getElementById('resReciente');
if(!rr)return;
if(!recent.length){rr.innerHTML='
No hay actividad.
';return;}
var cols={Pendiente:'var(--gold)',Abierta:'var(--red)',Activo:'var(--red)','En Proceso':'var(--blue)',Resuelto:'var(--green2)',Resuelta:'var(--green2)',Cancelado:'var(--gray)',Cerrada:'var(--gray)'};
rr.innerHTML=recent.slice(0,8).map(function(r){
var col=cols[r.status]||'var(--gray)';
return '
';
var ih='';
USERS.filter(function(u){return u.role==='inspector';}).forEach(function(u){
var tot=cases.filter(function(c){return c.assignedTo===u.name;}).length;
var act=cases.filter(function(c){return c.assignedTo===u.name&&c.status!=='Resuelto'&&c.status!=='Cancelado';}).length;
var res=cases.filter(function(c){return c.assignedTo===u.name&&c.status==='Resuelto';}).length;
ih+='
';
var bc={};pdfCases.forEach(function(c){if(c.barrio)bc[c.barrio]=(bc[c.barrio]||0)+1;});pdfQues.forEach(function(q){if(q.barrio)bc[q.barrio]=(bc[q.barrio]||0)+1;});pdfProps.forEach(function(p2){if(p2.barrio)bc[p2.barrio]=(bc[p2.barrio]||0)+1;});
var bh='';
Object.keys(bc).sort(function(a,b){return bc[b]-bc[a];}).forEach(function(b){
var pct=cases.length?Math.round(bc[b]/cases.length*100):0;
bh+='
';
}
// ── INSPECTORS TABLE ──────────────────────────────────────────
function renderIT(){
var rows='';
USERS.forEach(function(u){
var act=cases.filter(function(c){return c.assignedTo===u.name&&c.status!=='Resuelto'&&c.status!=='Cancelado';}).length;
var isMe=cUser&&cUser.user===u.user;
rows+='
'+
'
'+u.name+''+(isMe?' (tú)':'')+'
'+
'
'+u.user+'
'+
'
'+(u.email||'—')+'
'+
'
'+u.role.toUpperCase()+'
'+
'
'+act+'
'+
'
'+
'✏️'+
(!isMe?'✕':'')+
'
';
});
document.getElementById('ITB').innerHTML=rows;
}
function openIM(ukey){
if(!cUser||cUser.role!=='admin')return;
editingInsp=ukey;
var err=document.getElementById('iME');err.style.display='none';
if(ukey){
var u=USERS.find(function(x){return x.user===ukey;});if(!u)return;
document.getElementById('iMT').textContent='EDITAR INSPECTOR';
document.getElementById('IM_n').value=u.name;
document.getElementById('IM_u').value=u.user;
document.getElementById('IM_p').value='';
document.getElementById('IM_p').placeholder='Vacío = no cambiar';
document.getElementById('IM_r').value=u.role;
var imeEl=document.getElementById('IM_e');if(imeEl)imeEl.value=u.email||'';
document.getElementById('IM_u').readOnly=!!(cUser&&cUser.user===ukey);
}else{
document.getElementById('iMT').textContent='NUEVO INSPECTOR';
['IM_n','IM_u','IM_p'].forEach(function(id){document.getElementById(id).value='';});
document.getElementById('IM_p').placeholder='Mínimo 6 caracteres';
document.getElementById('IM_r').value='inspector';
document.getElementById('IM_u').readOnly=false;
}
document.getElementById('iModal').classList.add('on');
}
function closeIM(){document.getElementById('iModal').classList.remove('on');editingInsp=null;}
document.getElementById('iModal').addEventListener('click',function(e){if(e.target===this)closeIM();});
function saveIM(){
if(!cUser||cUser.role!=='admin')return;
var name=document.getElementById('IM_n').value.trim();
var user=document.getElementById('IM_u').value.trim().toLowerCase();
var pass=document.getElementById('IM_p').value.trim();
var role=document.getElementById('IM_r').value;
var err=document.getElementById('iME');err.style.display='none';
if(!name){err.textContent='Nombre requerido.';err.style.display='block';return;}
if(!user){err.textContent='Usuario requerido.';err.style.display='block';return;}
if(editingInsp){
var i=USERS.findIndex(function(x){return x.user===editingInsp;});if(i<0)return;
var emailVal=(document.getElementById('IM_e')||{}).value||'';
var on=USERS[i].name;
if(pass){if(pass.length<6){err.textContent='Mínimo 6 caracteres.';err.style.display='block';return;}USERS[i].pass=pass;}
USERS[i].name=name;USERS[i].role=role;USERS[i].email=emailVal;
if(on!==name)cases.forEach(function(c){if(c.assignedTo===on)c.assignedTo=name;});
if(db)db.collection('usuarios').doc(user).set(USERS[i]).catch(function(e){console.error(e);});
T('Inspector actualizado',name,'g');
}else{
if(USERS.find(function(x){return x.user===user;})){err.textContent='Usuario ya existe.';err.style.display='block';return;}
if(!pass||pass.length<6){err.textContent='Mínimo 6 caracteres.';err.style.display='block';return;}
var emailVal2=(document.getElementById('IM_e')||{}).value||'';
var nu={user:user,pass:pass,name:name,role:role,email:emailVal2};
USERS.push(nu);
if(db)db.collection('usuarios').doc(user).set(nu).catch(function(e){console.error(e);});
T('Inspector creado',name,'g');
}
closeIM();renderIT();fillInspSel('MN_as');fillInspSel('QU_insp');fillInspSel('PR_insp');
}
function delI(ukey){
if(!cUser||cUser.role!=='admin')return;
var u=USERS.find(function(x){return x.user===ukey;});
if(!u||!confirm('¿Eliminar a '+u.name+'?'))return;
cases.forEach(function(c){if(c.assignedTo===u.name)c.assignedTo='Sin asignar';});
USERS.splice(USERS.findIndex(function(x){return x.user===ukey;}),1);
if(db)db.collection('usuarios').doc(ukey).delete().catch(function(e){console.error(e);});
T('Inspector eliminado',u.name,'');renderIT();fillInspSel('MN_as');fillInspSel('QU_insp');fillInspSel('PR_insp');
}
// ── RESIDENTES ────────────────────────────────────────────────
function loadRes(){
if(!db)return;
var list=document.getElementById('resList');
list.innerHTML='
';});
}
function filterRes(){
var q=(v('resSearch')||'').toLowerCase();
renderRes(q?residentes.filter(function(r){return (r.nombre+r.apellido+r.usuario+r.barrio).toLowerCase().includes(q);}):residentes);
}
function renderRes(list){
var cc={};cases.forEach(function(c){if(c.residenteUsuario)cc[c.residenteUsuario]=(cc[c.residenteUsuario]||0)+1;});
var sEl=document.getElementById('resStats');
if(sEl)sEl.innerHTML=
'
';
}
// ── QUERELLAS MODULE ──────────────────────────────────────────
function loadQue(){
if(!db)return;
loader(true);
db.collection('querellas').orderBy('timestamp','desc').get()
.then(function(snap){
querellas=[];
snap.forEach(function(d){querellas.push(Object.assign({id:d.id},d.data()));});
loader(false);updQueStats();renderQ();
}).catch(function(e){loader(false);T('Error',e.message,'r');});
}
function updQueStats(){
var ab=0,pr=0,re=0;
querellas.forEach(function(q){
if(q.status==='Abierta')ab++;
else if(q.status==='En Proceso')pr++;
else if(q.status==='Resuelta')re++;
});
document.getElementById('qTot').textContent=querellas.length;
document.getElementById('qAb').textContent=ab;
document.getElementById('qPr').textContent=pr;
document.getElementById('qRe').textContent=re;
}
function filtQ(f){
aFiltQ=f;
document.querySelectorAll('.fbtn[data-qf]').forEach(function(b){b.classList.toggle('on',b.getAttribute('data-qf')===f);});
renderQ();
}
function renderQ(){
var srch=(v('qSrch')||'').toLowerCase();
var barr=v('qSrchB')||'';
var list=querellas.filter(function(q){
if(aFiltQ!=='todos'&&q.status!==aFiltQ)return false;
if(cUser&&cUser.role==='inspector'&&q.assignedTo!==cUser.name)return false;
if(srch&&!(q.id+q.tipo+q.address).toLowerCase().includes(srch))return false;
if(barr&&q.barrio!==barr)return false;
return true;
});
if(!list.length){document.getElementById('qList').innerHTML='
📋
No hay querellas
';return;}
var insps=USERS.filter(function(u){return u.role==='inspector';})
.map(function(u){return '';}).join('');
var isAdm=cUser&&cUser.role==='admin';
var h='';
list.forEach(function(q){
var qid=q.id;
var lcol={Abierta:'var(--red)','En Proceso':'var(--gold)',Resuelta:'var(--green2)',Cerrada:'var(--gray)'}[q.status]||'var(--gold)';
var hh=(q.history||[]).map(function(x){
return '
'+
'
'+x.type+'
'+x.notes+'
'+
'
'+x.ts+(x.by?' · '+x.by+'':'')+'
'+
'
';
}).join('');
var ats=ATYPES.map(function(a){
return ''+a.i+' '+a.l+'';
}).join('');
var asgnEl=isAdm?
'':
'';
h+='
'+
'
'+
'
'+qid+' '+sevBdg(q.severidad)+'
'+q.tipo+' · '+q.address+'
'+
bdg(q.status)+
'
'+
'
'+
'
'+
'
'+q.tipo+'
'+
'
'+q.severidad+'
'+
'
'+q.address+'
'+
'
'+q.barrio+'
'+
'
'+(q.articulo||'N/E')+'
'+
'
'+q.reporter+'
'+
'
'+q.assignedTo+'
'+
'
'+q.dateStr+'
'+
'
'+
'
'+q.description+'
'+
'
'+hh+'
'+
'
REGISTRAR GESTIÓN
'+
'
'+ats+'
'+
''+
'
'+
''+asgnEl+
'
'+
'GUARDAR GESTIÓN'+
'
'+
'
'+
'
';
});
document.getElementById('qList').innerHTML=h;
list.forEach(function(q){
var sel=document.getElementById('qasgn_'+q.id);
if(sel&&q.assignedTo)for(var i=0;i
🏚️
No hay propiedades
';return;}
var insps=USERS.filter(function(u){return u.role==='inspector';})
.map(function(u){return '';}).join('');
var isAdm=cUser&&cUser.role==='admin';
var h='';
list.forEach(function(p){
var pid=p.id;
var lcol={Activo:'var(--red)','En Proceso':'var(--gold)',Resuelto:'var(--green2)'}[p.status]||'var(--blue)';
var hh=(p.history||[]).map(function(x){
return '
'+
'
'+x.type+'
'+x.notes+'
'+
'
'+x.ts+(x.by?' · '+x.by+'':'')+'
'+
'
';
}).join('');
var ats=ATYPES.map(function(a){
return ''+a.i+' '+a.l+'';
}).join('');
var asgnEl=isAdm?
'':
'';
// Linked incidents
var inc=(querellas.filter(function(q){return q.address===p.address;}).length)+
(cases.filter(function(c){return c.address===p.address;}).length);
h+='
';
});
document.getElementById('pList').innerHTML=h;
list.forEach(function(p){
var sel=document.getElementById('pasgn_'+p.id);
if(sel&&p.assignedTo)for(var i=0;i
📭
No tienes reportes aún
';return;}
var h='';
all.forEach(function(c){
var cols={Pendiente:'var(--gold)',Abierta:'var(--red)','En Proceso':'var(--blue)',
Resuelto:'var(--green2)',Resuelta:'var(--green2)',Cancelado:'var(--gray)',
Activo:'var(--red)',Cerrada:'var(--gray)'};
var col=cols[c.status]||'var(--gold)';
var tipo=c.vehicleType||c.tipo||'Propiedad';
var extra=c.color?(' '+c.color):'';
h+='
';});
}
// ── CITIZEN SUBMIT VEHICLE ────────────────────────────────────
function submitVeh(){
if(!cRes){T('Error','Debes iniciar sesión','r');return;}
var vt=v('NR_vt'),vc=v('NR_vco'),ad=v('NR_ad').trim();
var err=document.getElementById('cVehErr');err.style.display='none';
if(!vt){err.textContent='Selecciona el tipo.';err.style.display='block';return;}
if(!vc){err.textContent='Selecciona la condición.';err.style.display='block';return;}
if(!ad){err.textContent='Ingresa la dirección.';err.style.display='block';return;}
if(!db){err.textContent='Sin conexión.';err.style.display='block';return;}
var btn=document.getElementById('NR_btn');btn.disabled=true;btn.textContent='Enviando...';loader(true);
nextID('CAM').then(function(cid){
var co={
id:cid,dateStr:dStr(),timeStr:tStr(),
vehicleType:vt,color:v('NR_vc')||'N/E',plate:v('NR_vp')||'N/D',
make:'',condition:vc,address:ad,
barrio:v('NR_ba')||'N/E',coords:v('NR_co'),timeLeft:v('NR_ti'),
reporter:cRes.nombre+' '+cRes.apellido,phone:cRes.telefono,
comments:v('NR_cm'),status:'Pendiente',assignedTo:'Sin asignar',
createdBy:'Portal Ciudadano',residenteUsuario:cRes.usuario,
history:[{type:'Reporte Ciudadano',icon:'ciudadano',
notes:'Enviado por '+cRes.nombre+' '+cRes.apellido+' — Portal Ciudadano.',
status:'Pendiente',ts:nowStr(),by:cRes.nombre+' '+cRes.apellido}]
};
return saveCase(co).then(function(){
savePhLocal(cid,photos.veh);photos.veh=[];renderPh('veh','cVehPrev');
return cid;
});
}).then(function(cid){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR REPORTE';
emailNuevoReporte(co.vehicleType,cid,co.address,co.reporter,'Vehículo Abandonado');
['NR_vt','NR_vc','NR_vco','NR_ad','NR_ba','NR_co','NR_ti','NR_cm'].forEach(function(id){sv(id);});
document.getElementById('GPS').style.display='none';
document.getElementById('cVehSucID').textContent=cid;
document.getElementById('cVehSuc').style.display='block';
document.getElementById('NR_btn').style.display='none';
T('Reporte enviado',cid,'g');
}).catch(function(e){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR REPORTE';
err.textContent='Error: '+e.message;err.style.display='block';
});
}
// ── CITIZEN SUBMIT QUERELLA ───────────────────────────────────
function submitQue(){
if(!cRes){T('Error','Debes iniciar sesión','r');return;}
var tipo=v('CQ_tipo'),sev=v('CQ_sev'),addr=v('CQ_addr').trim(),desc=v('CQ_desc').trim();
var err=document.getElementById('cQueErr');err.style.display='none';
if(!tipo){err.textContent='Selecciona el tipo de violación.';err.style.display='block';return;}
if(!sev){err.textContent='Selecciona la severidad.';err.style.display='block';return;}
if(!addr){err.textContent='Ingresa la dirección.';err.style.display='block';return;}
if(!desc){err.textContent='Describe la situación.';err.style.display='block';return;}
if(!db){err.textContent='Sin conexión.';err.style.display='block';return;}
var btn=document.getElementById('CQ_btn');btn.disabled=true;btn.textContent='Enviando...';loader(true);
nextID('QUE').then(function(qid){
var data={
id:qid,dateStr:dStr(),timeStr:tStr(),
tipo:tipo,severidad:sev,articulo:'',
address:addr,barrio:v('CQ_barr'),parcela:v('CQ_parc'),
reporter:cRes.nombre+' '+cRes.apellido,phone:cRes.telefono,
description:desc,assignedTo:'Sin asignar',
status:'Abierta',createdBy:'Portal Ciudadano',
residenteUsuario:cRes.usuario,
history:[{type:'Querella Ciudadana',icon:'ciudadano',
notes:'Enviada por '+cRes.nombre+' '+cRes.apellido+' — Portal Ciudadano.',
status:'Abierta',ts:nowStr(),by:cRes.nombre+' '+cRes.apellido}]
};
return saveDoc('querellas',qid,data).then(function(){
savePhLocal(qid,photos.que);photos.que=[];renderPh('que','cQuePrev');
return qid;
});
}).then(function(qid){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR QUERELLA';
emailNuevoReporte(tipo,qid,addr,cRes.nombre+' '+cRes.apellido,'Querella');
['CQ_tipo','CQ_sev','CQ_addr','CQ_barr','CQ_parc','CQ_desc'].forEach(function(id){sv(id);});
document.getElementById('cQueSucID').textContent=qid;
document.getElementById('cQueSuc').style.display='block';
document.getElementById('CQ_btn').style.display='none';
T('Querella enviada',qid,'g');
}).catch(function(e){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR QUERELLA';
err.textContent='Error: '+e.message;err.style.display='block';
});
}
// ── CITIZEN SUBMIT PROPIEDAD ──────────────────────────────────
function submitProp(){
if(!cRes){T('Error','Debes iniciar sesión','r');return;}
var tipo=v('CP_tipo'),addr=v('CP_addr').trim(),desc=v('CP_desc').trim();
var err=document.getElementById('cPropErr');err.style.display='none';
if(!tipo){err.textContent='Selecciona el tipo de problema.';err.style.display='block';return;}
if(!addr){err.textContent='Ingresa la dirección.';err.style.display='block';return;}
if(!desc){err.textContent='Describe el problema.';err.style.display='block';return;}
if(!db){err.textContent='Sin conexión.';err.style.display='block';return;}
var btn=document.getElementById('CP_btn');btn.disabled=true;btn.textContent='Enviando...';loader(true);
nextID('PROP').then(function(pid){
var data={
id:pid,dateStr:dStr(),timeStr:tStr(),
tipo:tipo,severidad:'Media',
address:addr,barrio:v('CP_barr'),
catastro:v('CP_cat'),dueno:v('CP_dueno'),
area:'',coords:'',telDueno:'',dirDueno:'',emailDueno:'',
description:desc,assignedTo:'Sin asignar',
status:'Activo',createdBy:'Portal Ciudadano',
residenteUsuario:cRes.usuario,
history:[{type:'Reporte Ciudadano',icon:'ciudadano',
notes:'Reportado por '+cRes.nombre+' '+cRes.apellido+' — Portal Ciudadano.',
status:'Activo',ts:nowStr(),by:cRes.nombre+' '+cRes.apellido}]
};
return saveDoc('propiedades',pid,data).then(function(){
savePhLocal(pid,photos.prop);photos.prop=[];renderPh('prop','cPropPrev');
return pid;
});
}).then(function(pid){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR REPORTE';
emailNuevoReporte(tipo,pid,addr,cRes.nombre+' '+cRes.apellido,'Propiedad');
['CP_tipo','CP_addr','CP_barr','CP_cat','CP_dueno','CP_desc'].forEach(function(id){sv(id);});
document.getElementById('cPropSucID').textContent=pid;
document.getElementById('cPropSuc').style.display='block';
document.getElementById('CP_btn').style.display='none';
T('Propiedad reportada',pid,'g');
}).catch(function(e){
loader(false);btn.disabled=false;btn.textContent='➤ ENVIAR REPORTE';
err.textContent='Error: '+e.message;err.style.display='block';
});
}
// ── GPS ───────────────────────────────────────────────────────
function doGPS(){
var el=document.getElementById('GPS');
el.className='gps';el.style.display='block';el.textContent='Obteniendo ubicación...';
if(!navigator.geolocation){el.className='gps e';el.textContent='GPS no disponible.';return;}
navigator.geolocation.getCurrentPosition(function(pos){
var lat=pos.coords.latitude.toFixed(6),lng=pos.coords.longitude.toFixed(6);
document.getElementById('NR_co').value=lat+', '+lng;
el.className='gps k';el.textContent='✓ '+lat+', '+lng;
},function(){el.className='gps e';el.textContent='No se pudo obtener GPS.';},{timeout:8000});
}
// ── INSPECTOR ─────────────────────────────────────────────────
function iTab(t){
document.getElementById('iTabCasos').style.display=t==='casos'?'block':'none';
document.getElementById('iTabGestion').style.display=t==='gestion'?'block':'none';
document.getElementById('itab-casos').classList.toggle('on',t==='casos');
var ig=document.getElementById('itab-gestion');
if(ig)ig.classList.toggle('on',t==='gestion');
}
function loadInspCases(){
if(!db||!cUser)return;
loader(true);
db.collection('casos').where('assignedTo','==',cUser.name).orderBy('timestamp','desc').get()
.then(function(snap){
loader(false);iCasesData=[];
snap.forEach(function(d){iCasesData.push(Object.assign({id:d.id},d.data()));});
if(!iCasesData.length){
document.getElementById('iCases').innerHTML='
✅
No tienes casos asignados
';
return;
}
var h='';
iCasesData.forEach(function(c){
var ph=loadPhLocal(c.id);
h+='
'+
'
'+
'
'+c.id+'
'+
'
'+c.vehicleType+' '+c.color+'
'+
'
📍 '+c.address+', '+c.barrio+'
'+
'
'+bdg(c.status)+
'
'+
(ph.length?'
📷 '+ph.length+' foto(s)
':'')+
'
Toca para gestionar →
'+
'
';
});
document.getElementById('iCases').innerHTML=h;
}).catch(function(e){loader(false);T('Error',e.message,'r');});
}
function openInspCase(cid){
var co=iCasesData.find(function(c){return c.id===cid;});if(!co)return;
var ph=loadPhLocal(cid);
var hh=(co.history||[]).map(function(x){
return '
'+
'
'+x.type+'
'+x.notes+'
'+
'
'+x.ts+(x.by?' · '+x.by+'':'')+'
'+
'
';
}).join('');
var ats=ATYPES.map(function(a){
return ''+a.i+' '+a.l+'';
}).join('');
var con=document.getElementById('iGestCase');
con.innerHTML='';
var bkBtn=document.createElement('button');
bkBtn.style.cssText='display:flex;align-items:center;gap:5px;padding:7px 12px;background:var(--navy);color:var(--gold);border:none;font-family:var(--fh);font-size:11px;letter-spacing:2px;margin-bottom:11px;cursor:pointer';
bkBtn.textContent='← MIS CASOS';
bkBtn.onclick=function(){iTab('casos');};
con.appendChild(bkBtn);
var card=document.createElement('div');
card.style.cssText='background:#fff;border:1px solid var(--border);border-left:4px solid var(--gold);padding:13px';
card.innerHTML=
'
';
con.appendChild(card);
var phInp=document.getElementById('iph_'+cid);
if(phInp)phInp.onchange=function(){addInspPh(cid,this);};
document.getElementById('itab-gestion').style.display='block';
iTab('gestion');
}
// ── PHOTO ENGINE ──────────────────────────────────────────────
function addPh(ctx,input){
var arr=photos[ctx]||[];
Array.from(input.files).slice(0,5-arr.length).forEach(function(file){
var r=new FileReader();
r.onload=function(e){arr.push(e.target.result);photos[ctx]=arr;renderPh(ctx,ctx==='veh'?'cVehPrev':ctx==='que'?'cQuePrev':ctx==='prop'?'cPropPrev':'mNewPrev');};
r.readAsDataURL(file);
});
input.value='';
}
function addInspPh(cid,input){
if(!photos.insp[cid])photos.insp[cid]=[];
Array.from(input.files).slice(0,5-photos.insp[cid].length).forEach(function(file){
var r=new FileReader();
r.onload=function(e){photos.insp[cid].push(e.target.result);renderInspPh(cid);};
r.readAsDataURL(file);
});
input.value='';
}
function renderPh(ctx,prevId){
var arr=photos[ctx]||[];
var prev=document.getElementById(prevId);if(!prev)return;
prev.innerHTML=arr.map(function(src,i){
return '
'+
'×
';
}).join('');
}
function renderInspPh(cid){
var arr=photos.insp[cid]||[];
var prev=document.getElementById('iprev_'+cid);if(!prev)return;
prev.innerHTML=arr.map(function(src,i){
return '
';
}
wGPS.forEach(function(c){
var pts=c.coords.split(',');
var lat=parseFloat(pts[0]),lng=parseFloat(pts[1]);
if(isNaN(lat)||isNaN(lng))return;
var col=cols[c.status]||'#8a9bb0';
var pulse=c.status==='Pendiente'?'animation:pulse 1.5s infinite;':'';
var mk=L.marker([lat,lng],{icon:L.divIcon({
html:'
',{maxWidth:240});
mk.addTo(map);mapMkrs.push(mk);
});
if(mapMkrs.length>0){var g=L.featureGroup(mapMkrs);map.fitBounds(g.getBounds().pad(0.1));}
}
// ── QR ────────────────────────────────────────────────────────
function showQR(){
document.getElementById('qrModal').classList.add('on');
setTimeout(function(){
var el=document.getElementById('qrCanvas');
if(!el)return;
el.innerHTML='';
if(typeof QRCode==='undefined'){T('Error','Librería QR no disponible','r');return;}
new QRCode(el,{text:'https://camuyalerta.pages.dev',width:220,height:220,
colorDark:'#0a1628',colorLight:'#ffffff',correctLevel:QRCode.CorrectLevel.H});
},200);
}
function closeQR(){document.getElementById('qrModal').classList.remove('on');}
document.getElementById('qrModal').addEventListener('click',function(e){if(e.target===this)closeQR();});
function downloadQR(){
var el=document.getElementById('qrCanvas');if(!el)return;
var img=el.querySelector('img'),qrc=el.querySelector('canvas');
var src=img?img.src:(qrc?qrc.toDataURL():null);
if(!src){T('Error','Genera el QR primero','r');return;}
var pc=document.createElement('canvas');pc.width=300;pc.height=380;
var ctx=pc.getContext('2d');
ctx.fillStyle='#ffffff';ctx.fillRect(0,0,300,380);
ctx.fillStyle='#0a1628';ctx.fillRect(0,0,300,60);
ctx.fillStyle='#e8a020';ctx.font='bold 20px Arial';ctx.textAlign='center';ctx.fillText('CAMUY ALERTA',150,26);
ctx.fillStyle='#ffffff';ctx.font='11px Arial';ctx.fillText('Reporta un vehiculo abandonado',150,46);
var qi=new Image();qi.onload=function(){
ctx.drawImage(qi,40,68,220,220);
ctx.fillStyle='#0a1628';ctx.font='bold 13px monospace';ctx.fillText('camuyalerta.pages.dev',150,312);
ctx.fillStyle='#8a9bb0';ctx.font='11px Arial';ctx.fillText('Municipio de Camuy, Puerto Rico',150,334);
ctx.fillText('Escanea con la camara de tu telefono',150,352);
ctx.fillStyle='#e8a020';ctx.fillRect(30,365,240,3);
var lnk=document.createElement('a');lnk.download='CamuyAlerta_QR.png';lnk.href=pc.toDataURL('image/png');lnk.click();
T('QR descargado','','g');
};qi.src=src;
}
function printQR(){
var el=document.getElementById('qrCanvas');if(!el)return;
var img=el.querySelector('img'),qrc=el.querySelector('canvas');
var src=img?img.src:(qrc?qrc.toDataURL():null);
if(!src){T('Error','Genera el QR primero','r');return;}
var w=window.open('','_blank');
w.document.write('Camuy Alerta QR'+
'
CAMUY ALERTA
'+
'
camuyalerta.pages.dev
'+
'
Escanea este código con la cámara de tu teléfono para reportar vehículos abandonados en Camuy.
'+
'
Municipio de Camuy · +1 787-898-2160
'+
' 🖨️ IMPRIMIR'+
'');
w.document.close();
setTimeout(function(){w.print();},500);
}
// ════════════════════════════════════════════════════════════
.fillRect(0,0,300,380);
ctx.fillStyle='#0a1628';ctx.fillRect(0,0,300,60);
ctx.fillStyle='#e8a020';ctx.font='bold 20px Arial';ctx.textAlign='center';ctx.fillText('CAMUY ALERTA',150,26);
ctx.fillStyle='#ffffff';ctx.font='11px Arial';ctx.fillText('Reporta un vehiculo abandonado',150,46);
var qi=new Image();qi.onload=function(){
ctx.drawImage(qi,40,68,220,220);
ctx.fillStyle='#0a1628';ctx.font='bold 13px monospace';ctx.fillText('camuyalerta.pages.dev',150,312);
ctx.fillStyle='#8a9bb0';ctx.font='11px Arial';ctx.fillText('Municipio de Camuy, Puerto Rico',150,334);
ctx.fillText('Escanea con la camara de tu telefono',150,352);
ctx.fillStyle='#e8a020';ctx.fillRect(30,365,240,3);
var lnk=document.createElement('a');lnk.download='CamuyAlerta_QR.png';lnk.href=pc.toDataURL('image/png');lnk.click();
T('QR descargado','','g');
};qi.src=src;
}
function printQR(){
var el=document.getElementById('qrCanvas');if(!el)return;
var img=el.querySelector('img'),qrc=el.querySelector('canvas');
var src=img?img.src:(qrc?qrc.toDataURL():null);
if(!src){T('Error','Genera el QR primero','r');return;}
var w=window.open('','_blank');
w.document.write('Camuy Alerta QR'+
'
CAMUY ALERTA
'+
'
camuyalerta.pages.dev
'+
'
Escanea este código con la cámara de tu teléfono para reportar vehículos abandonados en Camuy.