// ===================================================================== // 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 (
{completed}/{total}
); } 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 (
Cargando tablero…
); } 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 };