// invoice app — sidebar shell, invoice list, invoice detail
const { Icon, Button, StatusBadge, Avatar, kr, useState } = window;
function Sidebar() {
const nav = [
{ icon: 'file', label: 'Invoices', active: true },
{ icon: 'users', label: 'Clients' },
{ icon: 'chart', label: 'Reports' },
{ icon: 'settings', label: 'Settings' },
];
const apps = [['inv', 'invoice', true], ['ts', 'timesheet', false], ['re', 'reimburse', false]];
return (
);
}
function InvoiceList({ onOpen }) {
const { invoices } = window.INVOICE_DATA;
const [q, setQ] = useState('');
const list = invoices.filter(i => (i.id + i.client).toLowerCase().includes(q.toLowerCase()));
const outstanding = invoices.filter(i => i.status === 'sent' || i.status === 'overdue').reduce((s, i) => s + i.amount, 0);
const paid = invoices.filter(i => i.status === 'paid').reduce((s, i) => s + i.amount, 0);
return (
Invoices
}>New invoice
Outstandingkr {kr(outstanding)}
Paid this periodkr {kr(paid)}
Open invoices{invoices.filter(i => i.status !== 'paid' && i.status !== 'draft').length}
setQ(e.target.value)} placeholder="Search invoices or clients…" />
| Invoice | Client | Status | Due | Amount | |
{list.map(inv => (
onOpen(inv)}>
| {inv.id} |
{inv.client} |
|
{inv.due} |
kr {kr(inv.amount)} |
|
))}
);
}
function InvoiceDetail({ inv, onBack }) {
const { from } = window.INVOICE_DATA;
const subtotal = inv.items.reduce((s, [, qty, rate]) => s + qty * rate, 0);
const vat = Math.round(subtotal * 0.25);
const total = subtotal + vat;
return (
{inv.status !== 'paid' && }>Mark paid}
}>PDF
{inv.status === 'draft'
? }>Send
: }>Resend}
{from.org}
{from.name}
{from.email}
{from.orgnr}
| Description | Qty | Rate | Amount |
{inv.items.map(([desc, qty, rate], i) => (
| {desc} | {qty} | kr {kr(rate)} | kr {kr(qty * rate)} |
))}
Subtotalkr {kr(subtotal)}
VAT 25%kr {kr(vat)}
Totalkr {kr(total)}
);
}
function App() {
const [inv, setInv] = useState(null);
return (
{inv ? setInv(null)} /> : }
);
}
ReactDOM.createRoot(document.getElementById('root')).render();