1.3 ? "over" : ratio > 1 ? "warn" : ""}`} style={{ width: `${Math.min(ratio * 100, 100)}%` }} />
{ratio > 1 && (
A este ritmo ({project.velocity}h/dia), el proyecto ya esta {Math.round((ratio-1)*100)}% por encima del estimado.
{project.velocity > 0 && overload > 0 && ` Quedan ~${(overload / project.velocity).toFixed(1)} dias de trabajo extra sin presupuestar.`}
)}
{/* ── SECCION 3: ESTADO ACTUAL ──────────────────────────── */}
Estado actual
Estado
{D_pd.STATE_LABEL[project.state]}
Desde hace {project.daysInState} dias
Bloqueado
{project.blocked ? "SI - Hay bloqueos activos" : "NO - Fluye sin problemas"}
{project.statusHistory && project.statusHistory.length >= 2 && (
Estado anterior
{project.statusHistory[1]?.estadoAnterior || "—"}
)}
Ultima actualizacion
{project.lastUpdate}
{/* Cambio de estado rapido */}
Cambiar estado:
{D_pd.STATES.map(s => s !== project.state && (
onChangeState(project.id, s)}>
-> {D_pd.STATE_LABEL_SHORT[s]}
))}
{/* ── SECCION 4: TAREAS ────────────────────────────────── */}
Tareas ({project.tasks.filter(t=>t.done).length} completadas / {project.tasks.length} total)
{" "}{activeTasks > 0 && {activeTasks} pendientes }
{onAddTask && (
{ setShowAddTask(v => !v); setNewTaskDesc(""); setNewTaskWith(""); setNewTaskDate(""); setNewTaskType("task"); setNewTaskPrepFile(null); }}>
{showAddTask ? "Cancelar" : "+ Nueva tarea"}
)}
{showAddTask && onAddTask && (
{[["task","Tarea"], ["meeting","Reunión"], ["event","Evento"]].map(([v,l]) => (
setNewTaskType(v)}>{l}
))}
)}
{project.tasks.length === 0
? Sin tareas todavia. El asistente puede crearte tareas si le describis el proyecto.
:
{project.tasks.map(t => {
const td = t.fechaVencimiento ? new Date(t.fechaVencimiento) : null;
const tdl = td ? Math.ceil((td - today) / 86400000) : null;
const icon = t.done ? "V" : t.inProgress ? ">" : "[ ]";
const iconColor = t.done ? "var(--success)" : t.inProgress ? "var(--accent)" : "var(--fg-subtle)";
const isExpanded = expandedTaskId === t.id;
const subtareas = t.subtareas || [];
const isActiveTimer = activeTimer && activeTimer.taskId === t.id;
const taskElapsed = isActiveTimer ? timerDisplaySeconds : (t.elapsedSeconds || 0);
const isMeeting = t.taskType === "meeting";
return (
onToggleTask(project.id, t.id)}>
{isMeeting ? I_pd.meeting(15) : icon}
{t.name}
{isMeeting && (
)}
{!isMeeting && {t.priority} }
{t.estimated && {t.estimated}h estimado }
{subtareas.length > 0 && {subtareas.filter(s=>s.done).length}/{subtareas.length} subtareas }
{taskElapsed > 0 && (
{isActiveTimer && "▶ "}{fmtTimer(taskElapsed)}
)}
{td && !t.done && (
Vence: {tdl <= 0 ? `VENCIDA hace ${Math.abs(tdl)}d` : tdl === 0 ? "HOY" : tdl === 1 ? "MANANA" : t.fechaVencimiento}
)}
{t.done && t.completedAt && Completada: {new Date(t.completedAt).toLocaleDateString("es-AR")} }
{onStartTimer && !t.done && (
{
e.stopPropagation();
if (isActiveTimer) onPauseTimer();
else onStartTimer(project.id, t.id);
}}>
{isActiveTimer ? "⏸" : "▶"}
)}
{!t.done && (
{ e.stopPropagation(); editingTaskId === t.id ? setEditingTaskId(null) : startEditTask(t); }}>
✏️
)}
{confirmDeleteTaskId === t.id
?
{
e.stopPropagation();
setConfirmDeleteTaskId(null);
try {
await D_pd.API.deleteTask(project.id, t.id);
window.dispatchEvent(new CustomEvent("maestro:reloadProject", { detail: { projectId: project.id } }));
} catch(err) { alert("Error al eliminar: " + err.message); }
}}>
¿Seguro?
:
{ e.stopPropagation(); setConfirmDeleteTaskId(t.id); setTimeout(() => setConfirmDeleteTaskId(null), 3000); }}>
🗑️
}
{onUpdateTaskSubtasks && (
{ e.stopPropagation(); setExpandedTaskId(isExpanded ? null : t.id); setNewSubtarea(""); }}>
{isExpanded ? "▲" : "▼"} Sub
)}
{isExpanded && onUpdateTaskSubtasks && (
Subtareas
{subtareas.map((s, si) => (
{
const updated = subtareas.map((x, xi) => xi === si ? { ...x, done: !x.done } : x);
onUpdateTaskSubtasks(project.id, t.id, updated);
}}>
{s.done && V }
{s.text}
{
const updated = subtareas.filter((_, xi) => xi !== si);
onUpdateTaskSubtasks(project.id, t.id, updated);
}}>×
))}
setNewSubtarea(e.target.value)}
onKeyDown={e => {
if (e.key === "Enter" && newSubtarea.trim()) {
onUpdateTaskSubtasks(project.id, t.id, [...subtareas, { text: newSubtarea.trim(), done: false }]);
setNewSubtarea("");
}
}}
placeholder="Nueva subtarea... (Enter para agregar)"
style={{ flex: 1, padding: "5px 8px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 12 }}
/>
{
if (!newSubtarea.trim()) return;
onUpdateTaskSubtasks(project.id, t.id, [...subtareas, { text: newSubtarea.trim(), done: false }]);
setNewSubtarea("");
}}>+ Agregar
)}
{editingTaskId === t.id && (
e.stopPropagation()}>
Editar tarea
setEditTaskDesc(e.target.value)}
placeholder="Descripción"
style={{ padding: "6px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }} />
Fecha inicio
setEditTaskDateStart(e.target.value)}
style={{ padding: "6px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }} />
Fecha vencimiento
setEditTaskDate(e.target.value)}
style={{ padding: "6px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }} />
{!t.taskType || t.taskType === "task" ? (
Prioridad
setEditTaskPriority(e.target.value)}
style={{ padding: "6px 10px", borderRadius: "var(--r-sm)", border: "1px solid var(--border)", background: "var(--bg-base)", color: "var(--text-primary)", fontSize: 13 }}>
Alta
Media
Baja
) : null}
{editTaskDate &&
💡 Guardar con fecha sincronizará automáticamente con Google Calendar
}
setEditingTaskId(null)}>Cancelar
saveEditTask(t)}>
{savingEditTask ? "Guardando..." : "Guardar"}
)}
);
})}
}
{/* ── SECCION 5: BLOQUEOS ───────────────────────────────── */}
Bloqueos / Impedimentos ({project.blockers.filter(b=>!b.resolved).length} activos · {project.blockers.filter(b=>b.resolved).length} resueltos)
{ setShowAddBlocker(v => !v); setBlockerDesc(""); setBlockerTipo("otro"); }}>
{showAddBlocker ? "Cancelar" : "+ Agregar bloqueo"}
{showAddBlocker && (
)}
{project.blockers.length === 0 && !showAddBlocker
? Sin bloqueadores. El proyecto fluye sin problemas.
: <>
{project.blockers.filter(b => !b.resolved).length > 0 && (
{project.blockers.filter(b => !b.resolved).length} bloqueadores activos
{project.blockers.filter(b => !b.resolved).some(b => b.days > 3) && " — ALERTA: hay bloqueos de mas de 3 dias"}
)}
{project.blockers.map(b => {
const bc = PD_BLOCKER_COLOR[b.type] || "var(--fg-subtle)";
return (
{b.resolved ? "V" : "X"}
{b.desc}
{b.type}
{b.createdAt && Creado: {new Date(b.createdAt).toLocaleDateString("es-AR")} }
{b.resolved && b.resolvedAt
? Resuelto: {new Date(b.resolvedAt).toLocaleDateString("es-AR")}
: !b.resolved && b.days > 0 && {b.days} dias sin resolver
}
{!b.resolved && (
onResolveBlocker(project.id, b.id)}>
Marcar como resuelto
)}
);
})}
>
}
{/* ── SECCION 6: TIMELINE ───────────────────────────────── */}
Timeline / Historico
{[["all","Todo"], ["state","Estados"], ["hours","Horas"], ["tasks","Tareas"], ["notes","Notas"]].map(([v,l]) => (
setTlFilter(v)}>{l}
))}
{timelineEvents.length === 0
? Sin actividad registrada en esta categoria.
:
{timelineEvents.map((h, i) => {
const dotColor = h.type === "state" ? "var(--accent)" : h.type === "hours" || h.type === "time" ? "var(--success)" : h.type === "note" ? "var(--warning)" : h.type === "blocker" ? "var(--danger)" : "var(--fg-subtle)";
return (
);
})}
}
{/* ── SECCION 7: CONTEXTO Y RECURSOS ───────────────────── */}
Contexto y Recursos
{/* Links */}
Links y Referencias
{ setShowAddLink(v => !v); setLinkLabel(""); setLinkUrl(""); }}>
{showAddLink ? "Cancelar" : "+ Agregar link"}
{showAddLink && (
)}
{project.githubUrl && (
{I_pd.link ? I_pd.link(14) : null}
)}
{(project.links || []).map((l, i) => (
{I_pd.link ? I_pd.link(14) : null}
{
const updated = (project.links || []).filter((_, j) => j !== i);
await onEditProject(project.id, { links_json: JSON.stringify(updated) });
}}>×
))}
{!project.githubUrl && !(project.links || []).length && !showAddLink && (
Sin links. Hacé click en "+ Agregar link" para añadir referencias.
)}
{/* Google Drive + Calendar */}
{googleEnabled && (
Google
{/* Drive */}
📁
{driveLoading ? "Abriendo..." : driveFolder ? "Ver carpeta en Drive" : "Crear carpeta en Drive"}
{/* Calendar sync */}
{project.deadline ? (
<>
📅
{calendarSyncing ? "Sincronizando..." : calendarEventId ? "Actualizar en Calendar" : "Agregar deadline a Calendar"}
{calendarEventId && (
🗑
)}
>
) : (
Definí un deadline para sincronizar con Calendar
)}
{googleMsg && (
{googleMsg}
)}
)}
{/* Especificaciones tecnicas */}
{project.specsText && (
Especificaciones Tecnicas / Stack
{project.specsText}
)}
{/* Notas, Learnings, Decisiones */}
Notas, Decisiones y Aprendizajes ({project.notes.length})
{project.notes.length === 0
?
Sin notas todavia.
:
{project.notes.slice().reverse().map((n, i) => (
{PD_NOTE_ICON[n.type] || "Nota"}
{n.date}
{n.text}
))}
}
{/* ── SECCION 7.5: ESTRUCTURA DE LA APLICACION ─────────── */}
Estructura de la Aplicación
{/* Texto estructurado */}
Descripción textual
{!editStructureText && (
{ setEditStructureText(true); setStructureText(project.structureText || ""); }}>
{project.structureText ? "Editar" : "+ Agregar texto"}
)}
{editStructureText ? (
) : project.structureText ? (
{project.structureText}
) : (
Sin estructura de texto. Hacé click en "+ Agregar texto" para documentar el árbol de archivos o arquitectura.
)}
{/* Imagen */}
{project.structureImageUrl ? (
{
await onEditProject(project.id, { structure_image_url: null });
}}>
Quitar imagen
) : (
Sin imagen. Podés subir un diagrama, captura de pantalla o esquema de arquitectura.
)}
{/* ── SECCION 8: REGISTRO DE HORAS ─────────────────────── */}
{project.workHours && project.workHours.length > 0 && (
Registro de Horas ({project.workHours.length} entradas · {project.actualHours}h total)
{project.workHours.slice().sort((a, b) => b.date > a.date ? 1 : -1).map((wh, i) => (
{wh.date}
{wh.hours}h
{wh.desc || Sin descripcion }
))}
)}
{/* ── SECCION 9: PRESUPUESTO Y FACTURACIÓN ──────────────── */}
Presupuesto y Facturación
{ setEditBilling(v => !v); setBillingTarifa(String(project.tarifaHora || "")); setBillingPresup(String(project.presupuesto || "")); }}>
{editBilling ? "Cancelar" : (project.tarifaHora || project.presupuesto ? "Editar" : "+ Configurar")}
{editBilling && (
)}
{(project.tarifaHora || project.presupuesto) ? (() => {
const costoReal = (project.tarifaHora || 0) * project.actualHours;
const restante = (project.presupuesto || 0) - costoReal;
const pctPresup = project.presupuesto > 0 ? (costoReal / project.presupuesto) * 100 : null;
return (
{project.tarifaHora &&
}
{project.presupuesto &&
}
{project.tarifaHora &&
100 ? "var(--danger)" : "var(--success)"} large />}
{project.presupuesto && project.tarifaHora && (
= 0 ? "Restante" : "Exceso"} value={`$${Math.abs(restante).toFixed(2)}`}
sub={pctPresup !== null ? `${pctPresup.toFixed(0)}% del presupuesto` : ""}
color={restante >= 0 ? "var(--success)" : "var(--danger)"} />
)}
);
})() : (
Sin configuración de presupuesto. Hacé click en "+ Configurar" para agregar tarifa/hora y presupuesto.
)}
{/* Bottom spacer */}