/* store.jsx — model + reducer. ADAPTADO PARA API. * * Mudanças vs. versão localStorage: * - Nada de SEED_TASKS local: o estado começa vazio e é populado pela API. * - reducer continua sendo a fonte de UI state, mas todas as mutações * reais (CRUD) passam por window.Api e voltam via REPLACE_TASKS / * UPDATE_TASK_FROM_SERVER. * - localStorage só guarda preferências leves (mute, tweaks). */ const uid = () => Math.random().toString(36).slice(2, 9); const todayStr = () => { const d = new Date(); return `${String(d.getDate()).padStart(2,"0")}/${String(d.getMonth()+1).padStart(2,"0")}/${d.getFullYear()}`; }; const dayKey = (d = new Date()) => `${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}`; const CATEGORIES = { bug: { label: "Bug", color: "var(--c-bug)", bg: "var(--c-bug-bg)", icon: "Bug" }, projeto: { label: "Projeto", color: "var(--accent)", bg: "rgba(var(--accent-rgb),0.14)", icon: "Folder" }, feature: { label: "Feature", color: "var(--c-feature)", bg: "var(--c-feature-bg)", icon: "Bolt" }, melhoria: { label: "Melhoria", color: "var(--c-melhoria)", bg: "var(--c-melhoria-bg)", icon: "TrendUp" }, }; const PRIORITIES = { baixa: { label: "Baixa", color: "var(--c-baixa)", bg: "var(--c-baixa-bg)" }, alta: { label: "Alta", color: "var(--c-alta)", bg: "var(--c-alta-bg)" }, }; const COLUMNS = [ { id: "todo", title: "A Fazer", dot: "var(--txt-3)" }, { id: "doing", title: "Fazendo", dot: "var(--accent)" }, { id: "done", title: "Concluído", dot: "var(--c-melhoria)" }, ]; const REINFORCE = [ { e: "🔥", t: "Mandou bem!" }, { e: "⚡", t: "Tá voando hoje!" }, { e: "💪", t: "Mais uma fora!" }, { e: "🚀", t: "Imparável!" }, { e: "🎯", t: "Na mosca!" }, { e: "✨", t: "Caprichou!" }, { e: "🏆", t: "Lendário!" }, { e: "😎", t: "Suave demais!" }, ]; const xpForTask = (t) => { let base = { baixa: 14, alta: 30 }[t.priority] ?? 18; if (t.category === "bug") base += 6; return base; }; const levelNeed = (lvl) => 90 + lvl * 50; function levelInfo(xp) { let lvl = 1, rem = xp; while (rem >= levelNeed(lvl)) { rem -= levelNeed(lvl); lvl++; } const need = levelNeed(lvl); return { level: lvl, into: rem, need, pct: Math.round((rem / need) * 100) }; } const taskProgress = (t) => { if (t.checklist && t.checklist.length) return Math.round((t.checklist.filter(c => c.done).length / t.checklist.length) * 100); return t.progress ?? 0; }; /* ---------- shape normal do task vindo do server → shape de UI ---------- */ function normalizeTask(t) { return { id: t.id, title: t.title, desc: t.description || "", status: t.status, category: t.category, priority: t.priority, progress: Number(t.progress) || 0, checklist: (t.checklist || []).map(c => ({ id: c.id, text: c.text, done: !!c.done })), images: t.images || [], videos: t.videos || [], attachments: (t.images || []).length, thumb: t.thumb || null, createdBy: t.creator_name || "—", createdAt: t.created_at ? t.created_at.slice(0,10).split("-").reverse().join("/") : todayStr(), assignedTo: t.assigned_to || null, assigneeName: t.assignee_name || null, }; } function defaultState() { return { loading: true, loggedIn: false, user: null, tasks: [], game: { xp: 0, streak: 1, todayCount: 0, totalDone: 0, weekCount: 0 }, }; } function reducer(state, action) { switch (action.type) { case "BOOT": return { ...state, loading: false, loggedIn: !!action.user, user: action.user || null }; case "SET_USER": return { ...state, user: action.user }; case "SET_TASKS": return { ...state, tasks: (action.tasks || []).map(normalizeTask) }; case "UPSERT_TASK": { const t = normalizeTask(action.task); const idx = state.tasks.findIndex(x => x.id === t.id); const tasks = idx >= 0 ? state.tasks.map((x,i) => i===idx ? t : x) : [t, ...state.tasks]; return { ...state, tasks }; } case "REMOVE_TASK": return { ...state, tasks: state.tasks.filter(t => t.id !== action.id) }; case "SET_GAME": return { ...state, game: { ...state.game, ...action.game } }; case "LOGIN": return { ...state, loggedIn: true, user: action.user }; case "LOGOUT": return { ...defaultState(), loading: false }; default: return state; } } // XP local (efeito visual imediato; backend ainda não persiste XP — pode ser feito depois) function applyLocalXp(state, delta) { return { ...state, game: { ...state.game, xp: Math.max(0, (state.game.xp || 0) + delta) } }; } window.Store = { uid, todayStr, dayKey, CATEGORIES, PRIORITIES, COLUMNS, REINFORCE, xpForTask, levelInfo, levelNeed, taskProgress, reducer, defaultState, normalizeTask, applyLocalXp, };