Commercial Invoice Generator
blockdashboardB2B cross-border commercial invoice generator with HTS tariff codes, EORI tax identifiers, and CIF valuation totals.
Installation
$pnpm dlx uipkge-ng@latest add commercial-invoice-generator$npx uipkge-ng@latest add commercial-invoice-generator$yarn dlx uipkge-ng@latest add commercial-invoice-generator$bunx uipkge-ng@latest add commercial-invoice-generatorManifest:
https://uipkge.dev/r/angular/commercial-invoice-generator.jsonInstalls to:src/app/components/blocks/commercial-invoice-generator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
InvoiceIteminterface InvoiceItem {
id: string
description: string
htsCode: string
countryOfOrigin: string
qty: number
unitPrice: number
weightKg: number
}Files installed (2)
src/app/components/blocks/commercial-invoice-generator/commercial-invoice-generator.component.ts15.6 kB
import { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core' import { Download, LucideAngularModule, Plus, Printer, ShieldCheck, Trash2 } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface InvoiceItem { id: string description: string htsCode: string countryOfOrigin: string qty: number unitPrice: number weightKg: number } export interface CommercialInvoiceProps { invoiceNumber?: string incoterms?: string currency?: string className?: string } /** * CommercialInvoiceGenerator -- the React block, 1:1: an HTS customs invoice with * exporter / importer parties, editable line items (description, tariff code, origin, * weight, qty, unit price) and FOB + freight + insurance = CIF totals. Signing the * declaration locks every row into read-only text; "Unlock Edit" reopens it. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-commercial-invoice-generator, [ui-commercial-invoice-generator]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardDescriptionComponent, UiCardContentComponent, UiCardFooterComponent, UiInputComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, ], template: ` <div data-slot="commercial-invoice-generator" [class]="rootClass"> <!-- Top Action Card --> <div ui-card> <div ui-card-header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2"> <span class="text-foreground font-mono text-sm font-semibold">{{ invoiceNumber }}</span> <span ui-badge variant="outline">{{ incoterms }}</span> <span ui-badge [variant]="isSigned() ? 'default' : 'secondary'"> {{ isSigned() ? 'Customs Declared & Sealed' : 'Draft Declaration' }} </span> </div> <h3 ui-card-title class="text-xl">International Commercial Customs Invoice</h3> <p ui-card-description> Harmonized Tariff Schedule (HTS) compliance declaration for international customs clearance. </p> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="gap-1.5"> <lucide-icon [img]="Printer" class="size-3.5" aria-hidden="true" /> Print Form </button> <button ui-button variant="outline" size="sm" class="gap-1.5"> <lucide-icon [img]="Download" class="size-3.5" aria-hidden="true" /> Export EDI / PDF </button> @if (!isSigned()) { <button ui-button variant="default" size="sm" class="gap-1.5" (click)="isSigned.set(true)"> <lucide-icon [img]="ShieldCheck" class="size-3.5" aria-hidden="true" /> Sign Customs Declaration </button> } @else { <button ui-button variant="secondary" size="sm" class="gap-1.5" (click)="isSigned.set(false)"> Unlock Edit </button> } </div> </div> <div ui-card-content class="border-border grid grid-cols-1 gap-6 border-t pt-4 sm:grid-cols-2"> <!-- Exporter / Shipper --> <div class="border-border space-y-1 rounded-lg border p-3"> <div class="text-muted-foreground text-xs font-semibold">EXPORTER / SHIPPER (CONSIGNOR)</div> <div class="text-foreground text-sm font-medium">Global Aerospace Components Inc.</div> <div class="text-muted-foreground text-xs">9400 Aeronautics Way, Dock 4</div> <div class="text-muted-foreground text-xs">Seattle, WA 98108, United States</div> <div class="text-muted-foreground pt-1 font-mono text-xs">EORI / Tax ID: US-EIN-98441029</div> </div> <!-- Importer / Consignee --> <div class="border-border space-y-1 rounded-lg border p-3"> <div class="text-muted-foreground text-xs font-semibold">IMPORTER / BUYER (CONSIGNEE)</div> <div class="text-foreground text-sm font-medium">Nordic Robotics Systems AB</div> <div class="text-muted-foreground text-xs">Industrigatan 18, Byggnad B</div> <div class="text-muted-foreground text-xs">SE-417 56 Göteborg, Sweden</div> <div class="text-muted-foreground pt-1 font-mono text-xs">VAT / EORI: SE556012345601</div> </div> </div> </div> <!-- Line Items Table --> <div ui-card> <div ui-card-header class="flex flex-col gap-3 pb-3 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Customs Line Item Breakdown</h3> <p ui-card-description> Total Gross Net Weight: <span class="font-mono">{{ totalWeightKg().toFixed(1) }} kg</span> · Currency: {{ currency }} </p> </div> @if (!isSigned()) { <button ui-button variant="outline" size="sm" class="gap-1.5" (click)="addItem()"> <lucide-icon [img]="Plus" class="size-3.5" aria-hidden="true" /> Add Line Item </button> } </div> <div ui-card-content class="p-0"> <div ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="w-[300px]">Goods Description</th> <th ui-table-head>HTS / Tariff Code</th> <th ui-table-head>Origin (COO)</th> <th ui-table-head class="text-right">Net Wt (kg)</th> <th ui-table-head class="w-[90px] text-right">Qty</th> <th ui-table-head class="w-[120px] text-right">Unit Price</th> <th ui-table-head class="text-right">Customs Total</th> @if (!isSigned()) { <th ui-table-head class="w-[40px]"></th> } </tr> </thead> <tbody ui-table-body> @for (item of items(); track item.id) { <tr ui-table-row> <!-- Description --> <td ui-table-cell> @if (!isSigned()) { <ui-input [value]="item.description" (valueChange)="updateItem(item.id, { description: $event })" class="h-8 text-xs" /> } @else { <span class="text-foreground font-medium">{{ item.description }}</span> } </td> <!-- HTS Code --> <td ui-table-cell> @if (!isSigned()) { <ui-input [value]="item.htsCode" (valueChange)="updateItem(item.id, { htsCode: $event })" class="h-8 w-28 font-mono text-xs" /> } @else { <span class="font-mono text-xs font-semibold">{{ item.htsCode }}</span> } </td> <!-- Country of Origin --> <td ui-table-cell> @if (!isSigned()) { <ui-input [value]="item.countryOfOrigin" (valueChange)="updateItem(item.id, { countryOfOrigin: $event })" class="h-8 w-16 text-center font-mono text-xs uppercase" /> } @else { <span class="bg-muted rounded px-1.5 py-0.5 font-mono text-xs font-medium">{{ item.countryOfOrigin }}</span> } </td> <!-- Net Weight --> <td ui-table-cell class="text-right"> @if (!isSigned()) { <ui-input type="number" step="0.1" [value]="item.weightKg" (valueChange)="updateItem(item.id, { weightKg: toNumber($event) })" class="h-8 text-right font-mono text-xs" /> } @else { <span class="text-muted-foreground font-mono text-xs">{{ item.weightKg.toFixed(1) }}</span> } </td> <!-- Qty --> <td ui-table-cell class="text-right"> @if (!isSigned()) { <ui-input type="number" min="1" [value]="item.qty" (valueChange)="updateItem(item.id, { qty: toNumber($event) })" class="h-8 text-right font-mono text-xs" /> } @else { <span class="font-mono text-sm">{{ item.qty }}</span> } </td> <!-- Unit Price --> <td ui-table-cell class="text-right"> @if (!isSigned()) { <ui-input type="number" step="0.01" [value]="item.unitPrice" (valueChange)="updateItem(item.id, { unitPrice: toNumber($event) })" class="h-8 text-right font-mono text-xs" /> } @else { <span class="font-mono text-sm">{{ money(item.unitPrice) }}</span> } </td> <!-- Line Total --> <td ui-table-cell class="text-foreground text-right font-mono text-sm font-semibold"> {{ money(item.qty * item.unitPrice) }} </td> <!-- Delete --> @if (!isSigned()) { <td ui-table-cell> <button ui-button variant="ghost" size="icon" class="text-muted-foreground hover:text-destructive size-8" [attr.aria-label]="'Remove line item ' + item.description" (click)="removeItem(item.id)" > <lucide-icon [img]="Trash2" class="size-3.5" aria-hidden="true" /> </button> </td> } </tr> } </tbody> </div> </div> <!-- Totals & Tariff Summary Footer --> <div ui-card-footer class="border-border flex flex-col gap-4 border-t pt-4 sm:flex-row sm:items-start sm:justify-between" > <div class="text-muted-foreground space-y-1 text-xs sm:max-w-md"> <p class="text-foreground font-medium">Exporter Declaration & Legal Certification:</p> <p> I hereby certify that the information on this invoice is true and correct and that the contents of this shipment are as stated above. </p> </div> <div class="w-full space-y-1.5 sm:w-72"> <div class="text-muted-foreground flex justify-between text-xs"> <span>FOB Line Subtotal:</span> <span class="text-foreground font-mono font-medium">{{ money(subtotal()) }}</span> </div> <div class="text-muted-foreground flex justify-between text-xs"> <span>International Freight (Air):</span> <span class="text-foreground font-mono font-medium">{{ money(freightCharges()) }}</span> </div> <div class="text-muted-foreground flex justify-between text-xs"> <span>Cargo Marine Insurance:</span> <span class="text-foreground font-mono font-medium">{{ money(insuranceCharges()) }}</span> </div> <div class="border-border text-foreground flex justify-between border-t pt-2 text-sm font-semibold"> <span>Declared CIF Value:</span> <span class="font-mono text-base font-bold">{{ money(grandTotal()) }} USD</span> </div> </div> </div> </div> </div> `, }) export class UiCommercialInvoiceGeneratorComponent { @Input() invoiceNumber = 'INV-EXP-2026-9042' @Input() incoterms = 'DAP (Delivered at Place)' @Input() currency = 'USD' @Input('class') className?: string protected readonly Printer = Printer protected readonly Download = Download protected readonly ShieldCheck = ShieldCheck protected readonly Plus = Plus protected readonly Trash2 = Trash2 readonly isSigned = signal(false) readonly freightCharges = signal(450.0) readonly insuranceCharges = signal(85.0) readonly items = signal<InvoiceItem[]>([ { id: 'inv-1', description: 'Precision CNC Machined Aluminum Actuator Housing', htsCode: '8479.90.94', countryOfOrigin: 'US', qty: 50, unitPrice: 185.0, weightKg: 24.5, }, { id: 'inv-2', description: 'Brushless DC High-Torque Servo Motor 48V', htsCode: '8501.31.20', countryOfOrigin: 'DE', qty: 25, unitPrice: 320.0, weightKg: 31.0, }, { id: 'inv-3', description: 'Industrial Polyurethane Seal Gasket Kit', htsCode: '3926.90.99', countryOfOrigin: 'JP', qty: 100, unitPrice: 14.5, weightKg: 4.2, }, ]) readonly subtotal = computed(() => this.items().reduce((sum, item) => sum + item.qty * item.unitPrice, 0)) readonly totalWeightKg = computed(() => this.items().reduce((sum, item) => sum + item.weightKg, 0)) readonly grandTotal = computed(() => this.subtotal() + this.freightCharges() + this.insuranceCharges()) get rootClass(): string { return cn('w-full space-y-6', this.className) } addItem(): void { this.items.update((prev) => [ ...prev, { id: `inv-${Date.now()}`, description: 'General Industrial Hardware / Fasteners', htsCode: '7318.15.20', countryOfOrigin: 'US', qty: 10, unitPrice: 25.0, weightKg: 2.0, }, ]) } removeItem(id: string): void { this.items.update((prev) => prev.filter((i) => i.id !== id)) } updateItem(id: string, updates: Partial<InvoiceItem>): void { this.items.update((prev) => prev.map((item) => (item.id === id ? { ...item, ...updates } : item))) } /** React's `Number(e.target.value)` for the numeric cells. */ toNumber(value: string): number { return Number(value) } /** React's `$${n.toFixed(2)}`. */ money(value: number): string { return `$${value.toFixed(2)}` } }src/app/components/blocks/commercial-invoice-generator/index.ts0.1 kB
export { UiCommercialInvoiceGeneratorComponent, type InvoiceItem, type CommercialInvoiceProps, } from './commercial-invoice-generator.component'
Raw manifest:https://uipkge.dev/r/angular/commercial-invoice-generator.json