Order Management Page
blockcommerceE-commerce and SaaS order fulfillment admin table: header with export CSV and new order action, 4 KPI summary metric cards, search and multi-status filter toolbar with date range selector, selectable order rows with customer avatars and status badges, slide-over order details drawer with line items breakdown and tracking info, bulk action bar, and pagination.
Installation
$pnpm dlx uipkge-ng@latest add order-management-page$npx uipkge-ng@latest add order-management-page$yarn dlx uipkge-ng@latest add order-management-page$bunx uipkge-ng@latest add order-management-pageManifest:
https://uipkge.dev/r/angular/order-management-page.jsonInstalls to:src/app/components/blocks/order-management-page/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.
OrderIteminterface OrderItem {
name: string
sku: string
quantity: number
price: number
}CustomerInfointerface CustomerInfo {
name: string
email: string
avatar?: string
address: string
city: string
state: string
zip: string
}Orderinterface Order {
id: string
orderNumber: string
date: string
customer: CustomerInfo
items: OrderItem[]
status: OrderStatus
paymentMethod: string
subtotal: number
shipping: number
tax: number
total: number
carrier?: string
trackingNumber?: string
}npm dependencies
Files installed (2)
src/app/components/blocks/order-management-page/order-management-page.component.ts35.3 kB
// E-commerce and SaaS order fulfillment admin table: header actions, 4 KPI cards, // search + status filter toolbar, selectable rows with avatars and status badges, // slide-over order details drawer, bulk action bar, and pagination. Port of the // Vue/React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowUpRight, Calendar, ChevronDown, Clock, DollarSign, Download, Eye, FileText, LucideAngularModule, MoreHorizontal, PackageCheck, Plus, RotateCcw, Search, ShoppingCart, TrendingUp, Truck, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSheetCloseComponent, UiSheetComponent, UiSheetContentComponent, UiSheetDescriptionComponent, UiSheetFooterComponent, UiSheetHeaderComponent, UiSheetTitleComponent, } from '@/ui/sheet/sheet.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type OrderStatus = 'paid' | 'processing' | 'shipped' | 'refunded' export interface OrderItem { name: string sku: string quantity: number price: number } export interface CustomerInfo { name: string email: string avatar?: string address: string city: string state: string zip: string } export interface Order { id: string orderNumber: string date: string customer: CustomerInfo items: OrderItem[] status: OrderStatus paymentMethod: string subtotal: number shipping: number tax: number total: number carrier?: string trackingNumber?: string } export const orders: Order[] = [ { id: 'ord-1', orderNumber: '#ORD-8492', date: 'Oct 24, 2026', customer: { name: 'Sarah Chen', email: 'sarah.chen@example.com', address: '742 Evergreen Terrace', city: 'Springfield', state: 'OR', zip: '97477', }, items: [ { name: 'Ergonomic Mechanical Keyboard', sku: 'KB-ERGO-01', quantity: 1, price: 189 }, { name: 'Desk Mat Pro (Midnight)', sku: 'MAT-PRO-BLK', quantity: 1, price: 39 }, ], status: 'processing', paymentMethod: 'Mastercard •••• 8492', subtotal: 228, shipping: 0, tax: 18.24, total: 246.24, carrier: 'FedEx Priority', trackingNumber: 'FX-9281034-US', }, { id: 'ord-2', orderNumber: '#ORD-8491', date: 'Oct 24, 2026', customer: { name: 'Marcus Vance', email: 'marcus.v@acme-corp.com', address: '100 Enterprise Way, Suite 400', city: 'San Francisco', state: 'CA', zip: '94105', }, items: [ { name: 'UltraWide Monitor 34"', sku: 'MON-34-UW', quantity: 1, price: 699 }, { name: 'Dual Monitor Arm Mount', sku: 'ACC-ARM-02', quantity: 1, price: 129 }, { name: 'Braided DisplayPort Cable 2m', sku: 'CAB-DP-02', quantity: 1, price: 24 }, ], status: 'shipped', paymentMethod: 'Visa •••• 4242', subtotal: 852, shipping: 0, tax: 68.16, total: 920.16, carrier: 'UPS Ground', trackingNumber: '1Z999AA10123456784', }, { id: 'ord-3', orderNumber: '#ORD-8490', date: 'Oct 23, 2026', customer: { name: 'Elena Rostova', email: 'elena.rostova@techflow.io', address: '450 Innovation Parkway', city: 'Austin', state: 'TX', zip: '78701', }, items: [{ name: 'SaaS Enterprise Annual License', sku: 'LIC-ENT-YR', quantity: 1, price: 1200 }], status: 'paid', paymentMethod: 'Visa •••• 9104', subtotal: 1200, shipping: 0, tax: 0, total: 1200, }, { id: 'ord-4', orderNumber: '#ORD-8489', date: 'Oct 23, 2026', customer: { name: 'David Kim', email: 'david.kim@designly.co', address: '88 King Street West', city: 'Seattle', state: 'WA', zip: '98104', }, items: [ { name: 'USB-C Multiport Dock Pro', sku: 'HUB-USBC-8P', quantity: 2, price: 79 }, { name: 'Thunderbolt 4 Braided Cable', sku: 'CAB-TB4-1M', quantity: 2, price: 29 }, ], status: 'shipped', paymentMethod: 'Apple Pay', subtotal: 216, shipping: 0, tax: 17.28, total: 233.28, carrier: 'DHL Express', trackingNumber: 'DHL-8492019-EXP', }, { id: 'ord-5', orderNumber: '#ORD-8488', date: 'Oct 22, 2026', customer: { name: 'Olivia Taylor', email: 'olivia.t@summit.ai', address: '12 Marina Boulevard', city: 'Boston', state: 'MA', zip: '02210', }, items: [{ name: 'Noise-Cancelling Studio Headphones', sku: 'AUD-NC-700', quantity: 1, price: 349 }], status: 'refunded', paymentMethod: 'Visa •••• 1128', subtotal: 349, shipping: 0, tax: 27.92, total: 376.92, }, { id: 'ord-6', orderNumber: '#ORD-8487', date: 'Oct 22, 2026', customer: { name: 'Liam Johansson', email: 'liam.j@nordic.se', address: '240 Queen Street', city: 'New York', state: 'NY', zip: '10001', }, items: [ { name: 'Aluminum Laptop Riser', sku: 'STN-ALUM-SLV', quantity: 1, price: 65 }, { name: 'Wireless Precision Mouse', sku: 'MOU-WL-01', quantity: 1, price: 99 }, ], status: 'processing', paymentMethod: 'Mastercard •••• 5531', subtotal: 164, shipping: 0, tax: 13.12, total: 177.12, carrier: 'USPS Priority', trackingNumber: '9400100000000000000000', }, { id: 'ord-7', orderNumber: '#ORD-8486', date: 'Oct 21, 2026', customer: { name: 'Aisha Patel', email: 'aisha.patel@globalnet.com', address: '500 Central Avenue', city: 'Chicago', state: 'IL', zip: '60601', }, items: [{ name: 'Cloud Storage Tier 3 (500GB)', sku: 'SRV-STR-500', quantity: 1, price: 450 }], status: 'paid', paymentMethod: 'Amex •••• 3009', subtotal: 450, shipping: 0, tax: 36, total: 486, }, { id: 'ord-8', orderNumber: '#ORD-8485', date: 'Oct 20, 2026', customer: { name: 'Lucas Moreau', email: 'lucas.m@atelier.fr', address: '15 Rue de la Paix', city: 'Boulder', state: 'CO', zip: '80302', }, items: [ { name: 'Broadcast Studio Microphone', sku: 'MIC-POD-01', quantity: 1, price: 229 }, { name: 'Dual-Layer Pop Filter', sku: 'ACC-POP-01', quantity: 1, price: 25 }, { name: 'Gold-Plated XLR Cable 3m', sku: 'CAB-XLR-03', quantity: 1, price: 19 }, ], status: 'shipped', paymentMethod: 'Visa •••• 7712', subtotal: 273, shipping: 0, tax: 21.84, total: 294.84, carrier: 'FedEx Ground', trackingNumber: 'FX-7712093-US', }, ] const statusBadgeConfig: Record<OrderStatus, { label: string; class: string; dot: string }> = { paid: { label: 'Paid', class: 'bg-success/10 text-success border-success/20', dot: 'bg-success' }, processing: { label: 'Processing', class: 'bg-info/10 text-info border-info/20', dot: 'bg-info' }, shipped: { label: 'Shipped', class: 'bg-chart-1/10 text-chart-1 border-chart-1/20', dot: 'bg-chart-1' }, refunded: { label: 'Refunded', class: 'bg-destructive/10 text-destructive border-destructive/20', dot: 'bg-destructive', }, } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-order-management-page, [ui-order-management-page]', standalone: true, // The block renders its own root <div> (and the sheet portals to body): keep the host out of layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCheckboxComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiInputComponent, UiSheetCloseComponent, UiSheetComponent, UiSheetContentComponent, UiSheetDescriptionComponent, UiSheetFooterComponent, UiSheetHeaderComponent, UiSheetTitleComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="order-management-page" [class]="rootClass"> <!-- Page Header --> <div class="flex flex-wrap items-center justify-between gap-4"> <div> <h2 class="text-foreground text-2xl font-bold tracking-tight">Orders</h2> <p class="text-muted-foreground mt-1 text-sm">Manage and track customer purchases</p> </div> <div class="flex items-center gap-2"> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="text-xs"> <lucide-icon [img]="Download" class="mr-1.5 size-3.5" aria-hidden="true" /> Export CSV </button> <button ui-button size="sm" class="text-xs"> <lucide-icon [img]="Plus" class="mr-1.5 size-3.5" aria-hidden="true" /> New Order </button> </div> </div> <!-- 4 Stat/KPI Summary Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Orders</p> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ShoppingCart" class="size-4" aria-hidden="true" /> </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,429</span> <span class="text-success inline-flex items-center gap-0.5 text-xs font-medium"> <lucide-icon [img]="TrendingUp" class="size-3.5" aria-hidden="true" /> +8.2% </span> </div> <p class="text-muted-foreground mt-1 text-xs">vs. previous 30 days</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Pending Fulfillment</p> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Clock" class="size-4" aria-hidden="true" /> </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">28</span> <span class="bg-warning/10 text-warning inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium" > 12 urgent </span> </div> <p class="text-muted-foreground mt-1 text-xs">Requires warehouse dispatch</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Processing</p> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="PackageCheck" class="size-4" aria-hidden="true" /> </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">45</span> <span class="bg-info/10 text-info inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"> In queue </span> </div> <p class="text-muted-foreground mt-1 text-xs">Packaging & label generation</p> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Revenue</p> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="DollarSign" class="size-4" aria-hidden="true" /> </div> </div> <div class="mt-3 flex flex-wrap items-baseline justify-between gap-2"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$128,450</span> <span class="text-success inline-flex items-center gap-0.5 text-xs font-medium"> <lucide-icon [img]="ArrowUpRight" class="size-3.5" aria-hidden="true" /> +14.5% </span> </div> <p class="text-muted-foreground mt-1 text-xs">vs. previous period</p> </div> </div> </div> <!-- Search and Filter Toolbar --> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-1 flex-wrap items-center gap-2"> <ng-template #orderSearchIcon> <lucide-icon [img]="Search" class="size-4" aria-hidden="true" /> </ng-template> <div class="relative min-w-[240px] flex-1 sm:max-w-xs"> <ui-input [value]="search()" (valueChange)="search.set($event)" placeholder="Search order ID, customer..." [prefixIcon]="orderSearchIcon" class="h-9 pl-2 text-xs" /> </div> <div class="bg-muted/60 flex items-center gap-1 rounded-lg border p-1"> @for (status of statusOptions(); track status.value) { <button type="button" [class]="statusPillClass(status.value)" (click)="setStatusFilter(status.value)"> {{ status.label }} <span [class]="statusCountClass(status.value)">{{ status.count }}</span> </button> } </div> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="text-xs font-normal"> <lucide-icon [img]="Calendar" class="text-muted-foreground mr-1.5 size-3.5" aria-hidden="true" /> Last 30 days <lucide-icon [img]="ChevronDown" class="text-muted-foreground ml-1.5 size-3.5" aria-hidden="true" /> </button> </div> </div> <!-- Bulk Action Bar --> @if (selected().size > 0) { <div class="bg-muted/50 flex flex-wrap items-center justify-between gap-2 rounded-lg border px-4 py-2.5 shadow-xs" > <div class="flex items-center gap-2"> <span class="bg-primary text-primary-foreground flex size-5 items-center justify-center rounded-full text-xs font-medium" > {{ selected().size }} </span> <span class="text-foreground text-sm font-medium"> {{ selected().size }} {{ selected().size === 1 ? 'order' : 'orders' }} selected </span> </div> <div class="flex items-center gap-2"> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="text-xs" (click)="exportSelected()" > <lucide-icon [img]="Download" class="mr-1.5 size-3.5" aria-hidden="true" /> Export selected </button> <button ui-button variant="outline" size="sm" class="text-xs" (click)="markSelectedShipped()"> <lucide-icon [img]="Truck" class="mr-1.5 size-3.5" aria-hidden="true" /> Mark as shipped </button> <button ui-button variant="ghost" size="sm" class="text-destructive hover:text-destructive text-xs" (click)="clearSelection()" > Cancel </button> </div> </div> } <!-- Orders Table --> <div class="bg-card overflow-x-auto rounded-lg border shadow-xs"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="w-10"> <ui-checkbox [checked]="allSelected()" [indeterminate]="someSelected() && !allSelected()" aria-label="Select all orders" (checkedChange)="toggleAll($event === true)" /> </th> <th ui-table-head class="min-w-[110px]">Order ID</th> <th ui-table-head class="min-w-[100px]">Date</th> <th ui-table-head class="min-w-[200px]">Customer</th> <th ui-table-head class="min-w-[220px]">Items</th> <th ui-table-head class="min-w-[90px] text-right">Total</th> <th ui-table-head class="min-w-[120px]">Fulfillment</th> <th ui-table-head class="w-10 text-right"><span class="sr-only">Actions</span></th> </tr> </thead> <tbody ui-table-body> @for (order of filteredOrders(); track order.id) { <tr ui-table-row class="group hover:bg-muted/40 cursor-pointer transition-colors" (click)="openOrderDetails(order)" > <td ui-table-cell class="w-10" (click)="$event.stopPropagation()"> <ui-checkbox [checked]="selected().has(order.id)" [attr.aria-label]="'Select order ' + order.orderNumber" (checkedChange)="toggleRow(order.id, $event === true)" /> </td> <td ui-table-cell> <span class="text-foreground font-mono text-xs font-semibold hover:underline"> {{ order.orderNumber }} </span> </td> <td ui-table-cell class="text-muted-foreground text-xs tabular-nums">{{ order.date }}</td> <td ui-table-cell> <div class="flex items-center gap-2.5"> <ui-avatar class="size-7"> <ui-avatar-fallback class="text-xs font-medium">{{ initials(order.customer.name) }}</ui-avatar-fallback> </ui-avatar> <div class="min-w-0"> <p class="text-foreground truncate text-sm font-medium">{{ order.customer.name }}</p> <p class="text-muted-foreground truncate text-xs">{{ order.customer.email }}</p> </div> </div> </td> <td ui-table-cell> <div class="text-xs"> <span class="text-foreground font-medium"> {{ itemCount(order) }} {{ itemCount(order) === 1 ? 'item' : 'items' }} </span> <span class="text-muted-foreground ml-1.5 inline-block max-w-[180px] truncate align-bottom"> • {{ order.items[0]?.name }}{{ order.items.length > 1 ? ' +' + (order.items.length - 1) + ' more' : '' }} </span> </div> </td> <td ui-table-cell class="text-foreground text-right font-mono text-xs font-semibold tabular-nums"> ${{ formatCurrency(order.total) }} </td> <td ui-table-cell> <span ui-badge variant="outline" [class]="statusBadgeClass(order.status)"> <span [class]="statusDotClass(order.status)" aria-hidden="true"></span> {{ statusBadgeConfig[order.status].label }} </span> </td> <td ui-table-cell class="text-right" (click)="$event.stopPropagation()"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground" > <lucide-icon [img]="MoreHorizontal" class="size-4" aria-hidden="true" /> <span class="sr-only">Actions for {{ order.orderNumber }}</span> </button> <ui-dropdown-menu-content align="end" class="w-48"> <ui-dropdown-menu-label>Order actions</ui-dropdown-menu-label> <ui-dropdown-menu-item (select)="openOrderDetails(order)"> <lucide-icon [img]="Eye" class="mr-2 size-4" aria-hidden="true" /> View details </ui-dropdown-menu-item> <ui-dropdown-menu-item> <lucide-icon [img]="FileText" class="mr-2 size-4" aria-hidden="true" /> Download invoice </ui-dropdown-menu-item> @if (order.status === 'processing') { <ui-dropdown-menu-item> <lucide-icon [img]="Truck" class="mr-2 size-4" aria-hidden="true" /> Mark as shipped </ui-dropdown-menu-item> } <ui-dropdown-menu-separator /> @if (order.status !== 'refunded') { <ui-dropdown-menu-item class="text-destructive focus:text-destructive"> <lucide-icon [img]="RotateCcw" class="mr-2 size-4" aria-hidden="true" /> Issue refund </ui-dropdown-menu-item> } </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } @if (filteredOrders().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="8" class="text-muted-foreground h-32 text-center text-sm"> No orders match your filter criteria. </td> </tr> } </tbody> </table> </div> <!-- Pagination Footer --> <div class="flex flex-wrap items-center justify-between gap-4"> <p class="text-muted-foreground text-xs tabular-nums"> Showing 1-{{ filteredOrders().length }} of 1,429 orders </p> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="text-xs" disabled>Previous</button> <button ui-button variant="outline" size="sm" class="text-xs">Next</button> </div> </div> <!-- Quick Details Drawer / Sheet --> <div ui-sheet [open]="drawerOpen()" (openChange)="drawerOpen.set($event)"> <div ui-sheet-content side="right" class="flex w-full flex-col gap-0 overflow-y-auto p-0 sm:max-w-lg"> @if (selectedOrder(); as detail) { <div class="flex h-full flex-col"> <!-- Sheet Header --> <div ui-sheet-header class="space-y-1 border-b px-6 py-5"> <div class="flex items-center justify-between pr-6"> <span class="text-foreground font-mono text-base font-bold">{{ detail.orderNumber }}</span> <span ui-badge variant="outline" [class]="statusBadgeDrawerClass(detail.status)"> <span [class]="statusDotClass(detail.status)" aria-hidden="true"></span> {{ statusBadgeConfig[detail.status].label }} </span> </div> <h2 ui-sheet-title class="text-muted-foreground text-xs font-normal">Placed on {{ detail.date }}</h2> <p ui-sheet-description class="sr-only">Detailed view of order {{ detail.orderNumber }}</p> </div> <!-- Sheet Body --> <div class="flex-1 space-y-6 overflow-y-auto px-6 py-5 text-sm"> <!-- Customer & Shipping --> <div class="bg-muted/20 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Customer & Shipping </h4> <span class="text-muted-foreground font-mono text-xs">{{ detail.paymentMethod }}</span> </div> <div class="flex items-center gap-3"> <ui-avatar class="size-9"> <ui-avatar-fallback class="text-xs font-semibold"> {{ initials(detail.customer.name) }} </ui-avatar-fallback> </ui-avatar> <div class="min-w-0"> <p class="text-foreground font-medium">{{ detail.customer.name }}</p> <p class="text-muted-foreground text-xs">{{ detail.customer.email }}</p> </div> </div> <div class="text-muted-foreground border-t pt-2.5 text-xs"> <p class="text-foreground font-medium">Delivery address:</p> <p>{{ detail.customer.address }}</p> <p>{{ detail.customer.city }}, {{ detail.customer.state }} {{ detail.customer.zip }}</p> </div> </div> <!-- Fulfillment & Tracking if available --> @if (detail.carrier && detail.trackingNumber) { <div class="border-border bg-card space-y-2 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase" >Fulfillment</span > <span ui-badge variant="secondary" class="text-xs font-normal">In Transit</span> </div> <div class="flex items-center justify-between pt-1 text-xs"> <span class="text-muted-foreground">Carrier:</span> <span class="text-foreground font-medium">{{ detail.carrier }}</span> </div> <div class="flex items-center justify-between text-xs"> <span class="text-muted-foreground">Tracking ID:</span> <span class="text-foreground font-mono font-medium">{{ detail.trackingNumber }}</span> </div> </div> } <!-- Line Items --> <div class="space-y-3"> <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Line Items</h4> <div class="divide-border bg-card divide-y rounded-lg border"> @for (item of detail.items; track item.sku) { <div class="flex items-center justify-between gap-3 p-3.5"> <div class="min-w-0 flex-1"> <p class="text-foreground truncate text-sm font-medium">{{ item.name }}</p> <p class="text-muted-foreground font-mono text-xs"> SKU: {{ item.sku }} • Qty: {{ item.quantity }} × ${{ formatCurrency(item.price) }} </p> </div> <div class="text-foreground font-mono text-xs font-semibold tabular-nums"> ${{ formatCurrency(item.price * item.quantity) }} </div> </div> } </div> </div> <!-- Cost Breakdown --> <div class="bg-card space-y-2 rounded-lg border p-4 text-xs"> <div class="text-muted-foreground flex justify-between"> <span>Subtotal</span> <span class="text-foreground font-mono tabular-nums" >${{ formatCurrency(detail.subtotal) }}</span > </div> <div class="text-muted-foreground flex justify-between"> <span>Estimated Shipping</span> <span class="text-foreground font-mono tabular-nums"> {{ detail.shipping === 0 ? 'Free' : '$' + formatCurrency(detail.shipping) }} </span> </div> <div class="text-muted-foreground flex justify-between"> <span>Tax</span> <span class="text-foreground font-mono tabular-nums">${{ formatCurrency(detail.tax) }}</span> </div> <div class="text-foreground flex justify-between border-t pt-2 text-sm font-medium"> <span>Total</span> <span class="text-foreground font-mono font-bold tabular-nums"> ${{ formatCurrency(detail.total) }} </span> </div> </div> </div> <!-- Sheet Footer --> <div ui-sheet-footer class="bg-muted/10 flex flex-row items-center justify-between gap-2 border-t px-6 py-4" > <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="text-xs"> <lucide-icon [img]="Download" class="mr-1.5 size-3.5" aria-hidden="true" /> Invoice PDF </button> <button ui-button ui-sheet-close variant="secondary" size="sm" class="text-xs">Close</button> </div> </div> } </div> </div> </div> `, }) export class UiOrderManagementPageComponent { protected readonly ArrowUpRight = ArrowUpRight protected readonly Calendar = Calendar protected readonly ChevronDown = ChevronDown protected readonly Clock = Clock protected readonly DollarSign = DollarSign protected readonly Download = Download protected readonly Eye = Eye protected readonly FileText = FileText protected readonly MoreHorizontal = MoreHorizontal protected readonly PackageCheck = PackageCheck protected readonly Plus = Plus protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly ShoppingCart = ShoppingCart protected readonly TrendingUp = TrendingUp protected readonly Truck = Truck protected readonly statusBadgeConfig = statusBadgeConfig @Input('class') className?: string readonly search = signal('') readonly statusFilter = signal<'all' | OrderStatus>('all') readonly selected = signal<Set<string>>(new Set()) readonly selectedOrder = signal<Order | null>(null) readonly drawerOpen = signal(false) readonly filteredOrders = computed(() => { const query = this.search().trim().toLowerCase() return orders.filter((order) => { const matchesStatus = this.statusFilter() === 'all' || order.status === this.statusFilter() const matchesQuery = !query || order.orderNumber.toLowerCase().includes(query) || order.customer.name.toLowerCase().includes(query) || order.customer.email.toLowerCase().includes(query) return matchesStatus && matchesQuery }) }) readonly statusCounts = computed(() => { const counts: Record<string, number> = { all: orders.length, paid: 0, processing: 0, shipped: 0, refunded: 0 } for (const ord of orders) counts[ord.status] = (counts[ord.status] ?? 0) + 1 return counts }) readonly statusOptions = computed(() => [ { value: 'all', label: 'All', count: this.statusCounts().all }, { value: 'paid', label: 'Paid', count: this.statusCounts().paid }, { value: 'processing', label: 'Processing', count: this.statusCounts().processing }, { value: 'shipped', label: 'Shipped', count: this.statusCounts().shipped }, { value: 'refunded', label: 'Refunded', count: this.statusCounts().refunded }, ]) readonly allSelected = computed( () => this.filteredOrders().length > 0 && this.filteredOrders().every((ord) => this.selected().has(ord.id)), ) readonly someSelected = computed(() => this.filteredOrders().some((ord) => this.selected().has(ord.id))) get rootClass(): string { return cn('w-full space-y-6', this.className) } statusPillClass(value: string): string { return cn( 'rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.statusFilter() === value ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } statusCountClass(value: string): string { return cn( 'ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-normal tabular-nums', this.statusFilter() === value ? 'bg-muted text-foreground' : 'text-muted-foreground', ) } statusBadgeClass(status: OrderStatus): string { return cn('gap-1.5 px-2 py-0.5 text-xs font-medium capitalize', statusBadgeConfig[status].class) } statusBadgeDrawerClass(status: OrderStatus): string { return cn('gap-1.5 px-2.5 py-0.5 text-xs font-medium capitalize', statusBadgeConfig[status].class) } statusDotClass(status: OrderStatus): string { return cn('size-1.5 rounded-full', statusBadgeConfig[status].dot) } setStatusFilter(value: string): void { this.statusFilter.set(value as 'all' | OrderStatus) } itemCount(order: Order): number { return order.items.reduce((acc, item) => acc + item.quantity, 0) } toggleAll(checked: boolean): void { this.selected.set(checked ? new Set(this.filteredOrders().map((ord) => ord.id)) : new Set()) } toggleRow(id: string, checked: boolean): void { const next = new Set(this.selected()) if (checked) next.add(id) else next.delete(id) this.selected.set(next) } clearSelection(): void { this.selected.set(new Set()) } exportSelected(): void { this.clearSelection() } markSelectedShipped(): void { this.clearSelection() } openOrderDetails(order: Order): void { this.selectedOrder.set(order) this.drawerOpen.set(true) } formatCurrency(val: number): string { return val.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } initials(name: string): string { return name .split(' ') .map((p) => p[0]) .slice(0, 2) .join('') .toUpperCase() } }src/app/components/blocks/order-management-page/index.ts0.2 kB
export { UiOrderManagementPageComponent, type OrderStatus, type OrderItem, type CustomerInfo, type Order, orders, } from './order-management-page.component'
Raw manifest:https://uipkge.dev/r/angular/order-management-page.json