Inventory Reorder Forecast
blocklogisticsSupply chain inventory replenishment planner with lead-time demand forecasting, 4 forecasting KPI metric cards, safety stock triggers, multi-SKU demand table, and automated purchase order generation.
Installation
$pnpm dlx uipkge-ng@latest add inventory-reorder-forecast$npx uipkge-ng@latest add inventory-reorder-forecast$yarn dlx uipkge-ng@latest add inventory-reorder-forecast$bunx uipkge-ng@latest add inventory-reorder-forecastManifest:
https://uipkge.dev/r/angular/inventory-reorder-forecast.jsonInstalls to:src/app/components/blocks/inventory-reorder-forecast/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ReorderIteminterface ReorderItem {
id: string
sku: string
name: string
category: string
supplier: string
onHandStock: number
safetyStockThreshold: number
dailySalesVelocity: number
velocityTrend: number
daysOfInventoryRemaining: number
supplierLeadTimeDays: number
recommendedReorderQty: number
unitCost: number
estimatedPoCost: number
status: StockHealthStatus
}Files installed (2)
src/app/components/blocks/inventory-reorder-forecast/inventory-reorder-forecast.component.ts38.3 kB
import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertTriangle, ArrowUpRight, Boxes, Building2, CheckCircle2, Clock, DollarSign, Download, FileCheck, LucideAngularModule, PackageCheck, PackagePlus, Search, TrendingUp, X, Zap, } 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, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDialogCloseComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type StockHealthStatus = 'critical' | 'warning' | 'healthy' export interface ReorderItem { id: string sku: string name: string category: string supplier: string onHandStock: number safetyStockThreshold: number dailySalesVelocity: number velocityTrend: number daysOfInventoryRemaining: number supplierLeadTimeDays: number recommendedReorderQty: number unitCost: number estimatedPoCost: number status: StockHealthStatus } const reorderItems: ReorderItem[] = [ { id: 'sku-1', sku: 'SKU-RN-105-BLK', name: 'Aero Minimalist Runner 10.5', category: 'Footwear', supplier: 'Apex Footwear Ltd', onHandStock: 42, safetyStockThreshold: 80, dailySalesVelocity: 12.4, velocityTrend: 14.2, daysOfInventoryRemaining: 3.4, supplierLeadTimeDays: 14, recommendedReorderQty: 250, unitCost: 50.0, estimatedPoCost: 12500.0, status: 'critical', }, { id: 'sku-2', sku: 'SKU-PK-082-SLT', name: 'Technical Shell Parka L', category: 'Outerwear', supplier: 'Nordic Outerwear Co', onHandStock: 18, safetyStockThreshold: 50, dailySalesVelocity: 4.5, velocityTrend: 8.5, daysOfInventoryRemaining: 4.0, supplierLeadTimeDays: 21, recommendedReorderQty: 120, unitCost: 145.0, estimatedPoCost: 17400.0, status: 'critical', }, { id: 'sku-3', sku: 'SKU-HD-419-PRO', name: 'Pro Studio Headphones', category: 'Electronics', supplier: 'Sonic Acoustics Inc', onHandStock: 65, safetyStockThreshold: 85, dailySalesVelocity: 8.2, velocityTrend: 5.0, daysOfInventoryRemaining: 7.9, supplierLeadTimeDays: 10, recommendedReorderQty: 100, unitCost: 110.0, estimatedPoCost: 11000.0, status: 'warning', }, { id: 'sku-4', sku: 'SKU-CD-904-BRN', name: 'Leather Cardholder', category: 'Accessories', supplier: 'Tuscan Goods Ltd', onHandStock: 88, safetyStockThreshold: 100, dailySalesVelocity: 6.0, velocityTrend: 2.1, daysOfInventoryRemaining: 14.7, supplierLeadTimeDays: 12, recommendedReorderQty: 150, unitCost: 32.0, estimatedPoCost: 4800.0, status: 'warning', }, { id: 'sku-5', sku: 'SKU-WC-331-WHT', name: 'Wireless Charger Mat', category: 'Electronics', supplier: 'Volt Innovations', onHandStock: 340, safetyStockThreshold: 120, dailySalesVelocity: 8.1, velocityTrend: -1.4, daysOfInventoryRemaining: 42.0, supplierLeadTimeDays: 7, recommendedReorderQty: 100, unitCost: 25.5, estimatedPoCost: 2550.0, status: 'healthy', }, ] /** * InventoryReorderForecast -- the React block, 1:1: a supply chain inventory * replenishment planner with a facility selector, 4 forecasting KPI metric cards, safety * stock triggers, a multi-SKU demand table with search + category + health filters, and * single/bulk purchase order generation dialogs with a feedback banner. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-inventory-reorder-forecast, [ui-inventory-reorder-forecast]', 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, UiCardHeaderComponent, UiCardTitleComponent, UiDialogCloseComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="inventory-reorder-forecast" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2.5"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Boxes" class="size-4.5" /> </div> <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Inventory Demand & Reorder Forecast </h1> <span ui-badge variant="warning" class="border-warning/30 bg-warning/10 text-warning gap-1.5"> <span class="bg-warning size-1.5 animate-pulse rounded-full"></span> 4 SKUs Need Reorder </span> </div> <p class="text-muted-foreground text-xs sm:text-sm"> Multi-channel supply chain replenishment planner with lead-time demand forecasting and automated purchase order generation. </p> </div> <div class="flex flex-wrap items-center gap-2.5"> <!-- Facility Selector --> <ui-select [value]="facility()" (valueChange)="facility.set($event)"> <button ui-select-trigger class="w-full sm:w-[280px]"> <lucide-icon [img]="Building2" class="text-muted-foreground size-4 shrink-0" /> <ui-select-value placeholder="Select fulfillment facility" /> </button> <ui-select-content> <ui-select-item value="global-fulfillment">Global Fulfillment Center</ui-select-item> <ui-select-item value="na-west-hub">North America West Distribution Hub</ui-select-item> <ui-select-item value="eu-central-dc">European Central Logistics Depot</ui-select-item> <ui-select-item value="apac-regional">APAC Regional Hub · Singapore</ui-select-item> </ui-select-content> </ui-select> <!-- Export Plan Button --> <button ui-button aria-label="Download attachment" variant="outline" class="gap-1.5 shadow-xs" (click)="handleExportPlan()" > <lucide-icon [img]="Download" class="size-4" /> Export Replenishment Plan </button> <!-- Primary Action: Generate Bulk POs --> <button ui-button class="gap-1.5 shadow-xs" (click)="openBulkPoDialog()"> <lucide-icon [img]="PackagePlus" class="size-4" /> Generate Bulk POs </button> </div> </div> <!-- Notification / Action Feedback Banner --> @if (actionFeedback()) { <div class="border-primary/20 bg-primary/5 text-foreground flex items-center justify-between rounded-lg border px-4 py-3 text-sm shadow-xs transition-colors" > <div class="flex items-center gap-2.5"> <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> <span class="text-xs font-medium sm:text-sm">{{ actionFeedback() }}</span> </div> <button ui-button variant="ghost" size="xs" class="h-6 w-6 p-0" aria-label="Dismiss notification" (click)="actionFeedback.set(null)" > <lucide-icon [img]="X" class="size-3.5" /> </button> </div> } <!-- 4 Forecasting KPI Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Metric 1: Critical Reorder SKUs --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Critical Reorder SKUs </h3> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="AlertTriangle" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-baseline gap-2"> <div class="text-warning text-warning text-2xl font-bold tracking-tight tabular-nums"> 4 <span class="text-muted-foreground text-sm font-normal">SKUs</span> </div> <span ui-badge variant="warning" class="text-xs font-normal">Action Required</span> </div> <p class="text-muted-foreground pt-1 text-xs">Below safety threshold · 2 in urgent stockout zone</p> </div> </div> <!-- Metric 2: Estimated Stockout Days --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Estimated Stockout Days </h3> <div class="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Clock" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="flex items-baseline gap-2"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> 6.4 <span class="text-muted-foreground text-sm font-normal">Days</span> </div> <span ui-badge variant="destructive" class="text-xs font-normal">Critical Risk</span> </div> <p class="text-muted-foreground pt-1 text-xs">6.4 Days until stockout on top SKU · Urgent PO needed</p> </div> </div> <!-- Metric 3: Suggested Purchase Order Value --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Suggested PO Value </h3> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="DollarSign" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$48,250.00</div> <p class="text-muted-foreground flex items-center gap-1.5 pt-1 text-xs"> <span class="text-success flex items-center font-medium"> <lucide-icon [img]="Zap" class="mr-1 inline size-3" />4 replenishment POs </span> <span>· 620 units total</span> </p> </div> </div> <!-- Metric 4: Inventory Turnover Rate --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> Inventory Turnover Rate </h3> <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="TrendingUp" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> 8.4x <span class="text-muted-foreground text-sm font-normal">/ year</span> </div> <p class="text-muted-foreground flex items-center gap-1.5 pt-1 text-xs"> <span class="text-success flex items-center font-medium"> <lucide-icon [img]="ArrowUpRight" class="mr-0.5 inline size-3" />+1.2x </span> <span>vs industry benchmark (7.2x)</span> </p> </div> </div> </div> <!-- Reorder Demand Forecast Table Toolbar & Card --> <div ui-card class="shadow-xs"> <div ui-card-header class="border-border border-b pb-3"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-1 flex-wrap items-center gap-2"> <div class="relative w-full max-w-sm"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search SKU, product name, supplier..." class="h-9 pl-9 text-xs sm:text-sm" /> </div> <!-- Category Filter --> <ui-select [value]="categoryFilter()" (valueChange)="categoryFilter.set($event)"> <button ui-select-trigger class="h-9 w-[160px] text-xs"> <ui-select-value placeholder="All Categories" /> </button> <ui-select-content> <ui-select-item value="all">All Categories</ui-select-item> <ui-select-item value="Footwear">Footwear</ui-select-item> <ui-select-item value="Outerwear">Outerwear</ui-select-item> <ui-select-item value="Electronics">Electronics</ui-select-item> <ui-select-item value="Accessories">Accessories</ui-select-item> </ui-select-content> </ui-select> <!-- Status Filter --> <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)"> <button ui-select-trigger class="h-9 w-[170px] text-xs"> <ui-select-value placeholder="All Health Status" /> </button> <ui-select-content> <ui-select-item value="all">All Stock Status</ui-select-item> <ui-select-item value="critical">Critical Stockout (<7d)</ui-select-item> <ui-select-item value="warning">Reorder Warning (7-15d)</ui-select-item> <ui-select-item value="healthy">Healthy Stock (>30d)</ui-select-item> </ui-select-content> </ui-select> </div> <div class="text-muted-foreground flex items-center gap-2 text-xs"> <span> Showing <strong class="text-foreground tabular-nums">{{ filteredItems().length }}</strong> of {{ allItems.length }} forecast SKUs </span> </div> </div> </div> <div ui-card-content class="p-0"> <!-- Demand Table --> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="w-[240px] text-xs font-semibold">SKU Identifier & Product</th> <th ui-table-head class="text-right text-xs font-semibold">Current On-Hand</th> <th ui-table-head class="text-right text-xs font-semibold">Sales Velocity</th> <th ui-table-head class="min-w-[150px] text-xs font-semibold">Days Remaining</th> <th ui-table-head class="text-right text-xs font-semibold">Supplier Lead Time</th> <th ui-table-head class="text-right text-xs font-semibold">Recommended Qty</th> <th ui-table-head class="text-right text-xs font-semibold">Est. PO Cost</th> <th ui-table-head class="w-[120px] text-right text-xs font-semibold">Quick Action</th> </tr> </thead> <tbody ui-table-body> @for (item of filteredItems(); track item.id) { <tr ui-table-row class="hover:bg-muted/40 transition-colors"> <!-- SKU Identifier & Product Name --> <td ui-table-cell class="py-3"> <div class="space-y-1"> <div class="text-foreground font-mono text-xs font-semibold">{{ item.sku }}</div> <div class="text-foreground text-sm font-medium">{{ item.name }}</div> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span>{{ item.category }}</span> <span>·</span> <span>{{ item.supplier }}</span> </div> </div> </td> <!-- Current On-Hand Stock --> <td ui-table-cell class="py-3 text-right"> <div class="text-foreground text-sm font-semibold tabular-nums"> {{ item.onHandStock.toLocaleString() }} <span class="text-muted-foreground text-xs font-normal">units</span> </div> <div class="text-muted-foreground text-xs tabular-nums"> Safety: {{ item.safetyStockThreshold }} units </div> </td> <!-- Average Daily Sales Velocity --> <td ui-table-cell class="py-3 text-right"> <div class="text-foreground text-sm font-semibold tabular-nums"> {{ item.dailySalesVelocity.toFixed(1) }} <span class="text-muted-foreground text-xs font-normal">units/day</span> </div> <div class="text-xs font-medium tabular-nums" [class]="item.velocityTrend >= 0 ? 'text-success' : 'text-destructive'" > {{ item.velocityTrend >= 0 ? '+' : '' }}{{ item.velocityTrend.toFixed(1) }}% 7d </div> </td> <!-- Days of Inventory Remaining Badge --> <td ui-table-cell class="py-3"> <div class="space-y-1.5"> @if (item.status === 'critical') { <span ui-badge variant="destructive" class="gap-1 font-mono text-xs font-semibold"> <lucide-icon [img]="AlertTriangle" class="size-3" /> {{ item.daysOfInventoryRemaining.toFixed(1) }} Days </span> } @else if (item.status === 'warning') { <span ui-badge variant="warning" class="text-warning gap-1 font-mono text-xs font-semibold"> <lucide-icon [img]="Clock" class="size-3" /> {{ item.daysOfInventoryRemaining.toFixed(1) }} Days </span> } @else { <span ui-badge variant="success" class="gap-1 font-mono text-xs font-semibold"> <lucide-icon [img]="CheckCircle2" class="size-3" /> {{ item.daysOfInventoryRemaining.toFixed(1) }} Days </span> } <div class="text-muted-foreground text-xs"> {{ item.status === 'critical' ? 'Stockout before delivery' : item.status === 'warning' ? 'Reorder buffer thin' : 'Healthy inventory level' }} </div> </div> </td> <!-- Supplier Lead Time --> <td ui-table-cell class="py-3 text-right"> <div class="text-foreground font-mono text-sm font-semibold tabular-nums"> {{ item.supplierLeadTimeDays }} <span class="text-muted-foreground text-xs font-normal">Days</span> </div> <div class="text-muted-foreground text-xs">Mfg & Freight</div> </td> <!-- Recommended Reorder Qty --> <td ui-table-cell class="py-3 text-right"> <div class="text-foreground text-sm font-bold tabular-nums"> {{ item.recommendedReorderQty }} <span class="text-muted-foreground text-xs font-normal">units</span> </div> <div class="text-muted-foreground text-xs">Optimal batch</div> </td> <!-- Estimated PO Cost --> <td ui-table-cell class="py-3 text-right"> <div class="text-foreground font-mono text-sm font-bold tabular-nums"> {{ money(item.estimatedPoCost) }} </div> <div class="text-muted-foreground text-xs tabular-nums">{{ money(item.unitCost) }} / unit</div> </td> <!-- Quick Action Button --> <td ui-table-cell class="py-3 text-right"> <div class="flex items-center justify-end gap-1.5"> <button ui-button size="sm" class="gap-1 shadow-xs" [variant]="item.status === 'critical' ? 'default' : 'outline'" (click)="openCreatePoDialog(item)" > <lucide-icon [img]="PackageCheck" class="size-3.5" /> Create PO </button> </div> </td> </tr> } @if (filteredItems().length === 0) { <tr ui-table-row> <td ui-table-cell [attr.colspan]="8" class="text-muted-foreground h-32 text-center text-sm"> No inventory forecast items match your current filter criteria. </td> </tr> } </tbody> </table> </div> </div> </div> <!-- Create Purchase Order Draft Dialog --> <ui-dialog [open]="createPoDialogOpen()" (openChange)="createPoDialogOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="PackageCheck" class="size-4" /> </div> <h2 ui-dialog-title class="text-lg font-bold">Draft Purchase Order</h2> </div> <p ui-dialog-description class="text-muted-foreground text-xs"> Review demand velocity, set purchase order quantity, and dispatch replenishment order to supplier. </p> </div> @if (selectedSkuItem(); as selected) { <div class="space-y-4 py-2"> <!-- SKU Summary Banner --> <div class="border-border bg-muted/20 space-y-3 rounded-lg border p-3.5"> <div class="flex items-start justify-between gap-2"> <div> <div class="text-primary font-mono text-xs font-semibold">{{ selected.sku }}</div> <div class="text-foreground text-sm font-semibold">{{ selected.name }}</div> <div class="text-muted-foreground text-xs">{{ selected.supplier }} · {{ selected.category }}</div> </div> <span ui-badge [variant]=" selected.status === 'critical' ? 'destructive' : selected.status === 'warning' ? 'warning' : 'success' " class="text-xs" > {{ selected.daysOfInventoryRemaining }} Days Stock Left </span> </div> <div ui-separator></div> <div class="grid grid-cols-3 gap-2 text-xs"> <div> <span class="text-muted-foreground">On Hand:</span> <p class="text-foreground font-semibold tabular-nums">{{ selected.onHandStock }} units</p> </div> <div> <span class="text-muted-foreground">Daily Velocity:</span> <p class="text-foreground font-semibold tabular-nums">{{ selected.dailySalesVelocity }} / day</p> </div> <div> <span class="text-muted-foreground">Lead Time:</span> <p class="text-foreground font-semibold tabular-nums">{{ selected.supplierLeadTimeDays }} Days</p> </div> </div> </div> <!-- Quantity Input & Quick Chips --> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <label class="text-foreground text-xs font-medium">Reorder Quantity (Units)</label> <span class="text-muted-foreground text-xs"> Recommended: {{ selected.recommendedReorderQty }} units </span> </div> <ui-input [value]="customReorderQty()" (valueChange)="setCustomQty($event)" type="number" class="text-xs tabular-nums sm:text-sm" /> <div class="flex gap-1.5 pt-1"> <button type="button" ui-button variant="outline" size="xs" class="flex-1 text-xs" (click)="setQtyFraction(selected, 0.5)" > 50% ({{ halfQty(selected) }}) </button> <button type="button" ui-button variant="outline" size="xs" class="flex-1 text-xs" (click)="setQtyFraction(selected, 1)" > Recommended ({{ selected.recommendedReorderQty }}) </button> <button type="button" ui-button variant="outline" size="xs" class="flex-1 text-xs" (click)="setQtyFraction(selected, 1.5)" > 150% ({{ oneHalfQty(selected) }}) </button> </div> </div> <!-- Shipping Freight Method --> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium">Inbound Freight Method</label> <ui-select [value]="selectedShippingMethod()" (valueChange)="selectedShippingMethod.set($event)"> <button ui-select-trigger class="w-full text-xs"> <ui-select-value placeholder="Select shipping method" /> </button> <ui-select-content> <ui-select-item value="standard-freight"> Standard Consolidated Freight (Est. 14 Days) </ui-select-item> <ui-select-item value="expedited-air">Expedited Air Express (Est. 4 Days · +$420)</ui-select-item> <ui-select-item value="ocean-container"> Full Ocean Container FCL (Est. 28 Days · Economy) </ui-select-item> </ui-select-content> </ui-select> </div> <!-- Cost Calculation Summary Box --> <div class="border-border bg-muted/40 space-y-2 rounded-lg border p-3.5 text-xs"> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Unit Cost:</span> <span class="text-foreground font-mono tabular-nums">{{ money(selected.unitCost) }}</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground">Units to Order:</span> <span class="text-foreground font-mono tabular-nums">{{ customReorderQty() }} units</span> </div> <div ui-separator></div> <div class="flex items-center justify-between text-sm font-semibold"> <span class="text-foreground">Total Estimated PO Value:</span> <span class="text-primary font-mono text-base font-bold tabular-nums"> {{ money(dialogCalculatedCost()) }} </span> </div> </div> </div> } <div ui-dialog-footer class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> <button ui-button ui-dialog-close variant="outline">Cancel</button> <button ui-button class="gap-1.5" (click)="handleConfirmCreatePo()"> <lucide-icon [img]="FileCheck" class="size-4" /> Generate Purchase Order </button> </div> </ui-dialog-content> </ui-dialog> <!-- Bulk POs Generation Dialog --> <ui-dialog [open]="bulkPoDialogOpen()" (openChange)="bulkPoDialogOpen.set($event)"> <ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="PackagePlus" class="size-4" /> </div> <h2 ui-dialog-title class="text-lg font-bold">Generate Bulk Purchase Orders</h2> </div> <p ui-dialog-description class="text-muted-foreground text-xs"> Automatically create and batch replenishment purchase orders for all SKUs below safe safety threshold. </p> </div> <div class="space-y-4 py-2"> <!-- Summary Metrics --> <div class="border-border bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Destination Facility:</span> <span ui-badge variant="outline" class="text-xs font-medium">Global Fulfillment Center</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Total Purchase Orders:</span> <span class="text-foreground text-xs font-semibold tabular-nums">4 Vendor POs</span> </div> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Total Replenishment Units:</span> <span class="text-foreground text-xs font-semibold tabular-nums">620 Units</span> </div> <div ui-separator></div> <div class="flex items-center justify-between"> <span class="text-foreground text-xs font-semibold">Combined Total PO Value:</span> <span class="text-primary font-mono text-base font-bold tabular-nums">$48,250.00</span> </div> </div> <!-- PO List Breakdown --> <div class="space-y-2"> <div class="text-foreground text-xs font-semibold">Purchase Order Breakdown by Supplier</div> <div class="border-border space-y-2 rounded-md border p-2.5 text-xs"> @for (item of criticalAndWarningItems(); track item.id) { <div class="flex items-center justify-between py-1"> <div> <span class="text-foreground font-medium">{{ item.supplier }}</span> <div class="text-muted-foreground font-mono text-xs"> {{ item.sku }} ({{ item.recommendedReorderQty }} units) </div> </div> <span class="text-foreground font-mono font-semibold tabular-nums"> {{ money(item.estimatedPoCost) }} </span> </div> } </div> </div> </div> <div ui-dialog-footer class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> <button ui-button ui-dialog-close variant="outline">Cancel</button> <button ui-button class="gap-1.5" (click)="handleConfirmBulkPos()"> <lucide-icon [img]="PackagePlus" class="size-4" /> Generate 4 Purchase Orders </button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiInventoryReorderForecastComponent { protected readonly AlertTriangle = AlertTriangle protected readonly ArrowUpRight = ArrowUpRight protected readonly Boxes = Boxes protected readonly Building2 = Building2 protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly DollarSign = DollarSign protected readonly Download = Download protected readonly FileCheck = FileCheck protected readonly PackageCheck = PackageCheck protected readonly PackagePlus = PackagePlus protected readonly Search = Search protected readonly TrendingUp = TrendingUp protected readonly X = X protected readonly Zap = Zap protected readonly allItems = reorderItems @Input('class') className?: string readonly facility = signal('global-fulfillment') readonly searchQuery = signal('') readonly categoryFilter = signal('all') readonly statusFilter = signal('all') readonly createPoDialogOpen = signal(false) readonly bulkPoDialogOpen = signal(false) readonly selectedSkuItem = signal<ReorderItem | null>(null) readonly customReorderQty = signal<number>(250) readonly selectedShippingMethod = signal('standard-freight') readonly actionFeedback = signal<string | null>(null) readonly filteredItems = computed(() => { return reorderItems.filter((item) => { const q = this.searchQuery().trim().toLowerCase() const matchesSearch = !q || item.name.toLowerCase().includes(q) || item.sku.toLowerCase().includes(q) || item.category.toLowerCase().includes(q) || item.supplier.toLowerCase().includes(q) const matchesCategory = this.categoryFilter() === 'all' || item.category === this.categoryFilter() const matchesStatus = this.statusFilter() === 'all' || item.status === this.statusFilter() return matchesSearch && matchesCategory && matchesStatus }) }) readonly criticalAndWarningItems = computed(() => { return reorderItems.filter((item) => item.status === 'critical' || item.status === 'warning') }) readonly dialogCalculatedCost = computed(() => { const selected = this.selectedSkuItem() if (!selected) return 0 return this.customReorderQty() * selected.unitCost }) get rootClass(): string { return cn('w-full space-y-6', this.className) } money(value: number): string { return '$' + value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } setCustomQty(raw: string): void { this.customReorderQty.set(Number(raw) || 0) } setQtyFraction(item: ReorderItem, fraction: number): void { this.customReorderQty.set(Math.round(item.recommendedReorderQty * fraction)) } halfQty(item: ReorderItem): number { return Math.round(item.recommendedReorderQty * 0.5) } oneHalfQty(item: ReorderItem): number { return Math.round(item.recommendedReorderQty * 1.5) } openCreatePoDialog(item: ReorderItem): void { this.selectedSkuItem.set(item) this.customReorderQty.set(item.recommendedReorderQty) this.selectedShippingMethod.set('standard-freight') this.createPoDialogOpen.set(true) } handleConfirmCreatePo(): void { const selected = this.selectedSkuItem() if (!selected) return const poNumber = `PO-2026-${Math.floor(1000 + Math.random() * 9000)}` this.actionFeedback.set( `Draft ${poNumber} created for ${selected.sku} (${this.customReorderQty()} units · $${this.dialogCalculatedCost().toLocaleString('en-US', { minimumFractionDigits: 2 })}) routed to ${selected.supplier}.`, ) this.createPoDialogOpen.set(false) setTimeout(() => this.actionFeedback.set(null), 4500) } openBulkPoDialog(): void { this.bulkPoDialogOpen.set(true) } handleConfirmBulkPos(): void { this.actionFeedback.set( `Successfully generated 4 Purchase Orders (Total $48,250.00) for Global Fulfillment Center replenishment.`, ) this.bulkPoDialogOpen.set(false) setTimeout(() => this.actionFeedback.set(null), 5000) } handleExportPlan(): void { this.actionFeedback.set(`Replenishment forecast plan for ${reorderItems.length} SKUs exported successfully as CSV.`) setTimeout(() => this.actionFeedback.set(null), 4000) } }src/app/components/blocks/inventory-reorder-forecast/index.ts0.1 kB
export { UiInventoryReorderForecastComponent, type ReorderItem, type StockHealthStatus, } from './inventory-reorder-forecast.component'
Raw manifest:https://uipkge.dev/r/angular/inventory-reorder-forecast.json