/* 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 ( ); } 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('thProduct')} | {t('thQty')} | {t('revenue')} |
|---|---|---|
| {p.n} | {p.q} | {fmt(p.a)} |
| {t('noData')} | ||
| {t('thSku')} | {t('thProduct')} | {t('thStock')} |
|---|---|---|
| {p.sku} | {p.name} | {p.stock} {p.unit} |
| {t('stockOk')} | ||
{t('yearRevenue')}: {fmt(tax.revenue)} · VAT {tax.vatRate}%: {fmt(tax.vat)} · {t('pitLabel')} {tax.pitRate}%: {fmt(tax.pit)} · {t('totalTax')}: {fmt(tax.total)}
{t('emptyCart')}
}{t('loading')}
; return (| {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} |
{t('loading')}
; return (| {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)} |
{t('loading')}
; const statusLabel = s => s === 'paid' ? t('statusPaid') : s === 'partial' ? t('statusPartial') : t('statusUnpaid'); return (| {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')} | ||||||||
{t('loading')}
; return (| {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} |
{t('loading')}
; return ({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)} |
{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('setDesc')}
{t('qrPreview')}