- 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
177 lines
10 KiB
HTML
177 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en-ZA">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<meta name="theme-color" content="#6B4FE0" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<meta name="apple-mobile-web-app-title" content="Slim Uiltjies" />
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
<meta name="description" content="Invoice & payment management for Slim Uiltjies AfterCare — a safe place to learn, grow and shine." />
|
|
|
|
<title>Slim Uiltjies AfterCare · Invoices</title>
|
|
|
|
<link rel="manifest" href="manifest.webmanifest" />
|
|
<link rel="icon" type="image/svg+xml" href="icons/icon.svg" />
|
|
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32.png" />
|
|
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png" />
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link href="https://fonts.googleapis.com/css2?family=Grandstander:wght@400;500;600;700;800&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet" />
|
|
|
|
<link rel="stylesheet" href="css/styles.css" />
|
|
</head>
|
|
<body>
|
|
<!-- Splash / loading screen -->
|
|
<div id="splash" class="splash">
|
|
<div class="splash-inner">
|
|
<div class="splash-owl" id="splashOwl"></div>
|
|
<h1 class="splash-title">Slim Uiltjies</h1>
|
|
<p class="splash-sub">AfterCare · Invoices</p>
|
|
<div class="splash-spinner"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PIN login screen -->
|
|
<div id="loginScreen" class="login-screen hidden">
|
|
<div class="login-card">
|
|
<div class="login-owl" id="loginOwl"></div>
|
|
<h1 class="login-title">Slim Uiltjies</h1>
|
|
<p class="login-sub">AfterCare · Invoices</p>
|
|
<p class="login-prompt">Enter your PIN to continue</p>
|
|
<input
|
|
type="password"
|
|
inputmode="numeric"
|
|
pattern="[0-9]*"
|
|
id="pinInput"
|
|
class="pin-input"
|
|
placeholder="••••••"
|
|
maxlength="8"
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
/>
|
|
<button type="button" class="btn btn-primary btn-lg btn-block" id="loginBtn">Unlock</button>
|
|
<p class="pin-hint" id="pinHint">Test PIN: <strong id="pinDisplay">••••••</strong> <button type="button" class="pin-reveal" id="pinReveal" aria-label="Show PIN">👁</button></p>
|
|
<p class="server-hint" id="serverHint">Checking server…</p>
|
|
<p class="pin-error" id="pinError">Wrong PIN. Try again.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App shell -->
|
|
<div id="app" class="app hidden">
|
|
<!-- Header -->
|
|
<header class="app-header">
|
|
<button class="btn btn-icon btn-ghost" id="backBtn" aria-label="Back" title="Back">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
</button>
|
|
<div class="brand">
|
|
<div class="brand-mark" id="brandOwl"></div>
|
|
<div class="brand-text">
|
|
<div class="brand-name">Slim Uiltjies</div>
|
|
<div class="brand-sub" id="headerSubtitle">Invoices</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-actions" id="headerActions"></div>
|
|
<div class="header-right">
|
|
<span class="sync-status" id="syncStatus" title="Sync status">🟢</span>
|
|
<button class="btn btn-icon btn-ghost" id="menuBtn" aria-label="Menu" title="Menu">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Desktop sidebar (hidden on mobile) -->
|
|
<aside class="sidebar" id="sidebar">
|
|
<nav class="sidebar-nav">
|
|
<a href="#home" class="sidebar-link" data-tab="home">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
<a href="#invoices" class="sidebar-link" data-tab="invoices">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>
|
|
<span>Invoices</span>
|
|
</a>
|
|
<a href="#parents" class="sidebar-link" data-tab="parents">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
<span>Parents</span>
|
|
</a>
|
|
<a href="#children" class="sidebar-link" data-tab="children">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>
|
|
<span>Children</span>
|
|
</a>
|
|
<a href="#settings" class="sidebar-link" data-tab="settings">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
<span>Settings</span>
|
|
</a>
|
|
</nav>
|
|
<div class="sidebar-foot">
|
|
<div class="sync-indicator" id="syncIndicator">🟢 Connected</div>
|
|
<button class="btn btn-danger btn-block btn-sm" id="sidebarLogout">🔒 Log out</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main view container -->
|
|
<main id="view" class="view"></main>
|
|
|
|
<!-- Bottom tab bar (mobile only) -->
|
|
<nav class="tabbar" id="tabbar">
|
|
<a href="#home" class="tab" data-tab="home">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
|
<span>Home</span>
|
|
</a>
|
|
<a href="#invoices" class="tab" data-tab="invoices">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>
|
|
<span>Invoices</span>
|
|
</a>
|
|
<a href="#parents" class="tab" data-tab="parents">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
<span>Parents</span>
|
|
</a>
|
|
<a href="#children" class="tab" data-tab="children">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>
|
|
<span>Children</span>
|
|
</a>
|
|
<a href="#settings" class="tab" data-tab="settings">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
<span>Settings</span>
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Modal container -->
|
|
<div id="modalRoot"></div>
|
|
|
|
<!-- Toast container -->
|
|
<div id="toastRoot" class="toast-root"></div>
|
|
|
|
<!-- Hidden invoice render container (used for PDF export) -->
|
|
<div id="renderRoot" class="render-root" aria-hidden="true"></div>
|
|
|
|
<!-- Vendored libraries -->
|
|
<script src="vendor/jspdf.umd.min.js"></script>
|
|
<script src="vendor/html2canvas.min.js"></script>
|
|
|
|
<!-- App scripts (modules) -->
|
|
<script src="js/icons.js"></script>
|
|
<script src="js/db.js"></script>
|
|
<script src="js/seed.js"></script>
|
|
<script src="js/api.js"></script>
|
|
<script src="js/share.js"></script>
|
|
<script src="js/pdf.js"></script>
|
|
<script src="js/invoice.js"></script>
|
|
<script src="js/views.js"></script>
|
|
<script src="js/app.js"></script>
|
|
|
|
<script>
|
|
// Register service worker
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('sw.js').catch(() => {});
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|