feat: Implement free issue tracking and scheme discount handling in invoices and Shop POs
This commit is contained in:
@@ -34,9 +34,11 @@ const statusLabel = (invoice: InvoiceResponse) =>
|
||||
|
||||
// Removed lines stay on the document (struck through) but are excluded from every total.
|
||||
function totals(invoice: InvoiceResponse, payments: PaymentResponse[]) {
|
||||
const total = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
|
||||
const subtotal = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
|
||||
const discount = Math.min(invoice.discountAmount ?? 0, subtotal)
|
||||
const total = subtotal - discount
|
||||
const paid = payments.reduce((s, p) => s + p.amount, 0)
|
||||
return { total, paid, balance: total - paid }
|
||||
return { subtotal, discount, total, paid, balance: total - paid }
|
||||
}
|
||||
|
||||
export function InvoiceTemplate({ paper = 'A4', ...props }: InvoiceTemplateProps) {
|
||||
@@ -56,9 +58,12 @@ function A4Sheet({
|
||||
showAgentEdits,
|
||||
className,
|
||||
}: Omit<InvoiceTemplateProps, 'paper'>) {
|
||||
const { total, paid, balance } = totals(invoice, payments)
|
||||
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
|
||||
const status = statusLabel(invoice)
|
||||
const hasRemoved = invoice.items.some((i) => i.isRemoved)
|
||||
const hasFree = invoice.items.some((i) => i.isFree)
|
||||
// Free issues are listed after every paid line.
|
||||
const rows = [...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -107,6 +112,7 @@ function A4Sheet({
|
||||
<tr className="border-b border-neutral-300 text-xs uppercase tracking-wide text-neutral-500">
|
||||
<th className="px-2 py-2 text-left font-medium">Item</th>
|
||||
<th className="w-24 px-2 py-2 text-right font-medium">Qty</th>
|
||||
{hasFree && <th className="w-24 px-2 py-2 text-right font-medium">Free Issue</th>}
|
||||
<th className="w-36 px-2 py-2 text-right font-medium">Unit Price</th>
|
||||
<th className="w-40 px-2 py-2 text-right font-medium">Amount</th>
|
||||
{renderActions && <th className="w-24 px-2 py-2 print:hidden" />}
|
||||
@@ -118,15 +124,25 @@ function A4Sheet({
|
||||
<td colSpan={4} className="px-2 py-6 text-center text-neutral-500">No items.</td>
|
||||
</tr>
|
||||
) : (
|
||||
invoice.items.map((item) => (
|
||||
rows.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={cn('break-inside-avoid border-b border-neutral-200', item.isRemoved && 'text-neutral-400 line-through')}
|
||||
>
|
||||
<td className="px-2 py-2.5">{item.itemName}</td>
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
|
||||
<td className="px-2 py-2.5">
|
||||
{item.itemName}
|
||||
{item.isFree && item.schemeName && (
|
||||
<span className="block text-xs text-neutral-500 no-underline">Free issue · {item.schemeName}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{item.isFree ? '-' : renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
|
||||
</td>
|
||||
{hasFree && (
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{item.isFree ? (renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)) : '-'}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-2 py-2.5 text-right">
|
||||
{renderUnitPrice && !item.isRemoved ? renderUnitPrice(item) : fmtMoney(item.unitPrice)}
|
||||
</td>
|
||||
@@ -160,8 +176,14 @@ function A4Sheet({
|
||||
<dl className="w-72 break-inside-avoid text-sm">
|
||||
<div className="flex justify-between border-b py-1.5">
|
||||
<dt className="text-neutral-500">Subtotal</dt>
|
||||
<dd>{fmtMoney(total)}</dd>
|
||||
<dd>{fmtMoney(subtotal)}</dd>
|
||||
</div>
|
||||
{discount > 0 && (
|
||||
<div className="flex justify-between border-b py-1.5">
|
||||
<dt className="text-neutral-500">Scheme discount</dt>
|
||||
<dd>-{fmtMoney(discount)}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between border-y py-2 text-base font-bold" style={{ borderColor: RULE }}>
|
||||
<dt>Total</dt>
|
||||
<dd>{fmtMoney(total)}</dd>
|
||||
@@ -225,7 +247,7 @@ function ReceiptSheet({
|
||||
showAgentEdits,
|
||||
className,
|
||||
}: Omit<InvoiceTemplateProps, 'paper'> & { paper: Exclude<InvoicePaper, 'A4'> }) {
|
||||
const { total, paid, balance } = totals(invoice, payments)
|
||||
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
|
||||
const status = statusLabel(invoice)
|
||||
const narrow = paper === '58mm'
|
||||
const rule = <div className="my-2 border-t border-dashed border-black" />
|
||||
@@ -252,12 +274,21 @@ function ReceiptSheet({
|
||||
</div>
|
||||
{rule}
|
||||
<div className="space-y-1.5">
|
||||
{invoice.items.map((i) => (
|
||||
{[...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)).map((i) => (
|
||||
<div key={i.id} className={cn(i.isRemoved && 'line-through opacity-60')}>
|
||||
<p>{i.itemName}</p>
|
||||
<p className="flex justify-between">
|
||||
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
|
||||
<span>{num(i.quantity * i.unitPrice)}</span>
|
||||
{i.isFree ? (
|
||||
<>
|
||||
<span>{fmtQty(i.quantity)} FREE{i.schemeName ? ` (${i.schemeName})` : ''}</span>
|
||||
<span>{num(0)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
|
||||
<span>{num(i.quantity * i.unitPrice)}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -265,8 +296,14 @@ function ReceiptSheet({
|
||||
{rule}
|
||||
<p className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>{fmtMoney(total)}</span>
|
||||
<span>{fmtMoney(subtotal)}</span>
|
||||
</p>
|
||||
{discount > 0 && (
|
||||
<p className="flex justify-between">
|
||||
<span>Scheme discount</span>
|
||||
<span>-{fmtMoney(discount)}</span>
|
||||
</p>
|
||||
)}
|
||||
<div className="my-1 border-y-2 border-black py-1 text-sm font-bold">
|
||||
<p className="flex justify-between">
|
||||
<span>TOTAL</span>
|
||||
|
||||
Reference in New Issue
Block a user