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:
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user