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
@@ -0,0 +1,84 @@
-- AlterTable
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "Activity" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "ProjectCard" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- CreateTable
CREATE TABLE IF NOT EXISTS "AuditLog" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"actorUserId" TEXT,
"action" TEXT NOT NULL,
"method" TEXT NOT NULL,
"path" TEXT NOT NULL,
"entityType" TEXT,
"entityId" TEXT,
"summary" TEXT NOT NULL,
"meta" JSONB,
"ip" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE IF NOT EXISTS "SavedView" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"ownerUserId" TEXT,
"name" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"filters" JSONB NOT NULL DEFAULT '{}',
"isShared" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SavedView_pkey" PRIMARY KEY ("id")
);
-- CreateTable (implicit many-to-many ProjectCard <-> User assignees)
CREATE TABLE IF NOT EXISTS "_ProjectCardAssignees" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_createdAt_idx" ON "AuditLog"("organizationId", "createdAt");
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_actorUserId_createdAt_idx" ON "AuditLog"("organizationId", "actorUserId", "createdAt");
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_entityType_createdAt_idx" ON "AuditLog"("organizationId", "entityType", "createdAt");
CREATE INDEX IF NOT EXISTS "SavedView_organizationId_scope_idx" ON "SavedView"("organizationId", "scope");
CREATE INDEX IF NOT EXISTS "SavedView_organizationId_ownerUserId_idx" ON "SavedView"("organizationId", "ownerUserId");
CREATE UNIQUE INDEX IF NOT EXISTS "_ProjectCardAssignees_AB_unique" ON "_ProjectCardAssignees"("A", "B");
CREATE INDEX IF NOT EXISTS "_ProjectCardAssignees_B_index" ON "_ProjectCardAssignees"("B");
-- AddForeignKey
DO $$ BEGIN
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "_ProjectCardAssignees" ADD CONSTRAINT "_ProjectCardAssignees_A_fkey" FOREIGN KEY ("A") REFERENCES "ProjectCard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "_ProjectCardAssignees" ADD CONSTRAINT "_ProjectCardAssignees_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+51
View File
@@ -192,6 +192,8 @@ model Organization {
partyOpenings PartyOpeningBalance[]
commissionEntries CommissionEntry[]
cheques Cheque[]
auditLogs AuditLog[]
savedViews SavedView[]
}
model User {
@@ -233,6 +235,9 @@ model User {
confirmedMemories MemoryItem[] @relation("MemoryConfirmedBy")
memberships Membership[]
billingAccount BillingAccount?
auditLogs AuditLog[] @relation("AuditLogActor")
savedViews SavedView[] @relation("SavedViewOwner")
assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -329,6 +334,7 @@ model Company {
website String?
address String?
notes String?
tags String[] @default([])
customFields Json @default("{}")
people Person[]
deals Deal[]
@@ -368,6 +374,7 @@ model Person {
referrer String?
notes String?
mood PersonMood @default(newcomer)
tags String[] @default([])
customFields Json @default("{}")
loyaltyPoints Int @default(0)
loyaltyTierId String?
@@ -493,6 +500,7 @@ model Activity {
/// پیامک به موبایل مسئول هنگام رسیدن یادآور
reminderNotifySms Boolean @default(false)
completedAt DateTime?
tags String[] @default([])
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
@@ -1067,6 +1075,7 @@ model ProjectCard {
stage ProjectBoardStage @relation(fields: [stageId], references: [id], onDelete: Restrict)
title String
notes String?
tags String[] @default([])
position Int @default(0)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
@@ -1076,6 +1085,7 @@ model ProjectCard {
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("ProjectCardOwner", fields: [ownerId], references: [id], onDelete: SetNull)
assignees User[] @relation("ProjectCardAssignees")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -1546,3 +1556,44 @@ model PartyOpeningBalance {
@@unique([organizationId, personId])
@@index([personId])
}
/// لاگ فعالیت کاربران روی منابع سازمان
model AuditLog {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
actorUserId String?
actor User? @relation("AuditLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
action String
method String
path String
entityType String?
entityId String?
summary String
meta Json?
ip String?
createdAt DateTime @default(now())
@@index([organizationId, createdAt])
@@index([organizationId, actorUserId, createdAt])
@@index([organizationId, entityType, createdAt])
}
/// فیلترهای ذخیره‌شده برای لیست‌ها و کاریزها
model SavedView {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ownerUserId String?
owner User? @relation("SavedViewOwner", fields: [ownerUserId], references: [id], onDelete: SetNull)
name String
/// deals | people | companies | activities | project_cards
scope String
filters Json @default("{}")
isShared Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, scope])
@@index([organizationId, ownerUserId])
}