import { createContext, useContext, useState, useMemo, useRef } from "react"; import JSZip from "jszip"; import { INITIAL_DATA } from "../constants/index.js"; import { migrateData } from "../utils/storage.js"; import { useTheme } from "../hooks/useTheme.js"; const AppContext = createContext(null); // ── API helpers ─────────────────────────────────────────────────────────────── const getToken = () => localStorage.getItem("vf_token"); const authHdrs = () => ({ "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` }); async function apiFetch(path, options = {}) { const res = await fetch(`/api${path}`, { ...options, headers: { ...authHdrs(), ...(options.headers || {}) } }); if (!res.ok) throw new Error(`API ${path} → ${res.status}`); return res.json(); } // ── Provider ────────────────────────────────────────────────────────────────── export function AppProvider({ children }) { // ── Auth ────────────────────────────────────────────────────── const [loggedIn, setLoggedIn] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(false); const [avatar, setAvatarRaw] = useState(null); const setAvatar = async (base64) => { setAvatarRaw(base64); try { await apiFetch("/auth/me/avatar", { method: "PATCH", body: JSON.stringify({ avatar: base64 ?? null }) }); } catch (err) { console.error("Avatar opslaan mislukt:", err); } }; // ── UI ──────────────────────────────────────────────────────── const [darkMode, setDarkMode] = useState(true); const [locked, setLocked] = useState(false); const [tab, setTab] = useState(0); const [menuOpen, setMenuOpen] = useState(false); const [showUsers, setShowUsers] = useState(false); // ── Financiële data ─────────────────────────────────────────── const [data, setDataRaw] = useState(INITIAL_DATA); const [evRows, setEvRowsRaw] = useState([]); const [evVoortgangHistory, setEvVoortgangHistoryRaw] = useState([]); const saveTimer = useRef(null); const saveToApi = (newData, newEvRows, newEvVH) => { if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(async () => { try { await apiFetch("/data", { method: "PUT", body: JSON.stringify({ ...newData, evVoortgang: newEvRows, evVoortgangHistory: newEvVH }), }); } catch (err) { console.error("Opslaan mislukt:", err); } }, 800); }; const setData = (next) => { const d = typeof next === "function" ? next(data) : next; setDataRaw(d); saveToApi(d, evRows, evVoortgangHistory); }; const setEvRows = (rows) => { const r = typeof rows === "function" ? rows(evRows) : rows; setEvRowsRaw(r); saveToApi(data, r, evVoortgangHistory); }; const setEvVoortgangHistory = (history) => { const h = typeof history === "function" ? history(evVoortgangHistory) : history; setEvVoortgangHistoryRaw(h); saveToApi(data, evRows, h); }; // ── Gebruikers ──────────────────────────────────────────────── const [users, setUsersRaw] = useState(() => { try { return JSON.parse(localStorage.getItem("wbjw_users")) || []; } catch { return []; } }); const setUsers = (u) => { localStorage.setItem("wbjw_users", JSON.stringify(u)); setUsersRaw(u); }; // ── Thema ───────────────────────────────────────────────────── const T = useTheme(darkMode); // ── Afgeleide waarden ───────────────────────────────────────── const ev = data.eigenVermogen; const schuld = data.schuld; const cats = ev?.categories || []; const schuldBlokken = schuld?.blokken || []; const schuldHistory = schuld?.schuldHistory || []; const totEV = useMemo( () => cats.reduce((a, c) => a + c.items.reduce((b, i) => b + (i.waarde || 0), 0), 0), [cats] ); const totSchuld = useMemo( () => schuldBlokken.reduce((a, b) => a + b.items.reduce((c, i) => c + (i.waarde || 0), 0), 0), [schuldBlokken] ); const catTotals = useMemo( () => cats.map((c) => ({ ...c, tot: c.items.reduce((a, i) => a + (i.waarde || 0), 0) })), [cats] ); // ── Auth handlers ───────────────────────────────────────────── const login = async (user) => { setCurrentUser(user); setLoggedIn(true); setAvatarRaw(user.avatar ?? null); setLoading(true); try { const apiData = await apiFetch("/data"); const migrated = migrateData({ eigenVermogen: apiData.eigenVermogen, schuld: apiData.schuld, }); setDataRaw(migrated); setEvRowsRaw(apiData.evVoortgang || []); setEvVoortgangHistoryRaw(apiData.evVoortgangHistory || []); } catch (err) { console.error("Data laden mislukt:", err); } finally { setLoading(false); } }; const updateProfile = async (naam, email) => { const res = await apiFetch("/auth/me", { method: "PATCH", body: JSON.stringify({ naam, email }) }); localStorage.setItem("vf_token", res.token); setCurrentUser(res.user); }; const changePassword = async (huidig, nieuw) => { await apiFetch("/auth/me/password", { method: "POST", body: JSON.stringify({ huidig, nieuw }) }); }; const logout = () => { setCurrentUser(null); setAvatarRaw(null); setLoggedIn(false); setDataRaw(INITIAL_DATA); setEvRowsRaw([]); setEvVoortgangHistoryRaw([]); localStorage.removeItem("vf_token"); if (saveTimer.current) clearTimeout(saveTimer.current); }; // ── Backup / restore ────────────────────────────────────────── const doBackup = async () => { const zip = new JSZip(); const datum = new Date().toISOString().slice(0, 10); zip.file("data.json", JSON.stringify({ ...data, evVoortgang: evRows, evVoortgangHistory }, null, 2)); zip.file("meta.json", JSON.stringify({ versie: 1, datum, app: "vibefinance", gebruiker: currentUser?.email }, null, 2)); const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `vibefinance_backup_${datum}.zip`; a.click(); URL.revokeObjectURL(url); }; const doRestore = () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const zip = await JSZip.loadAsync(file); const entry = zip.file("data.json"); if (!entry) throw new Error("Geen data.json gevonden in backup."); const parsed = JSON.parse(await entry.async("string")); const migrated = migrateData({ eigenVermogen: parsed.eigenVermogen, schuld: parsed.schuld }); const newEvRows = parsed.evVoortgang || []; const newEvVH = parsed.evVoortgangHistory || []; setDataRaw(migrated); setEvRowsRaw(newEvRows); setEvVoortgangHistoryRaw(newEvVH); await apiFetch("/data", { method: "PUT", body: JSON.stringify({ ...migrated, evVoortgang: newEvRows, evVoortgangHistory: newEvVH }), }); } catch (err) { alert("Ongeldig of beschadigd backup-bestand: " + err.message); } }; input.click(); }; return ( {children} ); } export const useApp = () => { const ctx = useContext(AppContext); if (!ctx) throw new Error("useApp must be used inside "); return ctx; };