Menu

Change language

GitHub repo

Commercial Invoice Generator

blockdashboard

B2B cross-border commercial invoice generator with HTS tariff codes, EORI tax identifiers, and CIF valuation totals.

Installation

$npx uipkge-ng@latest add commercial-invoice-generator
Manifest:https://uipkge.dev/r/angular/commercial-invoice-generator.jsonInstalls to:src/app/components/blocks/commercial-invoice-generator/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

InvoiceItem
interface 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/commercial-invoice-generator.json