From 35b1c7ed27c98db1e464d320d8c9359a76422900 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Sat, 15 Aug 2026 03:59:23 +0400 Subject: [PATCH] feat: wire order watcher into admin topbar bell (badge + panel) Co-Authored-By: Claude Sonnet 5 --- .../admin/shell/admin-layout.component.html | 20 ++++- .../admin/shell/admin-layout.component.scss | 36 +++++++++ .../shell/admin-layout.component.spec.ts | 76 +++++++++++++++++++ .../admin/shell/admin-layout.component.ts | 14 ++++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/app/features/admin/shell/admin-layout.component.spec.ts diff --git a/src/app/features/admin/shell/admin-layout.component.html b/src/app/features/admin/shell/admin-layout.component.html index 9396f75..c2229e7 100644 --- a/src/app/features/admin/shell/admin-layout.component.html +++ b/src/app/features/admin/shell/admin-layout.component.html @@ -148,10 +148,28 @@ (click)="toggleNotifications()" > + @if (unreadCount() > 0) { + {{ unreadCount() }} + } @if (notificationsOpen()) { } diff --git a/src/app/features/admin/shell/admin-layout.component.scss b/src/app/features/admin/shell/admin-layout.component.scss index 0cb80fd..1bd2613 100644 --- a/src/app/features/admin/shell/admin-layout.component.scss +++ b/src/app/features/admin/shell/admin-layout.component.scss @@ -347,6 +347,21 @@ position: relative; } +.admin-layout__notifications-badge { + position: absolute; + top: 2px; + right: 2px; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 999px; + background: var(--color-danger, #ef4444); + color: #fff; + font-size: 10px; + line-height: 16px; + text-align: center; +} + .admin-layout__notifications-panel { position: absolute; right: 0; @@ -366,6 +381,27 @@ } } +.admin-layout__notification-item { + display: flex; + flex-direction: column; + gap: 2px; + width: 100%; + padding: 8px 10px; + border: none; + background: none; + text-align: left; + cursor: pointer; + border-radius: var(--radius-sm, 4px); +} + +.admin-layout__notification-item:hover { + background: var(--bg-secondary, #f4f6f5); +} + +.admin-layout__notification-order { font-weight: var(--font-weight-medium, 500); } +.admin-layout__notification-customer { color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); } +.admin-layout__notification-amount { font-size: var(--font-size-sm, 0.8125rem); } + .admin-layout__account { display: flex; align-items: center; diff --git a/src/app/features/admin/shell/admin-layout.component.spec.ts b/src/app/features/admin/shell/admin-layout.component.spec.ts new file mode 100644 index 0000000..3adb993 --- /dev/null +++ b/src/app/features/admin/shell/admin-layout.component.spec.ts @@ -0,0 +1,76 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { signal } from '@angular/core'; +import { AdminLayoutComponent } from './admin-layout.component'; +import { AdminOrderWatcherService } from './services/admin-order-watcher.service'; +import { AdminOrder } from '../orders/models/admin-order.model'; + +function makeOrder(id: string, orderNumber: string): AdminOrder { + return { + id, + orderNumber, + status: 'pending', + customer: { name: 'Test Customer', email: 't@example.com', phone: '' }, + payment: { method: 'card', status: 'paid', amount: 500, currency: 'RUB' }, + shipping: { address: '', method: '', trackingNumber: '' }, + items: [], + total: 500, + currency: 'RUB', + notes: '', + internalNotes: '', + timeline: [], + archived: false, + createdAt: '2026-08-15T10:00:00.000Z', + updatedAt: '2026-08-15T10:00:00.000Z', + }; +} + +describe('AdminLayoutComponent notifications bell', () => { + let watcherStub: { + recentOrders: ReturnType>; + unreadCount: ReturnType>; + start: jasmine.Spy; + markAllSeen: jasmine.Spy; + }; + + beforeEach(() => { + watcherStub = { + recentOrders: signal([makeOrder('o1', '1001')]), + unreadCount: signal(1), + start: jasmine.createSpy('start'), + markAllSeen: jasmine.createSpy('markAllSeen'), + }; + + TestBed.configureTestingModule({ + imports: [AdminLayoutComponent], + providers: [ + provideRouter([]), + { provide: AdminOrderWatcherService, useValue: watcherStub }, + ], + }); + }); + + it('starts the watcher once on construction', () => { + TestBed.createComponent(AdminLayoutComponent); + expect(watcherStub.start).toHaveBeenCalledTimes(1); + }); + + it('exposes unreadCount and recentOrders from the watcher', () => { + const fixture = TestBed.createComponent(AdminLayoutComponent); + const component = fixture.componentInstance; + expect(component.unreadCount()).toBe(1); + expect(component.recentOrders().map(o => o.id)).toEqual(['o1']); + }); + + it('marks orders seen when the notifications panel opens', () => { + const fixture = TestBed.createComponent(AdminLayoutComponent); + const component = fixture.componentInstance; + component.toggleNotifications(); + expect(component.notificationsOpen()).toBe(true); + expect(watcherStub.markAllSeen).toHaveBeenCalledTimes(1); + + component.toggleNotifications(); + expect(component.notificationsOpen()).toBe(false); + expect(watcherStub.markAllSeen).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/features/admin/shell/admin-layout.component.ts b/src/app/features/admin/shell/admin-layout.component.ts index db12d00..db783e8 100644 --- a/src/app/features/admin/shell/admin-layout.component.ts +++ b/src/app/features/admin/shell/admin-layout.component.ts @@ -12,6 +12,7 @@ import { IconComponent } from '../../../shared/ui/icon/icon.component'; import { AdminPreferencesService } from '../settings/services/admin-preferences.service'; import { UiRuntimeFacade } from '../../../facades/runtime/ui-runtime.facade'; import { ConfigService } from '../../../core/config/config.service'; +import { AdminOrderWatcherService } from './services/admin-order-watcher.service'; @Component({ selector: 'app-admin-layout', @@ -31,6 +32,10 @@ export class AdminLayoutComponent { private readonly preferences = inject(AdminPreferencesService); private readonly uiRuntime = inject(UiRuntimeFacade); private readonly configService = inject(ConfigService); + private readonly orderWatcher = inject(AdminOrderWatcherService); + + readonly unreadCount = this.orderWatcher.unreadCount; + readonly recentOrders = this.orderWatcher.recentOrders; readonly density = this.preferences.density; @@ -87,6 +92,7 @@ export class AdminLayoutComponent { constructor() { this.readRouteData(); + this.orderWatcher.start(); this.router.events .pipe( filter(event => event instanceof NavigationEnd), @@ -130,6 +136,9 @@ export class AdminLayoutComponent { toggleNotifications(): void { this.notificationsOpen.update(open => !open); + if (this.notificationsOpen()) { + this.orderWatcher.markAllSeen(); + } } adminLinkFor(entry: Extract): string[] { @@ -140,6 +149,11 @@ export class AdminLayoutComponent { return ['/', lang, 'backoffice', ...(entry.path ?? [])]; } + goToOrder(orderId: string): void { + void this.router.navigate([this.currentLang(), 'backoffice', 'orders', orderId]); + this.notificationsOpen.set(false); + } + breadcrumbLink(entry: AdminBreadcrumbEntry): string[] { return ['/', this.currentLang(), 'backoffice', ...(entry.path ?? [])]; }