import { Component, computed, effect, inject, signal, untracked } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; import { ShippingModeService } from '@africanies/africanies-core'; import type { AsyncQueryStateModel, FilterStateModel, PaginationMetaModel } from '@africanies/africanies-models'; import { ActionMenuComponent, type AfricaniesMenuItem, ButtonComponent, CellDefDirective, ChipComponent, type ChipVariant, ContentStackComponent, CopyButtonComponent, DEFAULT_PAGE_SIZE, emptyFilterState, FilterDrawerService, FilterQueryService, RowDetailDefDirective, type TableColumn, TableComponent, type TableSortChange, toFilterParams, trackShipmentsFilterConfig, } from '@africanies/africanies-ui'; import { DemoSectionComponent } from '../shared/demo-section.component'; import { PageHeaderComponent } from '../shared/page-header.component'; import { TABLE_COMPACT, TABLE_CONTENT_STACK, TABLE_CUSTOMERS, TABLE_LIST, } from '../snippets'; interface DemoShipment { reference: string; status: 'In transit' | 'Delivered' | 'Pending' | 'Exception'; mode: 'sfn' | 'stn'; destination: string; valueUsd: number; packagedBy: { name: string; email: string; at: Date; }; } const STATUSES = [ 'In transit', 'Delivered', 'Pending', 'Exception', ] as const satisfies readonly DemoShipment['status'][]; const DESTINATIONS = [ 'Lagos', 'Accra', 'Nairobi', 'Cairo', 'London', ] as const; const PACKAGERS = [ { name: 'Oladotun Adedeji', email: 'oladotun.a@africanies.com', }, { name: 'Ada Okonkwo', email: 'ada.o@africanies.com', }, { name: 'Chinedu Bello', email: 'chinedu.b@africanies.com', }, ] as const; type KycStatus = 'Pending' | 'Approved' | 'Rejected'; interface DemoCustomer { id: string; createdAt: Date; name: string; email: string; attempts: number; identifier: string; status: KycStatus; performedBy: string; companyType: string; incorpNo: string; incorpDate: Date; website: string; } const KYC_STATUSES = [ 'Pending', 'Approved', 'Rejected', ] as const satisfies readonly KycStatus[]; const CUSTOMER_NAMES = [ { name: 'Ada Okonkwo', email: 'ada.okonkwo@example.com' }, { name: 'Chinedu Bello', email: 'chinedu.bello@example.com' }, { name: 'Fatima Yusuf', email: 'fatima.yusuf@example.com' }, ] as const; const BUSINESS_NAMES = [ { name: 'Acme Logistics Ltd', email: 'ops@acmelogistics.ng' }, { name: 'Naija Trade Co', email: 'hello@naijatrade.co' }, { name: 'Sahara Imports Plc', email: 'contact@saharaimports.com' }, ] as const; const COMPANY_TYPES = ['Ltd', 'Llc', 'Plc'] as const; const BUSINESS_SITES = [ 'https://acmelogistics.ng', 'https://naijatrade.co', 'https://saharaimports.com', ] as const; const USER_ROWS: DemoCustomer[] = Array.from({ length: 6 }, (_, i) => { const person = CUSTOMER_NAMES[i % CUSTOMER_NAMES.length] ?? CUSTOMER_NAMES[0]; return { id: `USR-${2000 + i}`, createdAt: new Date(Date.UTC(2026, 6, 4 + i, 10, 12)), name: person.name, email: person.email, attempts: (i % 4) + 1, identifier: `NIN-${800100 + i}`, status: KYC_STATUSES[i % KYC_STATUSES.length] ?? 'Pending', performedBy: PACKAGERS[i % PACKAGERS.length]?.name ?? 'Oladotun Adedeji', companyType: '', incorpNo: '', incorpDate: new Date(Date.UTC(2019, 2, 12)), website: '', }; }); const BUSINESS_ROWS: DemoCustomer[] = Array.from({ length: 6 }, (_, i) => { const company = BUSINESS_NAMES[i % BUSINESS_NAMES.length] ?? BUSINESS_NAMES[0]; return { id: `BIZ-${3000 + i}`, createdAt: new Date(Date.UTC(2026, 5, 8 + i, 9, 30)), name: company.name, email: company.email, attempts: (i % 3) + 1, identifier: '', status: KYC_STATUSES[i % KYC_STATUSES.length] ?? 'Pending', performedBy: PACKAGERS[i % PACKAGERS.length]?.name ?? 'Oladotun Adedeji', companyType: COMPANY_TYPES[i % COMPANY_TYPES.length] ?? 'Ltd', incorpNo: `RC-${10400 + i}`, incorpDate: new Date(Date.UTC(2018 + (i % 6), 3, 15)), website: BUSINESS_SITES[i % BUSINESS_SITES.length] ?? BUSINESS_SITES[0], }; }); const ALL_ROWS: DemoShipment[] = Array.from({ length: 28 }, (_, i) => { const packager = PACKAGERS[i % PACKAGERS.length] ?? { name: 'Oladotun Adedeji', email: 'oladotun.a@africanies.com', }; return { reference: `${i % 2 === 0 ? 'SFN' : 'STN'}-${1000 + i}`, status: STATUSES[i % STATUSES.length] ?? 'Pending', mode: i % 2 === 0 ? 'sfn' : 'stn', destination: DESTINATIONS[i % DESTINATIONS.length] ?? 'Lagos', valueUsd: 400 + i * 175, packagedBy: { name: packager.name, email: packager.email, at: new Date(Date.UTC(2026, 7, 14, 13, 4, 22 + i)), }, }; }); /** * */ @Component({ selector: 'app-table-page', standalone: true, imports: [ TableComponent, CellDefDirective, RowDetailDefDirective, ActionMenuComponent, ChipComponent, ContentStackComponent, CopyButtonComponent, ButtonComponent, PageHeaderComponent, DemoSectionComponent, ], template: `
@for (kind of listKinds; track kind) { }
{{ row.reference }}
{{ row.status }} {{ row.mode.toUpperCase() }} {{ formatUsd(row.valueUsd) }} {{ row.destination }} {{ formatUsd(row.valueUsd) }} {{ row.mode === 'sfn' ? 'Export (SFN)' : 'Import (STN)' }} {{ row.status }}
@if (lastRowAction()) {

Last row action: {{ lastRowAction() }}

} @if (lastExpandedRow()) {

Expanded row: {{ lastExpandedRow() }}

} @if (lastExport()) {

{{ lastExport() }}

}
@for (kind of accountKinds; track kind) { }
{{ formatDate(row.createdAt) }}
{{ row.identifier }}
{{ row.status }} {{ formatDate(row.incorpDate) }} {{ row.website.replace('https://', '') }}
`, }) export class TablePage { private readonly filterDrawer = inject(FilterDrawerService); private readonly filterQuery = inject(FilterQueryService); private readonly route = inject(ActivatedRoute); private readonly shipping = inject(ShippingModeService); private lastShippingMode = this.shipping.mode(); protected readonly page = signal(1); protected readonly pageSize = signal(DEFAULT_PAGE_SIZE); protected readonly sort = signal(null); protected readonly listDemo = signal<'ready' | 'loading' | 'empty' | 'error'>('ready'); protected readonly lastRowAction = signal(null); protected readonly lastExpandedRow = signal(null); protected readonly lastExport = signal(null); protected readonly filterState = signal(emptyFilterState()); protected readonly refreshing = signal(false); protected readonly pageLoading = signal(false); protected readonly modeLoading = signal(false); protected readonly listKinds = ['ready', 'loading', 'empty', 'error'] as const; protected readonly activeFilterCount = computed(() => { const state = this.filterState(); const params = toFilterParams(state, trackShipmentsFilterConfig); return Object.keys(params).filter( (key) => key !== 'page' && key !== 'per_page' && key !== 'size' && key !== 'order', ).length; }); protected readonly rowActions = (row: DemoShipment): AfricaniesMenuItem[] => [ { label: 'Open', icon: 'eye', onClick: () => this.lastRowAction.set(`Open · ${row.reference}`), }, { label: 'Edit', icon: 'edit', onClick: () => this.lastRowAction.set(`Edit · ${row.reference}`), }, { label: 'Copy reference', icon: 'copy', onClick: () => this.lastRowAction.set(`Copy · ${row.reference}`), }, { label: 'Delete', icon: 'trash', danger: true, dividerBefore: true, onClick: () => this.lastRowAction.set(`Delete · ${row.reference}`), }, ]; protected readonly rowTrackBy = (row: DemoShipment) => row.reference; protected readonly columns: TableColumn[] = [ { key: 'reference', header: 'Reference', sortable: true }, { key: 'status', header: 'Status', sortable: true }, { key: 'mode', header: 'Mode' }, { key: 'destination', header: 'Destination', sortable: true }, { key: 'valueUsd', header: 'Value', sortable: true }, { key: 'packagedBy', header: 'Packaged by' }, { key: 'actions', header: '', width: '3.5rem' }, ]; protected readonly compactColumns: TableColumn[] = [ { key: 'reference', header: 'Reference' }, { key: 'destination', header: 'Destination' }, { key: 'status', header: 'Status' }, ]; protected readonly accountKind = signal<'user' | 'business'>('user'); protected readonly accountKinds = ['user', 'business'] as const; protected readonly userColumns: TableColumn[] = [ { key: 'createdAt', header: 'Date created', sortable: true }, { key: 'customer', header: 'Customer' }, { key: 'attempts', header: 'Attempts', sortable: true }, { key: 'identifier', header: 'Identifier' }, { key: 'status', header: 'Status', sortable: true }, { key: 'performedBy', header: 'Performed by' }, { key: 'actions', header: 'Action', width: '3.5rem' }, ]; protected readonly businessColumns: TableColumn[] = [ { key: 'createdAt', header: 'Date created', sortable: true }, { key: 'business', header: 'Business' }, { key: 'attempts', header: 'Attempts', sortable: true }, { key: 'companyType', header: 'Company type' }, { key: 'incorpNo', header: 'Incorp no' }, { key: 'incorpDate', header: 'Incorp date' }, { key: 'website', header: 'Website' }, { key: 'status', header: 'Status', sortable: true }, { key: 'actions', header: 'Action', width: '3.5rem' }, ]; protected readonly customerColumns = computed(() => this.accountKind() === 'business' ? this.businessColumns : this.userColumns, ); protected readonly customerRows = computed(() => this.accountKind() === 'business' ? BUSINESS_ROWS : USER_ROWS, ); protected readonly customerTrackBy = (row: DemoCustomer) => row.id; protected readonly customersCode = TABLE_CUSTOMERS; constructor() { this.syncFromUrl(); this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(() => { this.syncFromUrl(); }); effect(() => { const mode = this.shipping.mode(); if (mode === this.lastShippingMode) { return; } this.lastShippingMode = mode; untracked(() => this.onShippingModeChange()); }); } protected readonly tableCode = TABLE_LIST; protected readonly compactCode = TABLE_COMPACT; protected readonly contentStackCode = TABLE_CONTENT_STACK; protected readonly sortedRows = computed(() => { const current = this.sort(); const mode = this.shipping.mode(); const rows = ALL_ROWS.filter((row) => row.mode === mode); if (!current) { return rows; } const dir = current.direction === 'asc' ? 1 : -1; const key = current.key as keyof DemoShipment; return rows.sort((a, b) => { const av = a[key]; const bv = b[key]; if (typeof av === 'number' && typeof bv === 'number') { return (av - bv) * dir; } return String(av).localeCompare(String(bv)) * dir; }); }); protected readonly pageRows = computed(() => { const size = this.pageSize(); const start = (this.page() - 1) * size; return this.sortedRows().slice(start, start + size); }); protected readonly meta = computed((): PaginationMetaModel => { const per_page = this.pageSize(); const total_items = this.sortedRows().length; const total_pages = Math.max(1, Math.ceil(total_items / per_page)); const current_page = Math.min(this.page(), total_pages); return { current_page, per_page, total_items, total_pages, has_next_page: current_page < total_pages, has_previous_page: current_page > 1, }; }); protected readonly listState = computed((): AsyncQueryStateModel => { if (this.modeLoading()) { return { data: undefined, isLoading: true, isFetching: true, isError: false, error: null, }; } switch (this.listDemo()) { case 'loading': return { data: undefined, isLoading: true, isFetching: true, isError: false, error: null, }; case 'empty': return { data: [], isLoading: false, isFetching: false, isError: false, error: null, }; case 'error': return { data: undefined, isLoading: false, isFetching: false, isError: true, error: 'Could not load shipments.', }; default: return { data: this.pageRows(), isLoading: false, isFetching: this.refreshing(), isError: false, error: null, }; } }); protected setListDemo(kind: 'ready' | 'loading' | 'empty' | 'error'): void { this.listDemo.set(kind); } protected onSort(change: TableSortChange): void { this.sort.set(change); this.page.set(1); void this.filterQuery.setPage(1); } protected onPageChange(next: number): void { this.runPageLoad(() => this.page.set(next)); } protected onSizeChange(next: number): void { this.runPageLoad(() => { this.pageSize.set(next); this.page.set(1); }); } /** STN↔SFN: drop current rows and show the body loader — data is for the other mode. */ private onShippingModeChange(): void { if (this.modeLoading()) { return; } this.modeLoading.set(true); this.page.set(1); this.listDemo.set('ready'); setTimeout(() => this.modeLoading.set(false), 500); } /** * Simulate a server page fetch: keep rows, spin the pager, then apply the change. * @param apply */ private runPageLoad(apply: () => void): void { if (this.pageLoading()) { return; } this.pageLoading.set(true); setTimeout(() => { apply(); this.pageLoading.set(false); }, 500); } protected openFilters(): void { this.filterDrawer .open({ config: trackShipmentsFilterConfig, state: this.filterState(), title: 'Filter shipments', }) .afterClosed() .subscribe((result) => { if (result?.applied) { this.filterState.set(result.state); this.page.set(1); } }); } protected onRefresh(): void { if (this.refreshing()) { return; } this.refreshing.set(true); this.setListDemo('ready'); setTimeout(() => this.refreshing.set(false), 800); } protected onExport(): void { this.lastExport.set( `Export clicked · ${this.pageRows().length} rows on this page`, ); } protected onRowExpand(event: { row: DemoShipment; expanded: boolean }): void { this.lastExpandedRow.set( event.expanded ? event.row.reference : null, ); } protected readonly customerActions = (row: DemoCustomer): AfricaniesMenuItem[] => [ { label: 'Open', icon: 'eye', onClick: () => this.lastRowAction.set(`Open · ${row.name}`), }, { label: 'Edit', icon: 'edit', onClick: () => this.lastRowAction.set(`Edit · ${row.name}`), }, ]; protected kycStatusVariant(status: KycStatus): ChipVariant { switch (status) { case 'Approved': return 'success'; case 'Pending': return 'warning'; case 'Rejected': return 'danger'; } } protected statusVariant(status: DemoShipment['status']): ChipVariant { switch (status) { case 'Delivered': return 'success'; case 'In transit': return 'import'; case 'Pending': return 'warning'; case 'Exception': return 'danger'; } } protected formatUsd(value: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }).format(value); } protected formatDate(value: Date): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', }).format(value); } protected formatStamp(value: Date): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true, }).format(value); } /** * Seed pager + filters from the URL (`page`, `size`, filter keys). * No-ops into defaults when those queries are absent. */ private syncFromUrl(): void { const state = this.filterQuery.read(trackShipmentsFilterConfig); this.filterState.set(state); this.page.set(state.page ?? 1); this.pageSize.set(state.size ?? DEFAULT_PAGE_SIZE); } }