/* Slim Uiltjies — IndexedDB layer Vanilla IndexedDB. Stores: - business (single object) - parents (id, name, phone, email, address, notes, createdAt) - children (id, parentId, name, plan, monthlyFee, startDate, endDate, notes) - invoices (id, childId, parentId, period YYYY-MM, items[], totals{}, status, issueDate, dueDate, notes) - payments (id, invoiceId, date, amount, method, receivedBy, notes) - meta (seeded flag, version) */ (function () { 'use strict'; const DB_NAME = 'slimuiltjies'; const DB_VERSION = 1; const STORES = ['business', 'parents', 'children', 'invoices', 'payments', 'meta']; let _db = null; function open() { if (_db) return Promise.resolve(_db); return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (e) => { const db = e.target.result; STORES.forEach((s) => { if (!db.objectStoreNames.contains(s)) { const store = db.createObjectStore(s, { keyPath: 'id', autoIncrement: s !== 'business' && s !== 'meta' }); if (s === 'children') store.createIndex('parentId', 'parentId'); if (s === 'invoices') { store.createIndex('childId', 'childId'); store.createIndex('parentId', 'parentId'); store.createIndex('period', 'period'); store.createIndex('status', 'status'); } if (s === 'payments') store.createIndex('invoiceId', 'invoiceId'); if (s === 'parents') store.createIndex('name', 'name'); } }); }; req.onsuccess = () => { _db = req.result; resolve(_db); }; req.onerror = () => reject(req.error); }); } function tx(store, mode = 'readonly') { return open().then((db) => db.transaction(store, mode).objectStore(store)); } function p(req) { return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function add(store, value) { const t = await tx(store, 'readwrite'); const r = await p(t.add(value)); scheduleSync(); return r; } async function put(store, value) { const t = await tx(store, 'readwrite'); const r = await p(t.put(value)); scheduleSync(); return r; } async function get(store, id) { const t = await tx(store); return p(t.get(id)); } async function getAll(store) { const t = await tx(store); return p(t.getAll()); } async function del(store, id) { const t = await tx(store, 'readwrite'); const r = await p(t.delete(id)); scheduleSync(); return r; } async function clear(store) { const t = await tx(store, 'readwrite'); const r = await p(t.clear()); scheduleSync(); return r; } function scheduleSync() { try { if (window.SUAPI && window.SUAPI.schedulePush) window.SUAPI.schedulePush(); } catch {} } async function byIndex(store, indexName, value) { const t = await tx(store); return p(t.index(indexName).getAll(value)); } // Convenience: business info is a single object with id='business' async function getBusiness() { return get('business', 'business'); } async function saveBusiness(b) { return put('business', { ...b, id: 'business' }); } async function uploadLogo(dataUrl) { const b = await getBusiness(); const updated = { ...(b || { id: 'business', name: '' }), logoDataUrl: dataUrl }; return saveBusiness(updated); } async function removeLogo() { const b = await getBusiness(); if (!b) return; const updated = { ...b }; delete updated.logoDataUrl; return saveBusiness(updated); } async function getMeta(key) { const r = await get('meta', key); return r ? r.value : null; } async function setMeta(key, value) { return put('meta', { id: key, value }); } // Export / import (full backup as JSON) async function exportAll() { const out = {}; for (const s of STORES) out[s] = await getAll(s); return out; } async function importAll(data, { replace = false } = {}) { for (const s of STORES) { if (!data[s]) continue; if (replace) await clear(s); for (const rec of data[s]) { if (s === 'business') { await put(s, { ...rec, id: 'business' }); } else if (s === 'meta') { await put(s, rec); } else { await put(s, rec); } } } } // High-level helpers async function getParent(id) { return get('parents', Number(id)); } async function getChild(id) { return get('children', Number(id)); } async function getChildrenByParent(parentId) { return byIndex('children', 'parentId', Number(parentId)); } async function getInvoicesByParent(parentId) { return byIndex('invoices', 'parentId', Number(parentId)); } async function getInvoicesByChild(childId) { return byIndex('invoices', 'childId', Number(childId)); } async function getInvoicesByPeriod(period) { return byIndex('invoices', 'period', period); } async function getPaymentsByInvoice(invoiceId) { return byIndex('payments', 'invoiceId', Number(invoiceId)); } async function getAllPayments() { return getAll('payments'); } // Aggregate for dashboards async function dashboardStats() { const [parents, children, invoices, payments] = await Promise.all([ getAll('parents'), getAll('children'), getAll('invoices'), getAll('payments') ]); const now = new Date(); const thisPeriod = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; const totalPaid = payments.reduce((s, p) => s + (Number(p.amount) || 0), 0); const monthPayments = payments .filter((p) => (p.date || '').startsWith(thisPeriod)) .reduce((s, p) => s + (Number(p.amount) || 0), 0); const outstanding = invoices .filter((i) => i.status !== 'paid') .reduce((s, i) => s + (Number(i.balance) || 0), 0); const monthInvoices = invoices.filter((i) => i.period === thisPeriod); return { parents: parents.length, children: children.length, invoices: invoices.length, monthInvoices: monthInvoices.length, paid: invoices.filter((i) => i.status === 'paid').length, unpaid: invoices.filter((i) => i.status === 'unpaid').length, partial: invoices.filter((i) => i.status === 'partial').length, totalReceived: totalPaid, receivedThisMonth: monthPayments, outstanding, }; } window.SUDB = { open, add, put, get, getAll, del, clear, byIndex, getBusiness, saveBusiness, uploadLogo, removeLogo, getMeta, setMeta, getParent, getChild, getChildrenByParent, getInvoicesByParent, getInvoicesByChild, getInvoicesByPeriod, getPaymentsByInvoice, getAllPayments, dashboardStats, exportAll, importAll }; })();