`; w.document.write(html); w.document.close(); } }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => { const element = document.createElement('div'); element.innerHTML = `
${icon ? `` : ''}

${inv.invoiceNumber || 'Invoice'}

Total: ${inv.currency || 'USD'} ${Number(inv.total || 0).toFixed(2)}

`; const opt = { margin: 0.3, filename: `${inv.invoiceNumber || 'Invoice'}.pdf`, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, }; window.html2pdf().set(opt).from(element).save(); } }, "Download PDF"))))))))), 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 \u003ctr\u003e\u003ctd\u003eDiscount\u003c\u002ftd\u003e\u003ctd\u003e−${inv.currency || ' `; w.document.write(html); w.document.close(); } }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => { const element = document.createElement('div'); element.innerHTML = `
${icon ? `` : ''}

${inv.invoiceNumber || 'Invoice'}

Total: ${inv.currency || 'USD'} ${Number(inv.total || 0).toFixed(2)}

`; const opt = { margin: 0.3, filename: `${inv.invoiceNumber || 'Invoice'}.pdf`, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, }; window.html2pdf().set(opt).from(element).save(); } }, "Download PDF"))))))))), 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 \u003ctr\u003e\u003ctd\u003eTax\u003c\u002ftd\u003e\u003ctd\u003e${inv.currency || ' `; w.document.write(html); w.document.close(); } }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => { const element = document.createElement('div'); element.innerHTML = `
${icon ? `` : ''}

${inv.invoiceNumber || 'Invoice'}

Total: ${inv.currency || 'USD'} ${Number(inv.total || 0).toFixed(2)}

`; const opt = { margin: 0.3, filename: `${inv.invoiceNumber || 'Invoice'}.pdf`, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, }; window.html2pdf().set(opt).from(element).save(); } }, "Download PDF"))))))))), 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 \u003ctr class=\"total\"\u003e\n \u003ctd\u003eTotal\u003c\u002ftd\u003e\n \u003ctd\u003e\u003cb\u003e${inv.currency || ' `; w.document.write(html); w.document.close(); } }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => { const element = document.createElement('div'); element.innerHTML = `
${icon ? `` : ''}

${inv.invoiceNumber || 'Invoice'}

Total: ${inv.currency || 'USD'} ${Number(inv.total || 0).toFixed(2)}

`; const opt = { margin: 0.3, filename: `${inv.invoiceNumber || 'Invoice'}.pdf`, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, }; window.html2pdf().set(opt).from(element).save(); } }, "Download PDF"))))))))), 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\n \u003c\u002ftr\u003e\n\u003c\u002ftable\u003e\n ${signature ? `\u003cdiv class=\"signature\"\u003e\u003cimg src=\"${signature}\" alt=\"Signature\" \u002f\u003e\u003csmall\u003eAuthorized Signature\u003c\u002fsmall\u003e\u003c\u002fdiv\u003e` : ''}\n \u003cscript\u003ewindow.onload = () =\u003e window.print()\u003c\\\\\u002fscript\u003e\n \u003c\u002fbody\u003e\u003c\u002fhtml\u003e`;\n w.document.write(html);\n w.document.close();\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 Manager V3 with Payment kit","properties":{"9x0wbbfdjy7pctmz":{"index":0,"data":{"id":"9x0wbbfdjy7pctmz","shared":false,"key":"script","locales":{"en":{"defaultValue":""}}}},"2epvddvnzhcnq232":{"index":1,"data":{"id":"2epvddvnzhcnq232","shared":false,"key":"icon","locales":{"en":{"defaultValue":"https:\u002f\u002f12inchapps.com\u002fimage-bin\u002fuploads\u002f8550331027194bb01dc795c69d8683a8.png"}}}}},"llmConfig":{"properties":{"9x0wbbfdjy7pctmz":{"key":"script","isNeedGenerate":true,"describe":"","displayName":"script"},"2epvddvnzhcnq232":{"key":"icon","isNeedGenerate":true,"describe":"","displayName":"icon"}}}}},"rspvu6e7i200qiu3":{"index":5,"data":{"id":"rspvu6e7i200qiu3","createdAt":"2025-10-12T00:48:05.371Z","updatedAt":"2025-10-12T00:48:05.371Z","renderer":{"type":"react-component","script":"import React, { useState, useEffect, useRef } from '@blocklet\u002fpages-kit\u002fbuiltin\u002freact';\nimport {\n Box,\n Paper,\n Typography,\n TextField,\n Button,\n Stack,\n Select,\n MenuItem,\n Snackbar,\n Alert,\n Chip,\n CircularProgress,\n Collapse,\n useTheme,\n} from '@blocklet\u002fpages-kit\u002fbuiltin\u002fmui\u002fmaterial';\n\nexport default function LeadManager({\n title = 'Lead Manager',\n script = '', \u002f\u002f \u003c-- your deployed Google Apps Script \u002fexec URL\n icon = '',\n signature = '',\n}) {\n const theme = useTheme();\n const isDark = theme.palette.mode === 'dark';\n\n \u002f\u002f ------- UI + session state -------\n const [reloadSignal, setReloadSignal] = useState(0);\n const [showContract, setShowContract] = useState(false);\n const [contractId, setContractId] = useState(null);\n const [snackOpen, setSnackOpen] = useState(false);\n const [submitting, setSubmitting] = useState(false);\n\n \u002f\u002f Search collapse remember\n const SEARCH_KEY = 'leadmgr.searchOpen';\n const [searchOpen, setSearchOpen] = useState(() =\u003e {\n const saved = localStorage.getItem(SEARCH_KEY);\n return saved ? saved === '1' : true;\n });\n useEffect(() =\u003e {\n localStorage.setItem(SEARCH_KEY, searchOpen ? '1' : '0');\n }, [searchOpen]);\n\n \u002f\u002f ------- helpers -------\n const displayDate = (val) =\u003e {\n if (!val) return '';\n const d = new Date(val);\n return Number.isNaN(d.getTime()) ? String(val) : d.toLocaleDateString('en-US');\n };\n const toInputDate = (val) =\u003e {\n if (!val) return '';\n const d = new Date(val);\n return Number.isNaN(d.getTime()) ? '' : d.toISOString().split('T')[0];\n };\n\n const getContactLink = (v) =\u003e {\n const val = String(v ?? '').trim();\n if (!val) return { href: null, label: '', kind: null };\n if (val.includes('@')) return { href: `mailto:${val}`, label: val, kind: 'email' };\n const digits = val.replace(\u002f[^\\d+]\u002fg, '');\n return { href: digits.length \u003e= 7 ? `tel:${digits}` : null, label: val, kind: 'phone' };\n };\n\n const openContact = (href) =\u003e {\n try {\n window.open(href, '_top');\n } catch {\n alert('Please allow popups to call or email directly.');\n }\n };\n\n \u002f\u002f ------- form state -------\n const [form, setForm] = useState({\n businessName: '',\n contactPerson: '',\n phoneOrEmail: '',\n status: 'New',\n notes: '',\n followUpDate: '',\n created: '',\n product: '',\n estimate: '',\n });\n const handleFormChange = (e) =\u003e {\n const { name, value } = e.target;\n setForm((f) =\u003e ({ ...f, [name]: value }));\n };\n\n const [editingId, setEditingId] = useState(null);\n const [editData, setEditData] = useState({});\n\n \u002f\u002f ------- styles -------\n const cardBg = isDark ? '#1c1c1c' : '#fff';\n const textCol = isDark ? '#e8e8e8' : '#2d2d2d';\n const inputStyle = {\n '& .MuiOutlinedInput-root': {\n '& fieldset': { borderColor: isDark ? '#4a4a4a' : '#d8d8d8' },\n '&:hover fieldset': { borderColor: isDark ? '#7a7a7a' : '#bdbdbd' },\n '&.Mui-focused fieldset': { borderColor: '#1976d2' },\n },\n '& .MuiInputBase-input': {\n color: textCol,\n backgroundColor: isDark ? '#171717' : '#f8f8f8',\n borderRadius: 1.2,\n py: 1.25,\n px: 1,\n },\n };\n const padDateInput = {\n InputLabelProps: { shrink: true },\n InputProps: {\n sx: {\n py: 1.25,\n color: textCol,\n backgroundColor: isDark ? '#171717' : '#f8f8f8',\n borderRadius: 1.2,\n },\n },\n };\n\n \u002f\u002f ------- data load (JSONP) -------\n const [leads, setLeads] = useState([]);\n const [loading, setLoading] = useState(true);\n const [filter, setFilter] = useState('All');\n const [searchQuery, setSearchQuery] = useState('');\n\n const loadLeads = () =\u003e {\n if (!script) {\n setLoading(false);\n return;\n }\n setLoading(true);\n const CALLBACK = 'leadDataCallback';\n const old = document.getElementById(CALLBACK);\n if (old) old.remove();\n\n window[CALLBACK] = (data) =\u003e {\n setLeads(Array.isArray(data) ? data.reverse() : []);\n setLoading(false);\n try {\n delete window[CALLBACK];\n } catch { }\n };\n\n const scriptEl = document.createElement('script');\n scriptEl.id = CALLBACK;\n \u002f\u002f IMPORTANT: include sheet=Leads so doGet knows which sheet\n scriptEl.src = `${script}?sheet=Leads&callback=${CALLBACK}`;\n document.body.appendChild(scriptEl);\n };\n useEffect(() =\u003e loadLeads(), [reloadSignal]);\n\n \u002f\u002f ------- submit \u002f update \u002f delete (no-duplicate logic) -------\n const handleFormSubmit = async (e) =\u003e {\n e.preventDefault();\n if (submitting) return; \u002f\u002f double-click guard\n if (!script) return alert('Add your Google Apps Script endpoint via the \"script\" prop.');\n if (!form.businessName) return alert('Business name required');\n\n setSubmitting(true);\n const payload = {\n sheet: 'Leads',\n action: editingId ? 'update' : 'create',\n id: editingId || '',\n ...form,\n created: form.created || new Date().toISOString().split('T')[0],\n };\n\n try {\n await fetch(script, {\n method: 'POST',\n headers: { 'Content-Type': 'application\u002fjson' },\n body: JSON.stringify(payload),\n mode: 'no-cors',\n });\n setSnackOpen(true);\n \u002f\u002f reset\n setForm({\n businessName: '',\n contactPerson: '',\n phoneOrEmail: '',\n status: 'New',\n notes: '',\n followUpDate: '',\n created: '',\n product: '',\n estimate: '',\n });\n setEditingId(null);\n setEditData({});\n \u002f\u002f give the sheet a beat, then reload\n setTimeout(() =\u003e setReloadSignal((r) =\u003e r + 1), 800);\n } finally {\n setSubmitting(false);\n }\n };\n\n const saveEdit = async (lead) =\u003e {\n if (submitting) return;\n setSubmitting(true);\n const payload = { sheet: 'Leads', action: 'update', id: lead.id, ...editData };\n try {\n await fetch(script, {\n method: 'POST',\n headers: { 'Content-Type': 'application\u002fjson' },\n body: JSON.stringify(payload),\n mode: 'no-cors',\n });\n setEditingId(null);\n setEditData({});\n setTimeout(loadLeads, 800);\n } finally {\n setSubmitting(false);\n }\n };\n\n const deleteLead = async (id) =\u003e {\n if (!window.confirm('Delete this lead?')) return;\n await fetch(script, {\n method: 'POST',\n headers: { 'Content-Type': 'application\u002fjson' },\n body: JSON.stringify({ sheet: 'Leads', action: 'delete', id }),\n mode: 'no-cors',\n });\n setTimeout(loadLeads, 800);\n };\n\n \u002f\u002f ------- Contract View (signature pad + PDF) -------\n const ContractView = ({ id }) =\u003e {\n const [contractText, setContractText] = useState('');\n const [isLoading, setIsLoading] = useState(true);\n const padRef = useRef(null);\n const SigModRef = useRef(null);\n const canvasRef = useRef(null);\n const wrapRef = useRef(null);\n\n \u002f\u002f Build contract text from in-memory lead\n useEffect(() =\u003e {\n const lead = leads.find((l) =\u003e l.id === id);\n if (lead) {\n const today = new Date().toLocaleDateString('en-US');\n const base = `\nCONTRACT AGREEMENT\n-----------------------------\nClient: ${lead.businessName}\nContact: ${lead.contactPerson} (${lead.phoneOrEmail})\nProduct: ${lead.product || '—'}\nEstimate: ${lead.estimate || '—'}\nStatus: Sold\nDate: ${today}\n\nTerms:\n- 50% deposit due before work begins.\n- Remaining balance due upon delivery.\n- Includes one revision round.\n\nSigned,\nYour Company\n`.trim();\n setContractText(base);\n }\n setIsLoading(false);\n }, [id, leads]);\n\n \u002f\u002f Signature pad responsive setup\n useEffect(() =\u003e {\n let resizeObs;\n let canceled = false;\n\n const scaleCanvas = () =\u003e {\n const canvas = canvasRef.current;\n const wrap = wrapRef.current;\n if (!canvas || !wrap) return false;\n const cssW = wrap.clientWidth || 0;\n const cssH = 130;\n if (cssW \u003c= 0) return false;\n\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n const existing = padRef.current && !padRef.current.isEmpty() ? padRef.current.toData() : null;\n canvas.width = Math.floor(cssW * ratio);\n canvas.height = Math.floor(cssH * ratio);\n canvas.style.width = `${cssW}px`;\n canvas.style.height = `${cssH}px`;\n\n const ctx = canvas.getContext('2d');\n ctx.scale(ratio, ratio);\n\n if (!padRef.current && SigModRef.current) {\n const SignaturePad = SigModRef.current;\n padRef.current = new SignaturePad(canvas, { penColor: '#1976d2', backgroundColor: 'rgba(0,0,0,0)' });\n }\n if (padRef.current && existing) {\n try {\n padRef.current.fromData(existing);\n } catch { }\n }\n return true;\n };\n\n const init = async (tries = 0) =\u003e {\n if (canceled) return;\n if (!SigModRef.current) {\n const mod = await import('https:\u002f\u002fcdn.jsdelivr.net\u002fnpm\u002fsignature_pad@4.1.7\u002f+esm');\n SigModRef.current = mod.default || mod;\n }\n requestAnimationFrame(() =\u003e {\n const ok = scaleCanvas();\n if (!ok && tries \u003c 40) setTimeout(() =\u003e init(tries + 1), 100);\n });\n };\n\n init();\n const onResize = () =\u003e scaleCanvas();\n window.addEventListener('resize', onResize);\n if ('ResizeObserver' in window && wrapRef.current) {\n resizeObs = new ResizeObserver(() =\u003e scaleCanvas());\n resizeObs.observe(wrapRef.current);\n }\n return () =\u003e {\n canceled = true;\n window.removeEventListener('resize', onResize);\n if (resizeObs) resizeObs.disconnect();\n };\n }, []);\n\n const handleDownload = async () =\u003e {\n const { jsPDF } = await import('https:\u002f\u002fcdn.skypack.dev\u002fjspdf');\n const doc = new jsPDF({ unit: 'pt', format: 'letter' });\n\n if (icon) {\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.src = icon;\n await new Promise((r) =\u003e (img.onload = r));\n const pageW = doc.internal.pageSize.getWidth();\n const scale = Math.min(160 \u002f img.width, 80 \u002f img.height);\n doc.addImage(img, 'PNG', (pageW - img.width * scale) \u002f 2, 30, img.width * scale, img.height * scale);\n }\n\n doc.setFont('helvetica', 'bold');\n doc.setFontSize(16);\n doc.text('Contract Agreement', 306, 120, { align: 'center' });\n\n doc.setFont('helvetica', 'normal');\n const wrapped = doc.splitTextToSize(contractText, 540);\n doc.text(wrapped, 36, 160);\n\n if (padRef.current && !padRef.current.isEmpty()) {\n const sigDataUrl = padRef.current.toDataURL('image\u002fpng');\n doc.addImage(sigDataUrl, 'PNG', 60, 640, 200, 60);\n }\n if (signature) {\n const sigImg = new Image();\n sigImg.crossOrigin = 'anonymous';\n sigImg.src = signature;\n await new Promise((r) =\u003e (sigImg.onload = r));\n doc.addImage(sigImg, 'PNG', 360, 640, 200, 60);\n }\n\n doc.text('Client Signature', 60, 720);\n doc.text('Company Signature', 360, 720);\n doc.text(`Date: ${new Date().toLocaleDateString()}`, 60, 740);\n doc.save('Contract.pdf');\n };\n\n const handleClear = () =\u003e padRef.current?.clear();\n\n if (isLoading) return \u003cCircularProgress sx={{ m: 4 }} \u002f\u003e;\n\n return (\n \u003cPaper sx={{ p: 4, mt: 3, backgroundColor: cardBg }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\" mb={2}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 700, color: '#1976d2' }}\u003e\n Contract Agreement\n \u003c\u002fTypography\u003e\n \u003cButton onClick={() =\u003e setShowContract(false)}\u003eBack\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003cTextField\n multiline\n fullWidth\n rows={16}\n value={contractText}\n onChange={(e) =\u003e setContractText(e.target.value)}\n sx={{ mb: 2 }}\n InputProps={{\n style: { fontFamily: 'monospace', backgroundColor: isDark ? '#171717' : '#f8f8f8' },\n }}\n \u002f\u003e\n \u003cTypography sx={{ mb: 1, fontWeight: 600, color: '#1976d2' }}\u003eDraw Client Signature:\u003c\u002fTypography\u003e\n \u003cdiv ref={wrapRef}\u003e\n \u003ccanvas\n ref={canvasRef}\n style={{\n width: '100%',\n height: 130,\n border: '1px solid #ccc',\n borderRadius: 6,\n backgroundColor: isDark ? '#111' : '#f9f9f9',\n }}\n \u002f\u003e\n \u003c\u002fdiv\u003e\n \u003cStack direction=\"row\" spacing={1.5} sx={{ mt: 2, flexWrap: 'wrap' }}\u003e\n \u003cButton variant=\"outlined\" onClick={handleClear}\u003eClear\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={handleDownload}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n );\n };\n\n \u002f\u002f ------- UI -------\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#121212' : '#f4f6f8', minHeight: '100%' }}\u003e\n {!showContract ? (\n \u003c\u003e\n {\u002f* FORM *\u002f}\n \u003cPaper sx={{ p: 4, mb: 4, backgroundColor: cardBg }}\u003e\n \u003cTypography variant=\"h4\" align=\"center\" sx={{ color: '#1976d2', fontWeight: 800, mb: 3 }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n \u003cform onSubmit={handleFormSubmit}\u003e\n \u003cStack spacing={2}\u003e\n \u003cTextField name=\"businessName\" label=\"Business Name\" value={form.businessName} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField name=\"contactPerson\" label=\"Contact Person\" value={form.contactPerson} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField name=\"phoneOrEmail\" label=\"Phone or Email\" value={form.phoneOrEmail} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2}\u003e\n \u003cTextField name=\"product\" label=\"Product\" value={form.product} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField name=\"estimate\" label=\"Estimate ($)\" type=\"number\" value={form.estimate} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003c\u002fStack\u003e\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2}\u003e\n \u003cTextField name=\"created\" label=\"Created Date\" type=\"date\" value={toInputDate(form.created)} onChange={handleFormChange} fullWidth {...padDateInput} \u002f\u003e\n \u003cTextField name=\"followUpDate\" label=\"Follow-Up Date\" type=\"date\" value={toInputDate(form.followUpDate)} onChange={handleFormChange} fullWidth {...padDateInput} \u002f\u003e\n \u003c\u002fStack\u003e\n \u003cSelect name=\"status\" value={form.status} onChange={handleFormChange} fullWidth sx={inputStyle}\u003e\n \u003cMenuItem value=\"New\"\u003eNew\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"In Progress\"\u003eIn Progress\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Closed\"\u003eClosed\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n \u003cTextField name=\"notes\" label=\"Notes\" multiline rows={3} value={form.notes} onChange={handleFormChange} fullWidth sx={inputStyle} \u002f\u003e\n \u003cButton type=\"submit\" variant=\"contained\" disabled={submitting} sx={{ borderRadius: 2 }}\u003e\n {editingId ? 'Update Lead' : 'Submit Lead'}\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fform\u003e\n \u003c\u002fPaper\u003e\n\n {\u002f* DASHBOARD *\u002f}\n \u003cPaper sx={{ p: 3, backgroundColor: cardBg }}\u003e\n \u003cStack direction=\"row\" justifyContent=\"space-between\" alignItems=\"center\" mb={1}\u003e\n \u003cTypography variant=\"h5\" sx={{ color: '#1976d2', fontWeight: 700 }}\u003e\n Lead Dashboard\n \u003c\u002fTypography\u003e\n \u003cButton\n variant=\"outlined\"\n onClick={() =\u003e setSearchOpen((v) =\u003e !v)}\n sx={{ borderRadius: 2, px: 2.25, fontWeight: 600 }}\n \u003e\n \u003cspan role=\"img\" aria-label=\"search\"\u003e🔍\u003c\u002fspan\u003e Search\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n\n \u003cCollapse in={searchOpen}\u003e\n \u003cBox sx={{ pb: 2 }}\u003e\n \u003cTextField\n label=\"Search Leads\"\n variant=\"outlined\"\n size=\"small\"\n fullWidth\n value={searchQuery}\n onChange={(e) =\u003e setSearchQuery(e.target.value)}\n sx={{ mb: 1.5, ...inputStyle }}\n \u002f\u003e\n \u003cBox sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}\u003e\n {['All', 'New', 'In Progress', 'Closed'].map((s) =\u003e (\n \u003cChip\n key={s}\n label={s}\n onClick={() =\u003e setFilter(s)}\n sx={{\n fontWeight: 600,\n borderRadius: 1.5,\n backgroundColor: filter === s ? '#1976d2' : isDark ? '#2a2a2a' : '#e8e8e8',\n color: filter === s ? '#fff' : isDark ? '#e6e6e6' : '#333',\n }}\n \u002f\u003e\n ))}\n \u003c\u002fBox\u003e\n \u003c\u002fBox\u003e\n \u003c\u002fCollapse\u003e\n\n {loading ? (\n \u003cCircularProgress sx={{ mx: 'auto', my: 3, display: 'block' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {leads\n .filter((l) =\u003e (filter === 'All' ? true : String(l.status) === filter))\n .filter((l) =\u003e {\n const q = (searchQuery || '').toLowerCase().trim();\n if (!q) return true;\n const safe = (v) =\u003e String(v ?? '').toLowerCase();\n return [l.businessName, l.contactPerson, l.phoneOrEmail, l.product, l.status, l.notes].some((x) =\u003e\n safe(x).includes(q)\n );\n })\n .map((lead) =\u003e {\n const contact = getContactLink(lead.phoneOrEmail);\n const isEditing = editingId === lead.id;\n return (\n \u003cPaper key={lead.id} sx={{ p: 2, backgroundColor: isDark ? '#1b1b1b' : '#fafafa', borderRadius: 2 }}\u003e\n {isEditing ? (\n \u003cStack spacing={1.2}\u003e\n {['businessName', 'contactPerson', 'phoneOrEmail', 'product', 'estimate', 'notes', 'created', 'followUpDate'].map((field) =\u003e (\n \u003cTextField\n key={field}\n label={field.replace(\u002f([A-Z])\u002fg, ' $1')}\n type={field.includes('Date') ? 'date' : field === 'estimate' ? 'number' : 'text'}\n value={\n field.includes('Date')\n ? toInputDate(editData[field] ?? lead[field] ?? '')\n : editData[field] ?? lead[field] ?? ''\n }\n onChange={(e) =\u003e setEditData((d) =\u003e ({ ...d, [field]: e.target.value }))}\n {...(field.includes('Date') ? padDateInput : { sx: inputStyle })}\n \u002f\u003e\n ))}\n\n \u003cSelect\n value={editData.status ?? lead.status ?? 'New'}\n onChange={(e) =\u003e setEditData((d) =\u003e ({ ...d, status: e.target.value }))}\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"New\"\u003eNew\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"In Progress\"\u003eIn Progress\u003c\u002fMenuItem\u003e\n \u003cMenuItem value=\"Closed\"\u003eClosed\u003c\u002fMenuItem\u003e\n \u003c\u002fSelect\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1}\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e saveEdit(lead)} disabled={submitting}\u003e\n Save\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e setEditingId(null)}\u003e\n Cancel\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n ) : (\n \u003c\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 800 }}\u003e\n {lead.businessName}\n \u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003e{lead.contactPerson}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\" sx={{ mb: 0.5 }}\u003e\n {contact.href ? (\n \u003ca href={contact.href} style={{ color: '#1976d2', textDecoration: 'none' }}\u003e\n {contact.label}\n \u003c\u002fa\u003e\n ) : (\n \u003cspan style={{ color: '#999' }}\u003eNo contact info\u003c\u002fspan\u003e\n )}\n \u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eProduct: {lead.product}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eEstimate: ${lead.estimate}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eStatus: {lead.status}\u003c\u002fTypography\u003e\n \u003cTypography\n variant=\"body2\"\n sx={{ wordBreak: 'break-word', whiteSpace: 'pre-wrap', mb: 0.5, lineHeight: 1.5 }}\n \u003e\n Notes:{' '}\n {String(lead.notes || '')\n .split(\u002f(\\s+)\u002f)\n .map((word, i) =\u003e {\n const urlMatch = \u002f^(https?:\\\u002f\\\u002f[^\\s]+|www\\.[^\\s]+)\u002fi.exec(word);\n if (urlMatch) {\n const url = word.startsWith('http') ? word : `https:\u002f\u002f${word}`;\n return (\n \u003ca\n key={i}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{ color: '#1976d2', textDecoration: 'none', fontWeight: 500 }}\n \u003e\n {word}\n \u003c\u002fa\u003e\n );\n }\n return word;\n })}\n \u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eCreated: {displayDate(lead.created)}\u003c\u002fTypography\u003e\n \u003cTypography variant=\"body2\"\u003eFollow-Up: {displayDate(lead.followUpDate)}\u003c\u002fTypography\u003e\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ mt: 1 }}\u003e\n \u003cButton\n variant=\"outlined\"\n onClick={() =\u003e {\n setEditingId(lead.id);\n setEditData(lead);\n }}\n \u003e\n Edit\n \u003c\u002fButton\u003e\n \u003cButton\n variant=\"contained\"\n onClick={() =\u003e {\n setShowContract(true);\n setContractId(lead.id);\n }}\n \u003e\n View Contract\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteLead(lead.id)}\u003e\n Delete\n \u003c\u002fButton\u003e\n {contact.kind === 'phone' && contact.href && (\n \u003cButton variant=\"text\" onClick={() =\u003e openContact(contact.href)}\u003e\n Call\n \u003c\u002fButton\u003e\n )}\n {contact.kind === 'email' && contact.href && (\n \u003cButton variant=\"text\" onClick={() =\u003e openContact(contact.href)}\u003e\n Email\n \u003c\u002fButton\u003e\n )}\n \u003c\u002fStack\u003e\n \u003c\u002f\u003e\n )}\n \u003c\u002fPaper\u003e\n );\n })}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fPaper\u003e\n \u003c\u002f\u003e\n ) : (\n \u003cContractView id={contractId} \u002f\u003e\n )}\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\n Lead saved successfully\n \u003c\u002fAlert\u003e\n \u003c\u002fSnackbar\u003e\n \u003c\u002fBox\u003e\n );\n}\n"},"name":"Lead Manager V3","properties":{"334nh4n2e1h0plc4":{"index":2,"data":{"id":"334nh4n2e1h0plc4","shared":false,"key":"signature","locales":{"en":{"defaultValue":"https:\u002f\u002f12inchapps.com\u002fimage-bin\u002fuploads\u002f4360e972d456f4a6e9757b93614ba690.png"}}}},"dpgyqfqic0fqo0sm":{"index":1,"data":{"id":"dpgyqfqic0fqo0sm","shared":false,"key":"icon","locales":{"en":{"defaultValue":"https:\u002f\u002f12inchapps.com\u002fimage-bin\u002fuploads\u002f8550331027194bb01dc795c69d8683a8.png"}}}},"jxvib2kpy1uao9zy":{"index":0,"data":{"id":"jxvib2kpy1uao9zy","shared":false,"key":"script","locales":{"en":{"defaultValue":""}}}}},"description":"fixed duplicate. Contract good. Email button works. "}},"fyaovsotxlvag4iu":{"index":6,"data":{"id":"fyaovsotxlvag4iu","createdAt":"2025-10-12T15:42:52.015Z","updatedAt":"2025-10-12T15:42:52.015Z","renderer":{"type":"react-component","script":"import React, { useEffect, useState, useMemo } from '@blocklet\u002fpages-kit\u002fbuiltin\u002freact';\nimport {\n Box,\n Paper,\n Typography,\n TextField,\n Button,\n Stack,\n Snackbar,\n Alert,\n CircularProgress,\n Divider,\n useTheme,\n Dialog,\n DialogTitle,\n DialogContent,\n DialogActions,\n IconButton,\n MenuItem,\n} from '@blocklet\u002fpages-kit\u002fbuiltin\u002fmui\u002fmaterial';\n\nexport default function CustomerMaintenance({\n title = 'Customer Manager',\n script = '',\n icon = '',\n signature = '',\n}) {\n const theme = useTheme();\n const isDark = theme.palette.mode === 'dark';\n\n \u002f\u002f Load html2pdf.js once\n useEffect(() =\u003e {\n const s = document.createElement('script');\n s.src = 'https:\u002f\u002fcdnjs.cloudflare.com\u002fajax\u002flibs\u002fhtml2pdf.js\u002f0.10.1\u002fhtml2pdf.bundle.min.js';\n s.async = true;\n document.body.appendChild(s);\n }, []);\n\n \u002f\u002f ---------- Styles ----------\n const inputStyle = {\n '& .MuiOutlinedInput-root': {\n '& fieldset': { borderColor: isDark ? '#555' : '#e0e0e0' },\n '&:hover fieldset': { borderColor: isDark ? '#888' : '#bdbdbd' },\n '&.Mui-focused fieldset': { borderColor: '#1976d2' },\n },\n '& .MuiInputLabel-root': { color: isDark ? '#ccc' : '#555' },\n '& .MuiInputBase-input': {\n color: isDark ? '#eee' : '#333',\n backgroundColor: isDark ? '#222' : '#fafafa',\n borderRadius: 1,\n paddingLeft: 1,\n },\n };\n\n \u002f\u002f ---------- Helpers ----------\n const getContactLink = (v) =\u003e {\n const val = String(v ?? '').trim();\n if (!val) return { href: null, label: '', kind: null };\n if (val.includes('@')) return { href: `mailto:${val}`, label: val, kind: 'email' };\n const digits = val.replace(\u002f[^\\d+]\u002fg, '');\n return { href: digits.length \u003e= 7 ? `tel:${digits}` : null, label: val, kind: 'phone' };\n };\n\n const openContact = (href) =\u003e {\n try {\n window.open(href, '_top');\n } catch (err) {\n console.error('Failed to open contact link:', err);\n alert('Unable to open contact link — please allow popups for this site.');\n }\n };\n\n const getWebsiteLink = (url) =\u003e {\n if (!url) return null;\n return url.startsWith('http') ? url : `https:\u002f\u002f${url}`;\n };\n\n const displayDate = (val) =\u003e {\n if (!val) return '';\n const d = new Date(val);\n return isNaN(d) ? String(val) : d.toLocaleDateString('en-US');\n };\n\n \u002f\u002f ---------- JSONP ----------\n const jsonpLoad = (url, sheet = 'Data') =\u003e\n new Promise((resolve) =\u003e {\n try {\n const cb = `cb_${sheet}_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;\n const id = `jsonp_${cb}`;\n const old = document.getElementById(id);\n if (old) old.remove();\n\n window[cb] = (data) =\u003e {\n resolve(data);\n delete window[cb];\n const el = document.getElementById(id);\n if (el) el.remove();\n };\n\n const s = document.createElement('script');\n s.id = id;\n s.src = `${url}${url.includes('?') ? '&' : '?'}callback=${cb}`;\n document.body.appendChild(s);\n } catch {\n resolve([]);\n }\n });\n\n \u002f\u002f ---------- State ----------\n const [customers, setCustomers] = useState([]);\n const [leads, setLeads] = useState([]);\n const [invoices, setInvoices] = useState([]);\n const [loadingCustomers, setLoadingCustomers] = useState(true);\n const [loadingInvoices, setLoadingInvoices] = useState(true);\n const [loadingLeads, setLoadingLeads] = useState(true);\n const [reloadSignal, setReloadSignal] = useState(0);\n const [search, setSearch] = useState('');\n\n const [businessName, setBusinessName] = useState('');\n const [contactPerson, setContactPerson] = useState('');\n const [phoneOrEmail, setPhoneOrEmail] = useState('');\n const [website, setWebsite] = useState('');\n const [selectedLeadId, setSelectedLeadId] = useState('');\n const [editingCustomer, setEditingCustomer] = useState(null);\n\n const [snackMsg, setSnackMsg] = useState('');\n const [snackOpen, setSnackOpen] = useState(false);\n\n const [invoiceDialogOpen, setInvoiceDialogOpen] = useState(false);\n const [selectedInvoices, setSelectedInvoices] = useState([]);\n\n \u002f\u002f ---------- Loaders ----------\n const loadCustomers = async () =\u003e {\n setLoadingCustomers(true);\n const data = await jsonpLoad(`${script}?sheet=Customers`, 'Customers');\n setCustomers(Array.isArray(data) ? data : []);\n setLoadingCustomers(false);\n };\n\n const loadInvoices = async () =\u003e {\n setLoadingInvoices(true);\n const data = await jsonpLoad(`${script}?sheet=Invoices`, 'Invoices');\n setInvoices(Array.isArray(data) ? data.reverse() : []);\n setLoadingInvoices(false);\n };\n\n const loadLeads = async () =\u003e {\n setLoadingLeads(true);\n const data = await jsonpLoad(`${script}?sheet=Leads`, 'Leads');\n setLeads(Array.isArray(data) ? data : []);\n setLoadingLeads(false);\n };\n\n useEffect(() =\u003e {\n if (!script) return;\n loadCustomers();\n loadInvoices();\n loadLeads();\n }, [script, reloadSignal]);\n\n \u002f\u002f ---------- Open totals ----------\n const openTotals = useMemo(() =\u003e {\n const map = {};\n invoices.forEach((inv) =\u003e {\n const id = String(inv.customerId);\n if (inv.status !== 'Paid') {\n map[id] = (map[id] || 0) + Number(inv.total || 0);\n }\n });\n return map;\n }, [invoices]);\n\n const filteredCustomers = useMemo(() =\u003e {\n const q = search.toLowerCase();\n if (!q) return customers;\n return customers.filter(\n (c) =\u003e\n String(c.businessName || '').toLowerCase().includes(q) ||\n String(c.contactPerson || '').toLowerCase().includes(q) ||\n String(c.phoneOrEmail || '').toLowerCase().includes(q)\n );\n }, [customers, search]);\n\n \u002f\u002f ---------- CRUD ----------\n const handleSubmit = async (e) =\u003e {\n e.preventDefault();\n if (!businessName.trim()) return alert('Enter a business name.');\n\n const payload = {\n sheet: 'Customers',\n action: editingCustomer ? 'update' : 'create',\n id: editingCustomer ? editingCustomer.id : undefined,\n businessName,\n contactPerson,\n phoneOrEmail,\n website,\n };\n\n await fetch(script, {\n method: 'POST',\n headers: { 'Content-Type': 'application\u002fjson' },\n mode: 'no-cors',\n body: JSON.stringify(payload),\n });\n\n setSnackMsg(editingCustomer ? '✅ Customer updated' : '✅ Customer added');\n setSnackOpen(true);\n resetForm();\n setTimeout(() =\u003e setReloadSignal((n) =\u003e n + 1), 600);\n };\n\n const resetForm = () =\u003e {\n setBusinessName('');\n setContactPerson('');\n setPhoneOrEmail('');\n setWebsite('');\n setSelectedLeadId('');\n setEditingCustomer(null);\n };\n\n const editCustomer = (c) =\u003e {\n setEditingCustomer(c);\n setBusinessName(c.businessName || '');\n setContactPerson(c.contactPerson || '');\n setPhoneOrEmail(c.phoneOrEmail || '');\n setWebsite(c.website || '');\n window.scrollTo({ top: 0, behavior: 'smooth' });\n };\n\n const deleteCustomer = async (id) =\u003e {\n if (!window.confirm('Delete this customer?')) return;\n await fetch(script, {\n method: 'POST',\n headers: { 'Content-Type': 'application\u002fjson' },\n mode: 'no-cors',\n body: JSON.stringify({ sheet: 'Customers', action: 'delete', id }),\n });\n setTimeout(() =\u003e setReloadSignal((n) =\u003e n + 1), 600);\n };\n\n const createFromLead = (leadId) =\u003e {\n const lead = leads.find((l) =\u003e String(l.id) === String(leadId));\n if (!lead) return;\n setBusinessName(lead.businessName || '');\n setContactPerson(lead.contactPerson || '');\n setPhoneOrEmail(lead.phoneOrEmail || '');\n setWebsite(lead.website || '');\n };\n\n \u002f\u002f ---------- Invoices ----------\n const openInvoiceDialog = (customerId) =\u003e {\n const list = invoices.filter((inv) =\u003e String(inv.customerId) === String(customerId));\n setSelectedInvoices(list);\n setInvoiceDialogOpen(true);\n };\n const closeInvoiceDialog = () =\u003e {\n setInvoiceDialogOpen(false);\n setSelectedInvoices([]);\n };\n\n \u002f\u002f ✅ Print + PDF just like Invoice Manager\n const printInvoice = (inv) =\u003e {\n const w = window.open('', '_blank');\n if (!w) return alert('Pop-ups blocked.');\n\n const formattedDate = (date) =\u003e {\n if (!date) return '';\n const d = new Date(date);\n return d.toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n };\n\n const html = `\n \u003chtml\u003e\n \u003chead\u003e\n \u003ctitle\u003e${inv.invoiceNumber || 'Invoice'}\u003c\u002ftitle\u003e\n \u003cstyle\u003e\n @page { margin: 0; }\n body {\n font-family: Helvetica, Arial, sans-serif;\n margin: 40px;\n background: #fff;\n color: #111;\n }\n .header { text-align: center; margin-bottom: 40px; }\n .logo { max-width: 120px; margin-bottom: 10px; }\n h1 { margin: 0; font-size: 24px; }\n .brand {\n font-size: 14px;\n color: #777;\n letter-spacing: 1px;\n margin-top: 4px;\n }\n .info { margin-bottom: 25px; }\n .info p { margin: 3px 0; }\n table {\n width: 100%;\n border-collapse: collapse;\n margin-top: 10px;\n }\n th, td {\n padding: 8px 6px;\n border-bottom: 1px solid #ccc;\n text-align: left;\n }\n th { background: #f2f2f2; }\n .totals { margin-top: 30px; text-align: right; }\n .totals p { margin: 4px 0; font-size: 15px; }\n .totals p strong { font-size: 18px; }\n .notes { margin-top: 25px; font-style: italic; color: #666; }\n .signature { margin-top: 50px; text-align: right; }\n .signature img { max-width: 180px; }\n \u003c\u002fstyle\u003e\n \u003c\u002fhead\u003e\n \u003cbody\u003e\n \u003cdiv class=\"header\"\u003e\n ${icon ? `\u003cimg src=\"${icon}\" class=\"logo\"\u002f\u003e` : ''}\n \u003ch1\u003e${inv.invoiceNumber || 'Invoice'}\u003c\u002fh1\u003e\n \u003cdiv class=\"brand\"\u003e12inchapps.com\u003c\u002fdiv\u003e\n \u003cdiv\u003e${formattedDate(inv.createdDate)}\u003c\u002fdiv\u003e\n \u003c\u002fdiv\u003e\n\n \u003cdiv class=\"info\"\u003e\n \u003cp\u003e\u003cb\u003eBusiness:\u003c\u002fb\u003e ${inv.businessName || ''}\u003c\u002fp\u003e\n \u003cp\u003e\u003cb\u003eContact:\u003c\u002fb\u003e ${inv.contactPerson || ''}\u003c\u002fp\u003e\n \u003cp\u003e\u003cb\u003ePhone\u002fEmail:\u003c\u002fb\u003e ${inv.phoneOrEmail || ''}\u003c\u002fp\u003e\n ${inv.website ? `\u003cp\u003e\u003cb\u003eWebsite:\u003c\u002fb\u003e ${inv.website}\u003c\u002fp\u003e` : ''}\n \u003c\u002fdiv\u003e\n\n \u003ctable\u003e\n \u003cthead\u003e\n \u003ctr\u003e\u003cth\u003eItem\u003c\u002fth\u003e\u003cth\u003eQty\u003c\u002fth\u003e\u003cth\u003ePrice\u003c\u002fth\u003e\u003cth\u003eTotal\u003c\u002fth\u003e\u003c\u002ftr\u003e\n \u003c\u002fthead\u003e\n \u003ctbody\u003e\n ${(inv.items || '')\n .split('\\n')\n .filter((x) =\u003e x.trim())\n .map((line) =\u003e {\n const match = line.match(\u002f^(.*)\\((\\d+)\\s*[×x]\\s*\\$?([\\d.]+)\\)\u002f);\n if (match) {\n const name = match[1].trim();\n const qty = Number(match[2]);\n const price = Number(match[3]);\n const total = (qty * price).toFixed(2);\n return `\u003ctr\u003e\u003ctd\u003e${name}\u003c\u002ftd\u003e\u003ctd\u003e${qty}\u003c\u002ftd\u003e\u003ctd\u003e${price.toFixed(2)}\u003c\u002ftd\u003e\u003ctd\u003e${total}\u003c\u002ftd\u003e\u003c\u002ftr\u003e`;\n } else {\n return `\u003ctr\u003e\u003ctd colspan=\"4\"\u003e${line}\u003c\u002ftd\u003e\u003c\u002ftr\u003e`;\n }\n })\n .join('')\n }\n \u003c\u002ftbody\u003e\n \u003c\u002ftable\u003e\n\n \u003cdiv class=\"totals\"\u003e\n \u003cp\u003eSubtotal: ${Number(inv.subtotal || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003eDiscount: ${Number(inv.discount || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003eTax: ${Number(inv.tax || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003e\u003cstrong\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fstrong\u003e\u003c\u002fp\u003e\n \u003c\u002fdiv\u003e\n\n ${inv.notes ? `\u003cdiv class=\"notes\"\u003eNotes: ${inv.notes}\u003c\u002fdiv\u003e` : ''}\n ${signature ? `\u003cdiv class=\"signature\"\u003e\u003cimg src=\"${signature}\"\u002f\u003e\u003c\u002fdiv\u003e` : ''}\n\n \u003cscript\u003ewindow.onload = () =\u003e setTimeout(() =\u003e window.print(), 300);\u003c\\\\\u002fscript\u003e\n \u003c\u002fbody\u003e\n \u003c\u002fhtml\u003e`;\n\n w.document.write(html);\n w.document.close();\n };\n\n \u002f\u002f ✅ Instant PDF download\n const downloadInvoicePDF = (inv) =\u003e {\n const formattedDate = (date) =\u003e {\n if (!date) return '';\n const d = new Date(date);\n return d.toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n };\n\n const html = `\n \u003cdiv style=\"font-family: Helvetica, Arial, sans-serif; margin: 40px; color: #111;\"\u003e\n \u003cdiv style=\"text-align:center; margin-bottom:40px;\"\u003e\n ${icon ? `\u003cimg src=\"${icon}\" style=\"max-width:120px; margin-bottom:10px;\" \u002f\u003e` : ''}\n \u003ch1 style=\"margin:0; font-size:24px;\"\u003e${inv.invoiceNumber || 'Invoice'}\u003c\u002fh1\u003e\n \u003cdiv style=\"font-size:14px; color:#777; letter-spacing:1px; margin-top:4px;\"\u003e12inchapps.com\u003c\u002fdiv\u003e\n \u003cdiv\u003e${formattedDate(inv.createdDate)}\u003c\u002fdiv\u003e\n \u003c\u002fdiv\u003e\n\n \u003cdiv style=\"margin-bottom:25px;\"\u003e\n \u003cp\u003e\u003cb\u003eBusiness:\u003c\u002fb\u003e ${inv.businessName || ''}\u003c\u002fp\u003e\n \u003cp\u003e\u003cb\u003eContact:\u003c\u002fb\u003e ${inv.contactPerson || ''}\u003c\u002fp\u003e\n \u003cp\u003e\u003cb\u003ePhone\u002fEmail:\u003c\u002fb\u003e ${inv.phoneOrEmail || ''}\u003c\u002fp\u003e\n ${inv.website ? `\u003cp\u003e\u003cb\u003eWebsite:\u003c\u002fb\u003e ${inv.website}\u003c\u002fp\u003e` : ''}\n ${inv.currency ? `\u003cp\u003e\u003cb\u003eCurrency:\u003c\u002fb\u003e ${inv.currency}\u003c\u002fp\u003e` : ''}\n \u003c\u002fdiv\u003e\n\n \u003ctable style=\"width:100%; border-collapse:collapse; margin-top:10px;\"\u003e\n \u003cthead\u003e\n \u003ctr style=\"background:#f2f2f2;\"\u003e\n \u003cth style=\"padding:8px 6px; border-bottom:1px solid #ccc; text-align:left;\"\u003eItem\u003c\u002fth\u003e\n \u003cth style=\"padding:8px 6px; border-bottom:1px solid #ccc; text-align:left;\"\u003eQty\u003c\u002fth\u003e\n \u003cth style=\"padding:8px 6px; border-bottom:1px solid #ccc; text-align:left;\"\u003ePrice\u003c\u002fth\u003e\n \u003cth style=\"padding:8px 6px; border-bottom:1px solid #ccc; text-align:left;\"\u003eTotal\u003c\u002fth\u003e\n \u003c\u002ftr\u003e\n \u003c\u002fthead\u003e\n \u003ctbody\u003e\n ${(inv.items || '')\n .split('\\n')\n .filter(x =\u003e x.trim())\n .map(line =\u003e {\n const match = line.match(\u002f^(.*)\\((\\d+)\\s*[×x]\\s*\\$?([\\d.]+)\\)\u002f);\n if (match) {\n const name = match[1].trim();\n const qty = Number(match[2]);\n const price = Number(match[3]);\n const total = (qty * price).toFixed(2);\n return `\u003ctr\u003e\n \u003ctd style=\"padding:8px 6px; border-bottom:1px solid #ccc;\"\u003e${name}\u003c\u002ftd\u003e\n \u003ctd style=\"padding:8px 6px; border-bottom:1px solid #ccc;\"\u003e${qty}\u003c\u002ftd\u003e\n \u003ctd style=\"padding:8px 6px; border-bottom:1px solid #ccc;\"\u003e${price.toFixed(2)}\u003c\u002ftd\u003e\n \u003ctd style=\"padding:8px 6px; border-bottom:1px solid #ccc;\"\u003e${total}\u003c\u002ftd\u003e\n \u003c\u002ftr\u003e`;\n } else {\n return `\u003ctr\u003e\u003ctd colspan=\"4\" style=\"padding:8px 6px; border-bottom:1px solid #ccc;\"\u003e${line}\u003c\u002ftd\u003e\u003c\u002ftr\u003e`;\n }\n })\n .join('')\n }\n \u003c\u002ftbody\u003e\n \u003c\u002ftable\u003e\n\n \u003cdiv style=\"margin-top:30px; text-align:right;\"\u003e\n \u003cp\u003eSubtotal: ${Number(inv.subtotal || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003eDiscount: ${Number(inv.discount || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003eTax: ${Number(inv.tax || 0).toFixed(2)}\u003c\u002fp\u003e\n \u003cp\u003e\u003cstrong\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fstrong\u003e\u003c\u002fp\u003e\n \u003c\u002fdiv\u003e\n\n ${inv.notes ? `\u003cdiv style=\"margin-top:25px; font-style:italic; color:#666;\"\u003eNotes: ${inv.notes}\u003c\u002fdiv\u003e` : ''}\n ${signature ? `\u003cdiv style=\"margin-top:50px; text-align:right;\"\u003e\u003cimg src=\"${signature}\" style=\"max-width:180px;\" \u002f\u003e\u003c\u002fdiv\u003e` : ''}\n \u003c\u002fdiv\u003e\n `;\n\n const container = document.createElement('div');\n container.innerHTML = html;\n document.body.appendChild(container);\n\n const opt = {\n margin: 0.3,\n filename: `Invoice-${inv.invoiceNumber || 'invoice'}.pdf`,\n image: { type: 'jpeg', quality: 0.98 },\n html2canvas: { scale: 2 },\n jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' },\n };\n\n \u002f\u002f wait until html2pdf is ready\n const interval = setInterval(() =\u003e {\n if (window.html2pdf) {\n clearInterval(interval);\n window.html2pdf().set(opt).from(container).save().then(() =\u003e container.remove());\n }\n }, 300);\n };\n\n \u002f\u002f ---------- Render ----------\n return (\n \u003cBox sx={{ p: { xs: 2, md: 4 }, bgcolor: isDark ? '#111' : '#f5f5f5' }}\u003e\n \u003cTypography variant=\"h4\" sx={{ textAlign: 'center', fontWeight: 'bold', mb: 3, color: '#1976d2' }}\u003e\n {title}\n \u003c\u002fTypography\u003e\n\n {\u002f* FORM *\u002f}\n \u003cStack spacing={2} component=\"form\" onSubmit={handleSubmit}\u003e\n \u003cTextField\n select\n label=\"Create from Lead\"\n value={selectedLeadId}\n onChange={(e) =\u003e {\n setSelectedLeadId(e.target.value);\n createFromLead(e.target.value);\n }}\n fullWidth\n sx={inputStyle}\n \u003e\n \u003cMenuItem value=\"\"\u003eSelect Lead\u003c\u002fMenuItem\u003e\n {leads.map((l) =\u003e (\n \u003cMenuItem key={l.id} value={l.id}\u003e\n {l.businessName || l.contactPerson}\n \u003c\u002fMenuItem\u003e\n ))}\n \u003c\u002fTextField\u003e\n\n \u003cTextField label=\"Business Name\" value={businessName} onChange={(e) =\u003e setBusinessName(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField label=\"Contact Person\" value={contactPerson} onChange={(e) =\u003e setContactPerson(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField label=\"Phone or Email\" value={phoneOrEmail} onChange={(e) =\u003e setPhoneOrEmail(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n \u003cTextField label=\"Website\" value={website} onChange={(e) =\u003e setWebsite(e.target.value)} fullWidth sx={inputStyle} \u002f\u003e\n\n \u003cStack direction={{ xs: 'column', sm: 'row' }} spacing={2}\u003e\n \u003cButton type=\"submit\" variant=\"contained\" fullWidth\u003e\n {editingCustomer ? 'Update Customer' : 'Add Customer'}\n \u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" fullWidth onClick={resetForm}\u003e\n Clear\n \u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fStack\u003e\n\n {\u002f* SEARCH BAR *\u002f}\n \u003cTextField\n label=\"Search Customers\"\n value={search}\n onChange={(e) =\u003e setSearch(e.target.value)}\n fullWidth\n size=\"small\"\n sx={{ mt: 4, mb: 3, ...inputStyle }}\n \u002f\u003e\n\n {\u002f* CUSTOMER LIST *\u002f}\n {loadingCustomers ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {filteredCustomers.map((c) =\u003e {\n const contact = getContactLink(c.phoneOrEmail);\n const site = getWebsiteLink(c.website);\n return (\n \u003cPaper key={c.id} sx={{ p: 2, backgroundColor: isDark ? '#1e1e1e' : '#fff' }}\u003e\n \u003cTypography variant=\"h6\" sx={{ fontWeight: 'bold' }}\u003e{c.businessName}\u003c\u002fTypography\u003e\n \u003cTypography\u003eContact: {c.contactPerson}\u003c\u002fTypography\u003e\n \u003cTypography\u003ePhone\u002fEmail: {c.phoneOrEmail}\u003c\u002fTypography\u003e\n {site && (\n \u003cTypography\u003e\n Website:{' '}\n \u003ca href={site} target=\"_blank\" rel=\"noopener noreferrer\" style={{ color: '#1976d2', textDecoration: 'none' }}\u003e\n {c.website}\n \u003c\u002fa\u003e\n \u003c\u002fTypography\u003e\n )}\n {openTotals[String(c.id)] \u003e 0 && (\n \u003cTypography sx={{ mt: 1, color: '#d32f2f', fontWeight: 'bold' }}\u003e\n Open Invoices: ${openTotals[String(c.id)].toFixed(2)}\n \u003c\u002fTypography\u003e\n )}\n \u003cDivider sx={{ my: 1 }} \u002f\u003e\n \u003cStack direction=\"row\" spacing={1.5} flexWrap=\"wrap\"\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e editCustomer(c)}\u003eEdit\u003c\u002fButton\u003e\n \u003cButton variant=\"outlined\" color=\"error\" onClick={() =\u003e deleteCustomer(c.id)}\u003eDelete\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e openInvoiceDialog(c.id)}\u003eView Invoices\u003c\u002fButton\u003e\n {contact.kind === 'phone' && contact.href && (\n \u003cButton variant=\"outlined\" onClick={() =\u003e openContact(contact.href)}\u003eCall\u003c\u002fButton\u003e\n )}\n {contact.kind === 'email' && contact.href && (\n \u003cButton variant=\"outlined\" onClick={() =\u003e openContact(contact.href)}\u003eEmail\u003c\u002fButton\u003e\n )}\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n );\n })}\n \u003c\u002fStack\u003e\n )}\n\n {\u002f* INVOICE DIALOG *\u002f}\n \u003cDialog open={invoiceDialogOpen} onClose={closeInvoiceDialog} fullWidth maxWidth=\"md\"\u003e\n \u003cDialogTitle\u003e\n Customer Invoices\n \u003cIconButton onClick={closeInvoiceDialog} sx={{ position: 'absolute', right: 8, top: 8 }}\u003e×\u003c\u002fIconButton\u003e\n \u003c\u002fDialogTitle\u003e\n \u003cDialogContent dividers\u003e\n {loadingInvoices ? (\n \u003cCircularProgress sx={{ display: 'block', mx: 'auto' }} \u002f\u003e\n ) : selectedInvoices.length === 0 ? (\n \u003cTypography\u003eNo invoices found for this customer.\u003c\u002fTypography\u003e\n ) : (\n \u003cStack spacing={2}\u003e\n {selectedInvoices.map((inv) =\u003e (\n \u003cPaper key={inv.id} sx={{ p: 2, backgroundColor: isDark ? '#222' : '#fafafa' }}\u003e\n \u003cTypography variant=\"subtitle1\" sx={{ fontWeight: 'bold' }}\u003e{inv.invoiceNumber || 'Untitled Invoice'}\u003c\u002fTypography\u003e\n \u003cTypography\u003eStatus: {inv.status}\u003c\u002fTypography\u003e\n \u003cTypography\u003eTotal: ${Number(inv.total || 0).toFixed(2)}\u003c\u002fTypography\u003e\n \u003cTypography\u003eDue: {displayDate(inv.dueDate)}\u003c\u002fTypography\u003e\n {inv.items && \u003cTypography\u003eItems: {inv.items}\u003c\u002fTypography\u003e}\n {inv.notes && \u003cTypography sx={{ mt: 1, whiteSpace: 'pre-wrap' }}\u003eNotes: {inv.notes}\u003c\u002fTypography\u003e}\n \u003cStack direction=\"row\" spacing={1.5} sx={{ mt: 1 }}\u003e\n \u003cButton variant=\"outlined\" onClick={() =\u003e printInvoice(inv)}\u003ePrint\u003c\u002fButton\u003e\n \u003cButton variant=\"contained\" onClick={() =\u003e downloadInvoicePDF(inv)}\u003eDownload PDF\u003c\u002fButton\u003e\n \u003c\u002fStack\u003e\n \u003c\u002fPaper\u003e\n ))}\n \u003c\u002fStack\u003e\n )}\n \u003c\u002fDialogContent\u003e\n \u003cDialogActions\u003e\n \u003cButton onClick={closeInvoiceDialog}\u003eClose\u003c\u002fButton\u003e\n \u003c\u002fDialogActions\u003e\n \u003c\u002fDialog\u003e\n\n \u003cSnackbar open={snackOpen} autoHideDuration={2200} onClose={() =\u003e setSnackOpen(false)} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}\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":"Customer Manager V3","properties":{"xrtgzu4xz5tl839i":{"index":0,"data":{"id":"xrtgzu4xz5tl839i","shared":false,"key":"script","locales":{"en":{"defaultValue":""}}}}}}}},"supportedLocales":[{"locale":"en","name":"English"},{"locale":"zh","name":"简体中文"}],"config":{"defaultLocale":"en","publishedAt":1763003729266},"resources":{}}; `; w.document.write(html); w.document.close(); } }, "Print"), React.createElement(Button, { variant: "contained", onClick: () => { const element = document.createElement('div'); element.innerHTML = `
${icon ? `` : ''}

${inv.invoiceNumber || 'Invoice'}

Total: ${inv.currency || 'USD'} ${Number(inv.total || 0).toFixed(2)}

`; const opt = { margin: 0.3, filename: `${inv.invoiceNumber || 'Invoice'}.pdf`, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 3 }, jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, }; window.html2pdf().set(opt).from(element).save(); } }, "Download PDF"))))))))), 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; };