Add standalone Leads module for prospect outreach before customers.
Import large Excel lists with duplicate checks, assign and kanban follow-up, convert to Person when ready; also fix OTP mobile mask order and SMS brand name to منتکت. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeadStage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"position" INTEGER NOT NULL DEFAULT 0,
|
||||
"color" TEXT NOT NULL DEFAULT 'teal',
|
||||
"showOnBoard" BOOLEAN NOT NULL DEFAULT true,
|
||||
"isConverted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isLost" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isUnqualified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LeadStage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Lead" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"contactName" TEXT,
|
||||
"mobile" TEXT NOT NULL,
|
||||
"phone" TEXT,
|
||||
"city" TEXT,
|
||||
"region" TEXT,
|
||||
"trade" TEXT,
|
||||
"source" TEXT,
|
||||
"notes" TEXT,
|
||||
"estimatedValue" DECIMAL(18,2),
|
||||
"stageId" TEXT NOT NULL,
|
||||
"position" INTEGER NOT NULL DEFAULT 0,
|
||||
"assigneeId" TEXT,
|
||||
"createdById" TEXT,
|
||||
"lastContactAt" TIMESTAMP(3),
|
||||
"nextFollowUpAt" TIMESTAMP(3),
|
||||
"convertedPersonId" TEXT,
|
||||
"convertedAt" TIMESTAMP(3),
|
||||
"archivedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Lead_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeadEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"leadId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"body" TEXT,
|
||||
"meta" JSONB,
|
||||
"actorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LeadEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeadImportJob" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"createdById" TEXT,
|
||||
"fileName" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"totalCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"processedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"successCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"duplicateCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"rows" JSONB NOT NULL DEFAULT '[]',
|
||||
"errorSamples" JSONB NOT NULL DEFAULT '[]',
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LeadImportJob_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LeadStage_organizationId_position_idx" ON "LeadStage"("organizationId", "position");
|
||||
CREATE UNIQUE INDEX "LeadStage_organizationId_name_key" ON "LeadStage"("organizationId", "name");
|
||||
|
||||
CREATE UNIQUE INDEX "Lead_organizationId_mobile_key" ON "Lead"("organizationId", "mobile");
|
||||
CREATE INDEX "Lead_organizationId_stageId_position_idx" ON "Lead"("organizationId", "stageId", "position");
|
||||
CREATE INDEX "Lead_organizationId_assigneeId_idx" ON "Lead"("organizationId", "assigneeId");
|
||||
CREATE INDEX "Lead_organizationId_nextFollowUpAt_idx" ON "Lead"("organizationId", "nextFollowUpAt");
|
||||
CREATE INDEX "Lead_organizationId_lastContactAt_idx" ON "Lead"("organizationId", "lastContactAt");
|
||||
CREATE INDEX "Lead_organizationId_archivedAt_idx" ON "Lead"("organizationId", "archivedAt");
|
||||
CREATE INDEX "Lead_organizationId_createdAt_idx" ON "Lead"("organizationId", "createdAt");
|
||||
CREATE INDEX "Lead_organizationId_city_idx" ON "Lead"("organizationId", "city");
|
||||
CREATE INDEX "Lead_organizationId_trade_idx" ON "Lead"("organizationId", "trade");
|
||||
CREATE INDEX "Lead_organizationId_source_idx" ON "Lead"("organizationId", "source");
|
||||
|
||||
CREATE INDEX "LeadEvent_leadId_createdAt_idx" ON "LeadEvent"("leadId", "createdAt");
|
||||
CREATE INDEX "LeadEvent_organizationId_createdAt_idx" ON "LeadEvent"("organizationId", "createdAt");
|
||||
|
||||
CREATE INDEX "LeadImportJob_organizationId_status_createdAt_idx" ON "LeadImportJob"("organizationId", "status", "createdAt");
|
||||
CREATE INDEX "LeadImportJob_status_createdAt_idx" ON "LeadImportJob"("status", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LeadStage" ADD CONSTRAINT "LeadStage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "LeadStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "LeadEvent" ADD CONSTRAINT "LeadEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "LeadEvent" ADD CONSTRAINT "LeadEvent_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "LeadImportJob" ADD CONSTRAINT "LeadImportJob_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -203,6 +203,10 @@ model Organization {
|
||||
voipConnections VoipConnection[]
|
||||
reservationServices ReservationService[]
|
||||
reservationAppointments ReservationAppointment[]
|
||||
leads Lead[]
|
||||
leadStages LeadStage[]
|
||||
leadEvents LeadEvent[]
|
||||
leadImportJobs LeadImportJob[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -251,6 +255,8 @@ model User {
|
||||
assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees")
|
||||
teamMemberships TeamMember[]
|
||||
reservationAssignments ReservationAppointment[] @relation("ReservationAssignee")
|
||||
assignedLeads Lead[] @relation("LeadAssignee")
|
||||
createdLeads Lead[] @relation("LeadCreator")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1922,3 +1928,116 @@ model ReservationAppointment {
|
||||
@@index([personId])
|
||||
@@index([assigneeId, startsAt])
|
||||
}
|
||||
|
||||
/// مرحله/وضعیت لید — هم لیست وضعیت و هم ستون Kanban
|
||||
model LeadStage {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
position Int @default(0)
|
||||
color String @default("teal")
|
||||
/// نمایش در تخته Kanban
|
||||
showOnBoard Boolean @default(true)
|
||||
isConverted Boolean @default(false)
|
||||
isLost Boolean @default(false)
|
||||
isUnqualified Boolean @default(false)
|
||||
leads Lead[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId, position])
|
||||
@@unique([organizationId, name])
|
||||
}
|
||||
|
||||
/// مخاطب بالقوه — جدا از Person/مشتری
|
||||
model Lead {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
/// نام فروشگاه / شخص / عنوان نمایشی
|
||||
title String
|
||||
/// نام مالک یا مسئول
|
||||
contactName String?
|
||||
mobile String
|
||||
phone String?
|
||||
city String?
|
||||
region String?
|
||||
/// صنف
|
||||
trade String?
|
||||
source String?
|
||||
notes String?
|
||||
estimatedValue Decimal? @db.Decimal(18, 2)
|
||||
stageId String
|
||||
stage LeadStage @relation(fields: [stageId], references: [id], onDelete: Restrict)
|
||||
position Int @default(0)
|
||||
assigneeId String?
|
||||
assignee User? @relation("LeadAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
|
||||
createdById String?
|
||||
createdBy User? @relation("LeadCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
||||
lastContactAt DateTime?
|
||||
nextFollowUpAt DateTime?
|
||||
/// شخص مشتری پس از تبدیل
|
||||
convertedPersonId String?
|
||||
convertedAt DateTime?
|
||||
archivedAt DateTime?
|
||||
events LeadEvent[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([organizationId, mobile])
|
||||
@@index([organizationId, stageId, position])
|
||||
@@index([organizationId, assigneeId])
|
||||
@@index([organizationId, nextFollowUpAt])
|
||||
@@index([organizationId, lastContactAt])
|
||||
@@index([organizationId, archivedAt])
|
||||
@@index([organizationId, createdAt])
|
||||
@@index([organizationId, city])
|
||||
@@index([organizationId, trade])
|
||||
@@index([organizationId, source])
|
||||
}
|
||||
|
||||
model LeadEvent {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
leadId String
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
/// created | assigned | stage_changed | call | note | task | follow_up | converted | archived | import
|
||||
type String
|
||||
title String
|
||||
body String?
|
||||
meta Json?
|
||||
actorId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([leadId, createdAt])
|
||||
@@index([organizationId, createdAt])
|
||||
}
|
||||
|
||||
/// ایمپورت پسزمینه فایل اکسل لید
|
||||
model LeadImportJob {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdById String?
|
||||
fileName String?
|
||||
/// pending | processing | done | failed
|
||||
status String @default("pending")
|
||||
totalCount Int @default(0)
|
||||
processedCount Int @default(0)
|
||||
successCount Int @default(0)
|
||||
duplicateCount Int @default(0)
|
||||
errorCount Int @default(0)
|
||||
/// ردیفهای خام [{...}]
|
||||
rows Json @default("[]")
|
||||
/// خلاصه خطاها
|
||||
errorSamples Json @default("[]")
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([organizationId, status, createdAt])
|
||||
@@index([status, createdAt])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user