/* ShopBooks — React UI with I18N (English default + Vietnamese) */ const { useState, useEffect, useRef } = React; // ---------------- I18N ---------------- const I18N = { en: { pageTitle: 'ShopBooks - Small Business Accounting', navDashboard: '📊 Dashboard', navPos: '🛒 POS', navProducts: '📦 Products', navContacts: '👥 Contacts', navInvoices: '🧾 Invoices', navCashflow: '💰 Cash Flow', navTax: '🏛️ Tax', navSettings: '⚙️ Settings', loading: 'Loading…', error: 'Error: ', add: '➕ Add', del: 'Delete', saveSettings: '💾 Save Settings', exportExcel: '📥 Export Excel', noData: 'No data yet', dashTitle: 'Overview for month {month}', exportReport: '📥 Export Excel Report', revenue: 'Sales Revenue', grossProfit: 'Gross Profit (Rev - COGS)', expense: 'Expenses', netProfit: 'Net Profit', invoiceCount: 'Invoice Count', customerDebt: 'Customer Debt', supplierDebt: 'Supplier Debt', stockValue: 'Stock Value', byDay: 'Revenue by Day', topProducts: 'Top 5 Best Sellers', thProduct: 'Product', thQty: 'Qty', lowStock: 'Low Stock Alert (≤ 10)', thSku: 'SKU', thStock: 'Stock', stockOk: 'Stock OK 👍', taxEstimate: 'Estimated Business Household Tax {year}', yearRevenue: 'Annual Revenue', pitLabel: 'PIT', totalTax: 'Total Tax', emptyChart: 'No revenue this month. Create an invoice in POS.', posTitle: 'Point of Sale', posSearch: '🔍 Search products (name or SKU)...', stockPrefix: 'Stock: ', posInvoice: 'Invoice', emptyCart: 'Tap a product to add to invoice.', subtotal: 'Subtotal', discount: 'Discount', vatPct: 'VAT (%)', grand: 'Grand Total', paidLabel: 'Paid', changeLabel: 'Change', pmCash: '💵 Cash', pmTransfer: '🏦 Transfer', pmQr: '📱 QR', pmCard: '💳 Card', exact: 'Exact', scanToPay: 'Customer scans with banking app / MoMo / ZaloPay / VNPay to pay exactly', noBank: '⚠️ Bank account not configured — go to Settings to enter.', retailCustomer: 'Retail Customer', checkoutBtn: '💵 Checkout & Print Invoice', emptyInvoice: 'Cart is empty', invoiceCreated: '✅ Created invoice ', changePrefix: ' · Change ', prodTitle: 'Products & Inventory', phSku: 'SKU', phName: 'Product Name', phCategory: 'Category', phUnit: 'Unit', phCost: 'Cost Price', phSell: 'Sell Price', phStock: 'Stock', prodDeleteConfirm: 'Delete this product?', prodAdded: 'Product added', contactsTitle: 'Customers & Suppliers', typeCustomer: 'Customer', typeSupplier: 'Supplier', phCName: 'Name', phPhone: 'Phone', phAddress: 'Address', thDebt: 'Debt', contactAdded: 'Added', contactDeleteConfirm: 'Delete?', thType: 'Type', invTitle: 'Invoice Book', thCode: 'Code', thDate: 'Date', thContact: 'Contact', thTotal: 'Total', thPaid: 'Paid', thStatus: 'Status', printBtn: '🖨 Print', typeSale: '🧾 Sale', typePurchase: '📦 Purchase', noInvoices: 'No invoices yet', statusPaid: 'Paid', statusPartial: 'Partial', statusUnpaid: 'Unpaid', cfTitle: 'Cash Flow Book', cfIncome: '💰 Income', cfExpense: '💸 Expense', cfCatPh: 'Category (rent, utilities, salary...)', cfAmount: 'Amount', cfNote: 'Note', cfRecord: '➕ Record', cfRecorded: 'Recorded', thCategory: 'Category', taxTitle: 'Estimated Business Household Tax — {year}', taxDesc: 'Per regulations for retail business households (revenue-based): VAT 1% + PIT 0.5% on revenue.', thItem: 'Item', thAmount: 'Amount', taxRevenue: 'Annual Revenue {year}', taxVat: 'VAT {rate}%', taxPit: 'PIT {rate}%', taxTotal: 'Total Tax Due', setTitle: 'Shop & QR Payment Settings', setDesc: 'Enter the shop owner bank account — the VietQR code will automatically appear on the POS screen when the customer selects QR, scannable by any banking app / MoMo / ZaloPay / VNPay with the exact invoice amount pre-filled.', phShopName: 'Shop Name', phBankAcc: 'Bank Account Number', phAccName: 'Account Holder Name (UPPERCASE no accents)', qrPreview: 'Preview QR code (e.g. 50,000 ₫):', settingsSaved: '✅ Settings saved' }, vi: { pageTitle: 'ShopBooks - Kế toán Hộ kinh doanh', navDashboard: '📊 Tổng quan', navPos: '🛒 Bán hàng', navProducts: '📦 Hàng hóa', navContacts: '👥 Khách/NCC', navInvoices: '🧾 Hóa đơn', navCashflow: '💰 Thu - Chi', navTax: '🏛️ Thuế HKD', navSettings: '⚙️ Cài đặt', loading: 'Đang tải…', error: 'Lỗi: ', add: '➕ Thêm', del: 'Xóa', saveSettings: '💾 Lưu cài đặt', exportExcel: '📥 Xuất Excel', noData: 'Chưa có dữ liệu', dashTitle: 'Tổng quan tháng {month}', exportReport: '📥 Xuất báo cáo Excel', revenue: 'Doanh thu bán hàng', grossProfit: 'Lãi gộp (DT - Giá vốn)', expense: 'Chi phí', netProfit: 'Lợi nhuận ròng', invoiceCount: 'Số hóa đơn', customerDebt: 'Khách còn nợ', supplierDebt: 'Nợ nhà cung cấp', stockValue: 'Giá trị tồn kho', byDay: 'Doanh thu theo ngày', topProducts: 'Top 5 hàng bán chạy', thProduct: 'Hàng hóa', thQty: 'SL', lowStock: 'Cảnh báo tồn kho thấp (≤ 10)', thSku: 'Mã', thStock: 'Tồn', stockOk: 'Kho ổn 👍', taxEstimate: 'Thuế khoán HKD năm {year} (ước tính)', yearRevenue: 'Doanh thu năm', pitLabel: 'TNCN', totalTax: 'Tổng thuế', emptyChart: 'Chưa có doanh thu tháng này. Hãy tạo hóa đơn ở màn Bán hàng (POS).', posTitle: 'Bán hàng tại quầy', posSearch: '🔍 Tìm hàng (tên hoặc mã)...', stockPrefix: 'Tồn: ', posInvoice: 'Hóa đơn', emptyCart: 'Chạm vào hàng hóa để thêm vào hóa đơn.', subtotal: 'Tạm tính', discount: 'Giảm giá', vatPct: 'VAT (%)', grand: 'Khách phải trả', paidLabel: 'Khách đưa', changeLabel: 'Tiền thối', pmCash: '💵 Tiền mặt', pmTransfer: '🏦 CK', pmQr: '📱 QR', pmCard: '💳 Thẻ', exact: 'Đủ', scanToPay: 'Khách quét bằng app ngân hàng / MoMo / ZaloPay / VNPay để trả đúng', noBank: '⚠️ Chưa cấu hình tài khoản ngân hàng — vào Cài đặt để nhập.', retailCustomer: 'Khách lẻ', checkoutBtn: '💵 Thanh toán & In hóa đơn', emptyInvoice: 'Chưa có hàng trong hóa đơn', invoiceCreated: '✅ Đã tạo hóa đơn ', changePrefix: ' · Thối ', prodTitle: 'Quản lý hàng hóa & kho', phSku: 'Mã hàng (SKU)', phName: 'Tên hàng', phCategory: 'Nhóm hàng', phUnit: 'ĐVT', phCost: 'Giá vốn', phSell: 'Giá bán', phStock: 'Tồn kho', prodDeleteConfirm: 'Xóa hàng hóa này?', prodAdded: 'Đã thêm hàng', contactsTitle: 'Khách hàng & Nhà cung cấp', typeCustomer: 'Khách hàng', typeSupplier: 'Nhà cung cấp', phCName: 'Tên', phPhone: 'Điện thoại', phAddress: 'Địa chỉ', thDebt: 'Công nợ', contactAdded: 'Đã thêm', contactDeleteConfirm: 'Xóa?', thType: 'Loại', invTitle: 'Sổ hóa đơn', thCode: 'Số HĐ', thDate: 'Ngày', thContact: 'Đối tượng', thTotal: 'Tổng tiền', thPaid: 'Đã thu/chi', thStatus: 'Trạng thái', printBtn: '🖨 In', typeSale: '🧾 Bán', typePurchase: '📦 Nhập', noInvoices: 'Chưa có hóa đơn', statusPaid: 'Đã thu', statusPartial: 'Thu một phần', statusUnpaid: 'Chưa thu', cfTitle: 'Sổ thu - chi (quỹ tiền)', cfIncome: '💰 Thu', cfExpense: '💸 Chi', cfCatPh: 'Loại (thuê, điện nước, lương...)', cfAmount: 'Số tiền', cfNote: 'Ghi chú', cfRecord: '➕ Ghi sổ', cfRecorded: 'Đã ghi sổ', thCategory: 'Nhóm', taxTitle: 'Ước tính thuế khoán Hộ kinh doanh — năm {year}', taxDesc: 'Theo quy định cho hộ kinh doanh bán lẻ (kê khai theo doanh thu): VAT 1% + Thuế TNCN 0.5% trên doanh thu.', thItem: 'Chỉ tiêu', thAmount: 'Số tiền', taxRevenue: 'Doanh thu năm {year}', taxVat: 'Thuế GTGT (VAT) {rate}%', taxPit: 'Thuế TNCN {rate}%', taxTotal: 'Tổng thuế phải nộp', setTitle: 'Cài đặt cửa hàng & thanh toán QR', setDesc: 'Nhập tài khoản ngân hàng của chủ shop — mã VietQR sẽ tự hiện trên màn hình POS khi khách chọn QR, quét bằng mọi app ngân hàng / MoMo / ZaloPay / VNPay, điền sẵn đúng số tiền hóa đơn.', phShopName: 'Tên cửa hàng', phBankAcc: 'Số tài khoản ngân hàng', phAccName: 'Tên chủ tài khoản (IN HOA không dấu)', qrPreview: 'Xem trước mã QR (ví dụ 50.000 ₫):', settingsSaved: '✅ Đã lưu cài đặt' } }; let currentLang = localStorage.getItem('shopbooks-lang') || 'en'; function t(key, vars) { let s = I18N[currentLang][key]; if (s === undefined) s = I18N.en[key]; if (s === undefined) return key; if (vars) Object.keys(vars).forEach(k => { s = s.split('{' + k + '}').join(String(vars[k])); }); return s; } const fmt = n => (Math.round(+n || 0)).toLocaleString('vi-VN') + ' ₫'; const toast = m => { const tEl = document.getElementById('toast'); tEl.textContent = m; tEl.style.opacity = 1; setTimeout(() => tEl.style.opacity = 0, 2200); }; const api = async (url, method = 'GET', data) => { const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: data ? JSON.stringify(data) : undefined }); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status)); return j; }; const PM_ICON = { cash: '💵', transfer: '🏦', qr: '📱', card: '💳' }; // ---------------- DASHBOARD ---------------- function BarChart({ rows }) { if (!rows.length) return

{t('emptyChart')}

; const max = Math.max(...rows.map(r => r.s)) || 1; const w = Math.max(40, Math.floor(1000 / rows.length)); return ( {rows.map((r, i) => { const h = Math.max(2, r.s / max * 170); return ( {r.d}: {fmt(r.s)} {r.d.slice(8)} ); })} ); } function Dashboard() { const month = new Date().toISOString().slice(0, 7); const [d, setD] = useState(null); const [tax, setTax] = useState(null); const [err, setErr] = useState(''); useEffect(() => { Promise.all([api('/api/reports/dashboard?month=' + month), api('/api/tax?year=' + month.slice(0, 4))]) .then(([dd, tt]) => { setD(dd); setTax(tt); }) .catch(e => setErr(e.message)); }, [month]); if (err) return

{t('error')}{err}

; if (!d || !tax) return

{t('loading')}

; return (

{t('dashTitle', { month })} {t('exportReport')}

{t('revenue')}
{fmt(d.revenue)}
{t('grossProfit')}
{fmt(d.grossProfit)}
{t('expense')}
{fmt(d.expense)}
= 0 ? 'card green' : 'card red'}>
{t('netProfit')}
{fmt(d.netProfit)}
{t('invoiceCount')}
{d.invoiceCount}
{t('customerDebt')}
{fmt(d.debtCustomer)}
{t('supplierDebt')}
{fmt(d.debtSupplier)}
{t('stockValue')}
{fmt(d.stockValue)}

{t('byDay')}

{t('topProducts')}

{d.topProducts.length ? d.topProducts.map((p, i) => ) : }
{t('thProduct')}{t('thQty')}{t('revenue')}
{p.n}{p.q}{fmt(p.a)}
{t('noData')}

{t('lowStock')}

{d.lowStock.length ? d.lowStock.map((p, i) => ) : }
{t('thSku')}{t('thProduct')}{t('thStock')}
{p.sku}{p.name}{p.stock} {p.unit}
{t('stockOk')}

{t('taxEstimate', { year: tax.year })}

{t('yearRevenue')}: {fmt(tax.revenue)} · VAT {tax.vatRate}%: {fmt(tax.vat)} · {t('pitLabel')} {tax.pitRate}%: {fmt(tax.pit)} · {t('totalTax')}: {fmt(tax.total)}

); } // ---------------- POS ---------------- function POS() { const [products, setProducts] = useState([]); const [contacts, setContacts] = useState([]); const [config, setConfig] = useState({}); const [cart, setCart] = useState([]); const [search, setSearch] = useState(''); const [disc, setDisc] = useState('0'); const [vat, setVat] = useState('0'); const [paid, setPaid] = useState(''); const [payMethod, setPayMethod] = useState('cash'); const [customer, setCustomer] = useState(''); const paidFocused = useRef(false); useEffect(() => { Promise.all([api('/api/products'), api('/api/contacts'), api('/api/config').catch(() => ({}))]) .then(([p, c, cfg]) => { setProducts(p); setContacts(c); setConfig(cfg); }) .catch(e => toast(t('error') + e.message)); }, []); const sub = cart.reduce((s, l) => s + l.qty * l.price, 0); const discN = +disc || 0, vatN = +vat || 0; const total = sub - discN + Math.max(0, sub - discN) * vatN / 100; const paidN = +paid || 0; const change = paidN - total; useEffect(() => { if (!paidFocused.current) setPaid(total ? String(Math.round(total)) : ''); }, [total]); const addToCart = id => { const p = products.find(x => x.id === id); if (!p) return; setCart(c => { const line = c.find(x => x.id === id); if (line) return c.map(x => x.id === id ? { ...x, qty: x.qty + 1 } : x); return [...c, { id, name: p.name, price: p.sell_price, qty: 1 }]; }); }; const bump = (i, d) => setCart(c => c.map((l, idx) => idx === i ? { ...l, qty: l.qty + d } : l).filter(l => l.qty > 0)); const checkout = async () => { if (!cart.length) return toast(t('emptyInvoice')); const payload = { type: 'sale', contact_id: customer || null, items: cart.map(l => ({ product_id: l.id, qty: l.qty, price: l.price })), discount: discN, vat_rate: vatN, paid: paidN, payment_method: payMethod }; try { const r = await api('/api/invoices', 'POST', payload); toast(t('invoiceCreated') + r.code + (r.change > 0 ? t('changePrefix') + fmt(r.change) : '')); window.open('/invoice.html?id=' + r.id, '_blank'); setProducts(await api('/api/products')); setCart([]); setDisc('0'); setVat('0'); setCustomer(''); setPayMethod('cash'); } catch (e) { toast(t('error') + e.message); } }; const q = search.toLowerCase(); const filtered = products.filter(p => !q || p.name.toLowerCase().includes(q) || (p.sku || '').toLowerCase().includes(q)); const showQr = payMethod === 'qr' && total > 0; const qrReady = config.bank_bin && config.bank_account; const qrUrl = showQr && qrReady ? 'https://img.vietqr.io/image/' + config.bank_bin + '-' + config.bank_account + '-' + (config.qr_template || 'compact2') + '.png?amount=' + Math.round(total) + '&addInfo=' + encodeURIComponent('Thanh toan ShopBooks') + '&accountName=' + encodeURIComponent(config.account_name || '') : null; const pmKeys = ['cash', 'transfer', 'qr', 'card']; const pmLabels = { cash: t('pmCash'), transfer: t('pmTransfer'), qr: t('pmQr'), card: t('pmCard') }; return (

{t('posTitle')}

setSearch(e.target.value)} />
{filtered.map(p => (
addToCart(p.id)}>
{p.name}
{fmt(p.sell_price)}
{t('stockPrefix')}{p.stock} {p.unit}
))}

{t('posInvoice')}

{cart.length ? cart.map((l, i) => (
{l.name} {l.qty} {fmt(l.qty * l.price)}
)) :

{t('emptyCart')}

}
{t('subtotal')}{fmt(sub)}
{t('discount')} setDisc(e.target.value)} style={{ width: '110px' }} />
{t('vatPct')} setVat(e.target.value)} style={{ width: '110px' }} />
{t('grand')}{fmt(total)}
{t('paidLabel')} { paidFocused.current = true; }} onBlur={() => { paidFocused.current = false; }} onChange={e => setPaid(e.target.value)} />
{t('changeLabel')}= 0 ? '#16a34a' : '#dc2626' }}>{fmt(Math.max(0, change))}
{pmKeys.map(k => ( ))}
{[10000, 20000, 50000, 100000, 200000, 500000].map(v => ( ))}
{showQr && (qrReady ? VietQR
{t('scanToPay')} {fmt(total)}
:
{t('noBank')}
)}
); } // ---------------- PRODUCTS ---------------- function Products() { const [products, setProducts] = useState(null); const load = () => api('/api/products').then(setProducts).catch(e => toast(t('error') + e.message)); useEffect(() => { load(); }, []); const submit = async e => { e.preventDefault(); const form = e.target; const d = Object.fromEntries(new FormData(form).entries()); try { await api('/api/products', 'POST', d); toast(t('prodAdded')); form.reset(); load(); } catch (err) { toast(t('error') + err.message); } }; const del = async id => { if (!confirm(t('prodDeleteConfirm'))) return; await api('/api/products/' + id, 'DELETE'); load(); }; if (!products) return

{t('loading')}

; return (

{t('prodTitle')} {t('exportExcel')}

{products.map(p => ( ))}
{t('thSku')}{t('thProduct')}{t('thCategory')}{t('phCost')}{t('phSell')}{t('thStock')}
{p.sku}{p.name}{p.category} {fmt(p.cost_price)}{fmt(p.sell_price)} {p.stock} {p.unit}
); } // ---------------- CONTACTS ---------------- function Contacts() { const [contacts, setContacts] = useState(null); const load = () => api('/api/contacts').then(setContacts).catch(e => toast(t('error') + e.message)); useEffect(() => { load(); }, []); const submit = async e => { e.preventDefault(); const form = e.target; const d = Object.fromEntries(new FormData(form).entries()); await api('/api/contacts', 'POST', d); toast(t('contactAdded')); form.reset(); load(); }; const del = async id => { if (!confirm(t('contactDeleteConfirm'))) return; await api('/api/contacts/' + id, 'DELETE'); load(); }; if (!contacts) return

{t('loading')}

; return (

{t('contactsTitle')} {t('exportExcel')}

{contacts.map(c => ( ))}
{t('thType')}{t('phCName')}{t('phPhone')}{t('phAddress')}{t('thDebt')}
{c.type === 'customer' ? '👤 ' + t('typeCustomer') : '🏭 ' + t('typeSupplier')}{c.name}{c.phone}{c.address} 0 ? { color: '#d93025', fontWeight: 700 } : undefined}>{fmt(c.debt)}
); } // ---------------- INVOICES ---------------- function Invoices() { const [rows, setRows] = useState(null); useEffect(() => { api('/api/invoices').then(setRows).catch(e => toast(t('error') + e.message)); }, []); if (!rows) return

{t('loading')}

; const statusLabel = s => s === 'paid' ? t('statusPaid') : s === 'partial' ? t('statusPartial') : t('statusUnpaid'); return (

{t('invTitle')} {t('exportExcel')}

{rows.length ? rows.map(i => ( )) : }
{t('thCode')}{t('thType')}{t('thDate')}{t('thContact')}{t('thTotal')}{t('thPaid')}{t('thStatus')}
{i.code} {i.type === 'sale' ? t('typeSale') : t('typePurchase')} {i.created_at}{i.contact_name || t('retailCustomer')} {fmt(i.total)}{fmt(i.paid)} {statusLabel(i.status)} {PM_ICON[i.payment_method] || '💵'}
{t('noInvoices')}
); } // ---------------- CASHFLOW ---------------- function Cashflow() { const [rows, setRows] = useState(null); const load = () => api('/api/cashflows').then(setRows).catch(e => toast(t('error') + e.message)); useEffect(() => { load(); }, []); const submit = async e => { e.preventDefault(); const form = e.target; const d = Object.fromEntries(new FormData(form).entries()); await api('/api/cashflows', 'POST', d); toast(t('cfRecorded')); form.reset(); load(); }; const del = async id => { await api('/api/cashflows/' + id, 'DELETE'); load(); }; if (!rows) return

{t('loading')}

; return (

{t('cfTitle')} {t('exportExcel')}

{rows.map(f => ( ))}
{t('thDate')}{t('thType')}{t('thCategory')}{t('cfAmount')}{t('cfNote')}
{f.created_at} {f.type === 'income' ? t('cfIncome') : t('cfExpense')} {f.category}{fmt(f.amount)}{f.note}
); } // ---------------- TAX ---------------- function TaxPage() { const year = new Date().getFullYear(); const [tax, setTax] = useState(null); useEffect(() => { api('/api/tax?year=' + year).then(setTax).catch(e => toast(t('error') + e.message)); }, [year]); if (!tax) return

{t('loading')}

; return (

{t('taxTitle', { year })}

{t('taxDesc')}

{t('thItem')}{t('thAmount')}
{t('taxRevenue', { year })}{fmt(tax.revenue)}
{t('taxVat', { rate: tax.vatRate })}{fmt(tax.vat)}
{t('taxPit', { rate: tax.pitRate })}{fmt(tax.pit)}
{t('taxTotal')}{fmt(tax.total)}
); } // ---------------- SETTINGS ---------------- const BANKS = [['970422', 'MB Bank'], ['970436', 'Vietcombank'], ['970407', 'Techcombank'], ['970415', 'VietinBank'], ['970416', 'ACB'], ['970432', 'VPBank'], ['970418', 'BIDV'], ['970405', 'Agribank'], ['970448', 'OCB'], ['970403', 'Sacombank']]; function Settings() { const [cfg, setCfg] = useState({ shop_name: '', bank_bin: '970422', bank_account: '', account_name: '' }); const [ready, setReady] = useState(false); useEffect(() => { api('/api/config').then(c => { setCfg(prev => ({ ...prev, ...c })); setReady(true); }).catch(() => setReady(true)); }, []); const set = k => e => setCfg({ ...cfg, [k]: e.target.value }); const submit = async e => { e.preventDefault(); try { const saved = await api('/api/config', 'POST', { shop_name: cfg.shop_name, bank_bin: cfg.bank_bin, bank_account: cfg.bank_account, account_name: cfg.account_name }); setCfg(prev => ({ ...prev, ...saved })); toast(t('settingsSaved')); } catch (err) { toast(t('error') + err.message); } }; if (!ready) return

{t('loading')}

; const qrPrev = 'https://img.vietqr.io/image/' + cfg.bank_bin + '-' + cfg.bank_account + '-compact2.png?amount=50000&addInfo=Test&accountName=' + encodeURIComponent(cfg.account_name || ''); return (

{t('setTitle')}

{t('setDesc')}

{t('qrPreview')}

VietQR preview
); } // ---------------- APP / ROUTER ---------------- const PAGES = { dashboard: Dashboard, pos: POS, products: Products, contacts: Contacts, invoices: Invoices, cashflow: Cashflow, tax: TaxPage, settings: Settings }; const NAV_KEYS = ['dashboard', 'pos', 'products', 'contacts', 'invoices', 'cashflow', 'tax', 'settings']; const NAV_LABEL_KEYS = { dashboard: 'navDashboard', pos: 'navPos', products: 'navProducts', contacts: 'navContacts', invoices: 'navInvoices', cashflow: 'navCashflow', tax: 'navTax', settings: 'navSettings' }; function App() { const [page, setPage] = useState('dashboard'); const [lang, setLangState] = useState(currentLang); const setLang = l => { currentLang = l; localStorage.setItem('shopbooks-lang', l); setLangState(l); }; useEffect(() => { document.title = t('pageTitle'); document.documentElement.lang = lang; }, [lang]); const Page = PAGES[page]; return (
📒 ShopBooks
); } ReactDOM.createRoot(document.getElementById('root')).render();