doc compare
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import type { SupportLogEntryDto } from "@mrp/shared";
|
||||
import type { SupportLogEntryDto, SupportLogFiltersDto, SupportLogListDto, SupportLogSummaryDto } from "@mrp/shared";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const SUPPORT_LOG_LIMIT = 200;
|
||||
const SUPPORT_LOG_LIMIT = 500;
|
||||
const SUPPORT_LOG_RETENTION_DAYS = 14;
|
||||
|
||||
const supportLogs: SupportLogEntryDto[] = [];
|
||||
|
||||
@@ -17,12 +18,89 @@ function serializeContext(context?: Record<string, unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRetentionCutoff(now = new Date()) {
|
||||
return new Date(now.getTime() - SUPPORT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
function pruneSupportLogs(now = new Date()) {
|
||||
const cutoff = getRetentionCutoff(now).getTime();
|
||||
const retained = supportLogs.filter((entry) => new Date(entry.createdAt).getTime() >= cutoff);
|
||||
supportLogs.length = 0;
|
||||
supportLogs.push(...retained.slice(0, SUPPORT_LOG_LIMIT));
|
||||
}
|
||||
|
||||
function normalizeFilters(filters?: SupportLogFiltersDto): SupportLogFiltersDto {
|
||||
return {
|
||||
level: filters?.level,
|
||||
source: filters?.source?.trim() || undefined,
|
||||
query: filters?.query?.trim() || undefined,
|
||||
start: filters?.start,
|
||||
end: filters?.end,
|
||||
limit: filters?.limit,
|
||||
};
|
||||
}
|
||||
|
||||
function filterSupportLogs(filters?: SupportLogFiltersDto) {
|
||||
pruneSupportLogs();
|
||||
|
||||
const normalized = normalizeFilters(filters);
|
||||
const startMs = normalized.start ? new Date(normalized.start).getTime() : null;
|
||||
const endMs = normalized.end ? new Date(normalized.end).getTime() : null;
|
||||
const query = normalized.query?.toLowerCase();
|
||||
const limit = Math.max(0, Math.min(normalized.limit ?? 100, SUPPORT_LOG_LIMIT));
|
||||
|
||||
return supportLogs
|
||||
.filter((entry) => {
|
||||
if (normalized.level && entry.level !== normalized.level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.source && entry.source !== normalized.source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const createdAtMs = new Date(entry.createdAt).getTime();
|
||||
if (startMs != null && createdAtMs < startMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (endMs != null && createdAtMs > endMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [entry.source, entry.message, entry.contextJson].some((value) => value.toLowerCase().includes(query));
|
||||
})
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function buildSupportLogSummary(entries: SupportLogEntryDto[], totalCount: number, availableSources: string[]): SupportLogSummaryDto {
|
||||
return {
|
||||
totalCount,
|
||||
filteredCount: entries.length,
|
||||
sourceCount: availableSources.length,
|
||||
retentionDays: SUPPORT_LOG_RETENTION_DAYS,
|
||||
oldestEntryAt: entries.length > 0 ? entries[entries.length - 1]?.createdAt ?? null : null,
|
||||
newestEntryAt: entries.length > 0 ? entries[0]?.createdAt ?? null : null,
|
||||
levelCounts: {
|
||||
INFO: entries.filter((entry) => entry.level === "INFO").length,
|
||||
WARN: entries.filter((entry) => entry.level === "WARN").length,
|
||||
ERROR: entries.filter((entry) => entry.level === "ERROR").length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function recordSupportLog(entry: {
|
||||
level: SupportLogEntryDto["level"];
|
||||
source: string;
|
||||
message: string;
|
||||
context?: Record<string, unknown>;
|
||||
}) {
|
||||
pruneSupportLogs();
|
||||
|
||||
supportLogs.unshift({
|
||||
id: randomUUID(),
|
||||
level: entry.level,
|
||||
@@ -37,10 +115,25 @@ export function recordSupportLog(entry: {
|
||||
}
|
||||
}
|
||||
|
||||
export function listSupportLogs(limit = 50) {
|
||||
return supportLogs.slice(0, Math.max(0, limit));
|
||||
export function listSupportLogs(filters?: SupportLogFiltersDto): SupportLogListDto {
|
||||
pruneSupportLogs();
|
||||
const normalized = normalizeFilters(filters);
|
||||
const availableSources = [...new Set(supportLogs.map((entry) => entry.source))].sort();
|
||||
const entries = filterSupportLogs(normalized);
|
||||
|
||||
return {
|
||||
entries,
|
||||
summary: buildSupportLogSummary(entries, supportLogs.length, availableSources),
|
||||
availableSources,
|
||||
filters: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSupportLogCount() {
|
||||
pruneSupportLogs();
|
||||
return supportLogs.length;
|
||||
}
|
||||
|
||||
export function getSupportLogRetentionDays() {
|
||||
return SUPPORT_LOG_RETENTION_DAYS;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ const userSchema = z.object({
|
||||
password: z.string().min(8).nullable(),
|
||||
});
|
||||
|
||||
const supportLogQuerySchema = z.object({
|
||||
level: z.enum(["INFO", "WARN", "ERROR"]).optional(),
|
||||
source: z.string().trim().min(1).optional(),
|
||||
query: z.string().trim().optional(),
|
||||
start: z.string().datetime().optional(),
|
||||
end: z.string().datetime().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(500).optional(),
|
||||
});
|
||||
|
||||
function getRouteParam(value: unknown) {
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
@@ -50,11 +59,21 @@ adminRouter.get("/backup-guidance", requirePermissions([permissions.adminManage]
|
||||
});
|
||||
|
||||
adminRouter.get("/support-snapshot", requirePermissions([permissions.adminManage]), async (_request, response) => {
|
||||
return ok(response, await getSupportSnapshot());
|
||||
const parsed = supportLogQuerySchema.safeParse(_request.query);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Support snapshot filters are invalid.");
|
||||
}
|
||||
|
||||
return ok(response, await getSupportSnapshot(parsed.data));
|
||||
});
|
||||
|
||||
adminRouter.get("/support-logs", requirePermissions([permissions.adminManage]), async (_request, response) => {
|
||||
return ok(response, getSupportLogs());
|
||||
adminRouter.get("/support-logs", requirePermissions([permissions.adminManage]), async (request, response) => {
|
||||
const parsed = supportLogQuerySchema.safeParse(request.query);
|
||||
if (!parsed.success) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Support log filters are invalid.");
|
||||
}
|
||||
|
||||
return ok(response, getSupportLogs(parsed.data));
|
||||
});
|
||||
|
||||
adminRouter.get("/permissions", requirePermissions([permissions.adminManage]), async (_request, response) => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
SupportSnapshotDto,
|
||||
AuditEventDto,
|
||||
SupportLogEntryDto,
|
||||
SupportLogFiltersDto,
|
||||
SupportLogListDto,
|
||||
} from "@mrp/shared";
|
||||
|
||||
import { env } from "../../config/env.js";
|
||||
@@ -18,7 +20,7 @@ import { logAuditEvent } from "../../lib/audit.js";
|
||||
import { hashPassword } from "../../lib/password.js";
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
import { getLatestStartupReport } from "../../lib/startup-state.js";
|
||||
import { getSupportLogCount, listSupportLogs } from "../../lib/support-log.js";
|
||||
import { getSupportLogCount, getSupportLogRetentionDays, listSupportLogs } from "../../lib/support-log.js";
|
||||
|
||||
function mapAuditEvent(record: {
|
||||
id: string;
|
||||
@@ -48,13 +50,15 @@ function mapAuditEvent(record: {
|
||||
}
|
||||
|
||||
function mapSupportLogEntry(record: SupportLogEntryDto): SupportLogEntryDto {
|
||||
return { ...record };
|
||||
}
|
||||
|
||||
function mapSupportLogList(record: SupportLogListDto): SupportLogListDto {
|
||||
return {
|
||||
id: record.id,
|
||||
level: record.level,
|
||||
source: record.source,
|
||||
message: record.message,
|
||||
contextJson: record.contextJson,
|
||||
createdAt: record.createdAt,
|
||||
entries: record.entries.map(mapSupportLogEntry),
|
||||
summary: record.summary,
|
||||
availableSources: record.availableSources,
|
||||
filters: record.filters,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -657,7 +661,7 @@ export async function updateAdminUser(userId: string, payload: AdminUserInput, a
|
||||
|
||||
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||
const startupReport = getLatestStartupReport();
|
||||
const recentSupportLogs = listSupportLogs(50);
|
||||
const recentSupportLogs = listSupportLogs({ limit: 50 });
|
||||
const now = new Date();
|
||||
const reviewSessions = await listAdminAuthSessions();
|
||||
const [
|
||||
@@ -748,7 +752,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||
supportLogCount: getSupportLogCount(),
|
||||
startup: startupReport,
|
||||
recentAuditEvents: recentAuditEvents.map(mapAuditEvent),
|
||||
recentSupportLogs: recentSupportLogs.map(mapSupportLogEntry),
|
||||
recentSupportLogs: recentSupportLogs.entries.map(mapSupportLogEntry),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -863,9 +867,10 @@ export function getBackupGuidance(): BackupGuidanceDto {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSupportSnapshot(): Promise<SupportSnapshotDto> {
|
||||
export async function getSupportSnapshot(filters?: SupportLogFiltersDto): Promise<SupportSnapshotDto> {
|
||||
const diagnostics = await getAdminDiagnostics();
|
||||
const backupGuidance = getBackupGuidance();
|
||||
const supportLogs = listSupportLogs({ limit: 200, ...filters });
|
||||
const [users, roles] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where: { isActive: true },
|
||||
@@ -882,10 +887,16 @@ export async function getSupportSnapshot(): Promise<SupportSnapshotDto> {
|
||||
roleCount: roles,
|
||||
activeUserEmails: users.map((user) => user.email),
|
||||
backupGuidance,
|
||||
recentSupportLogs: diagnostics.recentSupportLogs,
|
||||
supportLogs: mapSupportLogList(supportLogs),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSupportLogs() {
|
||||
return listSupportLogs(100).map(mapSupportLogEntry);
|
||||
export function getSupportLogs(filters?: SupportLogFiltersDto) {
|
||||
return mapSupportLogList(listSupportLogs(filters));
|
||||
}
|
||||
|
||||
export function getSupportLogRetentionPolicy() {
|
||||
return {
|
||||
retentionDays: getSupportLogRetentionDays(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createPurchaseReceipt,
|
||||
createPurchaseOrder,
|
||||
getPurchaseOrderById,
|
||||
listPurchaseOrderRevisions,
|
||||
listPurchaseOrders,
|
||||
listPurchaseVendorOptions,
|
||||
updatePurchaseOrder,
|
||||
@@ -33,6 +34,7 @@ const purchaseOrderSchema = z.object({
|
||||
taxPercent: z.number().min(0).max(100),
|
||||
freightAmount: z.number().nonnegative(),
|
||||
notes: z.string(),
|
||||
revisionReason: z.string().optional(),
|
||||
lines: z.array(purchaseLineSchema),
|
||||
});
|
||||
|
||||
@@ -92,6 +94,20 @@ purchasingRouter.get("/orders/:orderId", requirePermissions(["purchasing.read"])
|
||||
return ok(response, order);
|
||||
});
|
||||
|
||||
purchasingRouter.get("/orders/:orderId/revisions", requirePermissions(["purchasing.read"]), async (request, response) => {
|
||||
const orderId = getRouteParam(request.params.orderId);
|
||||
if (!orderId) {
|
||||
return fail(response, 400, "INVALID_INPUT", "Purchase order id is invalid.");
|
||||
}
|
||||
|
||||
const order = await getPurchaseOrderById(orderId);
|
||||
if (!order) {
|
||||
return fail(response, 404, "PURCHASE_ORDER_NOT_FOUND", "Purchase order was not found.");
|
||||
}
|
||||
|
||||
return ok(response, await listPurchaseOrderRevisions(orderId));
|
||||
});
|
||||
|
||||
purchasingRouter.post("/orders", requirePermissions(["purchasing.write"]), async (request, response) => {
|
||||
const parsed = purchaseOrderSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { PurchaseLineInput, PurchaseOrderDetailDto, PurchaseOrderInput, PurchaseOrderStatus, PurchaseOrderSummaryDto, PurchaseVendorOptionDto } from "@mrp/shared";
|
||||
import type {
|
||||
PurchaseLineInput,
|
||||
PurchaseOrderDetailDto,
|
||||
PurchaseOrderInput,
|
||||
PurchaseOrderRevisionDto,
|
||||
PurchaseOrderRevisionSnapshotDto,
|
||||
PurchaseOrderStatus,
|
||||
PurchaseOrderSummaryDto,
|
||||
PurchaseVendorOptionDto,
|
||||
} from "@mrp/shared";
|
||||
import type { PurchaseReceiptDto, PurchaseReceiptInput } from "@mrp/shared/dist/purchasing/types.js";
|
||||
|
||||
import { logAuditEvent } from "../../lib/audit.js";
|
||||
@@ -102,6 +111,18 @@ type PurchaseReceiptRecord = {
|
||||
lines: PurchaseReceiptLineRecord[];
|
||||
};
|
||||
|
||||
type PurchaseOrderRevisionRecord = {
|
||||
id: string;
|
||||
revisionNumber: number;
|
||||
reason: string;
|
||||
snapshot: string;
|
||||
createdAt: Date;
|
||||
createdBy: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PurchaseOrderRecord = {
|
||||
id: string;
|
||||
documentNumber: string;
|
||||
@@ -121,6 +142,7 @@ type PurchaseOrderRecord = {
|
||||
};
|
||||
lines: PurchaseLineRecord[];
|
||||
receipts: PurchaseReceiptRecord[];
|
||||
revisions: PurchaseOrderRevisionRecord[];
|
||||
};
|
||||
|
||||
function roundMoney(value: number) {
|
||||
@@ -147,6 +169,10 @@ function getCreatedByName(createdBy: PurchaseReceiptRecord["createdBy"]) {
|
||||
return createdBy ? `${createdBy.firstName} ${createdBy.lastName}`.trim() : "System";
|
||||
}
|
||||
|
||||
function getUserDisplayName(user: { firstName: string; lastName: string } | null) {
|
||||
return user ? `${user.firstName} ${user.lastName}`.trim() : null;
|
||||
}
|
||||
|
||||
function mapPurchaseReceipt(record: PurchaseReceiptRecord, purchaseOrderId: string): PurchaseReceiptDto {
|
||||
const lines = record.lines.map((line: PurchaseReceiptLineRecord) => ({
|
||||
id: line.id,
|
||||
@@ -177,6 +203,21 @@ function mapPurchaseReceipt(record: PurchaseReceiptRecord, purchaseOrderId: stri
|
||||
};
|
||||
}
|
||||
|
||||
function parseRevisionSnapshot(snapshot: string): PurchaseOrderRevisionSnapshotDto {
|
||||
return JSON.parse(snapshot) as PurchaseOrderRevisionSnapshotDto;
|
||||
}
|
||||
|
||||
function mapPurchaseOrderRevision(record: PurchaseOrderRevisionRecord): PurchaseOrderRevisionDto {
|
||||
return {
|
||||
id: record.id,
|
||||
revisionNumber: record.revisionNumber,
|
||||
reason: record.reason,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
createdByName: getUserDisplayName(record.createdBy),
|
||||
snapshot: parseRevisionSnapshot(record.snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLines(lines: PurchaseLineInput[]) {
|
||||
return lines
|
||||
.map((line, index) => ({
|
||||
@@ -319,6 +360,10 @@ function mapPurchaseOrder(record: PurchaseOrderRecord): PurchaseOrderDetailDto {
|
||||
.slice()
|
||||
.sort((left, right) => right.receivedAt.getTime() - left.receivedAt.getTime())
|
||||
.map((receipt) => mapPurchaseReceipt(receipt, record.id));
|
||||
const revisions = record.revisions
|
||||
.slice()
|
||||
.sort((left, right) => right.revisionNumber - left.revisionNumber)
|
||||
.map(mapPurchaseOrderRevision);
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
@@ -341,9 +386,87 @@ function mapPurchaseOrder(record: PurchaseOrderRecord): PurchaseOrderDetailDto {
|
||||
lineCount: lines.length,
|
||||
lines,
|
||||
receipts,
|
||||
revisions,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPurchaseOrderRevisionSnapshot(document: PurchaseOrderDetailDto) {
|
||||
return JSON.stringify({
|
||||
documentNumber: document.documentNumber,
|
||||
vendorId: document.vendorId,
|
||||
vendorName: document.vendorName,
|
||||
status: document.status,
|
||||
issueDate: document.issueDate,
|
||||
taxPercent: document.taxPercent,
|
||||
taxAmount: document.taxAmount,
|
||||
freightAmount: document.freightAmount,
|
||||
subtotal: document.subtotal,
|
||||
total: document.total,
|
||||
notes: document.notes,
|
||||
paymentTerms: document.paymentTerms,
|
||||
currencyCode: document.currencyCode,
|
||||
lines: document.lines.map((line) => ({
|
||||
itemId: line.itemId,
|
||||
itemSku: line.itemSku,
|
||||
itemName: line.itemName,
|
||||
description: line.description,
|
||||
quantity: line.quantity,
|
||||
unitOfMeasure: line.unitOfMeasure,
|
||||
unitCost: line.unitCost,
|
||||
lineTotal: line.lineTotal,
|
||||
receivedQuantity: line.receivedQuantity,
|
||||
remainingQuantity: line.remainingQuantity,
|
||||
salesOrderId: line.salesOrderId,
|
||||
salesOrderLineId: line.salesOrderLineId,
|
||||
salesOrderNumber: line.salesOrderNumber,
|
||||
position: line.position,
|
||||
})),
|
||||
receipts: document.receipts.map((receipt) => ({
|
||||
id: receipt.id,
|
||||
receiptNumber: receipt.receiptNumber,
|
||||
purchaseOrderId: receipt.purchaseOrderId,
|
||||
receivedAt: receipt.receivedAt,
|
||||
notes: receipt.notes,
|
||||
createdAt: receipt.createdAt,
|
||||
createdByName: receipt.createdByName,
|
||||
warehouseId: receipt.warehouseId,
|
||||
warehouseCode: receipt.warehouseCode,
|
||||
warehouseName: receipt.warehouseName,
|
||||
locationId: receipt.locationId,
|
||||
locationCode: receipt.locationCode,
|
||||
locationName: receipt.locationName,
|
||||
totalQuantity: receipt.totalQuantity,
|
||||
lineCount: receipt.lineCount,
|
||||
lines: receipt.lines.map((line) => ({
|
||||
id: line.id,
|
||||
purchaseOrderLineId: line.purchaseOrderLineId,
|
||||
itemId: line.itemId,
|
||||
itemSku: line.itemSku,
|
||||
itemName: line.itemName,
|
||||
quantity: line.quantity,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function createPurchaseOrderRevision(documentId: string, detail: PurchaseOrderDetailDto, reason: string, actorId?: string | null) {
|
||||
const aggregate = await prisma.purchaseOrderRevision.aggregate({
|
||||
where: { purchaseOrderId: documentId },
|
||||
_max: { revisionNumber: true },
|
||||
});
|
||||
const nextRevisionNumber = (aggregate._max.revisionNumber ?? 0) + 1;
|
||||
|
||||
await prisma.purchaseOrderRevision.create({
|
||||
data: {
|
||||
purchaseOrderId: documentId,
|
||||
revisionNumber: nextRevisionNumber,
|
||||
reason,
|
||||
snapshot: buildPurchaseOrderRevisionSnapshot(detail),
|
||||
createdById: actorId ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function nextDocumentNumber() {
|
||||
const next = (await purchaseOrderModel.count()) + 1;
|
||||
return `PO-${String(next).padStart(5, "0")}`;
|
||||
@@ -423,6 +546,17 @@ const purchaseOrderInclude = Prisma.validator<Prisma.PurchaseOrderInclude>()({
|
||||
},
|
||||
orderBy: [{ receivedAt: "desc" }, { createdAt: "desc" }],
|
||||
},
|
||||
revisions: {
|
||||
include: {
|
||||
createdBy: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ revisionNumber: "desc" }],
|
||||
},
|
||||
});
|
||||
|
||||
function normalizeReceiptLines(lines: PurchaseReceiptInput["lines"]) {
|
||||
@@ -605,6 +739,23 @@ export async function getPurchaseOrderById(documentId: string) {
|
||||
return record ? mapPurchaseOrder(record as unknown as PurchaseOrderRecord) : null;
|
||||
}
|
||||
|
||||
export async function listPurchaseOrderRevisions(documentId: string) {
|
||||
const revisions = await prisma.purchaseOrderRevision.findMany({
|
||||
where: { purchaseOrderId: documentId },
|
||||
include: {
|
||||
createdBy: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ revisionNumber: "desc" }],
|
||||
});
|
||||
|
||||
return revisions.map((revision) => mapPurchaseOrderRevision(revision as PurchaseOrderRevisionRecord));
|
||||
}
|
||||
|
||||
export async function createPurchaseOrder(payload: PurchaseOrderInput, actorId?: string | null) {
|
||||
const validatedLines = await validateLines(payload.lines);
|
||||
if (!validatedLines.ok) {
|
||||
@@ -640,6 +791,7 @@ export async function createPurchaseOrder(payload: PurchaseOrderInput, actorId?:
|
||||
|
||||
const detail = await getPurchaseOrderById(created.id);
|
||||
if (detail) {
|
||||
await createPurchaseOrderRevision(created.id, detail, payload.revisionReason?.trim() || "Initial issue", actorId);
|
||||
await logAuditEvent({
|
||||
actorId,
|
||||
entityType: "purchase-order",
|
||||
@@ -700,6 +852,7 @@ export async function updatePurchaseOrder(documentId: string, payload: PurchaseO
|
||||
|
||||
const detail = await getPurchaseOrderById(documentId);
|
||||
if (detail) {
|
||||
await createPurchaseOrderRevision(documentId, detail, payload.revisionReason?.trim() || "Document edited", actorId);
|
||||
await logAuditEvent({
|
||||
actorId,
|
||||
entityType: "purchase-order",
|
||||
@@ -735,6 +888,7 @@ export async function updatePurchaseOrderStatus(documentId: string, status: Purc
|
||||
|
||||
const detail = await getPurchaseOrderById(documentId);
|
||||
if (detail) {
|
||||
await createPurchaseOrderRevision(documentId, detail, `Status changed to ${status}`, actorId);
|
||||
await logAuditEvent({
|
||||
actorId,
|
||||
entityType: "purchase-order",
|
||||
@@ -796,6 +950,7 @@ export async function createPurchaseReceipt(orderId: string, payload: PurchaseRe
|
||||
|
||||
const detail = await getPurchaseOrderById(orderId);
|
||||
if (detail) {
|
||||
await createPurchaseOrderRevision(orderId, detail, `Receipt posted on ${payload.receivedAt}`, createdById);
|
||||
await logAuditEvent({
|
||||
actorId: createdById,
|
||||
entityType: "purchase-order",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
SalesDocumentDetailDto,
|
||||
SalesDocumentInput,
|
||||
SalesDocumentRevisionDto,
|
||||
SalesDocumentRevisionSnapshotDto,
|
||||
SalesDocumentStatus,
|
||||
SalesDocumentSummaryDto,
|
||||
SalesDocumentType,
|
||||
@@ -66,6 +67,7 @@ type RevisionRecord = {
|
||||
id: string;
|
||||
revisionNumber: number;
|
||||
reason: string;
|
||||
snapshot: string;
|
||||
createdAt: Date;
|
||||
createdBy: {
|
||||
firstName: string;
|
||||
@@ -172,6 +174,10 @@ function getUserDisplayName(user: { firstName: string; lastName: string } | null
|
||||
return `${user.firstName} ${user.lastName}`.trim();
|
||||
}
|
||||
|
||||
function parseRevisionSnapshot(snapshot: string): SalesDocumentRevisionSnapshotDto {
|
||||
return JSON.parse(snapshot) as SalesDocumentRevisionSnapshotDto;
|
||||
}
|
||||
|
||||
function mapRevision(record: RevisionRecord): SalesDocumentRevisionDto {
|
||||
return {
|
||||
id: record.id,
|
||||
@@ -179,6 +185,7 @@ function mapRevision(record: RevisionRecord): SalesDocumentRevisionDto {
|
||||
reason: record.reason,
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
createdByName: getUserDisplayName(record.createdBy),
|
||||
snapshot: parseRevisionSnapshot(record.snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user