feat: wire order watcher into admin topbar bell (badge + panel)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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">
|
||||
@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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
76
src/app/features/admin/shell/admin-layout.component.spec.ts
Normal file
76
src/app/features/admin/shell/admin-layout.component.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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 ?? [])];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user