Menu

Change language

GitHub repo

Packing Slip Generator

blocklogistics

Shopify and ShipStation style warehouse order packing slip and picker verification checklist with warehouse header, customer ship-to routing, picker pack station barcode scan identifier, itemized pick list table with interactive packed checkboxes, bin location tags, SKU barcodes, weight calculations, and quality control inspector stamp.

Installation

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

Variants

Loading interactive previews…

Schema

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

PackingSlipItem
interface PackingSlipItem {
  id: string
  sku: string
  barcode: string
  name: string
  variant: string
  location: string
  qtyOrdered: number
  qtyPacked: number
  weightLbs: number
  notes?: string
  initialPacked?: boolean
}
PackingSlipGeneratorInputs
interface PackingSlipGeneratorInputs {
  packingSlipNo?: string
  orderNo?: string
  carrier?: string
  carrierService?: string
  trackingNo?: string
  orderDate?: string
  shipDate?: string
  warehouseName?: string
  warehouseFacility?: string
  warehouseAddress?: string
  warehousePhone?: string
  warehouseEmail?: string
  customerName?: string
  customerCompany?: string
  customerAddress?: string
  customerPhone?: string
  customerEmail?: string
  deliveryNotes?: string
  pickerName?: string
  pickerId?: string
  packStation?: string
  shift?: string
  boxSize?: string
  dunnageType?: string
  qcInspector?: string
  qcStamp?: string
  qcTimestamp?: string
  items?: PackingSlipItem[]
  showActions?: boolean
  initialPackedAll?: boolean
}

Files installed (2)

  • src/app/components/blocks/packing-slip-generator/packing-slip-generator.component.ts36.1 kB
    // Shopify / ShipStation style warehouse order packing slip with an interactive picker
    // verification checklist: warehouse header, ship-to / origin / carrier routing grid, picker
    // pack-station ribbon with a simulated Code-128 barcode, itemized pick list table with packed
    // checkboxes, carton specs, returns notice and a QC inspector stamp. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Box,
      Building2,
      Check,
      CheckCheck,
      CheckCircle2,
      Clock,
      Copy,
      FileDown,
      LucideAngularModule,
      MapPin,
      Printer,
      QrCode,
      RotateCcw,
      Scan,
      ShieldCheck,
      Tag,
      Truck,
      User,
      Warehouse,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableFooterComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface PackingSlipItem {
      id: string
      sku: string
      barcode: string
      name: string
      variant: string
      location: string
      qtyOrdered: number
      qtyPacked: number
      weightLbs: number
      notes?: string
      initialPacked?: boolean
    }
    
    export interface PackingSlipGeneratorInputs {
      packingSlipNo?: string
      orderNo?: string
      carrier?: string
      carrierService?: string
      trackingNo?: string
      orderDate?: string
      shipDate?: string
      warehouseName?: string
      warehouseFacility?: string
      warehouseAddress?: string
      warehousePhone?: string
      warehouseEmail?: string
      customerName?: string
      customerCompany?: string
      customerAddress?: string
      customerPhone?: string
      customerEmail?: string
      deliveryNotes?: string
      pickerName?: string
      pickerId?: string
      packStation?: string
      shift?: string
      boxSize?: string
      dunnageType?: string
      qcInspector?: string
      qcStamp?: string
      qcTimestamp?: string
      items?: PackingSlipItem[]
      showActions?: boolean
      initialPackedAll?: boolean
    }
    
    const defaultItems: PackingSlipItem[] = [
      {
        id: 'item-1',
        sku: '#SKU-84920',
        barcode: '849201928410',
        name: 'Aero Minimalist Runner',
        variant: 'Size 10.5 · Matte Black',
        location: 'Aisle 04 · Rack B · Shelf 02',
        qtyOrdered: 1,
        qtyPacked: 1,
        weightLbs: 1.8,
        notes: 'Eco-Knit Upper / EVA Cushioning Sole',
        initialPacked: true,
      },
      {
        id: 'item-2',
        sku: '#SKU-77219',
        barcode: '772198421093',
        name: 'HydroShield Waterproof Shell Jacket',
        variant: "Men's M · Alpine Green",
        location: 'Aisle 02 · Rack D · Shelf 01',
        qtyOrdered: 1,
        qtyPacked: 1,
        weightLbs: 1.6,
        notes: '3-Layer DWR Membrane / Sealed Seams',
        initialPacked: true,
      },
      {
        id: 'item-3',
        sku: '#SKU-31904',
        barcode: '319045812903',
        name: 'Ergonomic Trail Crew Socks (3-Pack)',
        variant: 'Charcoal / Grey · L',
        location: 'Aisle 08 · Rack A · Shelf 04',
        qtyOrdered: 1,
        qtyPacked: 1,
        weightLbs: 1.4,
        notes: 'Merino Wool Blend / Seamless Toe',
        initialPacked: false,
      },
    ]
    
    function buildPackedMap(items: PackingSlipItem[], packedAll: boolean): Record<string, boolean> {
      const map: Record<string, boolean> = {}
      for (const item of items) map[item.id] = packedAll ? true : (item.initialPacked ?? false)
      return map
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-packing-slip-generator, [ui-packing-slip-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,
        UiCheckboxComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableFooterComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="packing-slip-generator" [class]="rootClass">
          <!-- Top Action & Fulfillment Toolbar (Screen Only) -->
          @if (showActions()) {
            <div
              class="no-print 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="flex flex-wrap items-center gap-3">
                <div class="flex items-center gap-2">
                  <span class="text-foreground font-mono text-sm font-semibold tracking-tight">{{ packingSlipNo() }}</span>
                  <span ui-badge variant="outline" class="gap-1 font-mono text-xs">
                    <lucide-icon [img]="Tag" class="text-muted-foreground size-3" />
                    {{ orderNo() }}
                  </span>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-4 sm:block" />
    
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <lucide-icon [img]="Truck" class="text-primary size-3.5" />
                  <span class="text-foreground font-medium">{{ carrierService() }}</span>
                </div>
    
                <span ui-badge [variant]="isAllPacked() ? 'default' : 'secondary'" class="gap-1 text-xs font-medium">
                  @if (isAllPacked()) {
                    <lucide-icon [img]="CheckCircle2" class="size-3" />
                  } @else {
                    <lucide-icon [img]="Clock" class="size-3" />
                  }
                  {{
                    isAllPacked()
                      ? 'All Items Verified'
                      : verifiedItemsCount() + '/' + effectiveItems().length + ' Packed (' + progressPercentage() + '%)'
                  }}
                </span>
              </div>
    
              <div class="flex flex-wrap items-center gap-2">
                <button
                  ui-button
                  type="button"
                  variant="outline"
                  size="sm"
                  class="gap-1.5 text-xs font-medium"
                  (click)="isAllPacked() ? resetPackingChecklist() : markAllPacked()"
                >
                  @if (isAllPacked()) {
                    <lucide-icon [img]="RotateCcw" class="size-3.5" />
                  } @else {
                    <lucide-icon [img]="CheckCheck" class="size-3.5" />
                  }
                  {{ isAllPacked() ? 'Reset Checklist' : 'Verify All' }}
                </button>
    
                <button
                  ui-button
                  aria-label="Download attachment"
                  type="button"
                  variant="outline"
                  size="sm"
                  class="gap-1.5 text-xs font-medium"
                  [disabled]="isDownloading()"
                  (click)="handleDownloadPdf()"
                >
                  <lucide-icon [img]="FileDown" class="size-3.5" />
                  {{ isDownloading() ? 'Generating PDF...' : 'Download PDF' }}
                </button>
    
                <button ui-button type="button" size="sm" class="gap-1.5 text-xs font-medium" (click)="handlePrint()">
                  <lucide-icon [img]="Printer" class="size-3.5" />
                  Print 4x6 Slip
                </button>
              </div>
            </div>
          }
    
          <!-- Printable Packing Slip Paper Container -->
          <div
            class="packing-slip-paper border-border bg-card mx-auto w-full max-w-4xl space-y-6 rounded-xl border p-6 shadow-md transition-shadow sm:p-8"
          >
            <!-- Section 1: Warehouse & Slip Header -->
            <div class="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
              <div class="space-y-2">
                <div class="flex items-center gap-2.5">
                  <div
                    class="bg-primary text-primary-foreground flex size-10 items-center justify-center rounded-lg shadow-xs"
                  >
                    <lucide-icon [img]="Warehouse" class="size-5" />
                  </div>
                  <div>
                    <h2 class="text-foreground text-base font-semibold tracking-tight sm:text-lg">
                      {{ warehouseName() }}
                    </h2>
                    <p class="text-muted-foreground text-xs">{{ warehouseFacility() }}</p>
                  </div>
                </div>
                <div class="text-muted-foreground text-xs">
                  <p>{{ warehouseAddress() }}</p>
                  <p class="mt-0.5">{{ warehousePhone() }} · {{ warehouseEmail() }}</p>
                </div>
              </div>
    
              <div class="flex flex-col items-start gap-3 sm:items-end">
                <div
                  class="border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold"
                >
                  COMMERCIAL PACKING SLIP
                </div>
                <div class="space-y-1 text-left text-xs sm:text-right">
                  <div class="flex items-center gap-2 sm:justify-end">
                    <span class="text-muted-foreground">Packing Slip Ref:</span>
                    <span class="text-foreground font-mono font-semibold">{{ packingSlipNo() }}</span>
                  </div>
                  <div class="flex items-center gap-2 sm:justify-end">
                    <span class="text-muted-foreground">Order Reference:</span>
                    <span class="text-foreground font-mono font-semibold">{{ orderNo() }}</span>
                  </div>
                  <div class="flex items-center gap-2 sm:justify-end">
                    <span class="text-muted-foreground">Order Date:</span>
                    <span class="text-foreground font-medium">{{ orderDate() }}</span>
                  </div>
                  <div class="flex items-center gap-2 sm:justify-end">
                    <span class="text-muted-foreground">Ship Date:</span>
                    <span class="text-foreground font-medium">{{ shipDate() }}</span>
                  </div>
                </div>
              </div>
            </div>
    
            <ui-separator />
    
            <!-- Section 2: Routing Grid (Ship-To, Origin, Carrier) -->
            <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
              <div class="border-border/80 bg-muted/20 rounded-lg border p-4">
                <div class="text-muted-foreground mb-2.5 flex items-center gap-2 text-xs font-medium">
                  <lucide-icon [img]="User" class="text-primary size-3.5" />
                  Ship-To Recipient
                </div>
                <div class="space-y-1 text-xs">
                  <p class="text-foreground text-sm font-semibold">{{ customerName() }}</p>
                  <p class="text-foreground/80 font-medium">{{ customerCompany() }}</p>
                  <p class="text-muted-foreground">{{ customerAddress() }}</p>
                  <p class="text-muted-foreground">{{ customerPhone() }}</p>
                  @if (deliveryNotes()) {
                    <div class="border-warning/20 bg-warning/10 text-warning mt-2.5 rounded border p-2">
                      <p class="text-xs font-semibold">Delivery Note:</p>
                      <p class="text-xs">{{ deliveryNotes() }}</p>
                    </div>
                  }
                </div>
              </div>
    
              <div class="border-border/80 bg-muted/20 rounded-lg border p-4">
                <div class="text-muted-foreground mb-2.5 flex items-center gap-2 text-xs font-medium">
                  <lucide-icon [img]="Building2" class="text-primary size-3.5" />
                  Fulfillment Origin
                </div>
                <div class="space-y-1 text-xs">
                  <p class="text-foreground text-sm font-semibold">{{ warehouseName() }}</p>
                  <p class="text-foreground/80 font-medium">Outbound Logistics Dock #12</p>
                  <p class="text-muted-foreground">{{ warehouseAddress() }}</p>
                  <p class="text-muted-foreground">Support: {{ warehousePhone() }}</p>
                  <div class="text-muted-foreground mt-2.5 flex items-center gap-1.5 text-xs">
                    <lucide-icon [img]="Zap" class="text-success size-3" />
                    <span>Direct EDI Dispatch Cleared</span>
                  </div>
                </div>
              </div>
    
              <div class="border-border/80 bg-muted/20 rounded-lg border p-4">
                <div class="text-muted-foreground mb-2.5 flex items-center gap-2 text-xs font-medium">
                  <lucide-icon [img]="Truck" class="text-primary size-3.5" />
                  Carrier & Service
                </div>
                <div class="space-y-2 text-xs">
                  <div>
                    <p class="text-foreground text-sm font-semibold">{{ carrierService() }}</p>
                    <p class="text-muted-foreground">Standard Air Parcel · Guaranteed</p>
                  </div>
                  <div>
                    <span class="text-muted-foreground">Tracking Number:</span>
                    <div class="mt-1 flex items-center gap-2">
                      <span class="text-foreground font-mono text-xs font-semibold">{{ trackingNo() }}</span>
                      <button
                        type="button"
                        class="no-print border-border hover:bg-muted focus-visible:ring-ring inline-flex size-6 items-center justify-center rounded border focus-visible:ring-2 focus-visible:outline-none"
                        title="Copy Tracking Number"
                        (click)="copyTrackingNumber()"
                      >
                        @if (copiedTracking()) {
                          <lucide-icon [img]="Check" class="text-muted-foreground size-3" />
                        } @else {
                          <lucide-icon [img]="Copy" class="text-muted-foreground size-3" />
                        }
                      </button>
                    </div>
                  </div>
                  <div
                    class="text-muted-foreground border-border/40 flex items-center justify-between border-t pt-1 text-xs"
                  >
                    <span>Billing: Prepaid</span>
                    <span class="font-mono">Declared: $349.00</span>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Section 3: Picker & Pack Station Ribbon -->
            <div
              class="border-border bg-muted/40 flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between"
            >
              <div class="flex flex-wrap items-center gap-4">
                <div class="space-y-0.5">
                  <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                    <lucide-icon [img]="User" class="text-primary size-3.5" />
                    Picker & Station
                  </div>
                  <p class="text-foreground text-sm font-semibold">
                    {{ pickerName() }}
                    <span class="text-muted-foreground font-mono text-xs font-normal">({{ pickerId() }})</span>
                  </p>
                  <p class="text-muted-foreground text-xs">{{ packStation() }} · {{ shift() }}</p>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-10 md:block" />
    
                <div class="space-y-0.5">
                  <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                    <lucide-icon [img]="Scan" class="text-primary size-3.5" />
                    Packing Progress
                  </div>
                  <div class="flex items-center gap-2">
                    <div class="bg-muted border-border/50 h-2 w-28 overflow-hidden rounded-full border">
                      <div
                        class="bg-primary h-full transition-[width] duration-300"
                        [style.width.%]="progressPercentage()"
                      ></div>
                    </div>
                    <span class="text-foreground font-mono text-xs font-semibold tabular-nums">
                      {{ verifiedItemsCount() }}/{{ effectiveItems().length }} Verified
                    </span>
                  </div>
                </div>
              </div>
    
              <div
                class="border-border/60 bg-background/80 flex flex-col items-center justify-center rounded border px-3 py-2 text-center shadow-2xs sm:items-end"
              >
                <div class="flex h-8 items-center gap-[2px]" aria-label="Barcode">
                  <span class="bg-foreground h-8 w-[3px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[2px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[4px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[1px]"></span>
                  <span class="bg-foreground h-8 w-[3px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[5px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[2px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[4px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[3px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[5px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[2px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[3px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[4px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[5px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[3px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[4px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[2px]"></span>
                  <span class="h-8 w-[2px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[5px]"></span>
                  <span class="h-8 w-[1px] bg-transparent"></span>
                  <span class="bg-foreground h-8 w-[3px]"></span>
                </div>
                <p class="text-foreground font-mono text-xs font-semibold tracking-widest">*{{ barcodeRef() }}*</p>
              </div>
            </div>
    
            <!-- Section 4: Itemized Pick List Table -->
            <div class="space-y-2">
              <div class="flex items-center justify-between">
                <div class="flex items-center gap-2">
                  <h3 class="text-foreground text-sm font-semibold tracking-tight">Itemized Pick & Pack Checklist</h3>
                  <span ui-badge variant="outline" class="font-mono text-xs">
                    {{ effectiveItems().length }} Line Items
                  </span>
                </div>
                <span class="text-muted-foreground text-xs">Check items upon bin scan and physical carton packing</span>
              </div>
    
              <div class="border-border overflow-hidden rounded-lg border">
                <div class="overflow-x-auto">
                  <table ui-table>
                    <thead ui-table-header class="bg-muted/40">
                      <tr ui-table-row>
                        <th ui-table-head class="w-12 text-center">Packed</th>
                        <th ui-table-head class="w-48">Bin / Location</th>
                        <th ui-table-head class="w-36">SKU & Barcode</th>
                        <th ui-table-head>Item Name & Variant Details</th>
                        <th ui-table-head class="w-28 text-center">Qty Pick/Ord</th>
                        <th ui-table-head class="w-24 text-right">Weight</th>
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (item of effectiveItems(); track item.id) {
                        <tr ui-table-row [class]="rowClass(item.id)" (click)="toggleItemPacked(item.id)">
                          <td ui-table-cell class="text-center" (click)="$event.stopPropagation()">
                            <div class="flex items-center justify-center">
                              <ui-checkbox
                                [id]="'chk-' + item.id"
                                [checked]="!!packedState()[item.id]"
                                (checkedChange)="setItemPacked(item.id, $event)"
                              />
                            </div>
                          </td>
                          <td ui-table-cell>
                            <div
                              class="border-border bg-muted/60 text-foreground inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium"
                            >
                              <lucide-icon [img]="MapPin" class="text-primary size-3 shrink-0" />
                              <span>{{ item.location }}</span>
                            </div>
                          </td>
                          <td ui-table-cell>
                            <div class="space-y-0.5">
                              <p class="text-foreground font-mono text-xs font-semibold">{{ item.sku }}</p>
                              <p class="text-muted-foreground font-mono text-xs">UPC: {{ item.barcode }}</p>
                            </div>
                          </td>
                          <td ui-table-cell>
                            <div class="space-y-0.5">
                              <p [class]="itemNameClass(item.id)">{{ item.name }}</p>
                              <p class="text-muted-foreground text-xs">{{ item.variant }}</p>
                              @if (item.notes) {
                                <p class="text-muted-foreground/80 text-xs italic">{{ item.notes }}</p>
                              }
                            </div>
                          </td>
                          <td ui-table-cell class="text-center">
                            <div
                              class="border-border/80 bg-background text-foreground inline-flex items-center gap-1 rounded border px-2 py-0.5 font-mono text-xs font-semibold shadow-2xs"
                            >
                              <span [class]="packedState()[item.id] ? 'text-primary' : 'text-foreground'">{{
                                packedState()[item.id] ? item.qtyPacked : 0
                              }}</span>
                              <span class="text-muted-foreground font-normal">/</span>
                              <span class="tabular-nums">{{ item.qtyOrdered }}</span>
                            </div>
                          </td>
                          <td ui-table-cell class="text-right">
                            <span class="text-foreground font-mono text-xs font-medium tabular-nums">
                              {{ item.weightLbs.toFixed(1) }} lbs
                            </span>
                          </td>
                        </tr>
                      }
                    </tbody>
                    <tfoot ui-table-footer class="bg-muted/50 font-medium">
                      <tr ui-table-row>
                        <td ui-table-cell colspan="4" class="text-foreground text-left text-xs font-semibold">
                          Order Package Totals ({{ effectiveItems().length }} Distinct SKUs)
                        </td>
                        <td ui-table-cell class="text-foreground text-center font-mono text-xs font-semibold tabular-nums">
                          {{ totalPackedUnits() }} / {{ totalOrderedUnits() }} Units
                        </td>
                        <td ui-table-cell class="text-foreground text-right font-mono text-xs font-semibold tabular-nums">
                          {{ totalWeightLabel() }}
                        </td>
                      </tr>
                    </tfoot>
                  </table>
                </div>
              </div>
            </div>
    
            <!-- Section 5: Package Summary & Quality Control Certification -->
            <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
              <div class="border-border/80 bg-muted/20 space-y-2 rounded-lg border p-4">
                <div class="text-muted-foreground flex items-center gap-2 text-xs font-medium">
                  <lucide-icon [img]="Box" class="text-primary size-3.5" />
                  Carton Specifications
                </div>
                <div class="space-y-1.5 text-xs">
                  <div class="flex items-center justify-between">
                    <span class="text-muted-foreground">Packing Box:</span>
                    <span class="text-foreground font-medium">{{ boxSize() }}</span>
                  </div>
                  <div class="flex items-center justify-between">
                    <span class="text-muted-foreground">Total Units:</span>
                    <span class="text-foreground font-mono font-semibold">{{ totalOrderedUnits() }} items</span>
                  </div>
                  <div class="flex items-center justify-between">
                    <span class="text-muted-foreground">Gross Weight:</span>
                    <span class="text-foreground font-mono font-semibold">{{ totalWeightLabel() }}</span>
                  </div>
                  <div class="flex items-center justify-between">
                    <span class="text-muted-foreground">Void Fill / Dunnage:</span>
                    <span class="text-muted-foreground">{{ dunnageType() }}</span>
                  </div>
                </div>
              </div>
    
              <div class="border-border/80 bg-muted/20 space-y-2 rounded-lg border p-4">
                <div class="text-muted-foreground flex items-center gap-2 text-xs font-medium">
                  <lucide-icon [img]="QrCode" class="text-primary size-3.5" />
                  Hassle-Free Returns
                </div>
                <div class="text-muted-foreground space-y-1 text-xs">
                  <p class="text-foreground font-medium">Need to make an exchange or return?</p>
                  <p>
                    Scan the QR code or visit <span class="text-foreground font-mono">apexretail.com/returns</span> within
                    30 days.
                  </p>
                  <p class="text-muted-foreground/90 pt-1 text-xs">
                    Please include this packing slip with items in original condition.
                  </p>
                </div>
              </div>
    
              <div class="border-border/80 bg-muted/20 flex flex-col items-center justify-center rounded-lg border p-4">
                <div
                  class="border-success/70 bg-success/10 border-success/70 bg-success/15 w-full rotate-[-1.5deg] rounded-lg border-2 border-dashed p-3 text-center transition-transform hover:rotate-0"
                >
                  <div class="text-success flex items-center justify-center gap-1.5">
                    <lucide-icon [img]="ShieldCheck" class="size-4 shrink-0" />
                    <span class="font-mono text-xs font-semibold tracking-widest uppercase">{{ qcStamp() }}</span>
                  </div>
                  <p class="text-success dark:text-foreground mt-1 font-mono text-xs font-semibold">AUDITED & SEALED</p>
                  <p class="text-success/80 mt-0.5 text-xs">Inspector: {{ qcInspector() }}</p>
                  <p class="text-success/70 font-mono text-xs">{{ qcTimestamp() }}</p>
                </div>
              </div>
            </div>
    
            <!-- Section 6: Document Footer Note -->
            <div
              class="border-border text-muted-foreground flex flex-col items-center justify-between gap-2 border-t pt-4 text-xs sm:flex-row"
            >
              <p>Generated by Apex Logistics WMS v4.8.2 · Station ID: {{ packStation() }}</p>
              <p class="font-mono">DOC ID: PS-2026-849201-US-04 · Barcode Validated</p>
            </div>
          </div>
        </div>
      `,
    })
    export class UiPackingSlipGeneratorComponent {
      protected readonly Box = Box
      protected readonly Building2 = Building2
      protected readonly Check = Check
      protected readonly CheckCheck = CheckCheck
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly FileDown = FileDown
      protected readonly MapPin = MapPin
      protected readonly Printer = Printer
      protected readonly QrCode = QrCode
      protected readonly RotateCcw = RotateCcw
      protected readonly Scan = Scan
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Tag = Tag
      protected readonly Truck = Truck
      protected readonly User = User
      protected readonly Warehouse = Warehouse
      protected readonly Zap = Zap
    
      readonly packingSlipNo = signal('#PS-849201')
      readonly orderNo = signal('#ORD-92841')
      readonly carrier = signal('FedEx')
      readonly carrierService = signal('FedEx 2-Day Air')
      readonly trackingNo = signal('7849 2018 9284 1029')
      readonly orderDate = signal('Aug 21, 2026')
      readonly shipDate = signal('Aug 21, 2026')
      readonly warehouseName = signal('Apex Retail Warehouse #04')
      readonly warehouseFacility = signal('West Coast Omnichannel Hub')
      readonly warehouseAddress = signal('1040 North Industry Pkwy, Reno, NV 89502')
      readonly warehousePhone = signal('+1 (800) 555-0199')
      readonly warehouseEmail = signal('fulfillment-reno4@apexretail.io')
      readonly customerName = signal('Eleanor Vance')
      readonly customerCompany = signal('Vance Design Studios · Suite 400')
      readonly customerAddress = signal('742 Evergreen Terrace, Springfield, OR 97477')
      readonly customerPhone = signal('+1 (555) 839-2019')
      readonly customerEmail = signal('eleanor.vance@vancestudios.design')
      readonly deliveryNotes = signal('Gate Code: #4829. Leave at covered front porch if unavailable. Do not bend.')
      readonly pickerName = signal('Marcus Vance')
      readonly pickerId = signal('#EMP-4821')
      readonly packStation = signal('Pack Station #4')
      readonly shift = signal('Morning Shift · Bay Alpha')
      readonly boxSize = signal('Box #3 · 14" × 10" × 6"')
      readonly dunnageType = signal('Recycled Kraft Void Fill (1.2 oz)')
      readonly qcInspector = signal('Elena Rostova (#QC-89)')
      readonly qcStamp = signal('QC Passed · Station #4')
      readonly qcTimestamp = signal('2026-08-21 09:42 PST')
      readonly showActions = signal(true)
      readonly initialPackedAll = signal(false)
    
      private readonly itemsInput = signal<PackingSlipItem[] | undefined>(undefined)
    
      @Input('packingSlipNo') set packingSlipNoAttr(v: string | undefined) {
        if (v !== undefined) this.packingSlipNo.set(v)
      }
      @Input('orderNo') set orderNoAttr(v: string | undefined) {
        if (v !== undefined) this.orderNo.set(v)
      }
      @Input('carrier') set carrierAttr(v: string | undefined) {
        if (v !== undefined) this.carrier.set(v)
      }
      @Input('carrierService') set carrierServiceAttr(v: string | undefined) {
        if (v !== undefined) this.carrierService.set(v)
      }
      @Input('trackingNo') set trackingNoAttr(v: string | undefined) {
        if (v !== undefined) this.trackingNo.set(v)
      }
      @Input('orderDate') set orderDateAttr(v: string | undefined) {
        if (v !== undefined) this.orderDate.set(v)
      }
      @Input('shipDate') set shipDateAttr(v: string | undefined) {
        if (v !== undefined) this.shipDate.set(v)
      }
      @Input('warehouseName') set warehouseNameAttr(v: string | undefined) {
        if (v !== undefined) this.warehouseName.set(v)
      }
      @Input('warehouseFacility') set warehouseFacilityAttr(v: string | undefined) {
        if (v !== undefined) this.warehouseFacility.set(v)
      }
      @Input('warehouseAddress') set warehouseAddressAttr(v: string | undefined) {
        if (v !== undefined) this.warehouseAddress.set(v)
      }
      @Input('warehousePhone') set warehousePhoneAttr(v: string | undefined) {
        if (v !== undefined) this.warehousePhone.set(v)
      }
      @Input('warehouseEmail') set warehouseEmailAttr(v: string | undefined) {
        if (v !== undefined) this.warehouseEmail.set(v)
      }
      @Input('customerName') set customerNameAttr(v: string | undefined) {
        if (v !== undefined) this.customerName.set(v)
      }
      @Input('customerCompany') set customerCompanyAttr(v: string | undefined) {
        if (v !== undefined) this.customerCompany.set(v)
      }
      @Input('customerAddress') set customerAddressAttr(v: string | undefined) {
        if (v !== undefined) this.customerAddress.set(v)
      }
      @Input('customerPhone') set customerPhoneAttr(v: string | undefined) {
        if (v !== undefined) this.customerPhone.set(v)
      }
      @Input('customerEmail') set customerEmailAttr(v: string | undefined) {
        if (v !== undefined) this.customerEmail.set(v)
      }
      @Input('deliveryNotes') set deliveryNotesAttr(v: string | undefined) {
        if (v !== undefined) this.deliveryNotes.set(v)
      }
      @Input('pickerName') set pickerNameAttr(v: string | undefined) {
        if (v !== undefined) this.pickerName.set(v)
      }
      @Input('pickerId') set pickerIdAttr(v: string | undefined) {
        if (v !== undefined) this.pickerId.set(v)
      }
      @Input('packStation') set packStationAttr(v: string | undefined) {
        if (v !== undefined) this.packStation.set(v)
      }
      @Input('shift') set shiftAttr(v: string | undefined) {
        if (v !== undefined) this.shift.set(v)
      }
      @Input('boxSize') set boxSizeAttr(v: string | undefined) {
        if (v !== undefined) this.boxSize.set(v)
      }
      @Input('dunnageType') set dunnageAttr(v: string | undefined) {
        if (v !== undefined) this.dunnageType.set(v)
      }
      @Input('qcInspector') set qcInspectorAttr(v: string | undefined) {
        if (v !== undefined) this.qcInspector.set(v)
      }
      @Input('qcStamp') set qcStampAttr(v: string | undefined) {
        if (v !== undefined) this.qcStamp.set(v)
      }
      @Input('qcTimestamp') set qcTimestampAttr(v: string | undefined) {
        if (v !== undefined) this.qcTimestamp.set(v)
      }
      @Input('showActions') set showActionsAttr(v: boolean | undefined) {
        if (v !== undefined) this.showActions.set(!!v)
      }
      @Input('initialPackedAll') set initialPackedAllAttr(v: boolean | undefined) {
        if (v !== undefined) {
          this.initialPackedAll.set(!!v)
          this.packedState.set(buildPackedMap(this.effectiveItems(), !!v))
        }
      }
      @Input('items') set itemsAttr(v: PackingSlipItem[] | undefined) {
        if (v !== undefined) {
          this.itemsInput.set([...v])
          this.packedState.set(buildPackedMap(v, this.initialPackedAll()))
        }
      }
      @Input('class') className?: string
    
      readonly packedState = signal<Record<string, boolean>>(buildPackedMap(defaultItems, false))
      readonly copiedTracking = signal(false)
      readonly isPrinting = signal(false)
      readonly isDownloading = signal(false)
    
      readonly effectiveItems = computed(() => this.itemsInput() ?? defaultItems)
    
      readonly totalOrderedUnits = computed(() => this.effectiveItems().reduce((sum, item) => sum + item.qtyOrdered, 0))
      readonly totalPackedUnits = computed(() =>
        this.effectiveItems().reduce((sum, item) => sum + (this.packedState()[item.id] ? item.qtyPacked : 0), 0),
      )
      readonly totalWeight = computed(() => this.effectiveItems().reduce((sum, item) => sum + item.weightLbs, 0))
      readonly totalWeightLabel = computed(() => `${this.totalWeight().toFixed(1)} lbs`)
      readonly isAllPacked = computed(() => this.effectiveItems().every((item) => this.packedState()[item.id]))
      readonly verifiedItemsCount = computed(
        () => this.effectiveItems().filter((item) => this.packedState()[item.id]).length,
      )
      readonly progressPercentage = computed(() => {
        if (this.effectiveItems().length === 0) return 0
        return Math.round((this.verifiedItemsCount() / this.effectiveItems().length) * 100)
      })
      readonly barcodeRef = computed(() => this.orderNo().replace('#', ''))
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      rowClass(id: string): string {
        return cn(
          'cursor-pointer transition-colors select-none',
          this.packedState()[id] ? 'bg-primary/[0.02] dark:bg-primary/[0.04]' : 'hover:bg-muted/20',
        )
      }
    
      itemNameClass(id: string): string {
        return cn('text-foreground text-xs font-semibold', this.packedState()[id] && 'text-foreground/90')
      }
    
      toggleItemPacked(id: string): void {
        this.packedState.update((prev) => ({ ...prev, [id]: !prev[id] }))
      }
    
      setItemPacked(id: string, value: boolean | 'indeterminate'): void {
        this.packedState.update((prev) => ({ ...prev, [id]: value === true }))
      }
    
      markAllPacked(): void {
        const next: Record<string, boolean> = {}
        for (const item of this.effectiveItems()) next[item.id] = true
        this.packedState.set(next)
      }
    
      resetPackingChecklist(): void {
        const next: Record<string, boolean> = {}
        for (const item of this.effectiveItems()) next[item.id] = false
        this.packedState.set(next)
      }
    
      async copyTrackingNumber(): Promise<void> {
        try {
          await navigator.clipboard.writeText(this.trackingNo())
          this.copiedTracking.set(true)
          setTimeout(() => this.copiedTracking.set(false), 2000)
        } catch {
          // Clipboard unavailable (non-secure context): leave the icon unchanged.
        }
      }
    
      handlePrint(): void {
        this.isPrinting.set(true)
        if (typeof window !== 'undefined') window.print()
        setTimeout(() => this.isPrinting.set(false), 1000)
      }
    
      handleDownloadPdf(): void {
        this.isDownloading.set(true)
        setTimeout(() => this.isDownloading.set(false), 1500)
      }
    }
    
  • src/app/components/blocks/packing-slip-generator/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/packing-slip-generator.json