105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
"use client"
|
|
|
|
import { useFormContext } from "react-hook-form"
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import {
|
|
useDocumentTotals,
|
|
type DocumentLineItem,
|
|
} from "@/shared/hooks/use-document-totals"
|
|
import { DocumentTotalsSummary } from "@/shared/components/document-totals-summary"
|
|
|
|
import type { InvoiceFormValues } from "./invoice.schema"
|
|
|
|
/**
|
|
* Parse the numeric rate from an option label formatted as "Title (15%)".
|
|
* Returns undefined when no match.
|
|
*/
|
|
function parseTaxRateFromLabel(label: string | undefined | null): number | undefined {
|
|
if (!label) return undefined
|
|
const match = label.match(/\((\d+(?:\.\d+)?)%\)/)
|
|
return match ? Number(match[1]) : undefined
|
|
}
|
|
|
|
export function InvoiceFormSummary() {
|
|
const { watch } = useFormContext<InvoiceFormValues>()
|
|
|
|
const parts = watch("parts") ?? []
|
|
const services = watch("services") ?? []
|
|
const expenseItems = watch("expense_items") ?? []
|
|
const discountType = watch("discount")
|
|
const discountAmount = watch("discount_amount")
|
|
const taxRelation = watch("tax")
|
|
|
|
// Rate is embedded in the label: "VAT 15% (15%)" → parseTaxRateFromLabel → 15
|
|
const taxRate = parseTaxRateFromLabel(taxRelation?.label)
|
|
const taxLabel = taxRelation?.label ?? "Tax"
|
|
|
|
// Flatten all line items into the generic shape
|
|
const lineItems: DocumentLineItem[] = [
|
|
...parts.map((p) => ({
|
|
quantity: p.quantity,
|
|
rate: p.rate,
|
|
discount_amount: p.discount_amount,
|
|
})),
|
|
...services.map((s) => ({
|
|
quantity: s.quantity,
|
|
rate: s.rate,
|
|
discount_amount: s.discount_amount,
|
|
})),
|
|
...expenseItems.map((e) => ({
|
|
quantity: e.quantity,
|
|
rate: e.rate,
|
|
discount_amount: e.discount_amount,
|
|
})),
|
|
]
|
|
|
|
// Group breakdowns for display (only when more than one group has items)
|
|
const groupLabels: Record<string, number> = {}
|
|
if (parts.length > 0) {
|
|
groupLabels[`Parts (${parts.length})`] = parts.reduce(
|
|
(s, p) => s + (Number(p.quantity) || 0) * (Number(p.rate) || 0),
|
|
0,
|
|
)
|
|
}
|
|
if (services.length > 0) {
|
|
groupLabels[`Services (${services.length})`] = services.reduce(
|
|
(s, sv) => s + (Number(sv.quantity) || 0) * (Number(sv.rate) || 0),
|
|
0,
|
|
)
|
|
}
|
|
if (expenseItems.length > 0) {
|
|
groupLabels[`Expenses (${expenseItems.length})`] = expenseItems.reduce(
|
|
(s, e) => s + (Number(e.quantity) || 0) * (Number(e.rate) || 0),
|
|
0,
|
|
)
|
|
}
|
|
|
|
const totals = useDocumentTotals({
|
|
lineItems,
|
|
discountType,
|
|
discountAmount,
|
|
taxRate,
|
|
})
|
|
|
|
if (!totals.hasLineItems) return null
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
Summary
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DocumentTotalsSummary
|
|
totals={totals}
|
|
discountType={discountType}
|
|
taxLabel={taxLabel}
|
|
groupLabels={Object.keys(groupLabels).length > 1 ? groupLabels : undefined}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|