// =====================================================================
// MAESTRO — Vista Status IA (tablero pipeline con drag & drop)
// =====================================================================
const { useState: useSB, useEffect: useSBE, useMemo: useSBM, useRef: useSBR } = React;
const D_sb = window.MAESTRO_DATA;
const PIPELINE_STAGES = [
{ key: "idea", label: "Idea", color: "var(--fg-subtle)", bg: "var(--bg-subtle)" },
{ key: "poc", label: "POC", color: "#0ea5e9", bg: "rgba(14,165,233,.1)" },
{ key: "desarrollo", label: "Desarrollo", color: "#f59e0b", bg: "rgba(245,158,11,.1)" },
{ key: "testing", label: "Testing", color: "#a78bfa", bg: "rgba(167,139,250,.1)" },
{ key: "produccion", label: "Producción", color: "var(--success)", bg: "rgba(34,197,94,.1)" },
{ key: "pausado", label: "Pausado", color: "var(--fg-muted)", bg: "var(--bg-surface)" },
];
const ESTADO_COLOR = {
"en análisis": "var(--accent)",
"en desarrollo":"var(--success)",
"testing": "var(--warning)",
"listo": "var(--success)",
"producción": "#22c55e",
};
const PRIORIDAD_COLOR = { alta: "var(--danger)", media: "var(--warning)", baja: "var(--fg-muted)" };
function ProgressBar({ total, completed }) {
const pct = total > 0 ? Math.round((completed / total) * 100) : 0;
return (
);
}
function StatusCard({ project, onClick, onDragStart, isDragging }) {
const stage = PIPELINE_STAGES.find(s => s.key === (project.pipeline_stage || "idea")) || PIPELINE_STAGES[0];
const hasBlockers = project.blockers_open > 0;
return (
{
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", project.id);
onDragStart(project.id);
}}
onDragEnd={() => onDragStart(null)}
onClick={() => !isDragging && onClick(project)}
style={{
background: "var(--bg-surface)",
border: "1px solid var(--border)",
borderTop: `3px solid ${stage.color}`,
borderRadius: "var(--r-md)",
padding: "14px 16px",
cursor: "grab",
display: "flex",
flexDirection: "column",
gap: 10,
opacity: isDragging ? 0.45 : 1,
transition: "box-shadow .15s, transform .15s, opacity .15s",
userSelect: "none",
}}
onMouseEnter={e => { if (!isDragging) { e.currentTarget.style.boxShadow = "var(--shadow-md)"; e.currentTarget.style.transform = "translateY(-1px)"; }}}
onMouseLeave={e => { e.currentTarget.style.boxShadow = "none"; e.currentTarget.style.transform = "none"; }}
>
{project.nombre}
{project.prioridad}
{project.descripcion || "Sin descripción"}
{project.estado}
{hasBlockers && (
{project.blockers_open} bloqueante{project.blockers_open > 1 ? "s" : ""}
)}
{project.tasks_total > 0 && (
)}
);
}
function DetailModal({ project, onClose }) {
if (!project) return null;
const stage = PIPELINE_STAGES.find(s => s.key === (project.pipeline_stage || "idea")) || PIPELINE_STAGES[0];
const taskPct = project.tasks_total > 0 ? Math.round((project.tasks_completed / project.tasks_total) * 100) : null;
return (
e.target === e.currentTarget && onClose()}
>
{project.nombre}
{stage.label}
{project.estado}
Prioridad {project.prioridad}
Objetivo
{project.descripcion || "Sin descripción registrada."}
Responsable
{project.responsable || "—"}
{project.deadline && (
Deadline
{project.deadline}
)}
{project.tasks_total > 0 && (
Progreso de tareas ({taskPct}%)
{project.tasks_completed} completadas de {project.tasks_total} tareas
)}
{project.blockers_open > 0 && (
Bloqueantes activos ({project.blockers_open})
Hay {project.blockers_open} impedimento{project.blockers_open > 1 ? "s" : ""} activo{project.blockers_open > 1 ? "s" : ""} en este proyecto.
)}
{project.tags && (
Tags
{project.tags.split(",").map(t => t.trim()).filter(Boolean).map(t => (
{t}
))}
)}
);
}
function StatusBoard() {
const [projects, setProjects] = useSB([]);
const [loading, setLoading] = useSB(true);
const [error, setError] = useSB(null);
const [selectedProject, setSelectedProject] = useSB(null);
const [lastUpdated, setLastUpdated] = useSB(null);
const [draggingId, setDraggingId] = useSB(null);
const [dragOverStage, setDragOverStage] = useSB(null);
useSBE(() => { loadProjects(); }, []);
async function loadProjects() {
setLoading(true);
setError(null);
try {
const data = await D_sb.API.apiFetch("/status/projects");
setProjects(data);
setLastUpdated(new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" }));
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
async function moveTo(projectId, newStage) {
const project = projects.find(p => p.id === projectId);
if (!project || project.pipeline_stage === newStage) return;
// Optimistic update
setProjects(ps => ps.map(p => p.id === projectId ? { ...p, pipeline_stage: newStage } : p));
try {
await D_sb.API.apiFetch(`/projects/${projectId}`, {
method: "PATCH",
body: JSON.stringify({ pipeline_stage: newStage }),
});
} catch (e) {
// Revert on error
setProjects(ps => ps.map(p => p.id === projectId ? { ...p, pipeline_stage: project.pipeline_stage } : p));
}
}
const grouped = useSBM(() => {
const g = {};
PIPELINE_STAGES.forEach(s => g[s.key] = []);
projects.forEach(p => {
const key = p.pipeline_stage || "idea";
if (g[key]) g[key].push(p);
else g["idea"].push(p);
});
return g;
}, [projects]);
if (loading) {
return (
);
}
if (error) {
return (
Error al cargar: {error}
);
}
return (
{selectedProject && (
setSelectedProject(null)} />
)}
Status IA
Pipeline de proyectos
{projects.length} proyecto{projects.length !== 1 ? "s" : ""} · actualizado {lastUpdated}
{PIPELINE_STAGES.map(stage => {
const stageProjects = grouped[stage.key] || [];
const isOver = dragOverStage === stage.key;
return (
{ e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverStage(stage.key); }}
onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverStage(null); }}
onDrop={e => {
e.preventDefault();
const id = e.dataTransfer.getData("text/plain");
if (id) moveTo(id, stage.key);
setDragOverStage(null);
setDraggingId(null);
}}
>
{/* Column header */}
{stage.label}
{stageProjects.length}
{/* Drop zone + cards */}
{stageProjects.map(p => (
))}
{stageProjects.length === 0 && (
{isOver ? "Soltar aquí" : "Sin proyectos"}
)}
);
})}
);
}
window.MAESTRO_STATUS = { StatusBoard };