Initial commit: Voiced invoice desktop app
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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user