Offline-first invoicing app for Arun P, built by Bongbetic. - Tauri v2 (Rust) + React/TypeScript + IBM Carbon Design System UI - SQLite (rusqlite) storage with a persistent invoice-number series - Guided first-run setup, pre-filled vendor/bank details - Fixed-amount or rate x quantity line items (per sec/min/hour/session/unit) - GST as CGST+SGST or IGST, Indian amount-in-words - Optional signature, with a computer-generated note as fallback - Print-ready PDF export via @react-pdf/renderer - Windows cross-compile via cargo-xwin (NSIS installer) Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
193 lines
6.9 KiB
TypeScript
193 lines
6.9 KiB
TypeScript
import { UNIT_LABELS } from "../lib/types";
|
|
import { computeLineAmount } from "../lib/invoice";
|
|
import { formatAmount, formatDate } from "../lib/format";
|
|
import type { InvoicePdfProps } from "../pdf/InvoiceDocument";
|
|
|
|
const money = (n: number) => `₹${formatAmount(n)}`;
|
|
|
|
/** On-screen preview that mirrors the exported PDF layout. */
|
|
export function InvoicePreview(props: InvoicePdfProps) {
|
|
const {
|
|
vendor,
|
|
bank,
|
|
logoDataUri,
|
|
signatureDataUri,
|
|
number,
|
|
invoiceDate,
|
|
dueDate,
|
|
clientName,
|
|
clientAddress,
|
|
clientGstin,
|
|
poNumber,
|
|
placeOfSupplyStateCode,
|
|
items,
|
|
subtotal,
|
|
discount,
|
|
taxType,
|
|
taxRate,
|
|
cgst,
|
|
sgst,
|
|
igst,
|
|
total,
|
|
amountInWords,
|
|
notes,
|
|
} = props;
|
|
|
|
const isTaxInvoice = taxType !== "none";
|
|
const contact = [vendor.vendorEmail, vendor.vendorPhone].filter(Boolean).join(" · ");
|
|
|
|
return (
|
|
<div className="inv-preview-wrap">
|
|
<div className="inv-paper">
|
|
<div className="inv-top">
|
|
<div className="inv-vendor">
|
|
{logoDataUri ? <img className="inv-vendor__logo" src={logoDataUri} alt="" /> : null}
|
|
<div>
|
|
<h2 className="inv-vendor__name">{vendor.vendorName || "Vendor"}</h2>
|
|
<p className="inv-vendor__line">{vendor.vendorAddress}</p>
|
|
{contact ? <p className="inv-vendor__line">{contact}</p> : null}
|
|
{vendor.vendorPan ? <p className="inv-vendor__line">PAN: {vendor.vendorPan}</p> : null}
|
|
<p className="inv-vendor__line">GSTIN: {vendor.vendorGstin || "NA"}</p>
|
|
</div>
|
|
</div>
|
|
<div className="inv-meta">
|
|
<h3 className="inv-meta__title">{isTaxInvoice ? "Tax Invoice" : "Invoice"}</h3>
|
|
<div className="inv-meta__row">
|
|
<span className="inv-meta__label">Invoice No.</span>
|
|
<span className="inv-meta__value">{number}</span>
|
|
</div>
|
|
<div className="inv-meta__row">
|
|
<span className="inv-meta__label">Date</span>
|
|
<span className="inv-meta__value">{formatDate(invoiceDate)}</span>
|
|
</div>
|
|
{dueDate ? (
|
|
<div className="inv-meta__row">
|
|
<span className="inv-meta__label">Due Date</span>
|
|
<span className="inv-meta__value">{formatDate(dueDate)}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="inv-split">
|
|
<div className="inv-block" style={{ maxWidth: "60%" }}>
|
|
<p className="inv-block__label">Billed To</p>
|
|
<div className="inv-block__strong">{clientName || "—"}</div>
|
|
{clientAddress ? <div style={{ whiteSpace: "pre-line" }}>{clientAddress}</div> : null}
|
|
{clientGstin ? <div>GSTIN: {clientGstin}</div> : null}
|
|
{placeOfSupplyStateCode ? <div>Place of Supply: {placeOfSupplyStateCode}</div> : null}
|
|
</div>
|
|
{poNumber ? (
|
|
<div className="inv-block">
|
|
<p className="inv-block__label">PO Number</p>
|
|
<div className="inv-block__strong">{poNumber}</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<table className="inv-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Description</th>
|
|
<th className="num">Unit Price</th>
|
|
<th className="num">Qty</th>
|
|
<th className="num">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map((item, index) => (
|
|
<tr key={item.id ?? index}>
|
|
<td>{item.description || "—"}</td>
|
|
<td className="num">
|
|
{item.mode === "rate" ? `${money(item.rate)} ${UNIT_LABELS[item.unit]}` : "—"}
|
|
</td>
|
|
<td className="num">{item.mode === "rate" ? item.quantity : 1}</td>
|
|
<td className="num">{money(computeLineAmount(item))}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div className="inv-totals">
|
|
<div className="inv-totals__table">
|
|
<div className="inv-totals__row">
|
|
<span>Subtotal</span>
|
|
<span>{money(subtotal)}</span>
|
|
</div>
|
|
{discount > 0 ? (
|
|
<div className="inv-totals__row">
|
|
<span>Discount</span>
|
|
<span>- {money(discount)}</span>
|
|
</div>
|
|
) : null}
|
|
{taxType === "cgst_sgst" ? (
|
|
<>
|
|
<div className="inv-totals__row">
|
|
<span>CGST ({(taxRate / 2).toFixed(2)}%)</span>
|
|
<span>{money(cgst)}</span>
|
|
</div>
|
|
<div className="inv-totals__row">
|
|
<span>SGST ({(taxRate / 2).toFixed(2)}%)</span>
|
|
<span>{money(sgst)}</span>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
{taxType === "igst" ? (
|
|
<div className="inv-totals__row">
|
|
<span>IGST ({taxRate.toFixed(2)}%)</span>
|
|
<span>{money(igst)}</span>
|
|
</div>
|
|
) : null}
|
|
<div className="inv-totals__row inv-totals__row--grand">
|
|
<span>Total</span>
|
|
<span>{money(total)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="inv-words">Amount in words: {amountInWords}</p>
|
|
|
|
{notes ? (
|
|
<div style={{ marginTop: 16 }}>
|
|
<p className="inv-block__label">Notes</p>
|
|
<div style={{ whiteSpace: "pre-line" }}>{notes}</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="inv-bottom">
|
|
<div className="inv-block" style={{ maxWidth: "55%" }}>
|
|
{bank ? (
|
|
<>
|
|
<p className="inv-block__label">Pay To</p>
|
|
<div className="inv-block__strong">{bank.accountName || vendor.vendorName}</div>
|
|
<div>{bank.bankName}</div>
|
|
{bank.accountNo ? <div>Account No.: {bank.accountNo}</div> : null}
|
|
{bank.branch ? <div>Branch: {bank.branch}</div> : null}
|
|
{bank.ifsc ? <div>IFSC: {bank.ifsc}</div> : null}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
<div className="inv-signature">
|
|
{signatureDataUri ? (
|
|
<>
|
|
<img className="inv-signature__img" src={signatureDataUri} alt="Signature" />
|
|
<div className="inv-signature__name">{vendor.vendorName}</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="inv-signature__name">{vendor.vendorName}</div>
|
|
<p className="inv-fineprint">Computer-generated invoice; no signature is required.</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="inv-footer">
|
|
<span>{vendor.vendorName}</span>
|
|
<span>Generated with Voiced — a Bongbetic product</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|