// ROCA GOLF — Dashboard: proyectos y OCs desde la BD function ProjectPicker({ value, onChange, proyectos, alertas = [], pptoResumen = {} }) { return (
{proyectos.map(p => { const manualCount = alertas.filter(a => a.proyectoid === p.id && a.activa !== false).length; const pptoCount = (pptoResumen[p.id] || pptoResumen[String(p.id)])?.en_alerta || 0; const total = manualCount + pptoCount; const tipParts = []; if (manualCount > 0) tipParts.push(`${manualCount} alerta${manualCount !== 1 ? 's' : ''} manual${manualCount !== 1 ? 'es' : ''}`); if (pptoCount > 0) tipParts.push(`${pptoCount} presupuesto${pptoCount !== 1 ? 's' : ''} en alerta`); return ( ); })}
); } function ProjectKpis({ proyecto, tareas, presupuestos, combinados }) { const ocsImportadas = proyecto.ocs_importadas || 0; const ejercidoImportado = proyecto.ejercido_importado || 0; // "Ejercido" de la tarjeta principal incluye OCs vivas + históricas // importadas (rg_oc_importadas) — antes solo mostraba lo vivo del año en // curso, subestimando el gasto real del proyecto. Las importadas son // históricas por definición (ya resueltas en la otra plataforma), así que // se suman completas al ejercido, no al pendiente — "pendiente" sigue // siendo solo el de OCs vivas todavía por pagar. // FIX 2026-09-30: `proyecto.ejercido` es SUM(oc.total) — ya incluye lo pagado Y // lo pendiente, así que `pendiente` es un SUBCONJUNTO suyo. Sumarlos (como se // hacía antes) inflaba el total por el pendiente y el porcentaje no medía lo // pagado: Rancho El Paraguas mostraba "Ejercido de $3,328,562 · 83.2% pagado" // cuando lo comprometido era $2,768,830 y lo pagado $2,209,099 (79.8%). Afectaba // a los 18 proyectos ($15.6M inflados en total). Verificado contra la BD del // 30-sep: los datos estaban bien, el error era solo de esta tarjeta. // UNIFICACIÓN 2026-10-07: "pagado" ya no da por pagado TODO el histórico // importado. Las importadas traen su propio pagado/pendiente y no todas venían // liquidadas: en VALLE había 19 sin pagar ($3.84M), que esta tarjeta contaba // como pagadas mientras Admin. de Proyecto no. Ahora las dos pantallas usan la // misma regla (pagado de OCs vivas + lo que el histórico trae como pagado) y // este número es igual al "Total pagado, con IVA" de Admin. de Proyecto. Lo // importado sin pago registrado se muestra aparte, sin llamarlo ni pagado ni // pendiente: es una foto del día de la carga y hay que confirmarlo. Si el // backend aún no manda `pagado_importado`, se conserva el cálculo anterior. const ejercido = (proyecto.ejercido || 0) + ejercidoImportado; // comprometido: OCs vivas + histórico completo const pendiente = proyecto.pendiente || 0; // lo que falta pagar de las OCs vivas const pagadoImportado = proyecto.pagado_importado != null ? proyecto.pagado_importado : ejercidoImportado; const histSinPago = proyecto.importado_sin_pago || 0; const pagado = Math.max(0, (proyecto.ejercido || 0) - pendiente) + pagadoImportado; const pct = ejercido > 0 ? (pagado / ejercido) * 100 : 0; // Conteos por estatus de TODAS las OCs vigentes del proyecto (api/proyectos.php). // FIX 2026-10-07: antes se contaban sobre `tareas`, que son solo las 100 OCs que // carga el Gantt. VALLE mostraba "Pagadas 60 · Pendientes 40 de 100 OCs" y tiene // 487 pagadas, 3 parciales y 95 pendientes de 585. Si el backend aún no manda los // conteos, se conserva el cálculo anterior. const conteoReal = proyecto.ocs_pagadas != null; const pagadas = conteoReal ? proyecto.ocs_pagadas : tareas.filter(t => t.progreso >= 100).length; const parciales = conteoReal ? proyecto.ocs_parciales : tareas.filter(t => t.progreso === 50).length; const sinPagar = conteoReal ? proyecto.ocs_pendientes : tareas.filter(t => t.progreso === 0).length; const totalOcs = conteoReal ? (proyecto.total_ocs || 0) : tareas.length; // Claves con OC real pero sin tope definido (ni Contraloría ni local) — gasto // que hoy no se está comparando contra ningún límite, riesgo de pasar // desapercibido. "sin_tope" (Contraloría dijo explícitamente "sin límite") // NO cuenta aquí — es una decisión, no un vacío. const sinPresupuestoList = (presupuestos || []).filter(p => p.sin_presupuesto); const gastadoSinPresupuesto = sinPresupuestoList.reduce((s, p) => s + (p.gastado || 0), 0); // Claves individuales + presupuestos combinados actualmente en warn/alert/over. const clavesEnAlerta = (presupuestos || []).filter(p => p.severidad).length + (combinados || []).filter(c => c.severidad).length; return (
{proyecto.nombre}
MXN{fmtMoney(ejercido)}
Pagado ${fmtMoney(pagado)} de ${fmtMoney(ejercido)} {pct.toFixed(1)}% pagado · ${fmtMoney(pendiente)} pendiente
{histSinPago > 0 && (
Histórico sin pago registrado: ${fmtMoney(histSinPago)}
)}
Órdenes
{proyecto.total_ocs || tareas.length}
{shortDate(proyecto.inicio)} → {shortDate(proyecto.fin)}
Pagadas
{pagadas}
{parciales} con pago parcial
Pendientes
{sinPagar}
de {totalOcs} OCs
Gastado sin presupuesto
0 ? "var(--rg-flag-over)" : "var(--rg-muted)" }}> ${fmtMoney(gastadoSinPresupuesto, { compact: true })}
{sinPresupuestoList.length} clave{sinPresupuestoList.length !== 1 ? "s" : ""} sin tope
Claves en alerta
0 ? "var(--rg-flag-alert)" : "var(--rg-flag-ok)" }}>{clavesEnAlerta}
≥ 80% de su tope
OCs importadas
{ocsImportadas}
${fmtMoney(ejercidoImportado, { compact: true })} histórico
); } function NewAlertModal({ open, onClose, proyecto, tareas, onSave, prefill, saving }) { const today = new Date().toISOString().slice(0, 10); const [tareaid, setTareaid] = useState(""); const [fecha, setFecha] = useState(today); const [severidad, setSeveridad] = useState("warn"); const [titulo, setTitulo] = useState(""); const [desc, setDesc] = useState(""); useEffect(() => { if (open) { setTareaid(prefill?.tareaid ? String(prefill.tareaid) : (tareas[0] ? String(tareas[0].id) : "")); setFecha(prefill?.fecha || today); setSeveridad(prefill?.severidad || "warn"); setTitulo(prefill?.titulo || ""); setDesc(prefill?.descripcion || prefill?.desc || ""); } }, [open]); if (!open) return null; const tarea = tareas.find(t => t.id === +tareaid) || tareas[0]; return (
e.stopPropagation()}>

{prefill?.id ? "Editar alerta" : "Nueva alerta"}

Proyecto: {proyecto?.nombre}
{tareas.length > 0 && (
)}
setFecha(e.target.value)}/>
{tarea && (
{tarea.clave} · {tarea.desc}
)}
{[ { k: "warn", l: "Aviso 80%", c: "var(--rg-flag-warn)" }, { k: "alert", l: "Alerta 90%", c: "var(--rg-flag-alert)" }, { k: "over", l: "Excedido", c: "var(--rg-flag-over)" } ].map(s => ( ))}
setTitulo(e.target.value)}/>