/* Tab: Planning — dos listas de tareas (Álvaro a la izquierda, José a la derecha). Compartido por todo el equipo (no depende de quién tenga la sesión iniciada): crear tareas, marcarlas como completadas, y asignarles fecha y canal de YouTube. */ const PLANNING_PEOPLE = [ { id: "alvaro", label: "Álvaro" }, { id: "jose", label: "José" }, ]; function PlanningChannelChip({ ch, size = 16 }) { if (!ch) return null; const src = ch.logo || ch.avatarUrl || null; return ( {src ? : ch.initials} ); } function _planningFmtDate(iso) { if (!iso) return null; const d = new Date(iso + "T00:00:00"); if (isNaN(d.getTime())) return null; const today = new Date(); today.setHours(0, 0, 0, 0); const diffDays = Math.round((d - today) / 86400000); if (diffDays === 0) return "Hoy"; if (diffDays === 1) return "Mañana"; if (diffDays === -1) return "Ayer"; return d.toLocaleDateString("es-ES", { day: "2-digit", month: "short" }); } const PLANNING_BUCKETS = [ { id: "past", label: "Pasado" }, { id: "today", label: "Hoy" }, { id: "tomorrow", label: "Mañana" }, { id: "future", label: "En futuro" }, { id: "none", label: "Sin fecha" }, ]; function _planningBucket(task) { if (!task.due_date) return "none"; const d = new Date(task.due_date + "T00:00:00"); if (isNaN(d.getTime())) return "none"; const today = new Date(); today.setHours(0, 0, 0, 0); const diffDays = Math.round((d - today) / 86400000); if (diffDays < 0) return "past"; if (diffDays === 0) return "today"; if (diffDays === 1) return "tomorrow"; return "future"; } function PlanningTaskRow({ task, channels, onToggle, onDelete, onUpdate }) { const [menuOpen, setMenuOpen] = React.useState(null); // "channel" | "date" | null const ch = channels.find((c) => c.id === task.channel_id) || null; const dateLabel = _planningFmtDate(task.due_date); const today = new Date(); today.setHours(0, 0, 0, 0); const isOverdue = !task.completed && task.due_date && new Date(task.due_date + "T00:00:00") < today; React.useEffect(() => { if (!menuOpen) return; function onDocClick() { setMenuOpen(null); } document.addEventListener("click", onDocClick); return () => document.removeEventListener("click", onDocClick); }, [menuOpen]); return (
onToggle(task)} style={{ width: 15, height: 15, flexShrink: 0, cursor: "pointer", marginTop: 1 }}/>
{task.title}
{menuOpen === "channel" && (
e.stopPropagation()} style={{ left: 0, top: 24, minWidth: 180, maxHeight: 240, overflowY: "auto", padding: 4, }}> {channels.map((c) => ( ))}
)}
{menuOpen === "date" && (
e.stopPropagation()} style={{ left: 0, top: 24, padding: 8, minWidth: 170, }}> { onUpdate(task, { due_date: e.target.value }); setMenuOpen(null); }}/> {task.due_date && ( )}
)}
); } function PlanningColumn({ person, tasks, channels, onCreate, onToggle, onDelete, onUpdate }) { const [title, setTitle] = React.useState(""); const [channelId, setChannelId] = React.useState(""); const [dueDate, setDueDate] = React.useState(""); const [creating, setCreating] = React.useState(false); const pending = tasks.filter((t) => !t.completed); async function submit(e) { e.preventDefault(); if (!title.trim() || creating) return; setCreating(true); await onCreate({ assignee: person.id, title: title.trim(), channel_id: channelId || null, due_date: dueDate || null }); setTitle(""); setChannelId(""); setDueDate(""); setCreating(false); } return (

{person.label}

{pending.length} pendiente{pending.length !== 1 ? "s" : ""}
setTitle(e.target.value)} placeholder={`Nueva tarea para ${person.label}…`} style={{ fontSize: 13 }}/>
setDueDate(e.target.value)} style={{ flex: 1, fontSize: 12.5 }}/>
{tasks.length === 0 && (
Sin tareas todavía.
)} {PLANNING_BUCKETS.map((bucket, i) => { const bucketTasks = tasks.filter((t) => _planningBucket(t) === bucket.id); if (bucketTasks.length === 0) return null; return (
{bucket.label} ({bucketTasks.length})
{bucketTasks.map((t) => ( ))}
); })}
); } function PlanningTab({ channels }) { const [tasks, setTasks] = React.useState([]); const [loading, setLoading] = React.useState(true); const activeChannels = (channels || []).filter((c) => c.active !== false); function loadTasks() { fetch("/api/planning/tasks", { credentials: "same-origin" }) .then((r) => r.json()) .then((d) => setTasks(d.tasks || [])) .catch(() => {}) .finally(() => setLoading(false)); } React.useEffect(() => { loadTasks(); }, []); async function createTask(payload) { try { const r = await fetch("/api/planning/tasks", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const d = await r.json(); if (r.ok && d.success) setTasks((prev) => [...prev, d.task]); } catch { /* ignore */ } } async function toggleTask(task) { const nextCompleted = !task.completed; setTasks((prev) => prev.map((t) => t.id === task.id ? { ...t, completed: nextCompleted } : t)); try { await fetch(`/api/planning/tasks/${task.id}`, { method: "PUT", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed: nextCompleted }), }); } catch { /* ignore */ } } async function updateTask(task, patch) { setTasks((prev) => prev.map((t) => t.id === task.id ? { ...t, ...patch } : t)); try { await fetch(`/api/planning/tasks/${task.id}`, { method: "PUT", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); } catch { /* ignore */ } } async function deleteTask(task) { setTasks((prev) => prev.filter((t) => t.id !== task.id)); try { await fetch(`/api/planning/tasks/${task.id}`, { method: "DELETE", credentials: "same-origin" }); } catch { /* ignore */ } } return (
{loading ? (
Cargando…
) : (
{PLANNING_PEOPLE.map((person) => ( t.assignee === person.id)} channels={activeChannels} onCreate={createTask} onToggle={toggleTask} onDelete={deleteTask} onUpdate={updateTask}/> ))}
)}
); } Object.assign(window, { PlanningTab });