KLEINUILTIES-WEBAPP/js/db.js
Slim Uiltjies App 1939bf2ef4 Initial commit: Slim Uiltjies AfterCare invoice system
- Progressive Web App with Express + JSON backend
- PIN-based auth, multi-device sync
- Invoice + Statement PDF generation (branded, Grandstander + Nunito)
- Mobile + Desktop responsive layouts
- n8n backup integration
- Custom logo upload
- Settings for business info, fees, banking
- Bulk invoice generation
- Web Share API integration
2026-08-08 09:25:17 +02:00

205 lines
6.8 KiB
JavaScript

/* 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
};
})();