+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e + Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e + Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e + Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e + Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}};
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1762999766062},"resources":{}}; + (it.price || 0) + ')

'); }); } else { body.push('

' + String(inv.items || '') + '

'); } body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push(''); body.push('
Subtotal
+ Number(inv.subtotal || 0).toFixed(2) + '
Discount
+ Number(inv.discount || 0).toFixed(2) + '
Tax
+ Number(inv.tax || 0).toFixed(2) + '
Total
+ Number(inv.total || 0).toFixed(2) + '
'); if (signature) { body.push('
'); body.push(''); body.push('
Authorized Signature
'); } body.push(''); body.push(''); w.document.open(); w.document.write; /** ================= Filters (unchanged) ================= */ const [filterStatus, setFilterStatus] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const filteredInvoices = useMemo(() => { const q = (searchQuery || '').toLowerCase().trim(); return (invoices || []) .filter((inv) => (filterStatus === 'All' ? true : String(inv.status) === filterStatus)) .filter((inv) => { if (!q) return true; const s = (v) => String(v !== null && v !== void 0 ? v : '').toLowerCase(); return (s(inv.businessName).includes(q) || s(inv.contactPerson).includes(q) || s(inv.invoiceNumber).includes(q) || s(inv.notes).includes(q) || s(inv.status).includes(q)); }); }, [invoices, filterStatus, searchQuery]); /** ================= Payment Link ================= */ function generatePaymentLink(inv) { return __awaiter(this, void 0, void 0, function* () { try { const did = getCookie('connected_did'); if (!did) { setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.'); setSnackOpen(true); return; } // 🧩 normalize item list whether it's array or string let invoiceItems = []; if (Array.isArray(inv.items)) { invoiceItems = inv.items; } else if (typeof inv.items === 'string') { // parse string like "B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)" invoiceItems = inv.items.split(',').map((x) => { const m = x.match(/(.*)\((\d+)\s*×\s*\$?([\d.]+)\)/); return { name: m ? m[1].trim() : x.trim(), qty: m ? Number(m[2]) : 1, price: m ? Number(m[3]) : 0, }; }); } const lineItems = invoiceItems .map((it) => { var _a; const normalizedName = String((it === null || it === void 0 ? void 0 : it.name) || '').trim().toLowerCase(); // find matching pricingKey from nameToPricingKey const matchedKey = Object.keys(nameToPricingKey).find((k) => k.toLowerCase().trim() === normalizedName) || normalizedName.replace(/\s+/g, '').replace(/[^a-z0-9]/gi, ''); // get entry from priceTable const group = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[nameToPricingKey[it.name]]) || (priceTable === null || priceTable === void 0 ? void 0 : priceTable[matchedKey]); if (!group) return null; const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0]; // prefer stored invoice currency over current selection const activeCurrency = inv.currency || currency; const entry = (_a = group === null || group === void 0 ? void 0 : group[sizeKey]) === null || _a === void 0 ? void 0 : _a[activeCurrency]; const priceId = entry === null || entry === void 0 ? void 0 : entry.priceId; return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null; }) .filter(Boolean); if (!lineItems.length) { alert('No valid priceIds found for this invoice. Ensure item names match your products.json.'); console.log('DEBUG: invoiceItems', invoiceItems); console.log('DEBUG: priceTable keys', Object.keys(priceTable)); console.log('DEBUG: nameToPricingKey', nameToPricingKey); return; } setLinkLoading(true); yield fetch('/payment-kit/', { credentials: 'include', cache: 'no-store' }); let csrf = getCsrf(); if (!csrf) { yield fetch(`/api/session?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' }); csrf = getCsrf(); } const res = yield fetch('/payment-kit/api/checkout-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf, }, credentials: 'include', body: JSON.stringify({ create_mine: true, mode: 'payment', line_items: lineItems, success_url: `${window.location.origin}?success=true`, cancel_url: `${window.location.origin}?cancel=true`, metadata: { source: 'InvoiceManager', invoiceNumber: inv.invoiceNumber || '', customer: inv.businessName || '', total: inv.total || '', currency, }, }), }); const out = yield res.json(); if (!(out === null || out === void 0 ? void 0 : out.url)) throw new Error('Failed to generate payment link'); window.location.assign(out.url); // ✅ mobile-safe redirect setSnackMsg('Redirecting to payment page…'); setSnackOpen(true); } catch (err) { alert(err.message || 'Payment link error'); } finally { setLinkLoading(false); } }); } /** ================= Render (UI untouched) ================= */ return (React.createElement(Box, { sx: { p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' } }, React.createElement(Chip, { label: loggedIn ? 'Logged In' : 'Not Connected', color: loggedIn ? 'success' : 'warning', size: "small", onClick: handleBadgeClick, sx: { position: 'absolute', top: 12, right: 12, opacity: 0, // 👈 invisible pointerEvents: 'none', // 👈 can't click or tab to it zIndex: -1, // 👈 behind everything } }), React.createElement(Paper, { sx: { p: 4, mb: 4, backgroundColor: isDark ? '#1e1e1e' : '#fff', color: isDark ? '#eee' : '#333', borderRadius: 2, } }, React.createElement(Typography, { variant: "h4", sx: { mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' } }, title), React.createElement(Stack, { spacing: 2, component: "form", onSubmit: handleCreateOrUpdate }, loadingCustomers ? (React.createElement(CircularProgress, { sx: { mx: 'auto' } })) : (React.createElement(Select, { value: selectedCustomerId, onChange: (e) => setSelectedCustomerId(e.target.value), displayEmpty: true, fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Customer"), customers.map((c) => (React.createElement(MenuItem, { key: c.id, value: c.id }, c.businessName, " ", c.contactPerson ? `(${c.contactPerson})` : ''))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Select, { value: status, onChange: (e) => setStatus(e.target.value), fullWidth: true, sx: inputStyle }, React.createElement(MenuItem, { value: "Draft" }, "Draft"), React.createElement(MenuItem, { value: "Sent" }, "Sent"), React.createElement(MenuItem, { value: "Paid" }, "Paid"), React.createElement(MenuItem, { value: "Void" }, "Void"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, Object.assign({ label: "Due Date", type: "date", value: toInputDate(dueDate), onChange: (e) => setDueDate(e.target.value), fullWidth: true }, datePadProps, { sx: inputStyle })))), React.createElement(Grid, { container: true, spacing: 2, sx: { alignItems: 'center' } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Currency", select: true, value: currency, onChange: (e) => !editingInvoice && setCurrency(e.target.value), fullWidth: true, disabled: !!editingInvoice, sx: inputStyle }, React.createElement(MenuItem, { value: "USD" }, "USD"), React.createElement(MenuItem, { value: "USDC" }, "USDC"), React.createElement(MenuItem, { value: "ABT" }, "ABT"))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: useStandard, onChange: (e) => setUseStandard(e.target.checked), color: "primary" }), label: React.createElement(Typography, { variant: "subtitle2", sx: { fontWeight: 600, color: isDark ? '#ccc' : '#444', letterSpacing: '0.03em', } }, useStandard ? 'Using Catalog Products' : 'Manual Entry Mode') }))), React.createElement(Box, null, React.createElement(Typography, { variant: "h6", sx: { mb: 1.5, color: isDark ? '#ddd' : '#444' } }, "Line Items"), React.createElement(Stack, { spacing: 2 }, items.map((row, idx) => (React.createElement(Paper, { key: idx, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Grid, { container: true, spacing: 1.5 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, useStandard ? (React.createElement(Select, { value: row.name || '', onChange: (e) => { const displayName = e.target.value; const pricingKey = nameToPricingKey[displayName] || displayName; // fallback: key equals display name onSelectStandardItem(idx, pricingKey, displayName); }, fullWidth: true, displayEmpty: true, sx: inputStyle }, React.createElement(MenuItem, { value: "" }, "Select Product"), productsFlat.map((p) => { // show current-currency amount using OneSize (or first size) const sizes = (priceTable === null || priceTable === void 0 ? void 0 : priceTable[p.pricingKey]) ? Object.keys(priceTable[p.pricingKey]) : ['OneSize']; const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0]; const entry = getPricingEntry(p.pricingKey, sizeKey, currency); const amt = entry === null || entry === void 0 ? void 0 : entry.amount; return (React.createElement(MenuItem, { key: `${p.pricingKey}-${p.name}`, value: p.name }, p.name, amt != null ? ` (${amt})` : '')); }))) : (React.createElement(TextField, { label: "Item Name", value: row.name, onChange: (e) => setItemField(idx, 'name', e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Qty", type: "number", value: row.qty, onChange: (e) => setItemField(idx, 'qty', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 6, sm: 2 }, React.createElement(TextField, { label: "Unit Price", type: "number", value: row.price, onChange: (e) => setItemField(idx, 'price', e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 2 }, React.createElement(TextField, { label: "Line Total", value: (Number(row.qty || 0) * Number(row.price || 0)).toFixed(2), fullWidth: true, InputProps: { readOnly: true }, sx: inputStyle }))), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "flex-end", sx: { mt: 2 } }, React.createElement(Button, { variant: "outlined", color: "error", onClick: () => removeItem(idx), disabled: items.length <= 1 }, "Remove"), React.createElement(Button, { variant: "contained", onClick: addItem }, "Add Item"))))))), React.createElement(Grid, { container: true, spacing: 2 }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Discount ($)", type: "number", value: discount, onChange: (e) => setDiscount(e.target.value), fullWidth: true, sx: inputStyle })), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(TextField, { label: "Tax ($)", type: "number", value: tax, onChange: (e) => setTax(e.target.value), fullWidth: true, sx: inputStyle }))), React.createElement(TextField, { label: "Notes", value: notes, onChange: (e) => setNotes(e.target.value), fullWidth: true, multiline: true, rows: 3, sx: inputStyle }), React.createElement(Paper, { sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", subtotal.toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", total.toFixed(2)))), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 2, sx: { mt: 1 } }, React.createElement(Button, { type: "submit", variant: "contained", fullWidth: true }, editingInvoice ? 'Update Invoice' : 'Create Invoice'), React.createElement(Button, { variant: "outlined", onClick: resetForm, fullWidth: true }, "Clear")))), React.createElement(Paper, { sx: { p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 } }, React.createElement(Typography, { variant: "h5", sx: { textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' } }, "Invoice Dashboard"), React.createElement(TextField, { label: "Search Invoices (name, number, status)", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), fullWidth: true, size: "small", sx: Object.assign({ mb: 2 }, inputStyle) }), React.createElement(Stack, { direction: "row", spacing: 1.5, justifyContent: "center", flexWrap: "wrap", mb: 1.5 }, ['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) => (React.createElement(Chip, { key: s, label: s, onClick: () => setFilterStatus(s), sx: { cursor: 'pointer', backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0', color: filterStatus === s ? '#fff' : '#333', } })))), loadingInvoices ? (React.createElement(CircularProgress, { sx: { display: 'block', mx: 'auto' } })) : (React.createElement(Stack, { spacing: 2 }, filteredInvoices.map((inv) => (React.createElement(Box, { key: inv.id, sx: { p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 } }, React.createElement(Typography, { variant: "h6", sx: { fontWeight: 'bold' } }, inv.businessName), React.createElement(Typography, { variant: "body2" }, "Invoice #: ", inv.invoiceNumber), React.createElement(Typography, { variant: "body2" }, "Status: ", inv.status), React.createElement(Typography, { variant: "body2" }, "Created: ", displayDate(inv.createdDate)), React.createElement(Typography, { variant: "body2" }, "Due: ", displayDate(inv.dueDate)), React.createElement(Typography, { variant: "body2" }, "Total: $", Number(inv.total || 0).toFixed(2)), React.createElement(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, sx: { mt: 1, flexWrap: 'wrap' } }, React.createElement(Button, { variant: "outlined", onClick: () => loadInvoiceToEdit(inv) }, "Edit"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(inv) }, "Download PDF"), React.createElement(Button, { variant: "outlined", onClick: () => openView(inv) }, "View"), React.createElement(Button, { variant: "outlined", onClick: () => printInvoice(inv) }, "Print"), paymentLink && (React.createElement(Button, { variant: "outlined", color: "success", onClick: () => generatePaymentLink(inv), disabled: linkLoading }, linkLoading ? React.createElement(CircularProgress, { size: 16 }) : 'Payment Link')), React.createElement(Button, { variant: "outlined", color: "error", onClick: () => deleteInvoice(inv.id) }, "Delete")))))))), React.createElement(Dialog, { open: viewOpen, onClose: closeView, fullWidth: true, maxWidth: "md" }, React.createElement(DialogTitle, null, "View Invoice", React.createElement(IconButton, { onClick: closeView, sx: { position: 'absolute', right: 8, top: 8 } }, "\u00D7")), React.createElement(DialogContent, { dividers: true }, !viewInvoice ? (React.createElement(Typography, null, "Loading\u2026")) : (React.createElement(Box, { sx: { maxWidth: 800, mx: 'auto' } }, icon && (React.createElement(Box, { sx: { textAlign: 'center', mb: 1.5 } }, React.createElement("img", { src: icon, alt: "Logo", style: { maxWidth: 160, maxHeight: 80 } }))), React.createElement(Typography, { variant: "h5", align: "center", sx: { mb: 2, fontWeight: 700 } }, "Invoice"), React.createElement(Grid, { container: true, spacing: 1.5, sx: { mb: 1 } }, React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Invoice #"), React.createElement(Typography, null, viewInvoice.invoiceNumber || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Status"), React.createElement(Typography, null, viewInvoice.status || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Business"), React.createElement(Typography, null, viewInvoice.businessName || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Contact"), React.createElement(Typography, null, viewInvoice.contactPerson || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Created"), React.createElement(Typography, null, displayDate(viewInvoice.createdDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Due"), React.createElement(Typography, null, displayDate(viewInvoice.dueDate) || '—'))), React.createElement(Grid, { item: true, xs: 12, sm: 6 }, React.createElement(Paper, { sx: { p: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Currency"), React.createElement(Typography, null, viewInvoice.currency || 'USD')))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Items"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.items || ''))), React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Subtotal"), React.createElement(Typography, null, "$", Number(viewInvoice.subtotal || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Discount"), React.createElement(Typography, null, "-$", Number(viewInvoice.discount || 0).toFixed(2))), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, null, "Tax"), React.createElement(Typography, null, "$", Number(viewInvoice.tax || 0).toFixed(2))), React.createElement(Divider, { sx: { my: 1 } }), React.createElement(Stack, { direction: "row", justifyContent: "space-between" }, React.createElement(Typography, { fontWeight: "bold" }, "Total"), React.createElement(Typography, { fontWeight: "bold" }, "$", Number(viewInvoice.total || 0).toFixed(2)))), viewInvoice.notes && (React.createElement(Paper, { sx: { p: 1.5, mb: 1.5 } }, React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Notes"), React.createElement(Typography, { sx: { whiteSpace: 'pre-wrap' } }, String(viewInvoice.notes)))), signature && (React.createElement(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 } }, React.createElement("img", { src: signature, alt: "Signature", style: { maxWidth: 200, maxHeight: 60 } }), React.createElement(Typography, { variant: "caption", color: "text.secondary" }, "Authorized Signature")))))), React.createElement(DialogActions, null, viewInvoice && (React.createElement(React.Fragment, null, React.createElement(Button, { onClick: () => printInvoice(viewInvoice) }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => downloadPDF(viewInvoice) }, "Download PDF"))), React.createElement(Button, { onClick: closeView }, "Close"))), React.createElement(Snackbar, { open: snackOpen, autoHideDuration: 2200, onClose: () => setSnackOpen(false), anchorOrigin: { vertical: 'bottom', horizontal: 'center' } }, React.createElement(Alert, { severity: "success", sx: { width: '100%' } }, snackMsg)))); }; } // handle possible module.exports if (module.exports && module.exports !== moduleExports) { // if module.exports is used, use it first return typeof module.exports === 'object' ? module.exports : { default: module.exports }; } // ensure a default export if (!('default' in exports) && Object.keys(exports).length === 0) { // module has no exports, return null to indicate invalid return null; } return exports; }; + (it.price || 0) + ')\u003c\u002fp\u003e');\n });\n } else {\n body.push('\u003cp\u003e' + String(inv.items || '') + '\u003c\u002fp\u003e');\n }\n body.push('\u003c\u002fdiv\u003e');\n\n body.push('\u003ctable\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eSubtotal\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.subtotal || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.discount || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e
+ Number(inv.tax || 0).toFixed(2) + '\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003ctr class=total\u003e\u003ctd\u003eTotal\u003c\u002ftd\u003e\u003ctd\u003e\u003cb\u003e
+ Number(inv.total || 0).toFixed(2) + '\u003c\u002fb\u003e\u003c\u002ftd\u003e\u003c\u002ftr\u003e');\n body.push('\u003c\u002ftable\u003e');\n\n if (signature) {\n body.push('\u003cdiv style=\"text-align:right;margin-top:40px;\"\u003e');\n body.push('\u003cimg src=' + JSON.stringify(signature) + ' style=\"max-width:200px;\"\u003e');\n body.push('\u003cbr\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e');\n }\n\n body.push('\u003cscript\u003ewindow.onload=function(){window.print();}\u003c\u002fscript\u003e');\n body.push('\u003c\u002fbody\u003e\u003c\u002fhtml\u003e');\n\n w.document.open();\n w.document.write\n\n\n \u002f** ================= Filters (unchanged) ================= *\u002f\n const [filterStatus, setFilterStatus] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredInvoices = useMemo(() =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n return (invoices || [])\n .filter((inv) =\u003e (filterStatus === 'All' ? true : String(inv.status) === filterStatus))\n .filter((inv) =\u003e {\n if (!q) return true;\n const s = (v) =\u003e String(v ?? '').toLowerCase();\n return (\n s(inv.businessName).includes(q) ||\n s(inv.contactPerson).includes(q) ||\n s(inv.invoiceNumber).includes(q) ||\n s(inv.notes).includes(q) ||\n s(inv.status).includes(q)\n );\n });\n }, [invoices, filterStatus, searchQuery]);\n\n \u002f** ================= Payment Link ================= *\u002f\n async function generatePaymentLink(inv) {\n try {\n const did = getCookie('connected_did');\n if (!did) {\n setSnackMsg('⚠️ Payment Kit not connected — connect it in your Dashboard to generate links.');\n setSnackOpen(true);\n return;\n }\n\n \u002f\u002f 🧩 normalize item list whether it's array or string\n let invoiceItems = [];\n if (Array.isArray(inv.items)) {\n invoiceItems = inv.items;\n } else if (typeof inv.items === 'string') {\n \u002f\u002f parse string like \"B-Boy Sticker (1 × $5), Caddy Sticker (2 × $5)\"\n invoiceItems = inv.items.split(',').map((x) =\u003e {\n const m = x.match(\u002f(.*)\\((\\d+)\\s*×\\s*\\$?([\\d.]+)\\)\u002f);\n return {\n name: m ? m[1].trim() : x.trim(),\n qty: m ? Number(m[2]) : 1,\n price: m ? Number(m[3]) : 0,\n };\n });\n }\n\n const lineItems = invoiceItems\n .map((it) =\u003e {\n const normalizedName = String(it?.name || '').trim().toLowerCase();\n\n \u002f\u002f find matching pricingKey from nameToPricingKey\n const matchedKey =\n Object.keys(nameToPricingKey).find(\n (k) =\u003e k.toLowerCase().trim() === normalizedName\n ) || normalizedName.replace(\u002f\\s+\u002fg, '').replace(\u002f[^a-z0-9]\u002fgi, '');\n\n \u002f\u002f get entry from priceTable\n const group = priceTable?.[nameToPricingKey[it.name]] || priceTable?.[matchedKey];\n if (!group) return null;\n\n const sizeKey = group['OneSize'] ? 'OneSize' : Object.keys(group)[0];\n \u002f\u002f prefer stored invoice currency over current selection\n const activeCurrency = inv.currency || currency;\n const entry = group?.[sizeKey]?.[activeCurrency];\n const priceId = entry?.priceId;\n\n return priceId ? { price_id: priceId, quantity: Number(it.qty || 1) } : null;\n })\n .filter(Boolean);\n\n if (!lineItems.length) {\n alert('No valid priceIds found for this invoice. Ensure item names match your products.json.');\n console.log('DEBUG: invoiceItems', invoiceItems);\n console.log('DEBUG: priceTable keys', Object.keys(priceTable));\n console.log('DEBUG: nameToPricingKey', nameToPricingKey);\n return;\n }\n\n setLinkLoading(true);\n await fetch('\u002fpayment-kit\u002f', { credentials: 'include', cache: 'no-store' });\n\n let csrf = getCsrf();\n if (!csrf) {\n await fetch(`\u002fapi\u002fsession?t=${Date.now()}`, { credentials: 'include', cache: 'no-store' });\n csrf = getCsrf();\n }\n\n const res = await fetch('\u002fpayment-kit\u002fapi\u002fcheckout-sessions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\u002fjson',\n 'x-csrf-token': csrf,\n },\n credentials: 'include',\n body: JSON.stringify({\n create_mine: true,\n mode: 'payment',\n line_items: lineItems,\n success_url: `${window.location.origin}?success=true`,\n cancel_url: `${window.location.origin}?cancel=true`,\n metadata: {\n source: 'InvoiceManager',\n invoiceNumber: inv.invoiceNumber || '',\n customer: inv.businessName || '',\n total: inv.total || '',\n currency,\n },\n }),\n });\n\n const out = await res.json();\n if (!out?.url) throw new Error('Failed to generate payment link');\n\n window.location.assign(out.url); \u002f\u002f ✅ mobile-safe redirect\n setSnackMsg('Redirecting to payment page…');\n setSnackOpen(true);\n } catch (err) {\n alert(err.message || 'Payment link error');\n } finally {\n setLinkLoading(false);\n }\n }\n\n\n \u002f** ================= Render (UI untouched) ================= *\u002f\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5', position: 'relative' }}\u003e\n\n {\u002f* Hidden badge (still runs warm-up in background) *\u002f}\n \u003cChip\n label={loggedIn ? 'Logged In' : 'Not Connected'}\n color={loggedIn ? 'success' : 'warning'}\n size=\"small\"\n onClick={handleBadgeClick}\n sx={{\n position: 'absolute',\n top: 12,\n right: 12,\n opacity: 0, \u002f\u002f 👈 invisible\n pointerEvents: 'none', \u002f\u002f 👈 can't click or tab to it\n zIndex: -1, \u002f\u002f 👈 behind everything\n }}\n \u002f\u003e\n\n {\u002f* FORM *\u002f}\n \u003cPaper\n sx={{\n p: 4,\n mb: 4,\n backgroundColor: isDark ? '#1e1e1e' : '#fff',\n color: isDark ? '#eee' : '#333',\n borderRadius: 2,\n }}\n \u003e\n \u003cTypography variant=\"h4\" sx={{ mb: 3, textAlign: 'center', fontWeight: 'bold', color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n \u003cStack spacing={2} component=\"form\" onSubmit={handleCreateOrUpdate}\u003e\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cSelect\n value={selectedCustomerId}\n onChange={(e) =\u003e setSelectedCustomerId(e.target.value)}\n displayEmpty\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Customer\u003c\u002fMenuItem\u003e\n {customers.map((c) =\u003e (\n \u003cMenuItem key={c.id} value={c.id}\u003e\n {c.businessName} {c.contactPerson ? `(${c.contactPerson})` : ''}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fSelect\u003e\n )}\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cSelect value={status} onChange={(e) =\u003e setStatus(e.target.value)} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"Draft\"\u003eDraft\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Sent\"\u003eSent\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Paid\"\u003ePaid\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Void\"\u003eVoid\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Due Date\" type=\"date\" value={toInputDate(dueDate)} onChange={(e) =\u003e setDueDate(e.target.value)} fullWidth {...datePadProps} sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Currency and Catalog Toggle (clean, unified look) *\u002f}\n \u003cGrid container spacing={2} sx={{ alignItems: 'center' }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField\n label=\"Currency\"\n select\n value={currency}\n onChange={(e) =\u003e !editingInvoice && setCurrency(e.target.value)} \u002f\u002f ✅ prevent change while editing\n fullWidth\n disabled={!!editingInvoice} \u002f\u002f ✅ gray it out when viewing\u002fediting old invoices\n sx={inputStyle}\n \u003e\n\n \u003cMenuItem value=\"USD\"\u003eUSD\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"USDC\"\u003eUSDC\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"ABT\"\u003eABT\u003c\u002fMenuItem\u003e\n \u003c\u002fTextField\u003e\n \u003c\u002fGrid\u003e\n\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cFormControlLabel\n control={\n \u003cSwitch\n checked={useStandard}\n onChange={(e) =\u003e setUseStandard(e.target.checked)}\n color=\"primary\"\n \u002f\u003e\n }\n label={\n \u003cTypography\n variant=\"subtitle2\"\n sx={{\n fontWeight: 600,\n color: isDark ? '#ccc' : '#444',\n letterSpacing: '0.03em',\n }}\n \u003e\n {useStandard ? 'Using Catalog Products' : 'Manual Entry Mode'}\n \u003c\u002fTypography\u003e\n }\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n {\u002f* Line Items *\u002f}\n \u003cBox\u003e\n \u003cTypography variant=\"h6\" sx={{ mb: 1.5, color: isDark ? '#ddd' : '#444' }}\u003e\n Line Items\n \u003c\u002fTypography\u003e\n \u003cStack spacing={2}\u003e\n {items.map((row, idx) =\u003e (\n \u003cPaper key={idx} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cGrid container spacing={1.5}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n {useStandard ? (\n \u003cSelect\n value={row.name || ''}\n onChange={(e) =\u003e {\n const displayName = e.target.value;\n const pricingKey =\n nameToPricingKey[displayName] ||\n displayName; \u002f\u002f fallback: key equals display name\n onSelectStandardItem(idx, pricingKey, displayName);\n }}\n fullWidth\n displayEmpty\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Product\u003c\u002fMenuItem\u003e\n {productsFlat.map((p) =\u003e {\n \u002f\u002f show current-currency amount using OneSize (or first size)\n const sizes = priceTable?.[p.pricingKey]\n ? Object.keys(priceTable[p.pricingKey])\n : ['OneSize'];\n const sizeKey = sizes.includes('OneSize') ? 'OneSize' : sizes[0];\n const entry = getPricingEntry(p.pricingKey, sizeKey, currency);\n const amt = entry?.amount;\n return (\n \u003cMenuItem key={`${p.pricingKey}-${p.name}`} value={p.name}\u003e\n {p.name}{amt != null ? ` (${amt})` : ''}\n \u003c\u002fMenuItem\u003e\n );\n })}\n \u003c\u002fSelect\u003e\n ) : (\n \u003cTextField\n label=\"Item Name\"\n value={row.name}\n onChange={(e) =\u003e setItemField(idx, 'name', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n )}\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Qty\"\n type=\"number\"\n value={row.qty}\n onChange={(e) =\u003e setItemField(idx, 'qty', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={6} sm={2}\u003e\n \u003cTextField\n label=\"Unit Price\"\n type=\"number\"\n value={row.price}\n onChange={(e) =\u003e setItemField(idx, 'price', e.target.value)}\n fullWidth\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={2}\u003e\n \u003cTextField\n label=\"Line Total\"\n value={(Number(row.qty || 0) * Number(row.price || 0)).toFixed(2)}\n fullWidth\n InputProps={{ readOnly: true }}\n sx={inputStyle}\n \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"flex-end\" sx={{ mt: 2 }}\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e removeItem(idx)} disabled={items.length \u003c= 1}\u003eRemove\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={addItem}\u003eAdd Item\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n\n \u003cGrid container spacing={2}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Discount ($)\" type=\"number\" value={discount} onChange={(e) =\u003e setDiscount(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cTextField label=\"Tax ($)\" type=\"number\" value={tax} onChange={(e) =\u003e setTax(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cTextField label=\"Notes\" value={notes} onChange={(e) =\u003e setNotes(e.target.value)} fullWidth multiline rows={3} sx={inputStyle} \u002f\u003e\n\n \u003cPaper sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${subtotal.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${total.toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 1 }}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingInvoice ? 'Update Invoice' : 'Create Invoice'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={resetForm} fullWidth\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* Dashboard *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: isDark ? '#1e1e1e' : '#fff', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h5\" sx={{ textAlign: 'center', mb: 2, color: '#1976d2', fontWeight: 'bold' }}\u003e\n Invoice Dashboard\n \u003c\u002fTypography\u003e\n\n {\u002f* Search + Filter *\u002f}\n \u003cTextField\n label=\"Search Invoices (name, number, status)\"\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mb: 2, ...inputStyle }}\n \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} justifyContent=\"center\" flexWrap=\"wrap\" mb={1.5}\u003e\n {['All', 'Draft', 'Sent', 'Paid', 'Void'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilterStatus(s)}\n sx={{\n cursor: 'pointer',\n backgroundColor: filterStatus === s ? '#1976d2' : '#e0e0e0',\n color: filterStatus === s ? '#fff' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fStack\u003e\n\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredInvoices.map((inv) =\u003e (\n \u003cBox key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa', borderRadius: 2 }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{inv.businessName}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eInvoice #: {inv.invoiceNumber}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(inv.createdDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e loadInvoiceToEdit(inv)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e openView(inv)}\u003eView\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n\n {\u002f* Payment Link (unchanged button, new internals) *\u002f}\n {paymentLink && (\n \u003cButton\n variant=\"outlined\"\n color=\"success\"\n onClick={() =\u003e generatePaymentLink(inv)}\n disabled={linkLoading}\n \u003e\n {linkLoading ? \u003cCircularProgress size={16} \u002f\u003e : 'Payment Link'}\n \u003c\u002fButton\u003e\n )}\n\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteInvoice(inv.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fBox\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n\n {\u002f* View Modal *\u002f}\n \u003cDialog open={viewOpen} onClose={closeView} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n View Invoice\n \u003cIconButton onClick={closeView} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {!viewInvoice ? (\n \u003cTypography\u003eLoading…\u003c\u002fTypography\u003e\n ) : (\n \u003cBox sx={{ maxWidth: 800, mx: 'auto' }}\u003e\n {icon && (\n \u003cBox sx={{ textAlign: 'center', mb: 1.5 }}\u003e\n \u003cimg src={icon} alt=\"Logo\" style={{ maxWidth: 160, maxHeight: 80 }} \u002f\u003e\n \u003c\u002fBox\u003e\n )}\n\n \u003cTypography variant=\"h5\" align=\"center\" sx={{ mb: 2, fontWeight: 700 }}\u003e\n Invoice\n \u003c\u002fTypography\u003e\n\n \u003cGrid container spacing={1.5} sx={{ mb: 1 }}\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eInvoice #\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.invoiceNumber || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eStatus\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.status || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eBusiness\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.businessName || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eContact\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.contactPerson || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCreated\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.createdDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eDue\u003c\u002fTypography\u003e\n \u003cTypography\u003e{displayDate(viewInvoice.dueDate) || '—'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003cGrid item xs={12} sm={6}\u003e\n \u003cPaper sx={{ p: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eCurrency\u003c\u002fTypography\u003e\n \u003cTypography\u003e{viewInvoice.currency || 'USD'}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n \u003c\u002fGrid\u003e\n \u003c\u002fGrid\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eItems\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.items || '')}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eSubtotal\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.subtotal || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eDiscount\u003c\u002fTypography\u003e\u003cTypography\u003e-${Number(viewInvoice.discount || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography\u003eTax\u003c\u002fTypography\u003e\u003cTypography\u003e${Number(viewInvoice.tax || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\"\u003e\u003cTypography fontWeight=\"bold\"\u003eTotal\u003c\u002fTypography\u003e\u003cTypography fontWeight=\"bold\"\u003e${Number(viewInvoice.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n\n {viewInvoice.notes && (\n \u003cPaper sx={{ p: 1.5, mb: 1.5 }}\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eNotes\u003c\u002fTypography\u003e\n \u003cTypography sx={{ whiteSpace: 'pre-wrap' }}\u003e{String(viewInvoice.notes)}\u003c\u002fTypography\u003e\n \u003c\u002fPaper\u003e\n )}\n\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {signature && (\n \u003cBox sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 1 }}\u003e\n \u003cimg src={signature} alt=\"Signature\" style={{ maxWidth: 200, maxHeight: 60 }} \u002f\u003e\n \u003cTypography variant=\"caption\" color=\"text.secondary\"\u003eAuthorized Signature\u003c\u002fTypography\u003e\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fBox\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n {\u002f* ✅ FIX: use && (not `and`) *\u002f}\n {viewInvoice && (\n \u003c\u003e\n \u003cButton onClick={() =\u003e printInvoice(viewInvoice)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadPDF(viewInvoice)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002f\u003e\n )}\n \u003cButton onClick={closeView}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar\n open={snackOpen}\n autoHideDuration={2200}\n onClose={() =\u003e setSnackOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\n \u003e\n \u003cAlert severity=\"success\" sx={{ width: '100%' }}\u003e{snackMsg}\u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Invoice Component"}}},"instances":{"l7e2123ym4bkj5lt":{"componentId":"m33bo8cpigjykb7e","locales":{"en":{"props":{"locale":"en","InvoiceComponent":{"type":"__RENDER_NESTED_COMPONENT__","componentId":"v2cd7vt374lahqc2","props":{"locale":"en"}}}}}}}}