// ROCA GOLF — Gantt por insumo/presupuesto + alertas automáticas como pins
function daysBetween(a, b) {
const da = new Date(a + "T00:00:00"), db = new Date(b + "T00:00:00");
return Math.round((db - da) / 86400000);
}
function addDaysISO(iso, n) {
const d = new Date(iso + "T00:00:00");
d.setDate(d.getDate() + n);
return d.toISOString().slice(0, 10);
}
function monthLabelES(iso) {
const m = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"];
const d = new Date(iso + "T00:00:00");
return `${m[d.getMonth()]} ${d.getFullYear().toString().slice(2)}`;
}
function shortDate(iso) {
const d = new Date(iso + "T00:00:00");
return `${String(d.getDate()).padStart(2,"0")}/${String(d.getMonth()+1).padStart(2,"0")}`;
}
const GANTT_FAM_COLORS = {
"01": "#2d6a44", "02": "#a88838", "03": "#1976d2", "04": "#b22222",
"05": "#7c4dff", "06": "#0d2818", "07": "#0e8a7a", "08": "#c8651b"
};
// Colores de los dos tramos de la barra: verde lo PAGADO, amarillo lo que falta
// por pagar de las OCs vivas. Cerca del limite (>=90%) manda el color de alerta,
// que es la senal que ya se leia de un vistazo; el tramo pendiente va en el mismo
// tono pero translucido para que se siga distinguiendo que parte no se ha pagado.
function ganttTramos(pct) {
const c = ganttBarColor(pct);
if (pct >= 90) return { pagado: c, pendiente: c + "66" };
return { pagado: "#1baf7a", pendiente: "#d4a017" };
}
// Color de barra según % de utilización del presupuesto
function ganttBarColor(pct) {
if (pct >= 100) return "#b71c1c";
if (pct >= 90) return "#e53935";
if (pct >= 80) return "#f57c00";
if (pct >= 70) return "#f9a825";
return "#2d6a44";
}
function GanttChart({ proyecto, presupuestos, alertas, onAlertClick, onAddAlert }) {
const today = new Date().toISOString().slice(0, 10);
const [fullscreen, setFullscreen] = useState(false);
const [tooltip, setTooltip] = useState(null);
const [ordenarLimite, setOrdenarLimite] = useState(false);
const tooltipRef = useRef(null);
useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') { setFullscreen(false); setTooltip(null); } };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, []);
useEffect(() => {
if (!tooltip) return;
function onDown(e) {
if (tooltipRef.current && !tooltipRef.current.contains(e.target)) setTooltip(null);
}
document.addEventListener('mousedown', onDown);
document.addEventListener('touchstart', onDown);
return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('touchstart', onDown); };
}, [tooltip]);
async function openTooltip(pp, e) {
e.stopPropagation();
if (tooltip && tooltip.pp.insumoid === pp.insumoid) { setTooltip(null); return; }
const rect = e.currentTarget.getBoundingClientRect();
const x = Math.min(rect.left, window.innerWidth - 334);
const y = rect.bottom + 6;
setTooltip({ pp, ocs: [], loading: true, x, y });
try {
const data = await apiFetch(`presupuestos_proyecto.php?mode=ocs&proyectoid=${pp.proyectoid}&insumoid=${pp.insumoid}`);
setTooltip(t => t && t.pp.insumoid === pp.insumoid ? { ...t, ocs: Array.isArray(data) ? data : [], loading: false } : t);
} catch {
setTooltip(t => t && t.pp.insumoid === pp.insumoid ? { ...t, ocs: [], loading: false } : t);
}
}
const startISO = proyecto.inicio;
const endISO = proyecto.fin;
// Expandir rango para incluir todas las fechas de presupuestos
let effectiveStart = startISO;
let effectiveEnd = endISO;
(presupuestos || []).forEach(pp => {
if (pp.fecha_inicio && pp.fecha_inicio < effectiveStart) effectiveStart = pp.fecha_inicio;
if (pp.fecha_fin && pp.fecha_fin > effectiveEnd) effectiveEnd = pp.fecha_fin;
});
const totalDays = Math.max(1, daysBetween(effectiveStart, effectiveEnd));
const COL_LEFT = 300;
const ROW_H = 42;
const HEAD_H = 56; // meses(26) + semanas(30); left head forced to same via CSS height:56px
const PAD_BARS = 8;
const innerW = Math.max(1000, totalDays * 10);
const dayW = innerW / totalDays;
// Orden descendente por % del límite consumido (más cerca del límite = más
// arriba) — ayuda a ubicar rápido qué insumos están más cerca de excederse.
// pct siempre es numérico (nunca null, ver rg_oc_importadas/sin_tope), así
// que este sort nunca produce NaN.
const rowsBase = presupuestos || [];
const rows = ordenarLimite ? [...rowsBase].sort((a, b) => b.pct - a.pct) : rowsBase;
// Marcas de mes
const months = [];
let cur = new Date(effectiveStart + "T00:00:00");
cur.setDate(1);
while (cur <= new Date(effectiveEnd + "T00:00:00")) {
const iso = cur.toISOString().slice(0, 10);
const offset = daysBetween(effectiveStart, iso);
months.push({ iso, offset: Math.max(0, offset), label: monthLabelES(iso) });
cur.setMonth(cur.getMonth() + 1);
}
const weeks = [];
for (let d = 0; d <= totalDays; d += 7) weeks.push(d);
const todayOffset = daysBetween(effectiveStart, today);
const rowsH = Math.max(rows.length * ROW_H + 12, 60);
const THRESHOLDS = [70, 80, 90, 100];
return (
{/* Folio de OC importada es texto libre del Excel original — no
pasa por fmtFolio() (asume formato numérico de OC viva). */}
{oc.importada ? ('Hist. ' + oc.folio) : fmtFolio(oc.folio)}