// ===================================================================== // MAESTRO — Carpeta de proyecto (vista de pagina completa) // Sigue la estructura de ESPECIFICACION_CARPETA_PROYECTO.txt // ===================================================================== const { useState: usePD, useEffect: usePDE, useMemo: usePDM } = React; const D_pd = window.MAESTRO_DATA; const I_pd = window.MAESTRO_ICONS; const PD_BLOCKER_COLOR = { "esperando cliente": "var(--danger)", "sin acceso": "var(--danger)", "tecnico": "var(--warning)", "otro": "var(--fg-subtle)", }; const PD_PRIORITY_COLOR = { alta: "var(--danger)", media: "var(--warning)", baja: "var(--success)" }; const PD_NOTE_COLOR = { learning: "var(--accent)", decision: "var(--warning)", reflection: "var(--success)", otro: "var(--fg-subtle)" }; const PD_NOTE_ICON = { learning: "Aprendizaje", decision: "Decision", reflection: "Reflexion", otro: "Nota" }; // ── Helpers ────────────────────────────────────────────────────────── function PDCard({ children, style }) { return (
{children}
); } function PDSectionTitle({ children }) { return (

{children}

); } function PDMetric({ label, value, sub, color, large }) { return (
{label}
{value}
{sub &&
{sub}
}
); } // ── Inline forms ───────────────────────────────────────────────────── function AddHoursForm({ project, onSave, onCancel }) { const [h, setH] = usePD(""); const [desc, setDesc] = usePD(""); const [saving, setSaving] = usePD(false); const s = { padding: "7px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }; const submit = async (e) => { e.preventDefault(); if (!h || parseFloat(h) <= 0) return; setSaving(true); try { await onSave(parseFloat(h), desc); } finally { setSaving(false); } }; return (
setH(e.target.value)} style={{ ...s, width: 90 }} autoFocus /> setDesc(e.target.value)} style={{ ...s, flex: 1, minWidth: 180 }} />
); } function AddNoteForm({ onSave, onCancel }) { const [text, setText] = usePD(""); const [tipo, setTipo] = usePD("otro"); const [saving, setSaving] = usePD(false); const s = { padding: "7px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }; const submit = async (e) => { e.preventDefault(); if (!text.trim()) return; setSaving(true); try { await onSave(text.trim(), tipo); } finally { setSaving(false); } }; return (
setText(e.target.value)} style={{ ...s, flex: 1, minWidth: 200 }} autoFocus />
); } // ── Main component ──────────────────────────────────────────────────── function fmtTimer(s) { const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; if (h > 0) return `${h}:${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; return `${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; } function ProjectDetail({ project, onClose, onToggleTask, onResolveBlocker, onAddBlocker, onChangeState, onEditProject, onAddHours, onAddNote, onDeleteProject, onUploadStructureImage, onArchiveProject, onDuplicateProject, onUpdateTaskSubtasks, onAddTask, onUploadTaskPrepFile, activeTimer, timerDisplaySeconds, onStartTimer, onPauseTimer }) { const [showHours, setShowHours] = usePD(false); const [showNote, setShowNote] = usePD(false); const [editMode, setEditMode] = usePD(false); const [deleteConfirm, setDeleteConfirm] = usePD(false); const [tlFilter, setTlFilter] = usePD("all"); const [showAddLink, setShowAddLink] = usePD(false); const [linkLabel, setLinkLabel] = usePD(""); const [linkUrl, setLinkUrl] = usePD(""); const [savingLink, setSavingLink] = usePD(false); const [showAddBlocker, setShowAddBlocker] = usePD(false); const [blockerDesc, setBlockerDesc] = usePD(""); const [blockerTipo, setBlockerTipo] = usePD("otro"); const [savingBlocker, setSavingBlocker] = usePD(false); const [editStructureText, setEditStructureText] = usePD(false); const [structureText, setStructureText] = usePD(project.structureText || ""); const [savingStructure, setSavingStructure] = usePD(false); const [uploadingImage, setUploadingImage] = usePD(false); const [expandedTaskId, setExpandedTaskId] = usePD(null); const [newSubtarea, setNewSubtarea] = usePD(""); const [editBilling, setEditBilling] = usePD(false); const [billingTarifa, setBillingTarifa] = usePD(String(project.tarifaHora || "")); const [billingPresup, setBillingPresup] = usePD(String(project.presupuesto || "")); const [savingBilling, setSavingBilling] = usePD(false); const [showAddTask, setShowAddTask] = usePD(false); const [newTaskType, setNewTaskType] = usePD("task"); const [newTaskDesc, setNewTaskDesc] = usePD(""); const [newTaskWith, setNewTaskWith] = usePD(""); const [newTaskDate, setNewTaskDate] = usePD(""); const [newTaskDateStart, setNewTaskDateStart] = usePD(""); const [newTaskPrepFile, setNewTaskPrepFile] = usePD(null); const [savingNewTask, setSavingNewTask] = usePD(false); const [editingTaskId, setEditingTaskId] = usePD(null); const [editTaskDesc, setEditTaskDesc] = usePD(""); const [editTaskDate, setEditTaskDate] = usePD(""); const [editTaskDateStart, setEditTaskDateStart] = usePD(""); const [editTaskPriority, setEditTaskPriority] = usePD("media"); const [savingEditTask, setSavingEditTask] = usePD(false); const [confirmDeleteTaskId, setConfirmDeleteTaskId] = usePD(null); const startEditTask = (t) => { setEditingTaskId(t.id); setEditTaskDesc(t.name || ""); setEditTaskDate(t.fechaVencimiento || ""); setEditTaskDateStart(t.fechaInicio || ""); setEditTaskPriority(t.priority || "media"); }; const saveEditTask = async (t) => { setSavingEditTask(true); try { await D_pd.API.updateTask(project.id, t.id, { descripcion: editTaskDesc.trim() || t.name, fecha_inicio: editTaskDateStart || null, fecha_vencimiento: editTaskDate || null, prioridad: editTaskPriority, }); setEditingTaskId(null); // Recargar proyecto window.dispatchEvent(new CustomEvent("maestro:reloadProject", { detail: { projectId: project.id } })); } catch(e) { alert("Error al guardar: " + e.message); } finally { setSavingEditTask(false); } }; // ── Google integration state ──────────────────────────────────────── const [googleEnabled, setGoogleEnabled] = usePD(null); // null=loading, true/false const [driveFolder, setDriveFolder] = usePD(null); const [driveLoading, setDriveLoading] = usePD(false); const [calendarSyncing, setCalendarSyncing] = usePD(false); const [calendarEventId, setCalendarEventId] = usePD(project.calendarEventId || null); const [googleMsg, setGoogleMsg] = usePD(null); usePDE(() => { D_pd.API.googleStatus().then(s => setGoogleEnabled(s.enabled)).catch(() => setGoogleEnabled(false)); if (project.driveFolderId) setDriveFolder({ folder_id: project.driveFolderId, folder_url: `https://drive.google.com/drive/folders/${project.driveFolderId}` }); }, [project.id]); const handleDriveOpen = async () => { if (driveFolder) { window.open(driveFolder.folder_url, "_blank"); return; } setDriveLoading(true); setGoogleMsg(null); try { const f = await D_pd.API.googleDriveFolder(project.id); setDriveFolder(f); if (f.folder_url) window.open(f.folder_url, "_blank"); } catch(e) { setGoogleMsg("Error: " + e.message); } finally { setDriveLoading(false); } }; const handleCalendarSync = async () => { setCalendarSyncing(true); setGoogleMsg(null); try { const r = await D_pd.API.googleCalendarSync(project.id); setCalendarEventId(r.event_id); setGoogleMsg(r.action === "created" ? "✅ Evento creado en Calendar" : "✅ Evento actualizado en Calendar"); } catch(e) { setGoogleMsg("Error: " + e.message); } finally { setCalendarSyncing(false); } }; const handleCalendarDelete = async () => { if (!calendarEventId) return; setCalendarSyncing(true); setGoogleMsg(null); try { await D_pd.API.googleCalendarDeleteEvent(calendarEventId, project.id); setCalendarEventId(null); setGoogleMsg("🗑 Evento eliminado del calendario"); } catch(e) { setGoogleMsg("Error: " + e.message); } finally { setCalendarSyncing(false); } }; usePDE(() => { const onEsc = e => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onEsc); document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onEsc); document.body.style.overflow = ""; }; }, []); // ── Computed ──────────────────────────────────────────────────────── const today = usePDM(() => { const d = new Date(); d.setHours(0,0,0,0); return d; }, []); const deadlineDate = project.deadline ? new Date(project.deadline) : null; const daysLeft = deadlineDate ? Math.ceil((deadlineDate - today) / 86400000) : null; const ratio = project.actualHours / (project.estimatedHours || 1); const overload = project.actualHours - (project.estimatedHours || 0); const uniqueDays = new Set((project.workHours || []).map(w => w.date)).size; const activeTasks = project.tasks.filter(t => !t.done).length; const activeBlockers = project.blockers.filter(b => !b.resolved).length; const timelineEvents = usePDM(() => { const all = [...(project.history || [])]; if (tlFilter === "state") return all.filter(h => h.type === "state"); if (tlFilter === "hours") return all.filter(h => h.type === "hours" || h.type === "time"); if (tlFilter === "tasks") return all.filter(h => h.type === "task"); if (tlFilter === "notes") return all.filter(h => h.type === "note"); return all; }, [project.history, tlFilter]); const inStyle = { padding: "7px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13, width: "100%", boxSizing: "border-box" }; // ── Edit form state ───────────────────────────────────────────────── const [eDesc, setEDesc] = usePD(project.description || ""); const [ePrio, setEPrio] = usePD(project.priority || "media"); const [eHours, setEHours] = usePD(String(project.estimatedHours || "")); const [eDeadline, setEDeadline] = usePD(project.deadline || ""); const [eInicio, setEInicio] = usePD(project.fechaInicio || ""); const [eResp, setEResp] = usePD(project.responsable || ""); const [eSpecs, setESpecs] = usePD(project.specsText || ""); const [eSaving, setESaving] = usePD(false); const saveEdit = async (e) => { e.preventDefault(); setESaving(true); try { await onEditProject(project.id, { descripcion: eDesc || undefined, prioridad: ePrio, estimated_hours: eHours ? parseFloat(eHours) : undefined, deadline: eDeadline || undefined, fecha_inicio: eInicio || undefined, responsable: eResp || undefined, specs_text: eSpecs || undefined, }); setEditMode(false); } finally { setESaving(false); } }; return (
{/* ── TOP BAR ───────────────────────────────────────────────── */}
{project.name}
{!showHours && !showNote && ( <> )} {onDuplicateProject && ( )} {onArchiveProject && ( )} {!deleteConfirm ? : <> }
{/* ── INLINE FORMS (horas / nota) ───────────────────────────── */} {(showHours || showNote) && (
{showHours && { await onAddHours(project.id, h, d); setShowHours(false); }} onCancel={() => setShowHours(false)} />} {showNote && { await onAddNote(project.id, t, tp); setShowNote(false); }} onCancel={() => setShowNote(false)} />}
)} {/* ── SCROLLABLE CONTENT ────────────────────────────────────── */}
{/* ── EDIT FORM ──────────────────────────────────────────── */} {editMode && ( Editar proyecto
Descripcion
setEDesc(e.target.value)} style={inStyle} />
Prioridad
Responsable
setEResp(e.target.value)} placeholder="Alexis" style={inStyle} />
Horas estimadas
setEHours(e.target.value)} style={inStyle} />
Deadline
setEDeadline(e.target.value)} style={inStyle} />
Fecha de inicio
setEInicio(e.target.value)} style={inStyle} />
Especificaciones tecnicas / Stack