feat: show the full work history and load every lead stage on the board
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
title: 'عنوان',
|
||||
name: 'نام',
|
||||
firstName: 'نام',
|
||||
lastName: 'نام خانوادگی',
|
||||
fullName: 'نام',
|
||||
description: 'توضیحات',
|
||||
body: 'متن',
|
||||
mobile: 'موبایل',
|
||||
phone: 'تلفن',
|
||||
email: 'ایمیل',
|
||||
status: 'وضعیت',
|
||||
stageId: 'مرحله',
|
||||
priority: 'اولویت',
|
||||
dueAt: 'موعد',
|
||||
amount: 'مبلغ',
|
||||
subject: 'موضوع',
|
||||
type: 'نوع',
|
||||
city: 'شهر',
|
||||
address: 'آدرس',
|
||||
notes: 'یادداشت',
|
||||
contactName: 'نام مخاطب',
|
||||
};
|
||||
|
||||
const SECRET = /password|token|secret|otp|pin/i;
|
||||
|
||||
function textOf(value: unknown) {
|
||||
if (typeof value === 'string') return value.trim().replace(/\s+/g, ' ');
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
return '';
|
||||
}
|
||||
|
||||
function labelOf(source: unknown) {
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) return '';
|
||||
const body = source as Record<string, unknown>;
|
||||
const person = [textOf(body.firstName), textOf(body.lastName)].filter(Boolean).join(' ');
|
||||
return (
|
||||
person ||
|
||||
textOf(body.title) ||
|
||||
textOf(body.name) ||
|
||||
textOf(body.subject) ||
|
||||
textOf(body.fullName)
|
||||
).slice(0, 120);
|
||||
}
|
||||
|
||||
function changesOf(source: unknown) {
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) return '';
|
||||
const bits: string[] = [];
|
||||
for (const [key, value] of Object.entries(source as Record<string, unknown>)) {
|
||||
if (SECRET.test(key)) continue;
|
||||
const name = FIELD_LABELS[key];
|
||||
if (!name) continue;
|
||||
if (key.endsWith('Id')) {
|
||||
bits.push(name);
|
||||
} else {
|
||||
const text = textOf(value).slice(0, 40);
|
||||
if (!text) continue;
|
||||
bits.push(`${name}: ${text}`);
|
||||
}
|
||||
if (bits.length >= 4) break;
|
||||
}
|
||||
return bits.join('، ');
|
||||
}
|
||||
|
||||
export function auditStory(
|
||||
method: string,
|
||||
requestBody: unknown,
|
||||
responseBody: unknown,
|
||||
) {
|
||||
const label = labelOf(responseBody) || labelOf(requestBody);
|
||||
const detail = method === 'DELETE' ? '' : changesOf(requestBody);
|
||||
const summary = [label, detail].filter(Boolean).join(' — ').slice(0, 500);
|
||||
return {
|
||||
label,
|
||||
detail: detail.slice(0, 240),
|
||||
summary,
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,10 @@ class ListAuditQueryDto {
|
||||
@IsString()
|
||||
entityType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
action?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@@ -52,6 +56,7 @@ export class AuditLogsController {
|
||||
q: query.q,
|
||||
actorUserId: query.actorUserId,
|
||||
entityType: query.entityType,
|
||||
action: query.action,
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { auditStory } from './audit-detail';
|
||||
import { AuditLogsService } from './audit-logs.service';
|
||||
|
||||
const SKIP_PREFIXES = [
|
||||
@@ -73,7 +74,6 @@ export class AuditLogsInterceptor implements NestInterceptor {
|
||||
const actorUserId = req.user?.userId ?? req.user?.id ?? null;
|
||||
const { entityType, entityId } = inferEntity(path);
|
||||
const action = actionLabel(method, path);
|
||||
const summary = `${action} ${entityType ?? 'منبع'} از مسیر ${path.split('?')[0]}`;
|
||||
const ip =
|
||||
(req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() ||
|
||||
req.ip ||
|
||||
@@ -81,7 +81,11 @@ export class AuditLogsInterceptor implements NestInterceptor {
|
||||
|
||||
return next.handle().pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
next: (response) => {
|
||||
const story = auditStory(method, req.body, response);
|
||||
const summary =
|
||||
story.summary ||
|
||||
`${action} ${entityType ?? 'منبع'} از مسیر ${path.split('?')[0]}`;
|
||||
void this.audit.write({
|
||||
organizationId,
|
||||
actorUserId,
|
||||
@@ -91,6 +95,7 @@ export class AuditLogsInterceptor implements NestInterceptor {
|
||||
entityType,
|
||||
entityId,
|
||||
summary,
|
||||
meta: story.label || story.detail ? { label: story.label, detail: story.detail } : null,
|
||||
ip,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AuditLogsService {
|
||||
q?: string;
|
||||
actorUserId?: string;
|
||||
entityType?: string;
|
||||
action?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
},
|
||||
@@ -61,6 +62,7 @@ export class AuditLogsService {
|
||||
organizationId,
|
||||
...(opts.actorUserId ? { actorUserId: opts.actorUserId } : {}),
|
||||
...(opts.entityType ? { entityType: opts.entityType } : {}),
|
||||
...(opts.action ? { action: opts.action } : {}),
|
||||
};
|
||||
const search = buildSearchWhere<Prisma.AuditLogWhereInput>(opts.q, (t) => [
|
||||
{ summary: ci(t) },
|
||||
@@ -97,10 +99,17 @@ export class AuditLogsService {
|
||||
entityType: string | null;
|
||||
entityId: string | null;
|
||||
summary: string;
|
||||
meta: Prisma.JsonValue | null;
|
||||
ip: string | null;
|
||||
createdAt: Date;
|
||||
actor: { id: string; fullName: string } | null;
|
||||
}): AuditLogDto {
|
||||
const meta =
|
||||
row.meta && typeof row.meta === 'object' && !Array.isArray(row.meta)
|
||||
? (row.meta as { label?: unknown; detail?: unknown })
|
||||
: null;
|
||||
const label = typeof meta?.label === 'string' ? meta.label : null;
|
||||
const detail = typeof meta?.detail === 'string' ? meta.detail : null;
|
||||
return {
|
||||
id: row.id,
|
||||
action: row.action,
|
||||
@@ -109,6 +118,8 @@ export class AuditLogsService {
|
||||
entityType: row.entityType,
|
||||
entityId: row.entityId,
|
||||
summary: row.summary,
|
||||
label,
|
||||
detail,
|
||||
actor: row.actor,
|
||||
ip: row.ip,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user