/* 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" : ""}
{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 });