118 lines
5.0 KiB
TypeScript
118 lines
5.0 KiB
TypeScript
"use client"
|
||
|
||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||
import { Separator } from "@/shared/components/ui/separator"
|
||
import { formatCurrency, formatEnum } from "@/shared/utils/formatters"
|
||
import { cn } from "@/shared/lib/utils"
|
||
import { useBill } from "./bill-context"
|
||
|
||
export function BillTotalsSummary() {
|
||
const bill = useBill()
|
||
|
||
if (!bill) return null
|
||
|
||
const parts = bill.parts ?? []
|
||
const services = bill.services ?? []
|
||
const expenses = bill.expenses ?? []
|
||
|
||
const hasItems = parts.length > 0 || services.length > 0 || expenses.length > 0
|
||
if (!hasItems) return null
|
||
|
||
function lineTotal(items: { quantity?: string | number; rate?: string | number }[]) {
|
||
return items.reduce((sum, item) => {
|
||
const qty = parseFloat(String(item.quantity ?? 0))
|
||
const rate = parseFloat(String(item.rate ?? 0))
|
||
return sum + (isNaN(qty) || isNaN(rate) ? 0 : qty * rate)
|
||
}, 0)
|
||
}
|
||
|
||
const partsTotal = lineTotal(parts)
|
||
const servicesTotal = lineTotal(services)
|
||
const expensesTotal = lineTotal(expenses)
|
||
|
||
// Use API-computed values when available, fall back to manual calc
|
||
const subTotal = bill.sub_total ?? (partsTotal + servicesTotal + expensesTotal)
|
||
const taxAmount = bill.tax_amount ?? 0
|
||
const discountAmount = bill.discount_amount_major ?? 0
|
||
const total = bill.total ?? subTotal
|
||
const paymentsMade = bill.payments_made ?? 0
|
||
const balanceDue = bill.balance_due ?? total
|
||
const discount = bill.discount_type
|
||
|
||
return (
|
||
<Card>
|
||
<CardContent className="pt-6">
|
||
<div className="ml-auto max-w-sm space-y-2">
|
||
{parts.length > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Parts ({parts.length})</span>
|
||
<span>{formatCurrency(partsTotal)}</span>
|
||
</div>
|
||
)}
|
||
{services.length > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Services ({services.length})</span>
|
||
<span>{formatCurrency(servicesTotal)}</span>
|
||
</div>
|
||
)}
|
||
{expenses.length > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Expenses ({expenses.length})</span>
|
||
<span>{formatCurrency(expensesTotal)}</span>
|
||
</div>
|
||
)}
|
||
<Separator />
|
||
|
||
<div className="flex justify-between text-sm font-medium">
|
||
<span>Subtotal</span>
|
||
<span>{formatCurrency(subTotal)}</span>
|
||
</div>
|
||
|
||
{discountAmount > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Discount{discount && discount !== "no" ? ` (${formatEnum(discount)})` : ""}</span>
|
||
<span className="text-muted-foreground">−{formatCurrency(discountAmount)}</span>
|
||
</div>
|
||
)}
|
||
|
||
{taxAmount > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Tax{bill.tax?.name ? ` (${bill.tax.name})` : ""}</span>
|
||
<span>{formatCurrency(taxAmount)}</span>
|
||
</div>
|
||
)}
|
||
|
||
<Separator />
|
||
|
||
<div className={cn(
|
||
"flex justify-between rounded-lg px-3 py-2 text-base font-bold bg-muted/50",
|
||
)}>
|
||
<span>Total</span>
|
||
<span>{formatCurrency(total)}</span>
|
||
</div>
|
||
|
||
{paymentsMade > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Payments Made</span>
|
||
<span className="text-emerald-600">−{formatCurrency(paymentsMade)}</span>
|
||
</div>
|
||
)}
|
||
|
||
{paymentsMade > 0 && (
|
||
<>
|
||
<Separator />
|
||
<div className={cn(
|
||
"flex justify-between rounded-lg px-3 py-2 text-base font-semibold",
|
||
balanceDue > 0 ? "text-destructive bg-destructive/5" : "text-emerald-700 bg-emerald-50 dark:bg-emerald-950/20",
|
||
)}>
|
||
<span>Balance Due</span>
|
||
<span>{formatCurrency(balanceDue)}</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|