feat: add actor to order timeline audit trail

AdminOrderTimelineEntry had no actor field at all - order status
changes and refund requests were unattributed. Added actor: string,
populated from the signed-in admin's displayName (same pattern as
Users/Transactions), surfaced in the order detail timeline UI.

Real backend-issued orders still need a server-side audit trail;
this only covers the local mock gateway pending backend work
(BACKEND-API-REFERENCE.md §12).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 07:27:10 +04:00
parent 9fa3321322
commit f336420415
6 changed files with 24 additions and 5 deletions

View File

@@ -13,6 +13,9 @@
} }
</div> </div>
<p class="order-timeline__note">{{ entry.note }}</p> <p class="order-timeline__note">{{ entry.note }}</p>
@if (entry.actor) {
<span class="order-timeline__actor">{{ entry.actor }}</span>
}
</div> </div>
</li> </li>
} }

View File

@@ -55,3 +55,10 @@
font-size: var(--font-size-sm, 0.8125rem); font-size: var(--font-size-sm, 0.8125rem);
color: var(--text-secondary); color: var(--text-secondary);
} }
.order-timeline__actor {
display: block;
margin-top: 2px;
font-size: var(--font-size-xs, 0.75rem);
color: var(--text-light);
}

View File

@@ -8,6 +8,7 @@ export interface OrderTimelineEntry {
/** Pre-resolved, already-translated display text for this event. */ /** Pre-resolved, already-translated display text for this event. */
note: string; note: string;
orderNumber?: string; orderNumber?: string;
actor?: string;
} }
/** Reusable vertical event timeline - used on both the order detail page and the customer activity tab. */ /** Reusable vertical event timeline - used on both the order detail page and the customer activity tab. */

View File

@@ -33,6 +33,7 @@ export interface AdminOrderTimelineEntry {
status: AdminOrderStatus; status: AdminOrderStatus;
timestamp: string; timestamp: string;
eventKey: AdminOrderTimelineEventKey; eventKey: AdminOrderTimelineEventKey;
actor: string;
} }
export interface AdminOrder { export interface AdminOrder {

View File

@@ -54,6 +54,7 @@ export class AdminOrderDetailPageComponent {
note: this.translate.t('adminOrders.timelineEvent.' + entry.eventKey, { note: this.translate.t('adminOrders.timelineEvent.' + entry.eventKey, {
status: this.translate.t('adminOrders.status.' + entry.status), status: this.translate.t('adminOrders.status.' + entry.status),
}), }),
actor: entry.actor,
})); }));
}); });

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus } from '../models/admin-order.model'; import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus } from '../models/admin-order.model';
import { AdminOrdersGateway } from './admin-orders-gateway.interface'; import { AdminOrdersGateway } from './admin-orders-gateway.interface';
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
const STATUSES: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded']; const STATUSES: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
const CUSTOMER_NAMES = ['Anna Petrova', 'Karen Sargsyan', 'Ivan Ivanov', 'Mariam Grigoryan', 'Sergey Volkov', 'Lilit Hakobyan']; const CUSTOMER_NAMES = ['Anna Petrova', 'Karen Sargsyan', 'Ivan Ivanov', 'Mariam Grigoryan', 'Sergey Volkov', 'Lilit Hakobyan'];
@@ -10,8 +11,13 @@ const SEED_COUNT = 24;
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AdminOrdersLocalGateway implements AdminOrdersGateway { export class AdminOrdersLocalGateway implements AdminOrdersGateway {
private readonly adminAuth = inject(AdminAuthService);
private cache: AdminOrder[] | null = null; private cache: AdminOrder[] | null = null;
private get currentActor(): string {
return this.adminAuth.displayName() ?? 'admin';
}
loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult> { loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult> {
const all = this.ensureData(); const all = this.ensureData();
const filtered = all const filtered = all
@@ -36,7 +42,7 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
...order, ...order,
status, status,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), eventKey: 'statusChanged' as const }], timeline: [...order.timeline, { status, timestamp: new Date().toISOString(), eventKey: 'statusChanged' as const, actor: this.currentActor }],
})); }));
} }
@@ -45,7 +51,7 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
...order, ...order,
payment: { ...order.payment, status: 'refund_requested' }, payment: { ...order.payment, status: 'refund_requested' },
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
timeline: [...order.timeline, { status: order.status, timestamp: new Date().toISOString(), eventKey: 'refundRequested' as const }], timeline: [...order.timeline, { status: order.status, timestamp: new Date().toISOString(), eventKey: 'refundRequested' as const, actor: this.currentActor }],
})); }));
} }
@@ -122,8 +128,8 @@ export class AdminOrdersLocalGateway implements AdminOrdersGateway {
notes: '', notes: '',
internalNotes: '', internalNotes: '',
timeline: [ timeline: [
{ status: 'pending', timestamp: createdAt, eventKey: 'created' }, { status: 'pending', timestamp: createdAt, eventKey: 'created', actor: 'system' },
...(status !== 'pending' ? [{ status, timestamp: createdAt, eventKey: 'statusChanged' as const }] : []), ...(status !== 'pending' ? [{ status, timestamp: createdAt, eventKey: 'statusChanged' as const, actor: 'system' }] : []),
], ],
archived: false, archived: false,
createdAt, createdAt,