const { useState: useS, useEffect: useE, useCallback } = React;
const EVENT_ARTIST_ROLES = [
{ id: "acteur", label: "Acteur" },
{ id: "musicien", label: "Musicien" },
{ id: "metteur_en_scene", label: "Metteur en scène" },
{ id: "association", label: "Association" },
{ id: "autre", label: "Autre" },
];
const EVENTS_MAX = 200;
const EVENT_STATUS_LABEL = {
draft: "Brouillon",
scheduled: "À venir",
past: "Passé",
cancelled: "Annulé",
};
function toDatetimeLocalValue(iso) {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function formatEventDateLabel(startsAt, endsAt) {
const start = new Date(startsAt);
if (Number.isNaN(start.getTime())) return "";
const fmt = (d) =>
d.toLocaleString("fr-FR", {
weekday: "short",
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
});
if (!endsAt) return fmt(start);
const end = new Date(endsAt);
if (Number.isNaN(end.getTime())) return fmt(start);
return `${fmt(start)} → ${fmt(end)}`;
}
function emptyEventDraft() {
return {
id: "",
title: "",
startsAt: "",
endsAt: "",
location: "",
shortDescription: "",
posterUrl: "",
sourceUrl: "",
status: "scheduled",
artists: [],
};
}
function eventToDraft(ev) {
return {
id: ev.id,
title: ev.title || "",
startsAt: toDatetimeLocalValue(ev.startsAt),
endsAt: toDatetimeLocalValue(ev.endsAt),
location: ev.location || "",
shortDescription: ev.shortDescription || "",
posterUrl: ev.posterUrl || "",
sourceUrl: ev.sourceUrl || "",
status: ev.status,
artists: (ev.artists || []).map((a) => ({ ...a })),
};
}
function EventsScreen({ initialEvents = [], busy }) {
const [events, setEvents] = useS(() => initialEvents || []);
const [draft, setDraft] = useS(null);
const [loading, setLoading] = useS(false);
const [saving, setSaving] = useS(false);
const [msg, setMsg] = useS("");
const [err, setErr] = useS("");
const [describeBusy, setDescribeBusy] = useS(false);
const [posterBusy, setPosterBusy] = useS(false);
const [importUrl, setImportUrl] = useS("");
const [importText, setImportText] = useS("");
const [importing, setImporting] = useS(false);
const load = useCallback(async () => {
setLoading(true);
setErr("");
try {
const res = await GuyAPI.listEvents();
setEvents(Array.isArray(res?.events) ? res.events : []);
} catch (e) {
setErr(e?.message || "Impossible de charger les événements.");
} finally {
setLoading(false);
}
}, []);
useE(() => {
void load();
}, [load]);
const upcoming = events.filter(
(e) => e.displayStatus === "scheduled" || e.displayStatus === "draft",
);
const past = events.filter((e) => e.displayStatus === "past");
const cancelled = events.filter((e) => e.displayStatus === "cancelled");
function patchDraft(partial) {
setDraft((prev) => (prev ? { ...prev, ...partial } : prev));
}
function openNew() {
setErr("");
setMsg("");
setDraft(emptyEventDraft());
}
function openEdit(ev) {
setErr("");
setMsg("");
setDraft(eventToDraft(ev));
}
async function saveDraft() {
if (!draft) return;
if (!(draft.title || "").trim() || !draft.startsAt) {
setErr("Indiquez au moins un nom et une date de début.");
return;
}
setSaving(true);
setErr("");
setMsg("");
const payload = {
title: draft.title.trim(),
startsAt: draft.startsAt,
endsAt: draft.endsAt || null,
location: draft.location.trim(),
shortDescription: draft.shortDescription.trim(),
posterUrl: draft.posterUrl.trim() || null,
sourceUrl: draft.sourceUrl.trim() || null,
status: draft.status,
artists: (draft.artists || []).filter((a) => (a.name || "").trim()),
};
try {
const res = draft.id
? await GuyAPI.updateEvent(draft.id, payload)
: await GuyAPI.createEvent(payload);
const saved = res?.event;
if (saved) {
setEvents((prev) => {
const others = prev.filter((e) => e.id !== saved.id);
return [...others, saved].sort((a, b) => a.startsAt.localeCompare(b.startsAt));
});
setDraft(eventToDraft(saved));
} else {
await load();
}
setMsg("Événement enregistré.");
} catch (e) {
setErr(e?.message || "Enregistrement impossible.");
} finally {
setSaving(false);
}
}
async function removeDraft() {
if (!draft?.id) {
setDraft(null);
return;
}
if (!window.confirm("Supprimer cet événement ?")) return;
setSaving(true);
try {
await GuyAPI.deleteEvent(draft.id);
setEvents((prev) => prev.filter((e) => e.id !== draft.id));
setDraft(null);
setMsg("Événement supprimé.");
} catch (e) {
setErr(e?.message || "Suppression impossible.");
} finally {
setSaving(false);
}
}
async function describeDraft() {
if (!draft?.id) {
setErr("Enregistrez l’événement avant de générer le descriptif.");
return;
}
setDescribeBusy(true);
setErr("");
try {
const res = await GuyAPI.describeEvent(draft.id);
const saved = res?.event;
if (saved) {
setEvents((prev) => prev.map((e) => (e.id === saved.id ? saved : e)));
setDraft(eventToDraft(saved));
} else if (res?.description) {
patchDraft({ shortDescription: res.description });
}
setMsg("Descriptif rédigé. Relisez-le avant de le laisser tel quel.");
} catch (e) {
setErr(e?.message || "Génération impossible.");
} finally {
setDescribeBusy(false);
}
}
async function onPosterFile(file) {
if (!file) return;
setPosterBusy(true);
setErr("");
try {
const dataUrl = await resizeProductRefFile(file);
const res = await GuyAPI.uploadEventPoster(dataUrl);
if (!res?.url) throw new Error("Hébergement de l’affiche impossible.");
patchDraft({ posterUrl: res.url });
} catch (e) {
setErr(e?.message || "Import de l’affiche impossible.");
} finally {
setPosterBusy(false);
}
}
async function runImport(payload) {
setImporting(true);
setErr("");
setMsg("");
try {
const res = await GuyAPI.importEvents(payload);
const incoming = Array.isArray(res?.events) ? res.events : [];
if (incoming.length) {
setEvents((prev) => {
const byId = new Map(prev.map((e) => [e.id, e]));
for (const ev of incoming) byId.set(ev.id, ev);
return [...byId.values()].sort((a, b) => a.startsAt.localeCompare(b.startsAt));
});
} else {
await load();
}
setMsg(res?.message || "Import terminé.");
if (payload.text) setImportText("");
} catch (e) {
setErr(e?.message || "Import impossible.");
} finally {
setImporting(false);
}
}
async function onImportFile(file) {
if (!file) return;
const text = await file.text();
await runImport({ text, filename: file.name });
}
function addArtist() {
patchDraft({
artists: [...(draft?.artists || []), { name: "", role: "autre" }],
});
}
function patchArtist(i, key, value) {
const next = (draft?.artists || []).map((a, idx) =>
idx === i ? { ...a, [key]: value } : a,
);
patchDraft({ artists: next });
}
function removeArtist(i) {
patchDraft({
artists: (draft?.artists || []).filter((_, idx) => idx !== i),
});
}
function renderGroup(title, list) {
if (!list.length) return null;
return (
{title}
Calendrier interne : Guy s’en sert comme des produits pour les articles et l’autopilote.
{err}
: null} {msg ?{msg}
: null} {loading ?Chargement…
: null} {draft ? (Fichier ICS / CSV, ou l’URL d’un agenda public (OpenAgenda, page OT…). Les dates d’un même titre sont fusionnées.
ou un fichier / un collage ICS · CSV
Aucun événement. Importez un agenda ou ajoutez un rendez-vous local.
) : null} {renderGroup("À venir", upcoming)} {renderGroup("Passés", past)} {renderGroup("Annulés", cancelled)} > )}