import { useEffect, useMemo, useState } from "react"; import { Button, Column, Dropdown, Grid, InlineNotification, NumberInput, RadioButton, RadioButtonGroup, Select, SelectItem, TextArea, TextInput, Toggle, } from "@carbon/react"; import { Add, Save, TrashCan } from "@carbon/icons-react"; import { api } from "../lib/api"; import { EMPTY_ITEM, INDIAN_STATES, UNIT_LABELS } from "../lib/types"; import type { BankAccount, Client, InvoiceInput, InvoiceItem, LineMode, LineUnit, Settings, TaxType, } from "../lib/types"; import { computeLineAmount, computeTotals } from "../lib/invoice"; import { amountInWords } from "../lib/numberToWords"; import { addDays, formatAmount, todayIso } from "../lib/format"; import { buildPdfProps, exportPdfFromProps } from "../lib/pdf"; import { InvoicePreview } from "../components/InvoicePreview"; import { ImagePicker } from "../components/ImagePicker"; import type { InvoicePdfProps } from "../pdf/InvoiceDocument"; interface NewInvoiceProps { settings: Settings; onSettingsChange: () => void; } export default function NewInvoice({ settings }: NewInvoiceProps) { const today = todayIso(); const [invoiceDate, setInvoiceDate] = useState(today); const [dueDate, setDueDate] = useState(addDays(today, settings.paymentTermsDays)); const [number, setNumber] = useState(""); const [clients, setClients] = useState([]); const [clientId, setClientId] = useState(null); const [clientName, setClientName] = useState(""); const [clientAddress, setClientAddress] = useState(""); const [clientGstin, setClientGstin] = useState(""); const [poNumber, setPoNumber] = useState(""); const [placeOfSupply, setPlaceOfSupply] = useState(settings.vendorStateCode); const [saveClient, setSaveClient] = useState(false); const [items, setItems] = useState([{ ...EMPTY_ITEM }]); const [discount, setDiscount] = useState(0); const [taxesApplicable, setTaxesApplicable] = useState(settings.defaultTaxType !== "none"); const [taxType, setTaxType] = useState( settings.defaultTaxType === "none" ? "cgst_sgst" : settings.defaultTaxType, ); const [taxRate, setTaxRate] = useState(settings.defaultTaxRate); const [banks, setBanks] = useState([]); const [bankId, setBankId] = useState(settings.defaultBankId); const [signaturePath, setSignaturePath] = useState(settings.signaturePath); const [notes, setNotes] = useState(""); const [logoUri, setLogoUri] = useState(null); const [signatureUri, setSignatureUri] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [saved, setSaved] = useState(null); useEffect(() => { let active = true; (async () => { try { const [next, clientList, bankList] = await Promise.all([ api.peekNextInvoiceNumber(), api.listClients(), api.listBanks(), ]); if (!active) return; setNumber(next); setClients(clientList); setBanks(bankList); } catch (e) { if (active) setError(String(e)); } })(); return () => { active = false; }; }, []); useEffect(() => { let active = true; if (!settings.logoPath) { setLogoUri(null); return; } api.readAssetDataUri(settings.logoPath).then((uri) => active && setLogoUri(uri)).catch(() => {}); return () => { active = false; }; }, [settings.logoPath]); useEffect(() => { let active = true; if (!signaturePath) { setSignatureUri(null); return; } api.readAssetDataUri(signaturePath).then((uri) => active && setSignatureUri(uri)).catch(() => {}); return () => { active = false; }; }, [signaturePath]); const effectiveTaxType: TaxType = taxesApplicable ? taxType : "none"; const totals = useMemo( () => computeTotals(items, discount, effectiveTaxType, taxRate), [items, discount, effectiveTaxType, taxRate], ); const words = useMemo(() => amountInWords(totals.total), [totals.total]); const bank = useMemo(() => banks.find((b) => b.id === bankId) ?? null, [banks, bankId]); const previewProps: InvoicePdfProps = { vendor: settings, bank, logoDataUri: logoUri, signatureDataUri: signatureUri, number: number || "DRAFT", invoiceDate, dueDate, clientName, clientAddress, clientGstin, poNumber, placeOfSupplyStateCode: placeOfSupply, items, subtotal: totals.subtotal, discount: totals.discount, taxType: effectiveTaxType, taxRate, cgst: totals.cgst, sgst: totals.sgst, igst: totals.igst, total: totals.total, amountInWords: words, notes, }; const updateItem = (index: number, patch: Partial) => setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))); const addItem = () => setItems((prev) => [...prev, { ...EMPTY_ITEM }]); const removeItem = (index: number) => setItems((prev) => (prev.length === 1 ? prev : prev.filter((_, i) => i !== index))); const applyClient = (selected: Client) => { setClientId(selected.id); setClientName(selected.name); setClientAddress(selected.address); setClientGstin(selected.gstin); setPoNumber(selected.poNumber); if (selected.stateCode) setPlaceOfSupply(selected.stateCode); }; const resetForNext = async () => { setClientId(null); setClientName(""); setClientAddress(""); setClientGstin(""); setPoNumber(""); setSaveClient(false); setItems([{ ...EMPTY_ITEM }]); setDiscount(0); setNotes(""); setInvoiceDate(todayIso()); setDueDate(addDays(todayIso(), settings.paymentTermsDays)); try { setNumber(await api.peekNextInvoiceNumber()); setClients(await api.listClients()); } catch { /* ignore */ } }; const onSave = async () => { setSaving(true); setError(null); setSaved(null); try { const input: InvoiceInput = { invoiceDate, dueDate, clientId, clientName, clientAddress, clientGstin, poNumber, placeOfSupplyStateCode: placeOfSupply, subtotal: totals.subtotal, discount: totals.discount, taxType: effectiveTaxType, taxRate, cgstAmount: totals.cgst, sgstAmount: totals.sgst, igstAmount: totals.igst, total: totals.total, amountInWords: words, bankAccountId: bankId, bankSnapshot: bank ? JSON.stringify(bank) : "", signaturePath, notes, saveClient, items: items.map((item, index) => ({ ...item, sortOrder: index, amount: computeLineAmount(item), })), }; const invoice = await api.createInvoice(input); const props = await buildPdfProps(invoice, settings, bank); const path = await exportPdfFromProps(props, invoice.number); if (path) setSaved(path); await resetForNext(); } catch (e) { setError(String(e)); } finally { setSaving(false); } }; return (

New invoice

Next number: {number || "—"}

{error ? ( ) : null} {saved ? ( setSaved(null)} /> ) : null}
{ setInvoiceDate(e.target.value); setDueDate(addDays(e.target.value, settings.paymentTermsDays)); }} /> setDueDate(e.target.value)} /> (item ? item.name : "")} onChange={({ selectedItem }) => selectedItem && applyClient(selectedItem)} /> { setClientName(e.target.value); setClientId(null); }} />