import { useEffect, useMemo, useState } from 'react' import { Link, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { ArrowLeft } from 'lucide-react' import { CustomerSearchSelect } from '../components/CustomerSearchSelect' import { InvoiceDraftFields } from '../components/InvoiceDraftFields' import { isEmptyRichText, RichTextEditor } from '../components/RichTextEditor' import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import { ApiError, isAbortError } from '../lib/api' import { createInvoice, getInvoice, listInvoiceItemTemplates, listInvoiceTemplates, updateInvoice, } from '../services/invoiceService' import { listCustomers, type BusinessCustomer } from '../services/customerService' import type { InvoiceItemTemplate, InvoiceTemplate } from '../types/invoice' import { buildAccountsPayload, buildKeyPointsPayload, buildLineItemsPayload, draftsFromInvoice, draftsFromInvoiceTemplate, emptyDraftItem, type DraftAccount, type DraftKeyPoint, type DraftLineItem, } from '../utils/invoiceDraft' import { formatIrtPrice, parseIrtInput } from '../utils/irtPrice' import pageStyles from '../components/PageContent.module.css' import formStyles from '../components/InvoiceForm.module.css' import styles from './InvoicesPage.module.css' function effectivePrice(price: number, discountedPrice: number | null | undefined) { if (discountedPrice !== null && discountedPrice !== undefined && discountedPrice < price) { return discountedPrice } return price } function customerFromInvoiceUser(user: { id: string firstName: string | null lastName: string | null cell: string }): BusinessCustomer { const name = [user.firstName, user.lastName].filter(Boolean).join(' ').trim() return { id: user.id, cellNumber: user.cell, firstName: user.firstName, lastName: user.lastName, email: null, label: name || user.cell, } } export function IssueInvoicePage() { const { invoiceId = '' } = useParams() const isEdit = Boolean(invoiceId) const navigate = useNavigate() const location = useLocation() const [searchParams] = useSearchParams() const { showToast } = useToast() const t = useT() const urlUserId = searchParams.get('userId') || '' const presetCustomer = (location.state as { presetCustomer?: BusinessCustomer } | null) ?.presetCustomer const [itemTemplates, setItemTemplates] = useState([]) const [invoiceTemplates, setInvoiceTemplates] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [locked, setLocked] = useState(false) const [billedCustomer, setBilledCustomer] = useState( presetCustomer && presetCustomer.id === urlUserId ? presetCustomer : null, ) const [billedUserId, setBilledUserId] = useState(urlUserId) const customerLocked = isEdit || Boolean(urlUserId) const [sourceTemplateId, setSourceTemplateId] = useState('') const [invoiceTemplateId, setInvoiceTemplateId] = useState() const [invoiceName, setInvoiceName] = useState('') const [topText, setTopText] = useState('') const [notes, setNotes] = useState('') const [draftItems, setDraftItems] = useState([emptyDraftItem()]) const [keyPoints, setKeyPoints] = useState([]) const [accounts, setAccounts] = useState([]) const [selectedItemTemplateId, setSelectedItemTemplateId] = useState('') const [submitting, setSubmitting] = useState(false) const [formError, setFormError] = useState('') const listPath = '/invoices' const createTotal = useMemo(() => { return draftItems.reduce((sum, item) => { const price = parseIrtInput(item.price) ?? 0 const discounted = item.discountedPrice.trim() ? parseIrtInput(item.discountedPrice) : null return sum + effectivePrice(price, discounted) }, 0) }, [draftItems]) useEffect(() => { const controller = new AbortController() async function load() { setLoading(true) setError('') setLocked(false) try { const [items, templates] = await Promise.all([ listInvoiceItemTemplates(controller.signal), listInvoiceTemplates(controller.signal), ]) setItemTemplates(items.items.filter((i) => i.isActive)) setInvoiceTemplates(templates.items.filter((i) => i.isActive)) if (invoiceId) { const invoice = await getInvoice(invoiceId, controller.signal) if (invoice.status === 'approved') { setLocked(true) setError(t('issueInvoice.approvedLocked')) } if (invoice.user) { setBilledCustomer(customerFromInvoiceUser(invoice.user)) } setBilledUserId(invoice.userId) const drafts = draftsFromInvoice(invoice) setSourceTemplateId('') setInvoiceTemplateId(invoice.invoiceTemplateId ?? undefined) setInvoiceName(drafts.name) setTopText(drafts.topText) setNotes(drafts.notes) setDraftItems(drafts.items) setKeyPoints(drafts.keyPoints) setAccounts(drafts.accounts) setSelectedItemTemplateId('') } else if (urlUserId && !presetCustomer) { const listed = await listCustomers({ page: 1, pageSize: 100 }, controller.signal) const match = listed.items.find((c) => c.id === urlUserId) if (match) { setBilledCustomer({ id: match.id, cellNumber: match.cellNumber, firstName: match.firstName, lastName: match.lastName, email: match.email, label: match.label, }) setBilledUserId(match.id) } } } catch (err) { if (isAbortError(err)) return setError(err instanceof ApiError ? err.message : t('issueInvoice.loadError')) } finally { setLoading(false) } } void load() return () => controller.abort() // eslint-disable-next-line react-hooks/exhaustive-deps }, [invoiceId]) function resetBlankDraft() { setSourceTemplateId('') setInvoiceTemplateId(undefined) setInvoiceName('') setTopText('') setNotes('') setDraftItems([emptyDraftItem()]) setKeyPoints([]) setAccounts([]) setSelectedItemTemplateId('') setFormError('') } function applyInvoiceTemplate(templateId: string) { if (locked) return setSourceTemplateId(templateId) if (!templateId) { resetBlankDraft() return } const template = invoiceTemplates.find((tpl) => tpl.id === templateId) if (!template) return const drafts = draftsFromInvoiceTemplate(template) setInvoiceTemplateId(template.id) setInvoiceName(drafts.name) setTopText(drafts.topText) setDraftItems(drafts.items) setKeyPoints(drafts.keyPoints) setAccounts(drafts.accounts) setSelectedItemTemplateId('') setFormError('') } function handleCustomerChange(customer: BusinessCustomer | null) { setBilledCustomer(customer) setBilledUserId(customer?.id ?? '') } async function handleSubmit() { if (locked) return setFormError('') if (!billedUserId) { setFormError(t('issueInvoice.recipientRequired')) return } let items try { items = buildLineItemsPayload(draftItems, t) } catch (err) { setFormError(err instanceof Error ? err.message : t('issueInvoice.loadError')) return } setSubmitting(true) try { if (isEdit) { await updateInvoice(invoiceId, { userId: billedUserId, items, name: invoiceName.trim() || null, topText: isEmptyRichText(topText) ? null : topText, notes: notes.trim() || null, invoiceTemplateId: invoiceTemplateId ?? null, keyPoints: buildKeyPointsPayload(keyPoints), accounts: buildAccountsPayload(accounts), }) showToast(t('issueInvoice.updated'), 'success') } else { await createInvoice({ userId: billedUserId, items, name: invoiceName.trim() || undefined, topText: isEmptyRichText(topText) ? undefined : topText, notes: notes.trim() || undefined, invoiceTemplateId, keyPoints: buildKeyPointsPayload(keyPoints), accounts: buildAccountsPayload(accounts), }) showToast(t('issueInvoice.issued'), 'success') } navigate(listPath) } catch (err) { setFormError( err instanceof ApiError ? err.message : isEdit ? t('issueInvoice.updateError') : t('issueInvoice.createError'), ) } finally { setSubmitting(false) } } return (
{t('issueInvoice.backToInvoices')}

{isEdit ? t('title.editInvoice') : t('title.issueInvoice')}

{isEdit ? t('issueInvoice.subtitleEdit') : t('issueInvoice.subtitleNew')}

{error ?

{error}

: null} {loading ?

{t('issueInvoice.loading')}

: null} {!loading && !locked ? (
{!isEdit ? (
) : null}
setInvoiceName(e.target.value)} placeholder={t('issueInvoice.namePlaceholder')} />
{!isEdit && invoiceTemplates.length === 0 ? (

{t('issueInvoice.noTemplatesHint').split('{link}')[0]} {t('issueInvoice.noTemplatesLink')} {t('issueInvoice.noTemplatesHint').split('{link}')[1]}

) : null}
setNotes(e.target.value)} placeholder={t('issueInvoice.notesPlaceholder')} />
{t('issueInvoice.totalLabel')} {formatIrtPrice(createTotal)} } /> {formError ?

{formError}

: null}
) : null} {!loading && locked ? (
) : null}
) }