feat: add DISC profiles and a sales copilot as an org module

Help reps capture a customer's likely behavior style and get negotiation guidance, with the feature gated from super admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-18 21:04:26 +03:30
co-authored by Cursor
parent 96befd3867
commit 002f697dfe
27 changed files with 2838 additions and 26 deletions
@@ -0,0 +1,97 @@
-- AlterTable
ALTER TABLE "Person" ADD COLUMN "discLabel" TEXT,
ADD COLUMN "discReliable" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "discAssessedAt" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX "Person_organizationId_discLabel_idx" ON "Person"("organizationId", "discLabel");
-- CreateTable
CREATE TABLE "DiscAssessment" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"personId" TEXT NOT NULL,
"assessorId" TEXT NOT NULL,
"answers" JSONB NOT NULL,
"scoreD" INTEGER NOT NULL,
"scoreI" INTEGER NOT NULL,
"scoreS" INTEGER NOT NULL,
"scoreC" INTEGER NOT NULL,
"percentD" INTEGER NOT NULL,
"percentI" INTEGER NOT NULL,
"percentS" INTEGER NOT NULL,
"percentC" INTEGER NOT NULL,
"validCount" INTEGER NOT NULL,
"primaryTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"secondaryTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"label" TEXT NOT NULL,
"secondaryLabel" TEXT,
"reliable" BOOLEAN NOT NULL,
"confidence" TEXT NOT NULL,
"note" TEXT,
"status" TEXT NOT NULL DEFAULT 'confirmed',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DiscAssessment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SalesCopilotSession" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"personId" TEXT,
"leadId" TEXT,
"dealId" TEXT,
"userId" TEXT NOT NULL,
"topic" TEXT NOT NULL,
"goal" TEXT NOT NULL,
"customerStatement" TEXT NOT NULL,
"behaviorNotes" TEXT,
"analysis" JSONB NOT NULL,
"confirmedResponse" TEXT,
"nextAction" TEXT,
"followUpAt" TIMESTAMP(3),
"callOutcome" TEXT,
"agentNote" TEXT,
"status" TEXT NOT NULL,
"messageDraft" TEXT,
"activityId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SalesCopilotSession_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "DiscAssessment_organizationId_personId_createdAt_idx" ON "DiscAssessment"("organizationId", "personId", "createdAt");
CREATE INDEX "DiscAssessment_personId_idx" ON "DiscAssessment"("personId");
CREATE INDEX "SalesCopilotSession_organizationId_personId_createdAt_idx" ON "SalesCopilotSession"("organizationId", "personId", "createdAt");
CREATE INDEX "SalesCopilotSession_organizationId_leadId_idx" ON "SalesCopilotSession"("organizationId", "leadId");
CREATE INDEX "SalesCopilotSession_dealId_idx" ON "SalesCopilotSession"("dealId");
-- AddForeignKey
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_assessorId_fkey" FOREIGN KEY ("assessorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- ماژول برای محیط‌هایی که از قبل ماژول دارند روشن می‌شود و در سوپرادمین قابل خاموش کردن است
UPDATE "Organization"
SET "enabledModules" = array_append("enabledModules", 'behavior')
WHERE cardinality("enabledModules") > 0
AND NOT ('behavior' = ANY("enabledModules"));
-- مجوز رفتار برای نقش‌هایی که فروش دارند؛ پشتیبانی بدون این کلید می‌ماند
UPDATE "OrgRole"
SET "permissions" = array_append("permissions", 'behavior')
WHERE NOT ('behavior' = ANY("permissions"))
AND (
"systemKey" IN ('owner', 'admin', 'sales')
OR 'sales' = ANY("permissions")
);
+80
View File
@@ -161,6 +161,8 @@ model Organization {
teams Team[]
companies Company[]
people Person[]
discAssessments DiscAssessment[]
salesCopilotSessions SalesCopilotSession[]
pipelines Pipeline[]
deals Deal[]
activities Activity[]
@@ -311,6 +313,8 @@ model User {
routineTaskEvents RoutineTaskEvent[]
routineSettings RoutineUserSettings[]
deletionArchives DeletionArchive[] @relation("DeletionActor")
discAssessments DiscAssessment[] @relation("DiscAssessor")
salesCopilotSessions SalesCopilotSession[] @relation("CopilotAuthor")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -554,6 +558,11 @@ model Person {
routineTasks RoutineTask[]
outboundSmsLogs OutboundSmsLog[]
financialDocuments FinancialDocument[]
discLabel String?
discReliable Boolean @default(false)
discAssessedAt DateTime?
discAssessments DiscAssessment[]
salesCopilotSessions SalesCopilotSession[]
financialReceivables FinancialReceivable[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -569,6 +578,7 @@ model Person {
@@index([companyId])
@@index([ownerId])
@@index([loyaltyTierId])
@@index([organizationId, discLabel])
}
model Pipeline {
@@ -640,6 +650,7 @@ model Deal {
expectedCloseAt DateTime?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
salesCopilotSessions SalesCopilotSession[]
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
ownerId String?
@@ -2766,6 +2777,7 @@ model Lead {
convertedAt DateTime?
archivedAt DateTime?
events LeadEvent[]
salesCopilotSessions SalesCopilotSession[]
trackingLinks TrackingLink[]
trackingEvents TrackingEvent[]
routineTasks RoutineTask[]
@@ -3337,3 +3349,71 @@ model GamificationBonus {
@@index([organizationId, isActive])
}
/// ارزیابی رفتاری DISC؛ هر بار یک رکورد تازه است و تاریخچه پاک نمی‌شود.
model DiscAssessment {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
assessorId String
assessor User @relation("DiscAssessor", fields: [assessorId], references: [id], onDelete: Restrict)
answers Json
scoreD Int
scoreI Int
scoreS Int
scoreC Int
percentD Int
percentI Int
percentS Int
percentC Int
validCount Int
primaryTypes String[] @default([])
secondaryTypes String[] @default([])
label String
secondaryLabel String?
reliable Boolean
confidence String
note String?
status String @default("confirmed")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, personId, createdAt])
@@index([personId])
}
/// نتیجهٔ تأییدشدهٔ دستیار فروش. پیشنهاد خام بدون تأیید کارشناس اینجا ذخیره نمی‌شود.
model SalesCopilotSession {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
userId String
user User @relation("CopilotAuthor", fields: [userId], references: [id], onDelete: Restrict)
topic String
goal String
customerStatement String
behaviorNotes String?
analysis Json
confirmedResponse String?
nextAction String?
followUpAt DateTime?
callOutcome String?
agentNote String?
status String
messageDraft String?
activityId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, personId, createdAt])
@@index([organizationId, leadId])
@@index([dealId])
}