feat: enhance booking and audit log functionalities

- Implemented read-only locking for customer-requested container sizes and billing currency in the GlCreateBookingForm component.
- Added functionality to lock partner quantities based on shipment requests in the ConsolidationPartnerPanel.
- Introduced a new Leave action in the LogPassYardWorkModal to unassign bookings from trains.
- Enhanced the AuditLogsPage to support filtering by action and added a Go button for direct navigation to entity detail pages.
- Updated WagonCancellationsPage to handle odd-20ft credits requiring partner selection during rebooking.
- Improved TrainScheduleV2DetailPage to allow manual loading of cargo and display warnings for unassigned bookings.
- Added a new reference field to the audit logs for better searchability and tracking of actions.
- Created a migration to add the reference column to the audit logs table and established an index for efficient querying.
- Defined a registry for audit reference sources to streamline the retrieval of human identifiers for various entities.
This commit is contained in:
Marshal
2026-08-24 23:49:20 +00:00
parent 2a107e8ba3
commit d5a5085d6d
28 changed files with 1356 additions and 86 deletions

View File

@@ -4,6 +4,10 @@ import { PaginatedResponse } from '@edr/types';
import { AuditLog } from './entities/audit-log.entity';
import { AuditLogRepository } from './audit-log.repository';
import { AuditLogQueryDto } from './dto/audit-log-query.dto';
import {
AUDIT_REFERENCE_SOURCES,
UUID_PATTERN,
} from './audit-reference.registry';
import {
buildPaginationMeta,
normalizePagination,
@@ -25,6 +29,7 @@ export class AuditService {
*/
async record(entry: Partial<AuditLog>): Promise<void> {
try {
entry.reference = await this.resolveReference(entry.type, entry.resourceId);
await this.auditLogRepository.record(entry);
} catch (error) {
this.logger.error(
@@ -35,6 +40,34 @@ export class AuditService {
}
}
/**
* Best-effort human identifier (booking reference, train number, …) for the
* entity the action touched — one primary-key lookup against the table
* registered for the type. Always returns a string: '' when the type has no
* registered source, the id isn't a uuid (template codes), the row is gone,
* or the lookup itself fails. A missing reference must never cost the audit
* row, so failures degrade to '' rather than throwing.
*/
private async resolveReference(
type: string | undefined,
resourceId: string | null | undefined,
): Promise<string> {
const source = type ? AUDIT_REFERENCE_SOURCES[type] : undefined;
if (!source || !resourceId || !UUID_PATTERN.test(resourceId)) return '';
try {
const reference = await this.auditLogRepository.lookupReference(source, resourceId);
return reference?.slice(0, 64) ?? '';
} catch (error) {
this.logger.warn(
`Reference lookup failed for ${type} ${resourceId}: ${
error instanceof Error ? error.message : String(error)
}`,
);
return '';
}
}
/** Paginated, filtered audit history, newest first. */
async search(query: AuditLogQueryDto): Promise<PaginatedResponse<AuditLog>> {
const { page, pageSize, skip, take } = normalizePagination(query);
@@ -53,6 +86,10 @@ export class AuditService {
userId: query.userId,
method: query.method,
resourceId: query.resourceId,
reference: query.reference,
userName: query.userName,
title: query.title,
q: query.q,
isSuccess:
query.isSuccess === undefined ? undefined : query.isSuccess === 'true',
from,
@@ -68,4 +105,9 @@ export class AuditService {
async listTypes(): Promise<string[]> {
return this.auditLogRepository.distinctTypes();
}
/** Distinct action titles, for the action filter dropdown. */
async listActions(): Promise<string[]> {
return this.auditLogRepository.distinctTitles();
}
}