// Store: state, persistence, helpers const TODAY = new Date(); const STATES = { todo: { label: "À faire", color: "var(--todo)", bg: "var(--todo-bg)", icon: "circle" }, progress: { label: "En cours", color: "var(--progress)", bg: "var(--progress-bg)", icon: "halfcircle" }, done: { label: "Fait", color: "var(--done)", bg: "var(--done-bg)", icon: "check" }, postponed: { label: "Reportée", color: "var(--postpone)", bg: "var(--postpone-bg)", icon: "arrow" }, cancelled: { label: "Annulée", color: "var(--cancel)", bg: "var(--cancel-bg)", icon: "cross" }, }; const PRIORITIES = { low: { label: "Faible", color: "var(--prio-low)" }, mid: { label: "Moyenne", color: "var(--prio-mid)" }, high: { label: "Élevée", color: "var(--prio-high)" }, }; const KINDS = { task: { label: "Tâche", color: "var(--terracotta)", bg: "var(--terracotta-tint)" }, birthday: { label: "Anniversaire", color: "var(--birthday)", bg: "var(--birthday-bg)" }, appointment: { label: "Rendez-vous", color: "var(--event)", bg: "var(--event-bg)" }, family: { label: "Famille", color: "#8E6A4A", bg: "#F0E4D4" }, event: { label: "Événement", color: "#5E7A8C", bg: "#DCE6EC" }, }; // --- date helpers --- const pad = (n) => String(n).padStart(2, "0"); const ymd = (d) => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; const fromYmd = (s) => { const [y,m,d] = s.split("-").map(Number); return new Date(y, m-1, d); }; const sameDay = (a, b) => a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); const addDays = (d, n) => { const x = new Date(d); x.setDate(x.getDate()+n); return x; }; const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1); const endOfMonth = (d) => new Date(d.getFullYear(), d.getMonth()+1, 0); const isBefore = (a, b) => ymd(a) < ymd(b); const isAfter = (a, b) => ymd(a) > ymd(b); const MONTHS = ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"]; const DAYS_LONG = ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"]; const DAYS_SHORT = ["dim.","lun.","mar.","mer.","jeu.","ven.","sam."]; const DAYS_ONE = ["D","L","M","M","J","V","S"]; const fmtFull = (d) => `${DAYS_LONG[d.getDay()]} ${d.getDate()} ${MONTHS[d.getMonth()]} ${d.getFullYear()}`; const fmtShort = (d) => `${d.getDate()} ${MONTHS[d.getMonth()].slice(0,3)}`; const fmtRel = (d) => { const diff = Math.round((fromYmd(ymd(d)) - fromYmd(ymd(TODAY))) / 86400000); if (diff === 0) return "Aujourd'hui"; if (diff === 1) return "Demain"; if (diff === -1) return "Hier"; if (diff > 1 && diff < 7) return `Dans ${diff} j`; if (diff < 0 && diff > -7) return `Il y a ${-diff} j`; return fmtShort(d); }; // --- recurrence --- // recurrence: { kind: 'none'|'daily'|'weekly'|'monthly'|'custom', days?: [...ymd], weekday?: 0..6 } function occursOn(item, date) { const r = item.recurrence || { kind: "none" }; const start = fromYmd(item.date); if (isBefore(date, start)) return false; if (r.kind === "none") return sameDay(date, start); if (r.kind === "daily") return true; if (r.kind === "weekly") return date.getDay() === start.getDay(); if (r.kind === "monthly") return date.getDate() === start.getDate(); if (r.kind === "yearly") return date.getDate() === start.getDate() && date.getMonth() === start.getMonth(); if (r.kind === "custom") return (r.days || []).includes(ymd(date)); return false; } // Returns the per-day status for an item on a given date (overrides take precedence) function statusOn(item, date) { const k = ymd(date); if (item.overrides && item.overrides[k]) return item.overrides[k]; // For non-recurring items, default to the item's status if ((item.recurrence?.kind || "none") === "none") return item.status || "todo"; return "todo"; // recurring items reset to "todo" each day } // --- seed data (empty — no defaults) --- const seed = () => []; // --- store hook --- function useStore() { const [items, setItems] = React.useState(() => { try { const raw = localStorage.getItem("maisonnee:items"); if (raw) return JSON.parse(raw); } catch (e) {} return seed(); }); React.useEffect(() => { try { localStorage.setItem("maisonnee:items", JSON.stringify(items)); } catch (e) {} }, [items]); const upsert = (item) => { setItems(prev => { const i = prev.findIndex(x => x.id === item.id); if (i === -1) return [...prev, { overrides: {}, ...item }]; const copy = [...prev]; copy[i] = { ...prev[i], ...item }; return copy; }); }; const remove = (id) => setItems(prev => prev.filter(x => x.id !== id)); const setStatusOn = (id, dateStr, status) => { setItems(prev => prev.map(x => { if (x.id !== id) return x; // For non-recurring items, also update top-level status if ((x.recurrence?.kind || "none") === "none") { if (status === "not_today") { // reschedule to next day return { ...x, date: ymd(addDays(fromYmd(x.date), 1)) }; } return { ...x, status }; } // Recurring: store an override for that day if (status === "not_today") { // mark today cancelled-style and add a one-off task tomorrow? Easier: add a custom day. const next = ymd(addDays(fromYmd(dateStr), 1)); const newOverrides = { ...(x.overrides || {}), [dateStr]: "cancelled" }; // Add tomorrow as a one-shot occurrence via recurrence custom merge: // simplest: keep recurrence as-is; we represent "moved to tomorrow" with an override on tomorrow = "todo" // and a "ghost" via extraDays const extraDays = Array.from(new Set([...(x.extraDays||[]), next])); return { ...x, overrides: newOverrides, extraDays }; } const newOverrides = { ...(x.overrides || {}), [dateStr]: status }; return { ...x, overrides: newOverrides }; })); }; return { items, setItems, upsert, remove, setStatusOn }; } // --- queries --- function itemsOnDate(items, date) { const k = ymd(date); return items.filter(it => occursOn(it, date) || (it.extraDays || []).includes(k)); } // Expose to window for cross-script access Object.assign(window, { TODAY, STATES, PRIORITIES, KINDS, ymd, fromYmd, sameDay, addDays, startOfMonth, endOfMonth, isBefore, isAfter, MONTHS, DAYS_LONG, DAYS_SHORT, DAYS_ONE, fmtFull, fmtShort, fmtRel, occursOn, statusOn, useStore, itemsOnDate, seed, });