diff --git a/src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts b/src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts index b67274f..7970045 100644 --- a/src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts +++ b/src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts @@ -54,6 +54,7 @@ describe('AdminOrderWatcherService', () => { localStorage.removeItem('adminOrderWatcher.lastNotifiedOrderId.v1'); localStorage.removeItem('adminOrderWatcher.lastNotifiedOrderCreatedAt.v1'); localStorage.removeItem('adminOrderWatcher.lastAcknowledgedOrderId.v1'); + localStorage.removeItem('adminOrderWatcher.lastAcknowledgedOrderCreatedAt.v1'); localStorage.removeItem('adminOrderWatcher.pollIntervalMs.v1'); // Defaults to unauthenticated so the reactive effect stays a no-op and the @@ -157,6 +158,39 @@ describe('AdminOrderWatcherService', () => { expect(messages.some(m => m.includes('1000'))).toBe(false); })); + it('falls back to a timestamp comparison (not the whole page) for unreadCount when the acknowledged order id disappears', fakeAsync(() => { + service.start(); + tick(0); + + // Second poll introduces 'o3' and pushes 'o2' to index 1. + pollIndex = 1; + ordersByPoll.push([ + makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'), + makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'), + makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'), + ]); + tick(service.intervalMs()); + + // Acknowledge everything up to and including 'o3' (createdAt 11:00:00). + service.markAllSeen(); + expect(service.unreadCount()).toBe(0); + + // Third poll: 'o3' (the acknowledged id) is gone. Page contains orders + // both older and newer than o3's createdAt (2026-08-15T11:00:00.000Z). + pollIndex = 2; + ordersByPoll.push([ + makeOrder('o6', '1006', '2026-08-15T13:00:00.000Z'), + makeOrder('o5', '1005', '2026-08-15T12:00:00.000Z'), + makeOrder('o4', '1004', '2026-08-15T10:30:00.000Z'), + makeOrder('o0', '1000', '2026-08-15T08:00:00.000Z'), + ]); + tick(service.intervalMs()); + + // Only o6 and o5 are newer than o3's ack createdAt - the buggy behavior + // would report the full page length (4) instead of the correct count (2). + expect(service.unreadCount()).toBe(2); + })); + it('stop() clears the interval so no further polls occur', fakeAsync(() => { const gateway = TestBed.inject(AdminOrdersLocalGateway); const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough(); diff --git a/src/app/features/admin/shell/services/admin-order-watcher.service.ts b/src/app/features/admin/shell/services/admin-order-watcher.service.ts index 0223842..f9a8472 100644 --- a/src/app/features/admin/shell/services/admin-order-watcher.service.ts +++ b/src/app/features/admin/shell/services/admin-order-watcher.service.ts @@ -10,6 +10,7 @@ import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service const LAST_NOTIFIED_KEY = 'adminOrderWatcher.lastNotifiedOrderId.v1'; const LAST_NOTIFIED_AT_KEY = 'adminOrderWatcher.lastNotifiedOrderCreatedAt.v1'; const LAST_ACKNOWLEDGED_KEY = 'adminOrderWatcher.lastAcknowledgedOrderId.v1'; +const LAST_ACKNOWLEDGED_AT_KEY = 'adminOrderWatcher.lastAcknowledgedOrderCreatedAt.v1'; const POLL_INTERVAL_KEY = 'adminOrderWatcher.pollIntervalMs.v1'; export const DEFAULT_POLL_INTERVAL_MS = 15000; const MIN_POLL_INTERVAL_MS = 1000; @@ -29,6 +30,7 @@ export class AdminOrderWatcherService { readonly recentOrders: Signal = this.recentOrdersSignal.asReadonly(); private readonly lastAcknowledgedOrderIdSignal = signal(this.storage.getItem(LAST_ACKNOWLEDGED_KEY)); + private readonly lastAcknowledgedOrderCreatedAtSignal = signal(this.storage.getItem(LAST_ACKNOWLEDGED_AT_KEY)); readonly unreadCount = computed(() => { const orders = this.recentOrdersSignal(); @@ -40,7 +42,14 @@ export class AdminOrderWatcherService { return orders.length; } const idx = orders.findIndex(order => order.id === ackId); - return idx === -1 ? orders.length : idx; + if (idx !== -1) { + return idx; + } + // The acknowledged order id is no longer present in the current page + // (deleted, or many orders arrived since ack) - fall back to a timestamp + // comparison instead of treating the whole page as unread. + const ackCreatedAt = this.lastAcknowledgedOrderCreatedAtSignal(); + return ackCreatedAt === null ? orders.length : orders.filter(order => order.createdAt > ackCreatedAt).length; }); private readonly intervalMsSignal = signal(this.readStoredIntervalMs()); @@ -96,12 +105,14 @@ export class AdminOrderWatcherService { } markAllSeen(): void { - const newestId = this.recentOrdersSignal()[0]?.id; - if (!newestId) { + const newest = this.recentOrdersSignal()[0]; + if (!newest) { return; } - this.lastAcknowledgedOrderIdSignal.set(newestId); - this.storage.setItem(LAST_ACKNOWLEDGED_KEY, newestId); + this.lastAcknowledgedOrderIdSignal.set(newest.id); + this.lastAcknowledgedOrderCreatedAtSignal.set(newest.createdAt); + this.storage.setItem(LAST_ACKNOWLEDGED_KEY, newest.id); + this.storage.setItem(LAST_ACKNOWLEDGED_AT_KEY, newest.createdAt); } private scheduleNext(): void { @@ -148,7 +159,9 @@ export class AdminOrderWatcherService { // acknowledged so a fresh admin session doesn't see the whole history as unread. if (this.lastAcknowledgedOrderIdSignal() === null) { this.lastAcknowledgedOrderIdSignal.set(items[0].id); + this.lastAcknowledgedOrderCreatedAtSignal.set(items[0].createdAt); this.storage.setItem(LAST_ACKNOWLEDGED_KEY, items[0].id); + this.storage.setItem(LAST_ACKNOWLEDGED_AT_KEY, items[0].createdAt); } return; }