updates
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"use client"
|
||||
|
||||
import { Separator } from "@/shared/components/ui/separator"
|
||||
import { formatCurrency } from "@/shared/utils/formatters"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import type { DocumentTotals } from "@/shared/hooks/use-document-totals"
|
||||
|
||||
export type DocumentTotalsSummaryProps = {
|
||||
totals: DocumentTotals
|
||||
discountType?: string | null
|
||||
taxLabel?: string | null
|
||||
/** Override labels for line groups, keyed by group name */
|
||||
groupLabels?: Record<string, number>
|
||||
}
|
||||
|
||||
function SummaryRow({
|
||||
label,
|
||||
value,
|
||||
muted = false,
|
||||
className,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
muted?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex items-center justify-between text-sm", className)}>
|
||||
<span className={muted ? "text-muted-foreground" : ""}>{label}</span>
|
||||
<span className={muted ? "text-muted-foreground" : ""}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentTotalsSummary({
|
||||
totals,
|
||||
discountType,
|
||||
taxLabel,
|
||||
groupLabels,
|
||||
}: DocumentTotalsSummaryProps) {
|
||||
const {
|
||||
subTotal,
|
||||
lineItemDiscount,
|
||||
transactionDiscount,
|
||||
totalDiscount,
|
||||
taxAmount,
|
||||
total,
|
||||
hasLineItems,
|
||||
} = totals
|
||||
|
||||
if (!hasLineItems) return null
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Group breakdowns */}
|
||||
{groupLabels &&
|
||||
Object.entries(groupLabels).map(([label, amount]) => (
|
||||
<SummaryRow
|
||||
key={label}
|
||||
label={label}
|
||||
value={formatCurrency(amount)}
|
||||
muted
|
||||
/>
|
||||
))}
|
||||
|
||||
{groupLabels && Object.keys(groupLabels).length > 0 && <Separator />}
|
||||
|
||||
{/* Subtotal */}
|
||||
<SummaryRow label="Subtotal" value={formatCurrency(subTotal)} />
|
||||
|
||||
{/* Line-item discount */}
|
||||
{discountType === "line_item_level" && lineItemDiscount > 0 && (
|
||||
<SummaryRow
|
||||
label="Line Discounts"
|
||||
value={`– ${formatCurrency(lineItemDiscount)}`}
|
||||
muted
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Transaction-level discount */}
|
||||
{discountType === "transaction_level" && transactionDiscount > 0 && (
|
||||
<SummaryRow
|
||||
label="Discount"
|
||||
value={`– ${formatCurrency(transactionDiscount)}`}
|
||||
muted
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tax */}
|
||||
{taxAmount > 0 && (
|
||||
<SummaryRow
|
||||
label={taxLabel ?? "Tax"}
|
||||
value={`+ ${formatCurrency(taxAmount)}`}
|
||||
muted
|
||||
/>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Total */}
|
||||
<div className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 text-sm font-semibold",
|
||||
"bg-primary/10 text-primary",
|
||||
)}>
|
||||
<span>Total</span>
|
||||
<span>{formatCurrency(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -109,15 +109,14 @@ export function RhfResourceField<
|
||||
{triggerLabel ?? label}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{(items as any[]).length > 0
|
||||
? renderItems(items, helpers)
|
||||
: (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No items added yet.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
{
|
||||
items.length > 0 &&
|
||||
<CardContent>
|
||||
{(items as any[]).length > 0
|
||||
&& renderItems(items, helpers)
|
||||
}
|
||||
</CardContent>
|
||||
}
|
||||
</Card>
|
||||
{error && <FieldError>{error.message}</FieldError>}
|
||||
<ResourceSelectorDialog<TClient>
|
||||
|
||||
@@ -44,7 +44,11 @@ function ActionsCell<TData extends { id: string | number }>({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{options.onEdit && (
|
||||
<DropdownMenuItem onClick={() => options.onEdit!(row.original)}>
|
||||
<DropdownMenuItem onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
options.onEdit!(row.original)
|
||||
|
||||
}}>
|
||||
<Pencil className="size-3.5 text-muted-foreground" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
@@ -52,7 +56,10 @@ function ActionsCell<TData extends { id: string | number }>({
|
||||
{options.onDelete && (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => options.onDelete!(row.original)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
options.onDelete!(row.original)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
Delete
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* useDocumentTotals
|
||||
*
|
||||
* Pure calculation hook for invoice-style documents (invoices, bills, estimates,
|
||||
* credit notes, vendor credits, etc.).
|
||||
*
|
||||
* Inputs
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* lineItems – flat array of all items (parts + services + expenses)
|
||||
* discountType – 'no' | 'line_item_level' | 'transaction_level'
|
||||
* discountAmount – used when discountType === 'transaction_level'
|
||||
* taxRate – percentage (e.g. 15 for 15 %). Undefined → no tax.
|
||||
*
|
||||
* Calculation order
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* 1. subTotal = Σ (qty × rate)
|
||||
* 2. lineItemDiscount = Σ discount_amount (only when discountType === 'line_item_level')
|
||||
* 3. transactionDiscount = discountAmount (only when discountType === 'transaction_level')
|
||||
* 4. totalDiscount = lineItemDiscount + transactionDiscount
|
||||
* 5. afterDiscount = subTotal - totalDiscount
|
||||
* 6. taxAmount = afterDiscount × (taxRate / 100)
|
||||
* 7. total = afterDiscount + taxAmount
|
||||
*/
|
||||
|
||||
export type DocumentLineItem = {
|
||||
quantity: number
|
||||
rate: number
|
||||
discount_amount?: number
|
||||
}
|
||||
|
||||
export type UseDocumentTotalsInput = {
|
||||
lineItems: DocumentLineItem[]
|
||||
discountType?: string | null
|
||||
discountAmount?: number
|
||||
taxRate?: number
|
||||
}
|
||||
|
||||
export type DocumentTotals = {
|
||||
subTotal: number
|
||||
lineItemDiscount: number
|
||||
transactionDiscount: number
|
||||
totalDiscount: number
|
||||
afterDiscount: number
|
||||
taxAmount: number
|
||||
total: number
|
||||
hasLineItems: boolean
|
||||
}
|
||||
|
||||
export function useDocumentTotals({
|
||||
lineItems,
|
||||
discountType,
|
||||
discountAmount,
|
||||
taxRate,
|
||||
}: UseDocumentTotalsInput): DocumentTotals {
|
||||
const subTotal = lineItems.reduce((sum, item) => {
|
||||
const qty = Number(item.quantity) || 0
|
||||
const rate = Number(item.rate) || 0
|
||||
return sum + qty * rate
|
||||
}, 0)
|
||||
|
||||
const lineItemDiscount =
|
||||
discountType === "line_item_level"
|
||||
? lineItems.reduce((sum, item) => sum + (Number(item.discount_amount) || 0), 0)
|
||||
: 0
|
||||
|
||||
const transactionDiscount =
|
||||
discountType === "transaction_level" ? Number(discountAmount) || 0 : 0
|
||||
|
||||
const totalDiscount = lineItemDiscount + transactionDiscount
|
||||
const afterDiscount = Math.max(0, subTotal - totalDiscount)
|
||||
const taxAmount = taxRate ? afterDiscount * (taxRate / 100) : 0
|
||||
const total = afterDiscount + taxAmount
|
||||
|
||||
return {
|
||||
subTotal,
|
||||
lineItemDiscount,
|
||||
transactionDiscount,
|
||||
totalDiscount,
|
||||
afterDiscount,
|
||||
taxAmount,
|
||||
total,
|
||||
hasLineItems: lineItems.length > 0,
|
||||
}
|
||||
}
|
||||
@@ -24,3 +24,8 @@ export function toId(relation: RelationFieldValue | undefined): number | undefin
|
||||
return relation ? Number(relation.value) : undefined
|
||||
}
|
||||
|
||||
|
||||
export function getTodayDate() {
|
||||
const today = new Date().toISOString().split("T")[0]
|
||||
return today
|
||||
}
|
||||
Reference in New Issue
Block a user