Resumen por áreaPendientesOrden más antiguaPor procesoTerminados hoyActividad del equipo
Hola, soy tu asistente de producción. Tengo acceso en tiempo real a todas las órdenes y la actividad del equipo. ¿En qué te puedo ayudar?
⚠ Limpieza de datos
Elimina ordenes que fueron reimportadas automaticamente por el sistema. Las ordenes creadas manualmente NO se eliminan.
Configuración
Contraseñas de acceso
✓ Guardado
Notificaciones
Activa para recibir alertas cuando se agregan o actualizan órdenes.
Historial de esta orden
¿Quieres regresar esta orden a activas? Indica el motivo:
📋 Nota de Remisión —
Áreas de trabajo
Partidas
Cant.
Descripción
Costo U.
Total
IVA %:
Subtotal: $0.00
IVA: $0.00
TOTAL: $0.00
Anticipo: $0.00
SALDO: $0.00
Datos de entrega
Imágenes
w[0]).join('').toUpperCase().slice(0,2);}
function relTime(ts){
if(!ts)return'';
const d=ts.toDate?ts.toDate():new Date(ts);
const diff=Math.floor((Date.now()-d)/1000);
if(diff<60)return'hace un momento';
if(diff<3600)return`hace ${Math.floor(diff/60)} min`;
if(diff<86400)return`hace ${Math.floor(diff/3600)} h`;
return d.toLocaleDateString('es-MX',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'});
}
// ── LOGIN ──────────────────────────────────────────────────────────────────
function getKnownUsers(){return JSON.parse(localStorage.getItem('ot_known_users')||'[]');}
function saveKnownUser(n){const l=getKnownUsers();if(!l.includes(n)){l.unshift(n);localStorage.setItem('ot_known_users',JSON.stringify(l.slice(0,8)));}}
function renderLoginChips(){
document.getElementById('login-names').innerHTML=getKnownUsers().map(u=>`${u}`).join('');
}
window.selectName=n=>{document.getElementById('login-input').value=n;};
// Role selection
window.selRole=(role,el)=>{
_selRole=role;
document.querySelectorAll('.role-card').forEach(c=>c.classList.remove('sel'));
el.classList.add('sel');
document.getElementById('login-err').style.display='none';
document.getElementById('login-pass-wrap').style.display='block';
};
window.entrar=()=>{
const name=document.getElementById('login-input').value.trim();
if(!name){alert('Escribe tu nombre.');return;}
if(!_selRole){alert('Selecciona un rol.');return;}
const pass=document.getElementById('login-pass')?.value||'';
if(pass!==getPWs()[_selRole]){
document.getElementById('login-err').style.display='block';
return;
}
document.getElementById('login-err').style.display='none';
currentUser=name;
currentRole=_selRole;
localStorage.setItem('ot_user',name);
localStorage.setItem('ot_role',currentRole);
saveKnownUser(name);
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('app-screen').classList.remove('hidden');
document.getElementById('user-name-display').textContent=name;
document.getElementById('user-avatar').textContent=initials(name);
// Show/hide role badge and cfg tab
const rb=document.getElementById('role-badge');
if(rb)rb.textContent={'admin':'[ADMIN]','operador':'[OPER]','cliente':'[CLI]'}[currentRole]||'';
const cfgBtn=document.getElementById('tab-cfg-btn');
if(cfgBtn)cfgBtn.style.display=currentRole==='admin'?'':'none';
// Reset all tabs to visible first (in case previous role hid some)
document.querySelectorAll('.tab').forEach(function(t){
if(t.id!=='tab-cfg-btn') t.style.display='';
});
// Role-based tab visibility
document.querySelectorAll('.tab').forEach(function(t){
const oc=t.getAttribute('onclick')||'';
if(currentRole==='operador'){if(oc.includes("'dashboard'")||oc.includes("'respaldo'")||oc.includes("'cobrar'")||oc.includes("'cfg'"))t.style.display='none';}
if(currentRole!=='admin'){if(oc.includes("'cobrar'"))t.style.display='none';}
});
// Hide remision button in modal for operador
const remBtn=document.getElementById('modal-btn-remision');
if(remBtn)remBtn.style.display=(currentRole==='operador')?'none':'';
initApp();
// Request notification permission on login
setTimeout(()=>window.pedirPermNotif&&window.pedirPermNotif(),1000);
};
window.cambiarUsuario=()=>{
document.getElementById('app-screen').classList.add('hidden');
document.getElementById('login-screen').classList.remove('hidden');
document.querySelectorAll('.role-card').forEach(c=>c.classList.remove('sel'));
document.getElementById('login-pass-wrap').style.display='none';
document.getElementById('login-err').style.display='none';
renderLoginChips();
};
// Restore session
if(currentUser&¤tRole){
document.getElementById('login-input').value=currentUser;
const rcEl=document.getElementById('rc-'+currentRole);
if(rcEl){rcEl.classList.add('sel');_selRole=currentRole;}
document.getElementById('login-pass-wrap').style.display='block';
}
renderLoginChips();
// ── CARGA INICIAL DE DATOS (seed) ──────────────────────────────────────────
// Import disabled
window.limpiarOrdenesImportadas=async function(){
if(!confirm('ADVERTENCIA: Esto eliminara de ACTIVAS todas las ordenes marcadas como "importacion inicial". Las que usted creo manualmente NO se eliminan. Continuar?'))return;
try{
var snap=await getDocs(collection(db,'ordenes'));
var aEliminar=snap.docs.filter(function(d){
var cp=(d.data().creadoPor||'').toLowerCase();
return cp.includes('sistema')||cp.includes('importa');
});
if(!aEliminar.length){alert('No se encontraron ordenes de importacion. Ya estan limpias.');return;}
if(!confirm('Se eliminaran '+aEliminar.length+' ordenes importadas automaticamente. Las ordenes creadas manualmente quedan intactas. Confirmar?'))return;
var btn=document.getElementById('btn-cleanup');if(btn){btn.textContent='Eliminando...';btn.disabled=true;}
await Promise.all(aEliminar.map(function(d){return deleteDoc(doc(db,'ordenes',d.id));}));
alert('Listo: '+aEliminar.length+' ordenes importadas eliminadas. Las ordenes manuales siguen intactas.');
if(btn){btn.textContent='Limpiar completado';}
}catch(e){alert('Error: '+e.message);}
};
function initApp(){
// Pre-load remisiones cache at startup
window._remCache=window._remCache||{};
if(!window._remLoaded){
window._remLoaded=true;
getDocs(collection(db,'remisiones')).then(function(snap){
snap.docs.forEach(function(d){window._remCache[d.id]={...d.data()};});
if(window._activeTab==='dashboard')renderDashboard();
if(window._activeTab==='respaldo')renderRespaldo();
if(window._activeTab==='cobrar')renderCobrar();
// Update Por Cobrar badge always
updateCobrarBadge();
}).catch(function(){});
}
// Set active tab on init (default is activas)
window._activeTab=window._activeTab||'activas';
// Pre-load ALL terminados for search
if(!window._allTermLoaded){
window._allTermLoaded=true;
getDocs(query(collection(db,'terminados'),orderBy('terminadoEn','desc'))).then(function(snap){
window._allTerminados=snap.docs.map(function(d){return Object.assign({id:d.id},d.data());});
if(window._activeTab==='respaldo')renderRespaldo();
}).catch(function(){window._allTerminados=[];});
}
document.getElementById('hdr-date').textContent=new Date().toLocaleDateString('es-MX',{weekday:'long',year:'numeric',month:'long',day:'numeric'});
document.getElementById('nf-fecha').value=new Date().toISOString().split('T')[0];
// checkAndSeed disabled;
onSnapshot(query(collection(db,'ordenes'),orderBy('creadoEn','asc')), snap=>{
ordenes=snap.docs.map(d=>({id:d.id,...d.data()}));
window._ordenes=ordenes;
const maxOT=ordenes.reduce((mx,o)=>Math.max(mx,parseInt(o.ot)||0),0);
if(maxOT>getLastOT())setLastOT(maxOT);
actualizarLblOT&&actualizarLblOT();
renderActivas(); setSyncOk(true);
if(window._activeTab==='dashboard')renderDashboard();
if(window._activeTab==='respaldo')renderRespaldo();
window._urg=ordenes.filter(function(o){return (o.est||'').toUpperCase()==='URGENTE';});
window._updateUrgBar&&window._updateUrgBar();
var _ch=snap.docChanges().filter(function(c){return c.type==='modified';});
if(_ch.length>0&&typeof window._mostrarTableroAuto==='function'){var _co=Object.assign({id:_ch[0].doc.id},_ch[0].doc.data());window._mostrarTableroAuto(_co);}
}, ()=>setSyncOk(false));
onSnapshot(query(collection(db,'terminados'),where('terminadoEn','>=',startOfToday()),orderBy('terminadoEn','desc')), snap=>{
terminados=snap.docs.map(d=>({id:d.id,...d.data()}));
window._terminados=terminados;
renderHoy(); renderStats();
if(window.updateCobrarBadge)updateCobrarBadge();
if(window._activeTab==='cobrar')renderCobrar();
});
onSnapshot(query(collection(db,'historial'),orderBy('ts','desc'),limit(100)), snap=>{
historial=snap.docs.map(d=>({id:d.id,...d.data()}));
window._historial=historial;
renderHistorial(); renderActivas();
const users=[...new Set(historial.map(h=>h.usuario))].filter(Boolean);
const sel=document.getElementById('hist-filter');
const cur=sel.value;
sel.innerHTML=''+users.map(u=>``).join('');
});
}
function setSyncOk(ok){
document.getElementById('sync-dot').className='dot '+(ok?'online':'loading');
document.getElementById('sync-txt').textContent=ok?'En línea':'Sin conexión';
document.getElementById('sync-txt').style.color=ok?'var(--green)':'var(--red)';
}
async function logAccion(tipo,ot,cli,detalle=''){
await addDoc(collection(db,'historial'),{usuario:currentUser,tipo,ot:ot||'',cli:cli||'',detalle,ts:serverTimestamp()});
}
// ── STATS CLICABLES ────────────────────────────────────────────────────────
function renderStats(){
const total=ordenes.length;
const enProc=ordenes.filter(o=>o.proc==='PROCESO').length;
const hoyN=terminados.length;
const urg=ordenes.filter(o=>o.est==='URGENTE').length;
const mkStat=(key,n,cls,label)=>{
const active=statFilter===key?'active-filter':'';
return`
${n}
${label}
`;
};
document.getElementById('stats').innerHTML=
mkStat('activas',total,'blue','Activas')+
mkStat('proceso',enProc,'green','En proceso')+
mkStat('hoy',hoyN,'amber','Terminadas hoy')+
mkStat('urgente',urg,'red','Urgentes');
// Segunda fila: filtros por área
const AREAS=[
{key:'PRODUCCION',cls:'ap',label:'Producción',dot:'#3b82f6'},
{key:'CORTE LASER',cls:'al',label:'Corte Láser',dot:'#ef4444'},
{key:'CORTE Y DOBLEZ',cls:'ac',label:'Corte y Doblez',dot:'#22c55e'},
{key:'SOLDADURA',cls:'as',label:'Soldadura',dot:'#f59e0b'}
];
const mkAreaStat=(a)=>{
const n=ordenes.filter(o=>o.area===a.key).length;
const active=areaFilter===a.key?'active-filter':'';
return`
${n}
⬤ ${a.label}
`;
};
document.getElementById('area-stats').innerHTML=AREAS.map(mkAreaStat).join('');
}
window.toggleStatFilter=key=>{
if(statFilter===key){statFilter=null;}else{statFilter=key;}
// Si filtro es "hoy" ir a tab hoy
if(statFilter==='hoy'){switchTab('hoy');statFilter=null;return;}
updateFilterBar();
renderActivas();
};
window.clearStatFilter=()=>{statFilter=null;updateFilterBar();renderActivas();};
window.toggleAreaFilter=key=>{
if(areaFilter===key){areaFilter=null;}else{areaFilter=key;}
// Sincronizar con el select fa
const fa=document.getElementById('fa');
if(fa) fa.value=areaFilter||'';
updateFilterBar();
renderActivas();
};
window.clearAreaFilter=()=>{areaFilter=null;const fa=document.getElementById('fa');if(fa)fa.value='';updateFilterBar();renderActivas();};
function updateFilterBar(){
const bar=document.getElementById('filter-bar');
const txt=document.getElementById('filter-bar-txt');
if(!statFilter&&!areaFilter){bar.classList.add('hidden');renderStats();return;}
bar.classList.remove('hidden');
const labels={activas:'Todas las activas',proceso:'Órdenes en PROCESO',urgente:'Órdenes URGENTES'};
let parts=[];
if(statFilter) parts.push(labels[statFilter]||statFilter);
if(areaFilter) parts.push('Área: '+areaFilter);
txt.textContent='Filtro: '+parts.join(' + ');
renderStats();
}
// ── RENDER ACTIVAS ─────────────────────────────────────────────────────────
const AREA_CLASS={'PRODUCCION':'area-produccion','CORTE LASER':'area-laser','CORTE Y DOBLEZ':'area-corte','SOLDADURA':'area-soldadura'};
const SEC_CLASS={'PRODUCCION':'area-prod','CORTE LASER':'area-laser','CORTE Y DOBLEZ':'area-corte','SOLDADURA':'area-sold'};
const SEC_DOT={'PRODUCCION':'#3b82f6','CORTE LASER':'#ef4444','CORTE Y DOBLEZ':'#22c55e','SOLDADURA':'#f59e0b'};
function renderActivas(){
renderStats();
const q=(document.getElementById('q').value||'').toLowerCase().trim();
const fp=document.getElementById('fp').value;
// fa puede venir del select o del areaFilter de los botones
const faSelect=document.getElementById('fa').value;
const fa=areaFilter||faSelect;
const lastMov={};
historial.forEach(h=>{if(h.ot&&!lastMov[h.ot])lastMov[h.ot]=h;});
// Si hay búsqueda, mostrar resultados planos sin dividir por sección
if(q){
// Search across BOTH activas and terminados
const allOTs=ordenes.concat(window._allTerminados||terminados);
let results=allOTs.filter(o=>{
const haystack=(String(o.ot)+' '+o.cli+' '+(o.desc||'')+' '+(o.obs||'')+' '+(o.area||'')).toLowerCase();
return haystack.includes(q);
});
if(fp)results=results.filter(o=>o.proc===fp);
if(fa)results=results.filter(o=>o.area===fa);
if(statFilter==='proceso')results=results.filter(o=>o.proc==='PROCESO');
if(statFilter==='urgente')results=results.filter(o=>o.est==='URGENTE');
if(!results.length){
document.getElementById('activas-list').innerHTML=`
`;
box.scrollTop=box.scrollHeight;
// Construir contexto con datos actuales
const resumen=[
`Eres el asistente de producción de Inoxidables Roman. Usuario: ${currentUser}.`,
`Órdenes activas (${ordenes.length}): `+ordenes.map(o=>`OT${o.ot} ${o.cli} [${o.area}] proc:${o.proc||'ninguno'} obs:${o.obs||''}`).join(' | '),
`Terminados hoy (${terminados.length}): `+terminados.map(o=>`OT${o.ot} ${o.cli} por ${o.terminadoPor||'?'} a las ${o.hora?.split(' ')[1]||''}`).join(' | '),
`Actividad reciente: `+historial.slice(0,15).map(h=>`${h.usuario} ${h.tipo} OT${h.ot}`).join(' | '),
`Responde en español, breve y directo. Máximo 3 oraciones.`
].join('\n');
// Usar proxy de Claude.ai vía fetch con anthropic-dangerous-direct-browser-access
try{
const res=await fetch('https://api.anthropic.com/v1/messages',{
method:'POST',
headers:{
'Content-Type':'application/json',
'anthropic-dangerous-direct-browser-access':'true'
},
body:JSON.stringify({
model:'claude-haiku-4-5-20251001',
max_tokens:400,
system:resumen,
messages:[{role:'user',content:q}]
})
});
if(!res.ok) throw new Error('API error '+res.status);
const data=await res.json();
const reply=data.content?.find(b=>b.type==='text')?.text||'Sin respuesta.';
document.getElementById('thinking')?.remove();
box.innerHTML+=`
${reply.replace(/\n/g,' ')}
`;
}catch(e){
const t=document.getElementById('thinking');
const isPWA=window.matchMedia('(display-mode: standalone)').matches||window.navigator.standalone===true;
if(t){
if(isPWA){
t.innerHTML='⚠️ La IA requiere abrir en el navegador (Chrome/Safari), no desde la app instalada. Abre el enlace de Netlify en tu navegador.';
} else {
t.innerHTML=`⚠️ Error: ${e.message}. Verifica tu conexión e intenta de nuevo.`;
}
}
}
box.scrollTop=box.scrollHeight;
};
document.getElementById('modal').addEventListener('click',e=>{if(e.target===document.getElementById('modal'))cerrarModal();});
document.getElementById('modal-term').addEventListener('click',e=>{if(e.target===document.getElementById('modal-term'))cerrarModalTerm();});
// ══ DASHBOARD ════════════════════════════════════════════════════════════
function renderDashboard(){
var root=document.getElementById('dashboard-root');
if(!root)return;
var todas=ordenes.concat(terminados);
var remData=window._remCache||{};
var act=ordenes.length,ter=terminados.length;
var urg=ordenes.filter(function(o){return o.est==='URGENTE';}).length;
// ── Helpers ───────────────────────────────────────────────────────────────
var MX=function(n){return '$'+Number(n||0).toLocaleString('es-MX',{minimumFractionDigits:0,maximumFractionDigits:0});};
// ── Acumular datos por mes desde remisiones ────────────────────────────────
// Estructura por mes: {fab, cd, sold, laser, ventaTotal, pagado, anticipo, pedidos, entregados}
var mesesNombres=['ENE','FEB','MAR','ABR','MAY','JUN','JUL','AGO','SEP','OCT','NOV','DIC'];
var mesesData={};// key: "YYYY-MM"
// También acumular totales anuales por área desde remisiones
var totFab=0,totCD=0,totSold=0,totLaser=0,totPagado=0,totAnticipo=0,totPedidos=0,totEntregados=0;
// Para cada remisión, determinar mes y área
Object.keys(remData).forEach(function(ot){
var r=remData[ot];
var sub=(r.items||[]).reduce(function(a,i){return a+(Number(i.cant)||0)*(Number(i.costoU)||0);},0);
var impPct=(Number(r.impuesto)||0)/100;
var total=sub*(1+impPct);
var anticipo=Number(r.anticipo)||0;
var pagado=total-Math.max(0,total-anticipo); // pagado = anticipo si hay saldo
// Si saldo=0, toda la venta está pagada
var saldo=Math.max(0,total-anticipo);
if(saldo===0&&total>0)pagado=total; else pagado=anticipo;
var area=r.area||'';
// Buscar fecha
var fecha=r.fechaNota||r.fechaEntrega||r.updatedAt||'';
var yr='',mo='';
if(fecha){var pts=fecha.substring(0,10).split('-');if(pts.length>=2){yr=pts[0];mo=pts[1];}}
if(!yr){var om=todas.find(function(o){return String(o.ot)===String(ot);});if(om&&om.f){var fp=om.f.split('/');if(fp.length===3){mo=fp[1];yr=fp[2];}}}
var key=yr&&mo?yr+'-'+mo:'';
if(!key)return;
if(!mesesData[key])mesesData[key]={yr:yr,mo:mo,fab:0,cd:0,sold:0,laser:0,ventaTotal:0,pagado:0,anticipo:0,pedidos:0,entregados:0,porCobrar:0};
var md=mesesData[key];
md.ventaTotal+=total;
md.pagado+=pagado;
md.anticipo+=anticipo;
md.porCobrar+=saldo;
md.pedidos++;
if(r.entregado||saldo===0)md.entregados++;
if(area==='PRODUCCION')md.fab+=total;
else if(area==='CORTE Y DOBLEZ')md.cd+=total;
else if(area==='SOLDADURA')md.sold+=total;
else if(area==='CORTE LASER')md.laser+=total;
// Totales anuales
totFab+=(area==='PRODUCCION'?total:0);
totCD+=(area==='CORTE Y DOBLEZ'?total:0);
totSold+=(area==='SOLDADURA'?total:0);
totLaser+=(area==='CORTE LASER'?total:0);
totPagado+=pagado;totAnticipo+=anticipo;
totPedidos++;
if(r.entregado||saldo===0)totEntregados++;
});
var mesesKeys=Object.keys(mesesData).sort(function(a,b){return a.localeCompare(b);});
var totVentas=totFab+totCD+totSold+totLaser;
var totPorCobrar=mesesKeys.reduce(function(s,k){return s+mesesData[k].porCobrar;},0);
// ── SVG bar chart helper ──────────────────────────────────────────────────
function svgBars(labels,values,colors,opts){
var H=opts&&opts.h||120;
var n=labels.length;
if(!n||values.every(function(v){return !v;}))return '
Sin datos aún.
';
var mx=Math.max.apply(null,values.concat([1]));
var PAD_L=46,PAD_R=6,PAD_T=22,PAD_B=30;
var PW=600;
var gap=opts&&opts.gap!=null?opts.gap:5;
var bw=Math.max(8,Math.floor((PW-PAD_L-PAD_R-gap*(n-1))/n));
var ch=H-PAD_T-PAD_B;
var svg='';return svg;
}
// SVG barras agrupadas (stacked) — para ventas por área por mes
function svgGrouped(labels,series,colors,opts){
var H=opts&&opts.h||160;
var n=labels.length,ns=series.length;
if(!n)return '
Sin datos.
';
var maxVal=0;
series.forEach(function(s){s.forEach(function(v){if(v>maxVal)maxVal=v;});});
if(!maxVal)maxVal=1;
var PAD_L=50,PAD_R=6,PAD_T=22,PAD_B=30,PW=700;
var gap=10,bwGroup=Math.max(10,Math.floor((PW-PAD_L-PAD_R-gap*(n-1))/n));
var bw=Math.floor(bwGroup/ns)-1;
var ch=H-PAD_T-PAD_B;
var svg='';return svg;
}
function card(title,body,accent){
var bc=accent||'var(--border)';
return '
';
}
var AREA_COLORS={PRODUCCION:'#1e40af','CORTE LASER':'#dc2626','CORTE Y DOBLEZ':'#16a34a',SOLDADURA:'#d97706'};
var AC=['#1e40af','#dc2626','#16a34a','#d97706'];
var h='';
// ══ FILA 1 — KPIs principales (igual al Excel: VENTA TOTAL, TOTAL PAGADO, POR COBRAR, PEDIDOS, ENTREGADOS) ══
function kpi(v,l,c,sub){
return '
';
// ══ FILA 2 — KPIs de estado de órdenes + KPIs por área ══
h+='
';
// Estado
h+=kpi(act,'Activas','#3b82f6');
h+=kpi(ter,'Terminadas','#22c55e');
h+=kpi(urg,'Urgentes','#ef4444');
// Ventas por área
h+=kpi(totFab>0?MX(totFab):'—','Fab. Equipo','#1e40af');
h+=kpi(totLaser>0?MX(totLaser):'—','Corte Láser','#dc2626');
h+=kpi(totCD>0?MX(totCD):'—','Corte y Doblez','#16a34a');
h+=kpi(totSold>0?MX(totSold):'—','Soldadura','#d97706');
h+='
';
// ══ GRÁFICA 1 — Ventas por área (barras, igual al reporte mensual del Excel) ══
h+='
';
var areaNombres=['Fab. Equipo','Corte Láser','Corte y Doblez','Soldadura'];
var areaVals=[totFab,totLaser,totCD,totSold];
h+=card('Ventas por Área',
legend([['Fab. Equipo',AC[0]],['Corte Láser',AC[1]],['Corte y Doblez',AC[2]],['Soldadura',AC[3]]])
+svgBars(areaNombres,areaVals,AC,{h:160,gap:14}),
'#334155');
// ══ GRÁFICA 2 — Pedidos vs Entregados por mes ══
if(mesesKeys.length>0){
var pedMes=mesesKeys.map(function(k){return mesesData[k].pedidos;});
var entMes=mesesKeys.map(function(k){return mesesData[k].entregados;});
var mesLbls=mesesKeys.map(function(k){return mesesNombres[parseInt(mesesData[k].mo,10)-1];});
h+=card('Pedidos vs Entregados por Mes',
legend([['Total Pedidos','#3b82f6'],['Entregados','#22c55e']])
+svgGrouped(mesLbls,[pedMes,entMes],['#3b82f6','#22c55e'],{h:160}),
'#334155');
} else {
h+=card('Pedidos vs Entregados','
Sin datos aún.
','#334155');
}
h+='
';
// ══ GRÁFICA 3 — Venta Total por mes (igual al Excel) ══
if(mesesKeys.length>0){
var mesLbls2=mesesKeys.map(function(k){return mesesNombres[parseInt(mesesData[k].mo,10)-1];});
var ventaMes=mesesKeys.map(function(k){return mesesData[k].ventaTotal;});
var pagadoMes=mesesKeys.map(function(k){return mesesData[k].pagado;});
var cobrarMes=mesesKeys.map(function(k){return mesesData[k].porCobrar;});
h+=card('Venta Total por Mes (Pagado vs Por Cobrar)',
legend([['Total Vendido','#3b82f6'],['Total Pagado','#22c55e'],['Por Cobrar','#f59e0b']])
+svgGrouped(mesLbls2,[ventaMes,pagadoMes,cobrarMes],['#3b82f6','#22c55e','#f59e0b'],{h:190}),
'#334155');
}
// ══ GRÁFICA 4 — Desglose por área por mes (agrupado, igual al Excel) ══
if(mesesKeys.length>0){
var mesLbls3=mesesKeys.map(function(k){return mesesNombres[parseInt(mesesData[k].mo,10)-1];});
var fabMes=mesesKeys.map(function(k){return mesesData[k].fab;});
var laserMes=mesesKeys.map(function(k){return mesesData[k].laser;});
var cdMes=mesesKeys.map(function(k){return mesesData[k].cd;});
var soldMes=mesesKeys.map(function(k){return mesesData[k].sold;});
h+=card('Ventas por Área por Mes',
legend([['Fab. Equipo',AC[0]],['Corte Láser',AC[1]],['Corte y Doblez',AC[2]],['Soldadura',AC[3]]])
+svgGrouped(mesLbls3,[fabMes,laserMes,cdMes,soldMes],AC,{h:200}),
'#334155');
}
// ══ TABLA RESUMEN MENSUAL (igual al REPORTE MENSUAL del Excel) ══
if(mesesKeys.length>0){
var tblHdr='
'
+'
'
+'
MES
'
+'
FAB EQUIPO
'
+'
CORTE Y DOBLEZ
'
+'
SOLDADURA
'
+'
CORTE LÁSER
'
+'
VENTA TOTAL
'
+'
TOTAL PAGADO
'
+'
POR COBRAR
'
+'
PEDIDOS
'
+'
ENTREGADOS
'
+'
NO ENTREGADOS
'
+'
';
var acFab=0,acCD=0,acSold=0,acLaser=0,acVT=0,acPag=0,acCob=0,acPed=0,acEnt=0;
mesesKeys.forEach(function(k,idx){
var md=mesesData[k];
var noEnt=md.pedidos-md.entregados;
var bg=idx%2===0?'rgba(255,255,255,.02)':'transparent';
acFab+=md.fab;acCD+=md.cd;acSold+=md.sold;acLaser+=md.laser;
acVT+=md.ventaTotal;acPag+=md.pagado;acCob+=md.porCobrar;
acPed+=md.pedidos;acEnt+=md.entregados;
tblHdr+='
'
+'
'+mesesNombres[parseInt(md.mo,10)-1]+' '+md.yr+'
'
+'
'+(md.fab>0?MX(md.fab):'—')+'
'
+'
'+(md.cd>0?MX(md.cd):'—')+'
'
+'
'+(md.sold>0?MX(md.sold):'—')+'
'
+'
'+(md.laser>0?MX(md.laser):'—')+'
'
+'
'+(md.ventaTotal>0?MX(md.ventaTotal):'—')+'
'
+'
'+(md.pagado>0?MX(md.pagado):'—')+'
'
+'
'+(md.porCobrar>0?MX(md.porCobrar):'—')+'
'
+'
'+md.pedidos+'
'
+'
'+md.entregados+'
'
+'
'+noEnt+'
'
+'
';
});
// Fila de totales
tblHdr+='
'
+'
TOTAL ANUAL
'
+'
'+MX(acFab)+'
'
+'
'+MX(acCD)+'
'
+'
'+MX(acSold)+'
'
+'
'+MX(acLaser)+'
'
+'
'+MX(acVT)+'
'
+'
'+MX(acPag)+'
'
+'
'+MX(acCob)+'
'
+'
'+acPed+'
'
+'
'+acEnt+'
'
+'
'+(acPed-acEnt)+'
'
+'
';
tblHdr+='
';
h+=card('Reporte Mensual',tblHdr,'#334155');
}
root.innerHTML=h;
window.scrollTo({top:0,behavior:'smooth'});
}
// ══ POR COBRAR ═══════════════════════════════════════════════════════════════
function updateCobrarBadge(){
const allTerm=window._allTerminados||terminados;
const remData=window._remCache||{};
const count=allTerm.filter(function(o){
const r=remData[String(o.ot)];
if(!r)return true;
const sub=(r.items||[]).reduce(function(a,i){return a+(Number(i.cant)||0)*(Number(i.costoU)||0);},0);
const total=sub*(1+(Number(r.impuesto)||0)/100);
const saldo=Math.max(0,total-(Number(r.anticipo)||0));
return saldo>0||total===0;
}).length;
const badge=document.getElementById('cobrar-badge');
if(badge){if(count>0){badge.textContent=count;badge.style.display='inline';}else{badge.style.display='none';}}
}
window.updateCobrarBadge=updateCobrarBadge;
function renderCobrar(){
const root=document.getElementById('cobrar-root');
if(!root)return;
// Load all terminados if not yet loaded
const allTerm=window._allTerminados||terminados;
const remData=window._remCache||{};
const FMT=n=>'$'+Number(n||0).toLocaleString('es-MX',{minimumFractionDigits:0,maximumFractionDigits:0});
// Filtrar terminadas que NO están liquidadas (saldo > 0 o sin remisión)
const pendientes=allTerm.filter(function(o){
const r=remData[String(o.ot)];
if(!r)return true; // sin remisión = pendiente de cobro
const sub=(r.items||[]).reduce(function(a,i){return a+(Number(i.cant)||0)*(Number(i.costoU)||0);},0);
const imp=sub*(Number(r.impuesto)||0)/100;
const total=sub+imp;
const anticipo=Number(r.anticipo)||0;
const saldo=Math.max(0,total-anticipo);
return saldo>0||total===0; // tiene saldo pendiente o remisión vacía
});
// Actualizar badge
const badge=document.getElementById('cobrar-badge');
if(badge){if(pendientes.length>0){badge.textContent=pendientes.length;badge.style.display='inline';}else{badge.style.display='none';}}
if(!pendientes.length){
root.innerHTML='
'
+'
✅
'
+'
¡Todo cobrado!
'
+'
No hay órdenes terminadas con saldo pendiente.
'
+'
';
return;
}
// Calcular total por cobrar
var totalPorCobrar=0;
pendientes.forEach(function(o){
const r=remData[String(o.ot)];
if(!r)return;
const sub=(r.items||[]).reduce(function(a,i){return a+(Number(i.cant)||0)*(Number(i.costoU)||0);},0);
const imp=sub*(Number(r.impuesto)||0)/100;
const total=sub+imp;
const anticipo=Number(r.anticipo)||0;
totalPorCobrar+=Math.max(0,total-anticipo);
});
var h='';
// Header con total
h+='
';
});
root.innerHTML=h;
}
window.renderCobrar=renderCobrar;
// Marcar una orden como liquidada desde Por Cobrar (anticipo = total)
window.marcarLiquidada=async function(id,ot){
const remData=window._remCache||{};
const r=remData[String(ot)];
if(!r){alert('Esta orden no tiene remisión. Crea primero una nota de remisión con el monto total.');return;}
const sub=(r.items||[]).reduce(function(a,i){return a+(Number(i.cant)||0)*(Number(i.costoU)||0);},0);
const imp=sub*(Number(r.impuesto)||0)/100;
const total=sub+imp;
if(!total){alert('La remisión no tiene monto. Edítala primero con el total a cobrar.');return;}
if(!confirm('¿Marcar OT '+ot+' como liquidada? Se registrará el pago total de $'+Math.round(total).toLocaleString('es-MX')+'.'))return;
const updated={...r,anticipo:total,anticipoMetodo:r.anticipoMetodo||'Efectivo',updatedAt:new Date().toISOString()};
try{
await setDoc(doc(db,'remisiones',String(ot)),updated);
if(!window._remCache)window._remCache={};
window._remCache[String(ot)]=updated;
const termOT=(window._allTerminados||terminados).find(function(o){return String(o.ot)===String(ot);});
await logAccion('liquidar',ot,termOT?termOT.cli:'','marcada como liquidada');
renderCobrar();
if(window._activeTab==='dashboard')renderDashboard();
}catch(e){alert('Error: '+e.message);}
};
window.renderDashboard=renderDashboard;
function renderRespaldo(){
var root=document.getElementById('respaldo-root');
if(!root)return;
// Render immediately with available data
var todosTerminados=(window._allTerminados||terminados).map(function(o){
return Object.assign({},o,{est:'TERMINADA'});
});
var todas=ordenes.concat(todosTerminados).sort(function(a,b){return b.ot-a.ot;});
var remData=window._remCache||{};
var h='
';
todas.forEach(function(o){
var rem=remData[String(o.ot)];
var tot=rem?(rem.items||[]).reduce(function(s,i){return s+(Number(i.cant)||0)*(Number(i.costoU)||0);},0)*(1+(Number(rem.impuesto)||0)/100):0;
var ec={ACTIVA:'#3b82f6',URGENTE:'#ef4444',PAUSADA:'#f59e0b',TERMINADA:'#22c55e'}[o.est||'ACTIVA']||'#64748b';
h+='