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:
2026-09-27 17:05:11 +03:30
co-authored by Cursor
parent 22980e5152
commit c079295ff4
24 changed files with 4357 additions and 9 deletions
+119
View File
@@ -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])
}