import { useEffect, useMemo, useState } from 'react' import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { Copy, Eye, Pencil, Plus, Trash2 } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { Pagination } from '../components/Pagination' import { Breadcrumbs } from '../components/Breadcrumbs' import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal' import { Modal } from '../components/Modal' import { PageTitle } from '../components/PageTitle' import { isEmptyRichText } from '../components/RichTextEditor' import { Tooltip } from '../components/Tooltip' import { useToast } from '../context/ToastContext' import { useT } from '../i18n/useT' import type { BusinessMessageKey } from '../i18n/messages' import { ApiError, isAbortError } from '../lib/api' import { getInvoicePublicUrlFallback } from '../lib/config' import { getActiveBusinessDomain } from '../lib/businessContext' import { formatCellForDisplay } from '../lib/cellNumber' import { deleteInvoice, listInvoices, updateInvoiceStatus } from '../services/invoiceService' import type { BusinessCustomer } from '../services/customerService' import type { Invoice, InvoiceStatus } from '../types/invoice' import { formatIrtPrice } from '../utils/irtPrice' import pageStyles from '../components/PageContent.module.css' import formStyles from '../components/InvoiceForm.module.css' import styles from './InvoicesPage.module.css' const PAGE_SIZE = 20 const STATUS_OPTIONS: InvoiceStatus[] = ['draft', 'issued', 'approved', 'paid', 'cancelled'] function formatDate(value: string, locale: 'en' | 'fa') { const d = new Date(value) if (Number.isNaN(d.getTime())) return value const intlLocale = locale === 'fa' ? 'fa-IR' : 'en-US' return d.toLocaleDateString(intlLocale, { year: 'numeric', month: 'short', day: '2-digit' }) } function statusLabelKey(status: InvoiceStatus): BusinessMessageKey { return `invoices.status.${status}` as BusinessMessageKey } function statusClass(status: InvoiceStatus) { return styles[`status_${status}` as keyof typeof styles] ?? styles.status_draft } function canEditInvoice(status: InvoiceStatus) { return status !== 'approved' } function allowedStatusOptions(current: InvoiceStatus): InvoiceStatus[] { if (current !== 'approved') return STATUS_OPTIONS return STATUS_OPTIONS.filter( (option) => option === 'approved' || option === 'paid' || option === 'cancelled', ) } function customerName(invoice: Invoice): string { const name = [invoice.user?.firstName, invoice.user?.lastName].filter(Boolean).join(' ').trim() return name || '' } export function InvoicesPage() { const navigate = useNavigate() const location = useLocation() const [searchParams, setSearchParams] = useSearchParams() const t = useT() const { locale } = useLocale() const { showToast } = useToast() const filterUserId = searchParams.get('userId') || '' const presetCustomer = (location.state as { presetCustomer?: BusinessCustomer } | null) ?.presetCustomer const [invoices, setInvoices] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [page, setPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [total, setTotal] = useState(0) const [detailInvoice, setDetailInvoice] = useState(null) const [statusTarget, setStatusTarget] = useState(null) const [statusUpdating, setStatusUpdating] = useState(null) const [removeTarget, setRemoveTarget] = useState(null) async function reload(signal?: AbortSignal) { setLoading(true) setError('') try { const list = await listInvoices( { page, pageSize: PAGE_SIZE, userId: filterUserId || undefined }, signal, ) setInvoices(list.items) setTotalPages(list.totalPages) setTotal(list.total) } catch (err) { if (isAbortError(err)) return setError(err instanceof ApiError ? err.message : t('invoices.loadError')) } finally { setLoading(false) } } useEffect(() => { const controller = new AbortController() void reload(controller.signal) return () => controller.abort() // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, filterUserId]) useEffect(() => { setPage(1) }, [filterUserId]) const filterCustomerMatch = useMemo(() => { if (!filterUserId) return undefined return invoices.find((inv) => inv.userId === filterUserId) }, [invoices, filterUserId]) const filterCustomerName = useMemo(() => { if (!filterUserId) return '' if (presetCustomer && presetCustomer.id === filterUserId) { const fromPreset = [presetCustomer.firstName, presetCustomer.lastName] .filter(Boolean) .join(' ') .trim() if (fromPreset) return fromPreset if (presetCustomer.label?.trim()) return presetCustomer.label.trim() if (presetCustomer.cellNumber) return formatCellForDisplay(presetCustomer.cellNumber) } if (filterCustomerMatch) { const fromInvoice = customerName(filterCustomerMatch) if (fromInvoice) return fromInvoice } return '' }, [filterUserId, presetCustomer, filterCustomerMatch]) const issueState = presetCustomer && presetCustomer.id === filterUserId ? { presetCustomer } : filterCustomerMatch?.user ? { presetCustomer: { id: filterCustomerMatch.user.id, cellNumber: filterCustomerMatch.user.cell, firstName: filterCustomerMatch.user.firstName, lastName: filterCustomerMatch.user.lastName, email: null, label: filterCustomerName || filterCustomerMatch.user.cell, }, } : undefined function clearFilter() { const next = new URLSearchParams(searchParams) next.delete('userId') setSearchParams(next, { replace: true, state: null }) } function openStatusModal(invoice: Invoice) { setStatusTarget(invoice) } async function handleStatusSelect(next: InvoiceStatus) { if (!statusTarget || statusUpdating) return if (next === statusTarget.status) { setStatusTarget(null) return } setStatusUpdating(next) try { const updated = await updateInvoiceStatus(statusTarget.id, { status: next }) setInvoices((rows) => rows.map((row) => (row.id === updated.id ? updated : row))) setDetailInvoice((current) => (current?.id === updated.id ? updated : current)) showToast(t('invoices.statusUpdated'), 'success') setStatusTarget(null) } catch (err) { showToast(err instanceof ApiError ? err.message : t('invoices.statusUpdateError'), 'error') } finally { setStatusUpdating(null) } } async function handleRemove() { if (!removeTarget) return try { await deleteInvoice(removeTarget.id) showToast(t('invoices.removed'), 'success') setRemoveTarget(null) if (detailInvoice?.id === removeTarget.id) setDetailInvoice(null) await reload() } catch (err) { showToast(err instanceof ApiError ? err.message : t('invoices.removeError'), 'error') } } function invoicePublicUrl(invoice: Invoice) { return ( invoice.publicUrl || getInvoicePublicUrlFallback(invoice.publicId, getActiveBusinessDomain()) ) } async function copyPublicLink(invoice: Invoice) { const url = invoicePublicUrl(invoice) try { await navigator.clipboard.writeText(url) showToast(t('invoices.linkCopied'), 'success') } catch { showToast(t('invoices.linkCopyError'), 'error') } } const issueHref = filterUserId ? `/invoices/new?userId=${filterUserId}` : '/invoices/new' const countLabel = total === 1 ? t('invoices.countOne', { count: total }) : t('invoices.countMany', { count: total }) return (
{t('title.invoices')}

{t('invoices.subtitle')}

{t('invoices.templatesLink')}
{filterUserId ? (
{filterCustomerName ? t('invoices.filterBannerNamed', { name: filterCustomerName }) : t('invoices.filterBannerGeneric')}
) : null} {error ?

{error}

: null}
{loading ? t('invoices.loading') : countLabel}
{!loading && invoices.length === 0 ? ( ) : null} {invoices.map((invoice) => { const itemCount = invoice.items?.length ?? 0 const name = customerName(invoice) return ( ) })}
{t('invoices.col.name')} {t('invoices.col.customer')} {t('invoices.col.issued')} {t('invoices.col.status')} {t('invoices.col.total')} {t('invoices.col.link')} {t('invoices.col.actions')}
{t('invoices.empty')}
{invoice.name || `#${invoice.id}`}
{itemCount === 1 ? t('invoices.itemCountOne', { count: itemCount }) : t('invoices.itemCountMany', { count: itemCount })}
{name || '—'}
{invoice.user?.cell ? (
{formatCellForDisplay(invoice.user.cell)}
) : null}
{formatDate(invoice.issuedAt, locale)} {formatIrtPrice(invoice.total ?? 0)}
{invoicePublicUrl(invoice).replace(/^https?:\/\//, '')}
{totalPages > 1 ? (
{countLabel}
) : null}
setDetailInvoice(null)} xl > {detailInvoice ? ( <>
{t('invoices.detail.billedTo')} {customerName(detailInvoice) || '—'} {detailInvoice.user?.cell ? (
{formatCellForDisplay(detailInvoice.user.cell)}
) : null}
{t('invoices.detail.status')}
{t('invoices.detail.total')} {formatIrtPrice(detailInvoice.total ?? 0)}
{t('invoices.detail.link')}
{invoicePublicUrl(detailInvoice).replace(/^https?:\/\//, '')}
{detailInvoice.topText && !isEmptyRichText(detailInvoice.topText) ? (
) : null} {detailInvoice.notes ? (

{t('invoices.detail.notes', { notes: detailInvoice.notes })}

) : null}
{(detailInvoice.items ?? []).map((item) => (
{item.title} {item.discountedPrice !== null && item.discountedPrice < item.price ? ( <> {formatIrtPrice(item.price)}{' '} {formatIrtPrice(item.discountedPrice)} ) : ( formatIrtPrice(item.price) )}
{item.duration ? {item.duration} : null} {item.worktime ? {item.worktime} : null}
{item.description ? (

{item.description}

) : null}
))}
{(detailInvoice.keyPoints?.length ?? 0) > 0 ? (

{t('invoiceDraft.keyPoints')}

    {detailInvoice.keyPoints!.map((kp) => (
  • {kp.text}
  • ))}
) : null} {(detailInvoice.accounts?.length ?? 0) > 0 ? (

{t('invoiceDraft.accounts')}

{detailInvoice.accounts!.map((acc) => (
{acc.bankName}
{acc.accountHolderName ? {acc.accountHolderName} : null} {acc.cardNumber ? {acc.cardNumber} : null} {acc.iban ? {acc.iban} : null}
))}
) : null} ) : null} { if (!statusUpdating) setStatusTarget(null) }} > {statusTarget ? (
{allowedStatusOptions(statusTarget.status).map((option) => { const selected = statusTarget.status === option const saving = statusUpdating === option return ( ) })}
) : null}
setRemoveTarget(null)} onConfirm={() => void handleRemove()} />
) }