Files
Client_Fe/src/components/hexa/invoice/gym-invoice.tsx
T
Dhananjaya99 a1b870fa8a first commit
2026-09-28 11:48:14 +05:30

358 lines
9.8 KiB
TypeScript

import * as React from "react"
interface InvoiceAddress {
name: string
address: string[]
phone: string
email: string
}
interface InvoicePaymentRow {
label: string
amount: number
}
interface InvoicePayment {
subtotal: number
/**
* Empty when the plan is not split into installments.
* If populated, split-payment rows will be shown.
*/
payments: InvoicePaymentRow[]
totalPaid: number
balanceDue: number
}
interface GymInvoiceProps {
invoiceNo: string
invoiceDate: string
dueDate: string
/** Retained for API compatibility; the invoice no longer renders a logo. */
logoSrc?: string
logoAlt?: string
from: InvoiceAddress
billTo: InvoiceAddress
plan: {
gymPlanName: string
type: string
memberName: string
validityPeriod: string
}
payment: InvoicePayment
paymentMethod: string
preparedBy: string
notes: string[]
footerItems: string[]
className?: string
formatAmount: (amount: number) => string
/**
* This ref now captures the full A4 invoice page.
* The PDF download uses this exact node.
*/
bodyRef?: React.Ref<HTMLDivElement>
/**
* Kept for compatibility with existing caller.
* The footer is still inside the same invoice page.
*/
footerRef?: React.Ref<HTMLDivElement>
}
function GymInvoice({
invoiceNo,
invoiceDate,
dueDate,
from,
billTo,
plan,
payment,
paymentMethod,
preparedBy,
notes,
footerItems,
formatAmount,
bodyRef,
footerRef,
className = "",
}: GymInvoiceProps) {
return (
<div className={`mx-auto flex flex-col overflow-hidden bg-[#f4f4f4] px-6 py-6 font-sans ${className}`}>
<div
className="
mx-auto
h-[calc(1123px*var(--invoice-preview-scale))]
w-[calc(794px*var(--invoice-preview-scale))]
[--invoice-preview-scale:0.46]
sm:[--invoice-preview-scale:0.62]
md:[--invoice-preview-scale:1]
"
>
<div
className="
origin-top-left
transform-[scale(var(--invoice-preview-scale))]
"
>
<div
ref={bodyRef}
className="mx-auto flex h-280.75 w-198.5 flex-col overflow-hidden bg-white px-13 pt-10 pb-8 text-[#111111] font-sans"
>
{/* Header */}
<div className="flex items-start justify-between gap-4">
<h1 className="text-[40px] leading-none font-extrabold tracking-[2px] uppercase">
Invoice
</h1>
</div>
{/* Invoice meta */}
<div className="mt-7 grid gap-3">
<MetaRow label="Invoice No." value={invoiceNo} />
<MetaRow label="Invoice Date" value={invoiceDate} />
<MetaRow label="Due Date" value={dueDate} />
</div>
<Divider className="mt-7" />
{/* From / Bill To */}
<div className="mt-8 grid grid-cols-2 gap-16">
<InfoBlock title="From" data={from} />
<InfoBlock title="Bill To" data={billTo} />
</div>
{/* Membership / Service Details */}
<div className="mt-8">
<SectionTitle title="Membership / Service Details" />
<div className="mt-6 border-t-2 border-b border-t-[#111111] border-b-[#d1d1d1]">
<div className="grid grid-cols-[30%_18%_26%_26%] border-b-[1.5px] border-b-[#111111] [&>div:not(:last-child)]:border-r [&>div:not(:last-child)]:border-r-[#d8d8d8]">
<TableHeadCell>Gym Plan Name</TableHeadCell>
<TableHeadCell>Type</TableHeadCell>
<TableHeadCell>Name of Member(s)</TableHeadCell>
<TableHeadCell>Expiry Date / Validity Period</TableHeadCell>
</div>
<div className="grid grid-cols-[30%_18%_26%_26%] [&>div:not(:last-child)]:border-r [&>div:not(:last-child)]:border-r-[#d8d8d8]">
<TableCell>{plan.gymPlanName}</TableCell>
<TableCell>{plan.type}</TableCell>
<TableCell>{plan.memberName}</TableCell>
<TableCell>{plan.validityPeriod}</TableCell>
</div>
</div>
</div>
{/* Notes / Terms + Payment Summary */}
<div className="mt-8 grid grid-cols-[1fr_300px] items-start gap-10">
<div>
<SectionTitle title="Notes / Terms" />
<div className="mt-6 [&>p+p]:mt-4 [&>p:last-child]:border-b-0 [&>p:last-child]:pb-0">
{notes.map((note, i) => (
<p
key={i}
className="m-0 border-b border-b-[#d1d1d1] pb-3 text-[13px] leading-[1.45]"
>
{note}
</p>
))}
</div>
</div>
<PaymentSummary payment={payment} formatAmount={formatAmount} />
</div>
{/* Payment Method / Prepared By */}
<div className="mt-8 grid grid-cols-2 gap-16">
<div>
<SectionTitle title="Payment Method" />
<p className="mt-5 text-[14px]">{paymentMethod || "-"}</p>
</div>
<div>
<SectionTitle title="Prepared By" />
<p className="mt-5 text-[14px]">{preparedBy || "-"}</p>
</div>
</div>
{/* Footer stays inside the same one-page A4 receipt */}
<div ref={footerRef} className="mt-auto bg-white pt-6">
<Divider />
<div className="mt-5 flex flex-wrap items-center justify-center gap-5 text-[13px] text-[#555555]">
{footerItems.map((item, i) => (
<span key={i} className="flex items-center gap-5">
{i > 0 && <span aria-hidden>|</span>}
{item}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</div>
)
}
function Divider({ className = "" }: { className?: string }) {
return (
<div
className={`h-1.5 border-t-2 border-b-[3px] border-t-[#111111] border-b-[#8e1821] ${className}`}
/>
)
}
function MetaRow({ label, value }: { label: string; value: string }) {
return (
<div className="grid grid-cols-[130px_16px_1fr] items-center gap-x-3">
<span className="text-[13px] font-extrabold uppercase">{label}</span>
<span className="text-[13px]">:</span>
<p className="m-0 text-[14px]">{value}</p>
</div>
)
}
function SectionTitle({ title }: { title: string }) {
return (
<div>
<h2 className="m-0 text-[16px] font-extrabold tracking-[0.2px]">
{title}
</h2>
<span className="mt-1 block h-0.75 w-12 bg-[#8e1821]" />
</div>
)
}
function InfoBlock({ title, data }: { title: string; data: InvoiceAddress }) {
return (
<div className="min-w-0">
<SectionTitle title={title} />
<div className="mt-6">
<h3 className="m-0 mb-1.75 text-[14px] font-extrabold">
{data.name}
</h3>
{data.address.map((line, i) => (
<p
key={i}
className="m-0 mb-1.75 wrap-break-word text-[14px] leading-tight"
>
{line}
</p>
))}
<div className="mt-5">
<p className="m-0 mb-1.75 text-[14px] leading-tight">
{data.phone}
</p>
<p className="m-0 mb-1.75 wrap-break-word text-[14px] leading-tight">
{data.email}
</p>
</div>
</div>
</div>
)
}
function TableHeadCell({ children }: { children: React.ReactNode }) {
return (
<div className="box-border flex min-h-14 items-center justify-center px-3 py-3.5 text-center text-[12px] leading-tight font-extrabold">
{children}
</div>
)
}
function TableCell({ children }: { children: React.ReactNode }) {
return (
<div className="box-border flex min-h-13.5 items-center justify-center wrap-break-word px-3 py-3.5 text-center text-[13px] leading-[1.35]">
{children}
</div>
)
}
function PaymentSummary({
payment,
formatAmount,
}: {
payment: InvoicePayment
formatAmount: (amount: number) => string
}) {
const hasBreakdown = payment.payments.length > 0
return (
<div className="border border-[#111111]">
<SummaryRow label="Subtotal" amount={formatAmount(payment.subtotal)} />
{hasBreakdown ? (
<>
{payment.payments.map((row, i) => (
<SummaryRow
key={i}
label={row.label}
amount={formatAmount(row.amount)}
/>
))}
<SummaryRow
label="Total Paid"
amount={formatAmount(payment.totalPaid)}
/>
<SummaryRow
label="Balance Due"
amount={formatAmount(payment.balanceDue)}
highlight
/>
</>
) : (
<SummaryRow
label="Total"
amount={formatAmount(payment.subtotal)}
highlight
/>
)}
</div>
)
}
function SummaryRow({
label,
amount,
highlight = false,
}: {
label: string
amount: string
highlight?: boolean
}) {
return (
<div
className={`grid min-h-12 grid-cols-[1fr_auto] items-center gap-5 border-b border-b-[#d1d1d1] px-5 last:border-b-0 ${highlight
? "border-t-[1.5px] border-t-[#111111] bg-[#f7f7f7]"
: ""
}`}
>
<span
className={`font-extrabold uppercase ${highlight ? "text-[15px] font-black" : "text-[13px]"
}`}
>
{label}
</span>
<strong
className={`font-bold ${highlight ? "text-[15px] font-black" : "text-[13px]"
}`}
>
{amount}
</strong>
</div>
)
}
export { GymInvoice }
export type {
GymInvoiceProps,
InvoiceAddress,
InvoicePayment,
InvoicePaymentRow,
}