feat: wire order watcher into admin topbar bell (badge + panel)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-15 03:59:23 +04:00
parent 28f39a31f6
commit 35b1c7ed27
4 changed files with 145 additions and 1 deletions

View File

@@ -148,10 +148,28 @@
(click)="toggleNotifications()"
>
<app-icon name="bell" [size]="18" />
@if (unreadCount() > 0) {
<span class="admin-layout__notifications-badge">{{ unreadCount() }}</span>
}
</button>
@if (notificationsOpen()) {
<div class="admin-layout__notifications-panel" role="menu">
<p>{{ 'adminShell.topbar.notificationsEmpty' | translate }}</p>
@if (recentOrders().length === 0) {
<p>{{ 'adminShell.topbar.notificationsEmpty' | translate }}</p>
} @else {
@for (order of recentOrders(); track order.id) {
<button
type="button"
class="admin-layout__notification-item"
role="menuitem"
(click)="goToOrder(order.id)"
>
<span class="admin-layout__notification-order">#{{ order.orderNumber }}</span>
<span class="admin-layout__notification-customer">{{ order.customer.name }}</span>
<span class="admin-layout__notification-amount">{{ order.total }} {{ order.currency }}</span>
</button>
}
}
</div>
}
</div>

View File

@@ -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;

View File

@@ -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<typeof signal<AdminOrder[]>>;
unreadCount: ReturnType<typeof signal<number>>;
start: jasmine.Spy;
markAllSeen: jasmine.Spy;
};
beforeEach(() => {
watcherStub = {
recentOrders: signal<AdminOrder[]>([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);
});
});

View File

@@ -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<AdminNavEntry, { type: 'link' }>): 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 ?? [])];
}