Menu

Change language

GitHub repo

Bill Of Lading Viewer

blocklogistics

Maritime & Air Bill of Lading (BOL) shipping document viewer and customs compliance inspector with multi-party entity grid, transport routing, container & cargo manifest, freight terms, carrier stamp, and blockchain verification.

Installation

$npx uipkge-ng@latest add bill-of-lading-viewer
Manifest:https://uipkge.dev/r/angular/bill-of-lading-viewer.jsonInstalls to:src/app/components/blocks/bill-of-lading-viewer/

Variants

Loading interactive previews…

Schema

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

ContainerCargoItem
interface ContainerCargoItem {
  id: string
  containerNo: string
  sealNo: string
  containerType: string
  packagesCount: number
  packagesKind: string
  description: string
  hsCode: string
  grossWeightKg: number
  grossWeightLbs: number
  volumeCbm: number
  hazardClass?: string
  temperature?: string
}
ComplianceGate
interface ComplianceGate {
  id: string
  name: string
  status: 'passed' | 'warning' | 'pending'
  authority: string
  reference: string
  details: string
}

Files installed (2)

  • src/app/components/blocks/bill-of-lading-viewer/bill-of-lading-viewer.component.ts44.2 kB
    // Maritime & Air Bill of Lading (BOL) shipping document viewer and customs compliance
    // inspector: multi-party entity grid, transport routing, container & cargo manifest with
    // totals, freight terms, carrier stamp, blockchain verification drawer, and UN/EDIFACT
    // EDI payload tab. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, EventEmitter, Input, Output, computed, inject, signal } from '@angular/core'
    import {
      AlertCircle,
      Anchor,
      Building2,
      Check,
      CheckCircle2,
      Copy,
      Download,
      FileText,
      LucideAngularModule,
      Mail,
      MapPin,
      Phone,
      Printer,
      ShieldCheck,
      Ship,
    } 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,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableFooterComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface ContainerCargoItem {
      id: string
      containerNo: string
      sealNo: string
      containerType: string
      packagesCount: number
      packagesKind: string
      description: string
      hsCode: string
      grossWeightKg: number
      grossWeightLbs: number
      volumeCbm: number
      hazardClass?: string
      temperature?: string
    }
    
    export interface ComplianceGate {
      id: string
      name: string
      status: 'passed' | 'warning' | 'pending'
      authority: string
      reference: string
      details: string
    }
    
    export type BillOfLadingTab = 'document' | 'compliance' | 'edi'
    
    const cargoItems: ContainerCargoItem[] = [
      {
        id: 'cargo-1',
        containerNo: 'MSKU-948201-4',
        sealNo: 'SL-94021',
        containerType: "40' High Cube (40' HC)",
        packagesCount: 24,
        packagesKind: 'Pallets / 480 Cartons',
        description: 'Consumer Electronics: Pro Audio Studio Headphones, Dynamic Transducers & Wireless DAC Amplifiers',
        hsCode: '8518.30.00',
        grossWeightKg: 4850.0,
        grossWeightLbs: 10692.42,
        volumeCbm: 32.4,
        hazardClass: 'UN3481 Class 9 (Lithium Ion Batteries contained in equipment)',
        temperature: 'Ambient (Dry Van)',
      },
      {
        id: 'cargo-2',
        containerNo: 'MSKU-819034-2',
        sealNo: 'SL-94022',
        containerType: "40' High Cube (40' HC)",
        packagesCount: 16,
        packagesKind: 'Pallets / 320 Cartons',
        description: 'Audio Accessories: Braided MMCX Oxygen-Free Cables, Velour Replacement Earpads & Hard Travel Cases',
        hsCode: '8518.90.80',
        grossWeightKg: 2320.0,
        grossWeightLbs: 5114.72,
        volumeCbm: 18.6,
        hazardClass: 'Non-Hazardous / Standard Cargo',
        temperature: 'Ambient (Dry Van)',
      },
    ]
    
    const complianceGates: ComplianceGate[] = [
      {
        id: 'gate-1',
        name: 'EU EORI & VAT Identity Verification',
        status: 'passed',
        authority: 'European Customs Agency (VIES / EOS)',
        reference: 'NL864219034',
        details: 'Consignee EORI validated against EU Economic Operators Registry with valid Dutch VAT matching.',
      },
      {
        id: 'gate-2',
        name: 'ICS2 Entry Summary Declaration (ENS)',
        status: 'passed',
        authority: 'EU Customs Import Control System 2',
        reference: 'MRN: 26NL948201083921A',
        details: 'Pre-arrival cargo security manifest filed 24 hours prior to loading in Shanghai (Approved).',
      },
      {
        id: 'gate-3',
        name: 'HS Tariff Classification & Duty Schedule',
        status: 'passed',
        authority: 'TARIC European Customs Database',
        reference: 'HS 8518.30.00 / 8518.90.80',
        details: 'Eligible for 0.0% EU base tariff rate + standard 21.0% Netherlands Import VAT under Article 23.',
      },
      {
        id: 'gate-4',
        name: 'ISO 17712 High-Security Container Seals',
        status: 'passed',
        authority: 'International Maritime Organization (IMO)',
        reference: 'SL-94021 / SL-94022',
        details: 'High-security mechanical bolt seals verified intact at origin terminal container gate-in.',
      },
      {
        id: 'gate-5',
        name: 'IMDG Dangerous Goods UN3481 Compliance',
        status: 'passed',
        authority: 'International Maritime Dangerous Goods (IMDG)',
        reference: 'UN3481 PI966 Sec. II',
        details: 'Lithium-ion cells packed with equipment certified under special provision 188 for under-deck stowage.',
      },
      {
        id: 'gate-6',
        name: 'Global Sanctions & Denied Party Screening',
        status: 'passed',
        authority: 'UN, OFAC & EU Consolidated Sanction Lists',
        reference: 'SCR-2026-99104',
        details: '0 matches identified across all parties (Shipper, Consignee, Notify Party, Carrier vessel).',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-bill-of-lading-viewer, [ui-bill-of-lading-viewer]',
      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,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableFooterComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="bill-of-lading-viewer" [class]="rootClass">
          <!-- Top Action Bar / Document Control Header -->
          <div
            class="border-border bg-card flex flex-col gap-4 rounded-xl border p-4 shadow-xs sm:flex-row sm:items-center sm:justify-between"
          >
            <div class="space-y-1">
              <div class="flex flex-wrap items-center gap-2">
                <span ui-badge wrap variant="outline" class="font-mono text-xs tracking-wider uppercase">
                  {{ scacCode }} &bull; Maritime Ocean BOL
                </span>
                <button
                  type="button"
                  class="group focus-visible:ring-ring border-success/30 bg-success/10 text-success hover:bg-success/20 text-success inline-flex min-h-6 items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none"
                  (click)="toggleBlockchainDetails()"
                >
                  <lucide-icon [img]="ShieldCheck" class="size-3.5" />
                  <span>Verify Blockchain Authenticity</span>
                  <span class="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true"></span>
                </button>
              </div>
              <div class="flex flex-wrap items-center gap-2 pt-1">
                <h1 class="text-xl font-bold tracking-tight sm:text-2xl">{{ documentTitle }}</h1>
                <span class="text-muted-foreground font-mono text-base font-semibold tabular-nums">{{ bolNumber }}</span>
                <button
                  ui-button
                  variant="ghost"
                  size="icon"
                  class="text-muted-foreground hover:text-foreground size-7"
                  title="Copy BOL number"
                  (click)="handleCopyBol()"
                >
                  @if (copiedBol()) {
                    <lucide-icon [img]="Check" class="text-success size-3.5" />
                  } @else {
                    <lucide-icon [img]="Copy" class="size-3.5" />
                  }
                  <span class="sr-only">Copy BOL number</span>
                </button>
              </div>
              <p class="text-muted-foreground text-xs">
                Carrier: <strong class="text-foreground font-medium">{{ carrierName }}</strong> &bull; Issued:
                {{ issueDate }} in {{ placeOfIssue }}
              </p>
            </div>
    
            <!-- Action Buttons & Tab Switcher -->
            <div class="flex flex-wrap items-center gap-2 sm:self-center">
              <div class="border-border bg-muted/40 flex items-center rounded-lg border p-1">
                <button type="button" [class]="tabClass('document')" (click)="activeTab.set('document')">
                  Document View
                </button>
                <button type="button" [class]="tabClass('compliance', true)" (click)="activeTab.set('compliance')">
                  <span>Customs &amp; Compliance</span>
                  <span
                    ui-badge
                    wrap
                    variant="success"
                    class="size-4 justify-center p-0 text-center font-mono text-xs leading-4"
                    >6</span
                  >
                </button>
                <button type="button" [class]="tabClass('edi')" (click)="activeTab.set('edi')">Raw EDI / JSON</button>
              </div>
    
              <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs" (click)="handlePrint()">
                <lucide-icon [img]="Printer" class="size-3.5" />
                <span>Print</span>
              </button>
    
              <button
                ui-button
                aria-label="Download attachment"
                size="sm"
                class="gap-1.5 text-xs"
                (click)="handleDownloadPdf()"
              >
                <lucide-icon [img]="Download" class="size-3.5" />
                <span>Download Printable BOL</span>
              </button>
            </div>
          </div>
    
          <!-- Blockchain Cryptographic Proof Drawer / Card -->
          @if (showVerifyDetails()) {
            <div
              class="animate-in fade-in slide-in-from-top-2 border-success/30 bg-success/5 rounded-xl border p-4 text-xs transition-colors"
            >
              <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
                <div class="flex items-start gap-3">
                  <div class="bg-success/10 text-success rounded-lg p-2">
                    <lucide-icon [img]="ShieldCheck" class="size-5 shrink-0" />
                  </div>
                  <div class="space-y-1">
                    <div class="flex items-center gap-2">
                      <span class="text-foreground font-semibold">Cryptographic Bill of Lading Authenticity Verified</span>
                      <span ui-badge wrap variant="success" class="text-xs">Immutable Ledger Record</span>
                    </div>
                    <p class="text-muted-foreground">
                      Document state verified on {{ blockchainNetwork }}. Digital hash matches original carrier signature at
                      origin.
                    </p>
                  </div>
                </div>
    
                <div class="flex flex-wrap items-center gap-2 font-mono text-xs">
                  <span class="bg-background/80 border-border text-foreground rounded border px-2 py-1"
                    >Block #{{ blockchainBlock }}</span
                  >
                  <span
                    class="bg-background/80 border-border text-foreground max-w-xs truncate rounded border px-2 py-1"
                    [attr.title]="blockchainTxHash"
                  >
                    Tx: {{ blockchainTxHash }}
                  </span>
                </div>
              </div>
            </div>
          }
    
          <!-- TAB 1: OFFICIAL BILL OF LADING PAPER DOCUMENT -->
          @if (activeTab() === 'document') {
            <div
              class="border-border bg-card overflow-hidden rounded-xl border shadow-xs print:border-none print:shadow-none"
            >
              <!-- Formal Document Header -->
              <div class="border-border bg-muted/20 border-b p-6 sm:p-8">
                <div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
                  <div class="space-y-3">
                    <div class="flex items-center gap-3">
                      <div
                        class="bg-primary text-primary-foreground flex size-10 items-center justify-center rounded-lg shadow-xs"
                      >
                        <lucide-icon [img]="Anchor" class="size-5" />
                      </div>
                      <div>
                        <h2 class="text-lg font-semibold tracking-tight sm:text-xl">{{ carrierName }}</h2>
                        <p class="text-muted-foreground font-mono text-xs">
                          SCAC: {{ scacCode }} &bull; Standard Ocean Carrier Bill of Lading
                        </p>
                      </div>
                    </div>
                    <p class="text-muted-foreground max-w-xl text-xs leading-relaxed">
                      Issued in accordance with the International Convention for the Unification of Certain Rules of Law
                      relating to Bills of Lading (Hague-Visby Rules) and UN Multimodal Transport Guidelines.
                    </p>
                  </div>
    
                  <div class="border-border bg-background/80 flex flex-col gap-2 rounded-lg border p-4 sm:min-w-[280px]">
                    <div class="flex items-center justify-between gap-4">
                      <span class="text-muted-foreground text-xs font-medium">Bill of Lading No.</span>
                      <span class="text-primary font-mono text-sm font-semibold tabular-nums">{{ bolNumber }}</span>
                    </div>
                    <div class="flex items-center justify-between gap-4">
                      <span class="text-muted-foreground text-xs">Booking Reference:</span>
                      <span class="font-mono text-xs font-semibold tabular-nums">{{ bookingReference }}</span>
                    </div>
                    <div class="flex items-center justify-between gap-4">
                      <span class="text-muted-foreground text-xs">Export Reference:</span>
                      <span class="font-mono text-xs tabular-nums">{{ exportReference }}</span>
                    </div>
                    <ui-separator class="my-1" />
                    <div class="flex items-center justify-between gap-4">
                      <span class="text-muted-foreground text-xs">Document Status:</span>
                      <span ui-badge wrap variant="default" class="text-xs font-medium">Negotiable / Original</span>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Section 1: Parties Grid (Shipper, Consignee, Notify Party) -->
              <div
                class="divide-border border-border grid grid-cols-1 divide-y border-b md:grid-cols-3 md:divide-x md:divide-y-0"
              >
                <div class="bg-card space-y-3 p-5 sm:p-6">
                  <div class="flex items-center justify-between">
                    <span class="text-primary font-mono text-xs font-semibold">1. Shipper / Exporter</span>
                    <lucide-icon [img]="Building2" class="text-muted-foreground size-4" />
                  </div>
                  <div class="space-y-1.5 text-xs">
                    <p class="text-foreground text-sm font-semibold">{{ shipperName }}</p>
                    <p class="text-muted-foreground leading-relaxed">{{ shipperAddress }}</p>
                    <div class="text-muted-foreground space-y-1 pt-2">
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Phone" class="text-muted-foreground size-3 shrink-0" />
                        <span class="tabular-nums">{{ shipperContact }}</span>
                      </p>
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Mail" class="text-muted-foreground size-3 shrink-0" />
                        <span>{{ shipperEmail }}</span>
                      </p>
                      <p class="text-foreground pt-1 font-mono">
                        <span class="text-muted-foreground">EORI / Tax ID:</span> {{ shipperEori }}
                      </p>
                    </div>
                  </div>
                </div>
    
                <div class="bg-card space-y-3 p-5 sm:p-6">
                  <div class="flex items-center justify-between">
                    <span class="text-primary font-mono text-xs font-semibold">2. Consignee / Importer</span>
                    <lucide-icon [img]="MapPin" class="text-muted-foreground size-4" />
                  </div>
                  <div class="space-y-1.5 text-xs">
                    <p class="text-foreground text-sm font-semibold">{{ consigneeName }}</p>
                    <p class="text-muted-foreground leading-relaxed">{{ consigneeAddress }}</p>
                    <div class="text-muted-foreground space-y-1 pt-2">
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Phone" class="text-muted-foreground size-3 shrink-0" />
                        <span class="tabular-nums">{{ consigneeContact }}</span>
                      </p>
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Mail" class="text-muted-foreground size-3 shrink-0" />
                        <span>{{ consigneeEmail }}</span>
                      </p>
                      <p class="text-foreground pt-1 font-mono">
                        <span class="text-muted-foreground">EORI / VAT:</span> {{ consigneeEori }}
                      </p>
                    </div>
                  </div>
                </div>
    
                <div class="bg-card space-y-3 p-5 sm:p-6">
                  <div class="flex items-center justify-between">
                    <span class="text-primary font-mono text-xs font-semibold">3. Notify Party</span>
                    <lucide-icon [img]="FileText" class="text-muted-foreground size-4" />
                  </div>
                  <div class="space-y-1.5 text-xs">
                    <p class="text-foreground text-sm font-semibold">{{ notifyPartyName }}</p>
                    <p class="text-muted-foreground leading-relaxed">{{ notifyPartyAddress }}</p>
                    <div class="text-muted-foreground space-y-1 pt-2">
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Phone" class="text-muted-foreground size-3 shrink-0" />
                        <span class="tabular-nums">{{ notifyPartyContact }}</span>
                      </p>
                      <p class="flex items-center gap-1.5">
                        <lucide-icon [img]="Mail" class="text-muted-foreground size-3 shrink-0" />
                        <span>{{ notifyPartyEmail }}</span>
                      </p>
                      <p class="text-foreground pt-1 font-medium">
                        <span class="text-muted-foreground">Attention:</span> {{ notifyPartyAttention }}
                      </p>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Section 2: Transport & Routing Details Grid -->
              <div class="border-border bg-muted/10 border-b p-5 sm:p-6">
                <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
                  <div class="border-border bg-card space-y-1 rounded-lg border p-3 shadow-xs">
                    <span class="text-muted-foreground text-xs font-medium">Pre-Carriage By</span>
                    <p class="text-foreground text-xs font-semibold">{{ preCarriageBy }}</p>
                    <span class="text-muted-foreground text-xs">Receipt: {{ placeOfReceipt }}</span>
                  </div>
    
                  <div class="border-border bg-card space-y-1 rounded-lg border p-3 shadow-xs">
                    <span class="text-muted-foreground text-xs font-medium">Ocean Vessel &amp; Voyage</span>
                    <p class="text-foreground flex items-center gap-1.5 text-xs font-semibold">
                      <lucide-icon [img]="Ship" class="text-primary size-3.5 shrink-0" />
                      <span>{{ vesselVoyage }}</span>
                    </p>
                    <span class="text-muted-foreground font-mono text-xs">{{ vesselImo }} &bull; {{ vesselFlag }}</span>
                  </div>
    
                  <div class="border-border bg-card space-y-1 rounded-lg border p-3 shadow-xs">
                    <span class="text-muted-foreground text-xs font-medium">Port of Loading (POL)</span>
                    <p class="text-foreground text-xs font-semibold">{{ portOfLoading }}</p>
                    <span class="text-muted-foreground text-xs">Departure: Aug 23, 2026</span>
                  </div>
    
                  <div class="border-border bg-card space-y-1 rounded-lg border p-3 shadow-xs">
                    <span class="text-muted-foreground text-xs font-medium">Port of Discharge (POD)</span>
                    <p class="text-foreground text-xs font-semibold">{{ portOfDischarge }}</p>
                    <span class="text-muted-foreground text-xs">Delivery: {{ placeOfDelivery }}</span>
                  </div>
                </div>
              </div>
    
              <!-- Section 3: Container & Cargo Manifest Table -->
              <div class="space-y-4 p-5 sm:p-6">
                <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                  <div>
                    <h3 class="text-foreground text-sm font-semibold">Container &amp; Cargo Description</h3>
                    <p class="text-muted-foreground text-xs">
                      Itemized manifest of goods, seal identification, Harmonized Tariff codes, and gross weight
                    </p>
                  </div>
                  <span ui-badge wrap variant="outline" class="w-fit font-mono text-xs"
                    >FCL / FCL &bull; CY-CY Movement</span
                  >
                </div>
    
                <div class="border-border overflow-hidden rounded-lg border">
                  <div class="overflow-x-auto">
                    <table ui-table density="cozy">
                      <thead ui-table-header>
                        <tr ui-table-row>
                          <th ui-table-head class="text-xs">Container No. &amp; Seal No.</th>
                          <th ui-table-head class="text-xs">Packages &amp; Kind</th>
                          <th ui-table-head class="text-xs">Description of Goods</th>
                          <th ui-table-head class="text-xs">HS Tariff Code</th>
                          <th ui-table-head class="text-right text-xs">Gross Wt (kg)</th>
                          <th ui-table-head class="text-right text-xs">Volume (CBM)</th>
                        </tr>
                      </thead>
                      <tbody ui-table-body>
                        @for (item of cargoItemsList; track item.id) {
                          <tr ui-table-row>
                            <td ui-table-cell class="align-top">
                              <div class="text-foreground font-mono text-xs font-semibold">{{ item.containerNo }}</div>
                              <div class="text-muted-foreground font-mono text-xs">Seal: {{ item.sealNo }}</div>
                              <span ui-badge wrap variant="secondary" class="mt-1 text-xs font-normal">{{
                                item.containerType
                              }}</span>
                            </td>
                            <td ui-table-cell class="align-top">
                              <div class="text-foreground text-xs font-medium tabular-nums">
                                {{ item.packagesCount }} Pallets
                              </div>
                              <div class="text-muted-foreground text-xs">{{ item.packagesKind }}</div>
                            </td>
                            <td ui-table-cell class="max-w-md align-top">
                              <div class="text-foreground text-xs leading-relaxed font-medium">{{ item.description }}</div>
                              @if (item.hazardClass) {
                                <div class="text-warning mt-1 flex items-center gap-1 text-xs">
                                  <lucide-icon [img]="AlertCircle" class="size-3 shrink-0" />
                                  <span>{{ item.hazardClass }}</span>
                                </div>
                              }
                            </td>
                            <td ui-table-cell class="text-foreground align-top font-mono text-xs tabular-nums">
                              {{ item.hsCode }}
                            </td>
                            <td
                              ui-table-cell
                              class="text-foreground text-right align-top font-mono text-xs font-semibold tabular-nums"
                            >
                              {{ item.grossWeightKg.toLocaleString('en-US', { minimumFractionDigits: 2 }) }} kg
                              <div class="text-muted-foreground font-normal">
                                {{ item.grossWeightLbs.toLocaleString('en-US', { minimumFractionDigits: 1 }) }} lbs
                              </div>
                            </td>
                            <td ui-table-cell class="text-foreground text-right align-top font-mono text-xs tabular-nums">
                              {{ item.volumeCbm.toFixed(2) }} m³
                            </td>
                          </tr>
                        }
                      </tbody>
                      <tfoot ui-table-footer>
                        <tr ui-table-row>
                          <td ui-table-cell [attr.colspan]="2" class="text-xs font-semibold">
                            Totals: {{ cargoItemsList.length }} Containers &bull; {{ totalPackagesCount() }} Pallets
                          </td>
                          <td ui-table-cell [attr.colspan]="2" class="text-muted-foreground text-xs">
                            Dangerous Goods Stowage Declared &bull; Certified Dry Van
                          </td>
                          <td ui-table-cell class="text-foreground text-right font-mono text-xs font-semibold tabular-nums">
                            {{ totalGrossWeightKg().toLocaleString('en-US', { minimumFractionDigits: 2 }) }} kg
                          </td>
                          <td ui-table-cell class="text-foreground text-right font-mono text-xs font-semibold tabular-nums">
                            {{ totalVolumeCbm().toFixed(2) }} m³
                          </td>
                        </tr>
                      </tfoot>
                    </table>
                  </div>
                </div>
              </div>
    
              <!-- Section 4: Freight Charges & Commercial Terms -->
              <div
                class="divide-border border-border grid grid-cols-1 divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-4"
              >
                <div class="bg-muted/10 space-y-1.5 p-5">
                  <span class="text-muted-foreground text-xs font-medium">Freight &amp; Charges</span>
                  <div class="flex items-center gap-2">
                    <span ui-badge wrap variant="default" class="text-xs font-medium tracking-wide uppercase">{{
                      freightTerms
                    }}</span>
                  </div>
                  <p class="text-muted-foreground pt-1 text-xs">
                    Payable at: <strong class="text-foreground">{{ freightPayableAt }}</strong>
                  </p>
                </div>
    
                <div class="bg-muted/10 space-y-1.5 p-5">
                  <span class="text-muted-foreground text-xs font-medium">Declared Value (Customs)</span>
                  <p class="text-foreground font-mono text-sm font-semibold tabular-nums">{{ declaredValue }}</p>
                  <p class="text-muted-foreground text-xs">Commercial Invoice #INV-2026-8942</p>
                </div>
    
                <div class="bg-muted/10 space-y-1.5 p-5">
                  <span class="text-muted-foreground text-xs font-medium">Marine Cargo Insurance</span>
                  <p class="text-foreground font-mono text-xs font-semibold">{{ insuranceCert }}</p>
                  <p class="text-muted-foreground text-xs">Institute Cargo Clauses (A) All Risks</p>
                </div>
    
                <div class="bg-muted/10 space-y-1.5 p-5">
                  <span class="text-muted-foreground text-xs font-medium">Incoterms &amp; Letter of Credit</span>
                  <p class="text-foreground text-xs font-semibold">{{ incoterms }}</p>
                  <p class="text-muted-foreground truncate font-mono text-xs" [attr.title]="letterOfCredit">
                    {{ letterOfCredit }}
                  </p>
                </div>
              </div>
    
              <!-- Section 5: Carrier Signature & Authorization Stamp -->
              <div class="border-border bg-card border-t p-6 sm:p-8">
                <div class="grid grid-cols-1 gap-6 md:grid-cols-12 md:items-center">
                  <div class="space-y-2 md:col-span-6">
                    <span class="text-primary text-xs font-semibold">Carrier Authorization &amp; Endorsement</span>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      IN WITNESS whereof the carrier, through its authorized master or port agent, has signed three (3)
                      original Bills of Lading, one of which being accomplished, the others to stand void.
                    </p>
                    <div class="text-muted-foreground flex flex-col gap-1 pt-2 font-mono text-xs">
                      <p>
                        <span class="text-foreground font-semibold">Authorized Agent:</span> Capt. H. van der Meer (Master /
                        Port Agent)
                      </p>
                      <p>
                        <span class="text-foreground font-semibold">Digital Signature SHA-256:</span>
                        9b2d8fe14157a3e8...c72194b0
                      </p>
                      <p>
                        <span class="text-foreground font-semibold">Issued:</span> {{ issueDate }} at Port of Shanghai,
                        China
                      </p>
                    </div>
                  </div>
    
                  <div class="flex justify-center md:col-span-3">
                    <div
                      class="border-primary/40 bg-primary/5 text-primary rotate-[-2deg] rounded-lg border-2 border-dashed p-4 text-center font-mono text-xs tracking-widest uppercase shadow-xs"
                    >
                      <div class="mb-1 flex justify-center">
                        <lucide-icon [img]="Ship" class="text-primary size-6" />
                      </div>
                      <div class="text-sm font-semibold tracking-tight">MAERSK LINE A/S</div>
                      <div class="text-muted-foreground text-xs font-semibold">PORT OF SHANGHAI AGENCY</div>
                      <div class="border-primary/30 text-success mt-1 border-t pt-1 text-xs font-semibold">
                        OFFICIALLY ENDORSED
                      </div>
                    </div>
                  </div>
    
                  <div
                    class="border-border bg-muted/20 flex flex-col items-center justify-center rounded-lg border p-4 text-center md:col-span-3"
                  >
                    <svg
                      viewBox="0 0 100 100"
                      class="text-foreground size-20 shrink-0"
                      fill="currentColor"
                      aria-label="QR Code"
                    >
                      <rect x="2" y="2" width="28" height="28" rx="3" fill="none" stroke="currentColor" stroke-width="4" />
                      <rect x="10" y="10" width="12" height="12" rx="1.5" />
                      <rect x="70" y="2" width="28" height="28" rx="3" fill="none" stroke="currentColor" stroke-width="4" />
                      <rect x="78" y="10" width="12" height="12" rx="1.5" />
                      <rect x="2" y="70" width="28" height="28" rx="3" fill="none" stroke="currentColor" stroke-width="4" />
                      <rect x="10" y="78" width="12" height="12" rx="1.5" />
                      <rect x="36" y="6" width="6" height="6" rx="1" />
                      <rect x="48" y="6" width="6" height="6" rx="1" />
                      <rect x="58" y="6" width="6" height="6" rx="1" />
                      <rect x="6" y="36" width="6" height="6" rx="1" />
                      <rect x="18" y="36" width="6" height="6" rx="1" />
                      <rect x="36" y="36" width="6" height="6" rx="1" />
                      <rect x="48" y="36" width="6" height="6" rx="1" />
                      <rect x="58" y="36" width="6" height="6" rx="1" />
                      <rect x="74" y="36" width="6" height="6" rx="1" />
                      <rect x="86" y="36" width="6" height="6" rx="1" />
                      <rect x="36" y="48" width="6" height="6" rx="1" />
                      <rect x="48" y="48" width="6" height="6" rx="1" />
                      <rect x="58" y="48" width="6" height="6" rx="1" />
                      <rect x="36" y="60" width="6" height="6" rx="1" />
                      <rect x="48" y="60" width="6" height="6" rx="1" />
                      <rect x="58" y="60" width="6" height="6" rx="1" />
                      <rect x="36" y="76" width="6" height="6" rx="1" />
                      <rect x="48" y="76" width="6" height="6" rx="1" />
                      <rect x="58" y="76" width="6" height="6" rx="1" />
                      <rect x="74" y="76" width="6" height="6" rx="1" />
                      <rect x="86" y="76" width="6" height="6" rx="1" />
                      <rect x="74" y="86" width="6" height="6" rx="1" />
                      <rect x="86" y="86" width="6" height="6" rx="1" />
                    </svg>
                    <span class="text-foreground mt-2 font-mono text-xs font-medium"
                      >Scan for Port Customs e-Verification</span
                    >
                    <span class="text-muted-foreground text-xs">UIPKGE Trust Protocol</span>
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- TAB 2: CUSTOMS & COMPLIANCE INSPECTOR -->
          @if (activeTab() === 'compliance') {
            <div class="space-y-6">
              <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
                <div ui-card class="border-border bg-card">
                  <div ui-card-header class="p-4 pb-2">
                    <p ui-card-description class="text-xs">Customs Risk Assessment</p>
                    <div class="flex items-baseline gap-2">
                      <h3 ui-card-title class="text-success text-2xl font-bold">99 / 100</h3>
                      <span ui-badge wrap variant="success" class="text-xs">Low Risk</span>
                    </div>
                  </div>
                  <div ui-card-content class="text-muted-foreground p-4 pt-1 text-xs">
                    Green channel clearance granted for Rotterdam entry. Pre-arrival ICS2 security declaration verified.
                  </div>
                </div>
    
                <div ui-card class="border-border bg-card">
                  <div ui-card-header class="p-4 pb-2">
                    <p ui-card-description class="text-xs">Import Duties &amp; VAT Estimation</p>
                    <div class="flex items-baseline gap-2">
                      <h3 ui-card-title class="text-foreground font-mono text-2xl font-bold">$29,925.00</h3>
                      <span ui-badge wrap variant="outline" class="font-mono text-xs">EUR 27,665</span>
                    </div>
                  </div>
                  <div ui-card-content class="text-muted-foreground p-4 pt-1 text-xs">
                    0% Base Duty (TARIC 8518.30.00) + 21.0% Netherlands Import VAT under Article 23 reverse-charge
                    mechanism.
                  </div>
                </div>
    
                <div ui-card class="border-border bg-card">
                  <div ui-card-header class="p-4 pb-2">
                    <p ui-card-description class="text-xs">Container Seal Audit</p>
                    <div class="flex items-baseline gap-2">
                      <h3 ui-card-title class="text-foreground text-2xl font-bold">2 / 2 Valid</h3>
                      <span ui-badge wrap variant="success" class="text-xs">ISO 17712 Compliant</span>
                    </div>
                  </div>
                  <div ui-card-content class="text-muted-foreground p-4 pt-1 text-xs">
                    Bolt seals SL-94021 &amp; SL-94022 confirmed tamper-free at origin gate inspection.
                  </div>
                </div>
              </div>
    
              <div ui-card class="border-border bg-card">
                <div ui-card-header class="p-5 pb-3">
                  <div class="flex items-center justify-between">
                    <div>
                      <h3 ui-card-title class="text-base font-semibold">
                        Customs Compliance &amp; Regulatory Audit Checklist
                      </h3>
                      <p ui-card-description class="text-xs">
                        International trade compliance checks required for European Union port entry and inland transit
                      </p>
                    </div>
                    <span ui-badge wrap variant="success" class="gap-1 text-xs">
                      <lucide-icon [img]="CheckCircle2" class="size-3.5" />
                      <span>All 6 Gates Passed</span>
                    </span>
                  </div>
                </div>
                <div ui-card-content class="space-y-3 p-5 pt-0">
                  <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
                    @for (gate of complianceGatesList; track gate.id) {
                      <div class="border-border bg-muted/20 flex items-start gap-3 rounded-lg border p-3.5 shadow-xs">
                        <div class="bg-success/10 text-success mt-0.5 shrink-0 rounded-full p-1">
                          <lucide-icon [img]="CheckCircle2" class="size-4" />
                        </div>
                        <div class="space-y-1 text-xs">
                          <div class="flex items-center justify-between gap-2">
                            <span class="text-foreground font-semibold">{{ gate.name }}</span>
                            <span class="text-muted-foreground font-mono text-xs font-medium">{{ gate.reference }}</span>
                          </div>
                          <p class="text-muted-foreground leading-relaxed">{{ gate.details }}</p>
                          <span class="text-primary block text-xs font-medium">Authority: {{ gate.authority }}</span>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- TAB 3: ELECTRONIC DATA INTERCHANGE (EDI) / JSON MANIFEST -->
          @if (activeTab() === 'edi') {
            <div class="space-y-4">
              <div ui-card class="border-border bg-card">
                <div ui-card-header class="p-5 pb-3">
                  <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                    <div>
                      <h3 ui-card-title class="text-base font-semibold">Electronic Data Interchange (UN/EDIFACT IFTMIN)</h3>
                      <p ui-card-description class="text-xs">
                        Standardized electronic consignment instructions formatted per UN/EDIFACT D.01B maritime transport
                        standards
                      </p>
                    </div>
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      class="gap-1.5 self-start text-xs sm:self-auto"
                      (click)="handleCopyEdi()"
                    >
                      @if (copiedEdi()) {
                        <lucide-icon [img]="Check" class="text-success size-3.5" />
                      } @else {
                        <lucide-icon [img]="Copy" class="size-3.5" />
                      }
                      <span>{{ copiedEdi() ? 'Copied EDI Payload' : 'Copy EDI Payload' }}</span>
                    </button>
                  </div>
                </div>
                <div ui-card-content class="p-5 pt-0">
                  <pre
                    class="border-border bg-muted/40 text-foreground overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed select-all"
                    >{{ generateEdiText() }}</pre>
                </div>
              </div>
            </div>
          }
        </div>
      `,
    })
    export class UiBillOfLadingViewerComponent {
      protected readonly AlertCircle = AlertCircle
      protected readonly Anchor = Anchor
      protected readonly Building2 = Building2
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Copy = Copy
      protected readonly Download = Download
      protected readonly FileText = FileText
      protected readonly Mail = Mail
      protected readonly MapPin = MapPin
      protected readonly Phone = Phone
      protected readonly Printer = Printer
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Ship = Ship
    
      protected readonly cargoItemsList = cargoItems
      protected readonly complianceGatesList = complianceGates
    
      @Input() documentTitle = 'Straight Bill of Lading (Original)'
      @Input() bolNumber = '#BOL-2026-894210'
      @Input() carrierName = 'Maersk Line A/S'
      @Input() scacCode = 'MAEU'
      @Input() bookingReference = 'BKG-771920-EU'
      @Input() exportReference = 'EXP-2026-0812'
      @Input() issueDate = 'August 21, 2026'
      @Input() placeOfIssue = 'Shanghai, China'
      @Input() shipperName = 'Shenzhen AudioTech Precision Ltd.'
      @Input() shipperAddress = 'Building 4, High-Tech Industrial Park, Nanshan District, Shenzhen, Guangdong 518057, China'
      @Input() shipperContact = '+86 755 8892 1044'
      @Input() shipperEmail = '[email protected]'
      @Input() shipperEori = 'CN-91440300MA5EXP01'
      @Input() consigneeName = 'SonicAura Europe B.V.'
      @Input() consigneeAddress = 'Keizersgracht 421, 1016 EK Amsterdam, Netherlands'
      @Input() consigneeContact = '+31 20 894 3300'
      @Input() consigneeEmail = '[email protected]'
      @Input() consigneeEori = 'NL864219034 / VAT: NL864219034B01'
      @Input() notifyPartyName = 'Rotterdam Intermodal Logistics GmbH'
      @Input() notifyPartyAddress = 'Willemskade 18, 3016 DK Rotterdam, Netherlands'
      @Input() notifyPartyContact = '+31 10 400 9820'
      @Input() notifyPartyEmail = '[email protected]'
      @Input() notifyPartyAttention = 'Customs Brokerage Dept. / Desk 4B'
      @Input() preCarriageBy = 'Feeder Barge "Yangtze River 12"'
      @Input() placeOfReceipt = 'Shenzhen Inland Depot, China (CNSZX)'
      @Input() vesselVoyage = 'Ever Given / V.042W'
      @Input() vesselImo = 'IMO 9811000'
      @Input() vesselFlag = 'Panama (PA)'
      @Input() portOfLoading = 'Shanghai Port, China (CNSHA)'
      @Input() portOfDischarge = 'Rotterdam Port, Netherlands (NLRTM)'
      @Input() placeOfDelivery = 'Duisburg Intermodal Terminal, Germany (DEDUI)'
      @Input() finalDestination = 'Duisburg Central DC #4 (DEDUI)'
      @Input() freightTerms = 'Prepaid'
      @Input() freightPayableAt = 'Shanghai, China'
      @Input() declaredValue = '$142,500.00 USD'
      @Input() insuranceCert = '#INS-9421-MAR'
      @Input() incoterms = 'CIF Rotterdam Port (Incoterms 2020)'
      @Input() letterOfCredit = 'LC-BNP-2026-99410-EU (BNP Paribas)'
      @Input() blockchainTxHash = '0x71e4d3a8904f84c8a5142d131fba827b5e4c91a2'
      @Input() blockchainBlock = '59,182,904'
      @Input() blockchainNetwork = 'Polygon Mainnet (Decentralized Cargo Registry)'
      @Input() set initialTab(v: BillOfLadingTab) {
        this.activeTab.set(v ?? 'document')
      }
      @Input('class') className?: string
    
      @Output() readonly downloadPdf = new EventEmitter<void>()
      @Output() readonly verifyBlockchain = new EventEmitter<void>()
      @Output() readonly print = new EventEmitter<void>()
    
      readonly activeTab = signal<BillOfLadingTab>('document')
      readonly copiedBol = signal(false)
      readonly copiedEdi = signal(false)
      readonly showVerifyDetails = signal(false)
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.copyTimer))
      }
    
      readonly totalPackagesCount = computed(() => cargoItems.reduce((acc, item) => acc + item.packagesCount, 0))
      readonly totalGrossWeightKg = computed(() => cargoItems.reduce((acc, item) => acc + item.grossWeightKg, 0))
      readonly totalGrossWeightLbs = computed(() => cargoItems.reduce((acc, item) => acc + item.grossWeightLbs, 0))
      readonly totalVolumeCbm = computed(() => cargoItems.reduce((acc, item) => acc + item.volumeCbm, 0))
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6', this.className)
      }
    
      tabClass(tab: BillOfLadingTab, withIcon = false): string {
        return cn(
          'focus-visible:ring-ring rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
          withIcon && 'flex items-center gap-1.5',
          this.activeTab() === tab
            ? 'bg-background text-foreground shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      generateEdiText(): string {
        return `UNB+UNOA:2+${this.scacCode}:ZZ+SONICAURA:ZZ+260821:1400+0000001'
    UNH+1+IFTMIN:D:01B:UN:EAN004'
    BGM+705+${this.bolNumber.replace('#', '')}+9'
    DTM+137:20260821:102'
    NAD+CZ+${this.shipperEori}::9++${this.shipperName}+${this.shipperAddress}+SHENZHEN++518057+CN'
    NAD+CN+NL864219034::9++${this.consigneeName}+${this.consigneeAddress}+AMSTERDAM++1016EK+NL'
    NAD+NI+++${this.notifyPartyName}+${this.notifyPartyAddress}+ROTTERDAM++3016DK+NL'
    TDT+20+V.042W+1+13+MAEU:172:20+++${this.vesselImo}::11:${this.vesselVoyage}'
    LOC+9+CNSHA:139:6+SHANGHAI PORT'
    LOC+11+NLRTM:139:6+ROTTERDAM PORT'
    LOC+7+DEDUI:139:6+DUISBURG TERMINAL'
    EQD+CN+MSKU9482014+45G1:102:5++2+5'
    SEL+SL94021+SH'
    GID+1+24:BX+PRO AUDIO HEADPHONES:85183000'
    MEA+WT+G+KGM:4850.00'
    MEA+VOL+AAW+MTQ:32.40'
    MOA+39:142500:USD'
    UNT+18+1'
    UNZ+1+0000001'`
      }
    
      handleCopyBol(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(this.bolNumber)
          this.copiedBol.set(true)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => this.copiedBol.set(false), 2000)
        }
      }
    
      handleCopyEdi(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(this.generateEdiText())
          this.copiedEdi.set(true)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => this.copiedEdi.set(false), 2000)
        }
      }
    
      handlePrint(): void {
        this.print.emit()
        if (typeof window !== 'undefined') window.print()
      }
    
      handleDownloadPdf(): void {
        this.downloadPdf.emit()
        this.handlePrint()
      }
    
      toggleBlockchainDetails(): void {
        this.showVerifyDetails.update((v) => !v)
        this.verifyBlockchain.emit()
      }
    }
    
  • src/app/components/blocks/bill-of-lading-viewer/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/bill-of-lading-viewer.json