Add Didar gap features: lost-reason reports, audit logs, tags, card assignees, and saved-views API.

Ignore Graphify local artifacts and expose new modules in superadmin.
This commit is contained in:
2026-09-26 14:59:48 +03:30
parent 35a618d0af
commit 6463275c3d
30 changed files with 1406 additions and 21 deletions
+20
View File
@@ -57,6 +57,11 @@ export class CreateProjectCardDto {
@MaxLength(4000)
notes?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
@@ -76,6 +81,11 @@ export class CreateProjectCardDto {
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
ownerId?: string | null;
@IsOptional()
@IsArray()
@IsString({ each: true })
assigneeIds?: string[];
}
export class UpdateProjectCardDto {
@@ -90,6 +100,11 @@ export class UpdateProjectCardDto {
@MaxLength(4000)
notes?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@IsOptional()
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
@@ -109,6 +124,11 @@ export class UpdateProjectCardDto {
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
ownerId?: string | null;
@IsOptional()
@IsArray()
@IsString({ each: true })
assigneeIds?: string[];
}
export class MoveProjectCardDto {
@@ -19,6 +19,7 @@ import {
ownerWhere,
resolveOwnerId,
} from '../common/record-scope';
import { normalizeTags } from '../common/tags';
const DEFAULT_STAGES = ['صف', 'در حال انجام', 'بازبینی', 'انجام‌شده'] as const;
@@ -27,6 +28,7 @@ const cardInclude = {
company: { select: { id: true, name: true } },
deal: { select: { id: true, title: true } },
owner: { select: { id: true, fullName: true } },
assignees: { select: { id: true, fullName: true } },
} satisfies Prisma.ProjectCardInclude;
type CardActor = Pick<Actor, 'userId' | 'role'>;
@@ -192,6 +194,7 @@ export class ProjectBoardsService {
_max: { position: true },
});
const assigneeIds = [...new Set((dto.assigneeIds ?? []).map((id) => id.trim()).filter(Boolean))];
const created = await this.prisma.projectCard.create({
data: {
organizationId,
@@ -199,11 +202,15 @@ export class ProjectBoardsService {
stageId: stage.id,
title: dto.title.trim(),
notes: emptyToNull(dto.notes),
tags: normalizeTags(dto.tags),
personId: emptyToNull(dto.personId),
companyId: emptyToNull(dto.companyId),
dealId: emptyToNull(dto.dealId),
ownerId: resolveOwnerId(actor, dto.ownerId),
position: (maxPos._max.position ?? -1) + 1,
...(assigneeIds.length
? { assignees: { connect: assigneeIds.map((id) => ({ id })) } }
: {}),
},
include: cardInclude,
});
@@ -234,6 +241,7 @@ export class ProjectBoardsService {
const data: Prisma.ProjectCardUncheckedUpdateInput = {};
if (dto.title !== undefined) data.title = dto.title.trim();
if ('notes' in dto) data.notes = emptyToNull(dto.notes ?? null);
if ('tags' in dto) data.tags = normalizeTags(dto.tags);
if ('personId' in dto) data.personId = emptyToNull(dto.personId ?? null);
if ('companyId' in dto) data.companyId = emptyToNull(dto.companyId ?? null);
if ('dealId' in dto) data.dealId = emptyToNull(dto.dealId ?? null);
@@ -246,7 +254,18 @@ export class ProjectBoardsService {
const row = await this.prisma.projectCard.update({
where: { id },
data,
data: {
...data,
...(dto.assigneeIds
? {
assignees: {
set: [...new Set(dto.assigneeIds.map((x) => x.trim()).filter(Boolean))].map(
(uid) => ({ id: uid }),
),
},
}
: {}),
},
include: cardInclude,
});
return this.toCardDto(row);
@@ -411,6 +430,7 @@ export class ProjectBoardsService {
id: string;
title: string;
notes: string | null;
tags?: string[];
position: number;
boardId: string;
stageId: string;
@@ -420,11 +440,13 @@ export class ProjectBoardsService {
company: { id: string; name: string } | null;
deal: { id: string; title: string } | null;
owner: { id: string; fullName: string } | null;
assignees?: Array<{ id: string; fullName: string }>;
}): ProjectCardDto {
return {
id: row.id,
title: row.title,
notes: row.notes,
tags: row.tags ?? [],
position: row.position,
boardId: row.boardId,
stageId: row.stageId,
@@ -437,6 +459,7 @@ export class ProjectBoardsService {
company: row.company,
deal: row.deal,
owner: row.owner,
assignees: row.assignees ?? [],
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};