generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum UserRole { owner admin sales support } enum DealStatus { open won lost } enum ActivityType { call meeting task note } enum ActivityStatus { open done canceled } enum ActivityPriority { low medium high very_low very_high } enum PersonMood { newcomer happy unhappy special } enum CustomFieldEntity { person company deal } enum CustomFieldType { text number date boolean select } enum QuoteStatus { draft sent accepted rejected expired } enum InvoiceStatus { draft issued partially_paid paid canceled } enum PaymentMethod { cash transfer card check other } enum InstallmentStatus { pending paid canceled } enum TicketStatus { open in_progress waiting resolved closed } enum TicketPriority { low normal high urgent } enum AutomationTrigger { person_created lead_submitted deal_created deal_stage_changed deal_won deal_lost ticket_created ticket_resolved invoice_issued invoice_paid activity_done sms_link_clicked } enum AutomationAction { create_activity create_notification add_loyalty_points } enum NoteAiSuggestionStatus { pending applied dismissed failed } enum MemorySourceType { note manual } model Organization { id String @id @default(cuid()) name String slug String @unique /// زمینه فعالیت سازمان — فهرست هم‌تراز صنایع دیدار businessField String? baseCurrency String @default("IRR") enabledCurrencies String[] @default(["IRR", "USD", "EUR"]) /// ماژول‌های فعال منو برای این محیط enabledModules String[] @default([]) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt users User[] memberships Membership[] orgRoles OrgRole[] teams Team[] companies Company[] people Person[] pipelines Pipeline[] deals Deal[] activities Activity[] notes Note[] noteRevisions NoteRevision[] customFields CustomFieldDefinition[] products Product[] notifications Notification[] noteAttachments NoteAttachment[] leadForms LeadForm[] quotes Quote[] invoices Invoice[] salesGoals SalesGoal[] exchangeRates ExchangeRate[] tickets Ticket[] faqArticles FaqArticle[] portalOtps PortalOtp[] projectBoards ProjectBoard[] projectCards ProjectCard[] loyaltyTiers LoyaltyTier[] loyaltySettings LoyaltySettings? loyaltyLedgers LoyaltyLedger[] loyaltyRewards LoyaltyReward[] loyaltyRedemptions LoyaltyRedemption[] automationRules AutomationRule[] messageAutomationRules MessageAutomationRule[] messageAutomationJobs MessageAutomationJob[] memoryItems MemoryItem[] noteAiSuggestions NoteAiSuggestion[] accountingConnections AccountingConnection[] accountingSyncJobs AccountingSyncJob[] warehouses Warehouse[] stockMovements StockMovement[] partyOpenings PartyOpeningBalance[] commissionEntries CommissionEntry[] cheques Cheque[] auditLogs AuditLog[] savedViews SavedView[] apiKeys OrganizationApiKey[] webhookEndpoints WebhookEndpoint[] smsCampaigns SmsCampaign[] wooCommerceConnections WooCommerceConnection[] montifyConnections MontifyConnection[] voipConnections VoipConnection[] reservationServices ReservationService[] reservationAppointments ReservationAppointment[] leads Lead[] leadStages LeadStage[] activityStages ActivityStage[] activityTypeDefs ActivityTypeDef[] leadEvents LeadEvent[] leadImportJobs LeadImportJob[] conversationSettings ConversationSettings? conversationRooms ConversationRoom[] conversationHiddenMessages ConversationHiddenMessage[] conversationPinnedMessages ConversationPinnedMessage[] conversationRoomUserPins ConversationRoomUserPin[] conversationRoomMembers ConversationRoomMember[] conversationRoomUserMutes ConversationRoomUserMute[] conversationMessageReactions ConversationMessageReaction[] trackingLinks TrackingLink[] trackingEvents TrackingEvent[] routineCategories RoutineCategory[] routines Routine[] routineTasks RoutineTask[] routineTaskEvents RoutineTaskEvent[] routineUserSettings RoutineUserSettings[] gamificationSettings GamificationSettings? gamificationRules GamificationRule[] gamificationTransactions GamificationTransaction[] gamificationWallets GamificationWallet[] gamificationRewards GamificationReward[] gamificationRedemptions GamificationRedemption[] gamificationBadges GamificationBadge[] gamificationUserBadges GamificationUserBadge[] gamificationBonuses GamificationBonus[] } model User { id String @id @default(cuid()) email String /// موبایل نرمال‌شده (مثلاً 9121234567) — برای ورود با موبایل mobile String? passwordHash String fullName String role UserRole @default(sales) /// مسیر فایل آواتار در فضای آپلود avatarStorageKey String? isActive Boolean @default(true) lastLoginAt DateTime? orgRoleId String? orgRole OrgRole? @relation(fields: [orgRoleId], references: [id], onDelete: SetNull) emailVerifiedAt DateTime? mobileVerifiedAt DateTime? organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) ownedDeals Deal[] @relation("DealOwner") assignedDeals Deal[] @relation("DealAssignees") createdDeals Deal[] @relation("DealCreator") ownedQuotes Quote[] @relation("QuoteOwner") ownedInvoices Invoice[] @relation("InvoiceOwner") ownedPeople Person[] @relation("PersonOwner") ownedCompanies Company[] @relation("CompanyOwner") ownedProjectCards ProjectCard[] @relation("ProjectCardOwner") activities Activity[] @relation("ActivityAssignee") assignedActivities Activity[] @relation("ActivityAssignees") createdActivities Activity[] @relation("ActivityCreator") notes Note[] @relation("NoteAuthor") noteRevisions NoteRevision[] @relation("NoteRevisionEditor") noteAttachments NoteAttachment[] @relation("NoteAttachmentUploader") notifications Notification[] salesGoals SalesGoal[] @relation("SalesGoalUser") commissionPercent Decimal @default(0) @db.Decimal(8, 2) commissionEntries CommissionEntry[] receivedCheques Cheque[] @relation("ChequeOwner") assignedTickets Ticket[] @relation("TicketAssignee") reportedTickets Ticket[] @relation("TicketReporter") ticketMessages TicketMessage[] @relation("TicketMessageAuthor") confirmedMemories MemoryItem[] @relation("MemoryConfirmedBy") memberships Membership[] billingAccount BillingAccount? auditLogs AuditLog[] @relation("AuditLogActor") savedViews SavedView[] @relation("SavedViewOwner") tablePreferences UserTablePreference[] assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees") teamMemberships TeamMember[] reservationAssignments ReservationAppointment[] @relation("ReservationAssignee") assignedLeads Lead[] @relation("LeadAssignee") createdLeads Lead[] @relation("LeadCreator") assignedRoutines Routine[] @relation("RoutineAssignee") createdRoutines Routine[] @relation("RoutineCreator") routineTasks RoutineTask[] @relation("RoutineTaskAssignee") createdRoutineTasks RoutineTask[] @relation("RoutineTaskCreator") routineTaskEvents RoutineTaskEvent[] routineSettings RoutineUserSettings[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, email]) @@unique([mobile]) @@index([organizationId]) @@index([email]) @@index([orgRoleId]) } model OrgRole { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String @default("") color String @default("teal") permissions String[] @default([]) /// owner | admin | sales | support — نقش‌های سیستمی قابل حذف نیستند systemKey String? /// نقش والد برای سلسله‌مراتب زیرنقش‌ها parentId String? parent OrgRole? @relation("OrgRoleTree", fields: [parentId], references: [id], onDelete: Restrict) children OrgRole[] @relation("OrgRoleTree") users User[] memberships Membership[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, name]) @@index([organizationId]) @@index([parentId]) @@index([organizationId, parentId]) } model Membership { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) role UserRole @default(sales) orgRoleId String? orgRole OrgRole? @relation(fields: [orgRoleId], references: [id], onDelete: SetNull) isActive Boolean @default(true) commissionPercent Decimal @default(0) @db.Decimal(8, 2) /// own | team | org — سطح دید داده‌ها برای نقش‌های غیرمالک/مدیر dataVisibility String @default("own") /// سطح دید تفکیکی هر بخش، مثلاً {"contacts":"org","deals":"own"}؛ بخش‌های بدون مقدار = dataVisibility dataVisibilityScopes Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, organizationId]) @@index([organizationId, role]) @@index([userId]) @@index([orgRoleId]) } /// گروه کاری داخل سازمان برای دیدپذیری مشترک model Team { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String @default("") members TeamMember[] routines Routine[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, name]) @@index([organizationId]) } model TeamMember { id String @id @default(cuid()) teamId String team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) /// member | manager role String @default("member") createdAt DateTime @default(now()) @@unique([teamId, userId]) @@index([userId]) @@index([teamId]) } /// حساب سوپرادمین پلتفرم — جدا از User سازمانی model PlatformAdmin { id String @id @default(cuid()) /// ورود اصلی با موبایل نرمال‌شده (مثلاً 0912…) mobile String? @unique email String? @unique passwordHash String fullName String isActive Boolean @default(true) lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } /// اشتراک SaaS روی مالک حساب (نه روی هر عضویت) model BillingAccount { id String @id @default(cuid()) ownerUserId String @unique owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade) /// trial | monthly | quarterly | semiannual | yearly | lifetime | custom planCode String @default("trial") /// active | expired | suspended status String @default("active") startsAt DateTime @default(now()) expiresAt DateTime maxOrganizations Int @default(1) /// رزرو برای درگاه/لندینگ بعدی externalPaymentRef String? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@index([expiresAt]) } model Company { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String /// کد داخلی شرکت (مثل «کد شرکت» در دیدار) code String? /// منبع ورود: manual | didar | tahesab | excel | other importSource String @default("manual") /// شناسه در مبدأ (مثلاً کد دیدار شرکت) sourceRef String? nationalId String? economicCode String? registrationNumber String? phone String? otherPhones String? fax String? email String? otherEmails String? website String? province String? city String? postalCode String? address String? groupName String? notes String? registeredAt DateTime? tags String[] @default([]) customFields Json @default("{}") people Person[] deals Deal[] activities Activity[] smartNotes Note[] quotes Quote[] invoices Invoice[] ownerId String? owner User? @relation("CompanyOwner", fields: [ownerId], references: [id], onDelete: SetNull) tickets Ticket[] projectCards ProjectCard[] routineTasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId]) @@index([organizationId, name]) @@index([organizationId, email]) @@index([organizationId, phone]) @@index([organizationId, sourceRef]) @@index([organizationId, code]) @@index([ownerId]) } model Person { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) firstName String lastName String /// نام قبل از همگام‌سازی مبدأ (مثلاً ته‌حساب) previousFirstName String? previousLastName String? /// منبع ورود: manual | tahesab | didar | excel | phone | lead_form | other importSource String @default("manual") /// شناسه در مبدأ (کد دیدار، کد مشتری ته‌حساب، …) sourceRef String? /// منبع و کد قبلی وقتی شخص از منبع دیگری دوباره وارد شده (مثلاً دیدار قبل از ته‌حساب) previousImportSource String? previousSourceRef String? email String? phone String? mobile String? position String? /// آدرس وب‌سایت website String? /// پیج اینستاگرام instagram String? /// نام معرف referrer String? /// لیست آدرس‌ها (JSON array of strings) addresses Json @default("[]") notes String? mood PersonMood @default(newcomer) tags String[] @default([]) customFields Json @default("{}") loyaltyPoints Int @default(0) loyaltyTierId String? loyaltyTier LoyaltyTier? @relation(fields: [loyaltyTierId], references: [id], onDelete: SetNull) loyaltyLedgers LoyaltyLedger[] loyaltyRedemptions LoyaltyRedemption[] deals Deal[] activities Activity[] smartNotes Note[] quotes Quote[] invoices Invoice[] reservationAppointments ReservationAppointment[] ownerId String? owner User? @relation("PersonOwner", fields: [ownerId], references: [id], onDelete: SetNull) tickets Ticket[] projectCards ProjectCard[] memoryItems MemoryItem[] accountingLinks PersonAccountingLink[] accountingDocuments AccountingDocumentLog[] openingBalances PartyOpeningBalance[] cheques Cheque[] trackingLinks TrackingLink[] trackingEvents TrackingEvent[] routineTasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId]) @@index([organizationId, email]) @@index([organizationId, mobile]) @@index([organizationId, lastName, firstName]) @@index([organizationId, importSource]) @@index([organizationId, sourceRef]) @@index([organizationId, previousSourceRef]) @@index([organizationId, loyaltyPoints]) @@index([companyId]) @@index([ownerId]) @@index([loyaltyTierId]) } model Pipeline { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String isDefault Boolean @default(false) stages PipelineStage[] deals Deal[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId]) @@index([organizationId, isDefault]) } model PipelineStage { id String @id @default(cuid()) pipelineId String pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade) name String position Int isWon Boolean @default(false) isLost Boolean @default(false) deals Deal[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([pipelineId, position]) } model Deal { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) pipelineId String pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade) stageId String stage PipelineStage @relation(fields: [stageId], references: [id], onDelete: Restrict) title String /// کد دستی معامله code String? /// منبع ورود: manual | didar | excel importSource String @default("manual") /// شناسه در مبدأ (کد دیدار معامله) sourceRef String? amount Decimal? @db.Decimal(18, 2) currency String @default("IRR") status DealStatus @default(open) /// احتمال موفقیت (۰ تا ۱۰۰) probability Int? /// شیوه آشنایی source String? /// شیوه آشنایی دلخواه sourceDetail String? referrer String? lostReason String? lostNote String? /// تاریخ انجام (موفق/ناموفق شدن) closedAt DateTime? notes String? tags String[] @default([]) isStale Boolean @default(false) isRotten Boolean @default(false) customFields Json @default("{}") expectedCloseAt DateTime? personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) ownerId String? owner User? @relation("DealOwner", fields: [ownerId], references: [id], onDelete: SetNull) createdById String? createdBy User? @relation("DealCreator", fields: [createdById], references: [id], onDelete: SetNull) assignees User[] @relation("DealAssignees") position Int @default(0) activities Activity[] smartNotes Note[] lineItems DealLineItem[] quotes Quote[] invoices Invoice[] tickets Ticket[] projectCards ProjectCard[] routineTasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId]) @@index([pipelineId, stageId]) @@index([stageId, position]) @@index([personId]) @@index([companyId]) @@index([ownerId]) @@index([organizationId, sourceRef]) } /// مراحل/ستون‌های فعالیت (تب و کانبان) — kind وضعیت فعالیت داخل این مرحله را تعیین می‌کند model ActivityStage { 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("green") kind ActivityStatus @default(open) activities Activity[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, name]) @@index([organizationId, position]) } /// نوع فعالیت قابل تعریف توسط سازمان با فیلدهای اختصاصی model ActivityTypeDef { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String /// نوع پایه — مبنای گزارش‌ها، یادآورها و سازگاری baseType ActivityType @default(task) icon String @default("check") color String @default("green") /// تعریف فیلدها: [{ key, label, type, options?, required?, placeholder? }] fields Json @default("[]") position Int @default(0) isActive Boolean @default(true) /// نوع اصلیِ هر baseType — حذف‌شدنی نیست isSystem Boolean @default(false) activities Activity[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, name]) @@index([organizationId, position]) } model Activity { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) type ActivityType @default(task) status ActivityStatus @default(open) priority ActivityPriority @default(medium) title String description String? dueAt DateTime? durationMinutes Int? reminderAt DateTime? /// اعلان داخل پنل هنگام رسیدن یادآور reminderNotifyApp Boolean @default(true) /// پیامک به موبایل مسئول هنگام رسیدن یادآور reminderNotifySms Boolean @default(false) /// زمان ارسال یادآور؛ با تغییر reminderAt پاک می‌شود reminderSentAt DateTime? completedAt DateTime? tags String[] @default([]) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) assigneeId String? assignee User? @relation("ActivityAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) /// مسئولان مشترک (علاوه بر assigneeId به‌عنوان مسئول اصلی) assignees User[] @relation("ActivityAssignees") createdById String? createdBy User? @relation("ActivityCreator", fields: [createdById], references: [id], onDelete: SetNull) notes Note[] routineTasks RoutineTask[] /// مرحله (ستون کانبان)؛ null یا مرحلهٔ ناهم‌نوع با status → اولین مرحلهٔ هم‌نوع stageId String? stage ActivityStage? @relation(fields: [stageId], references: [id], onDelete: SetNull) /// نوع سفارشی؛ null → نوع اصلیِ همان type typeDefId String? typeDef ActivityTypeDef? @relation(fields: [typeDefId], references: [id], onDelete: SetNull) /// مقادیر فیلدهای اختصاصی نوع (کلید فیلد → مقدار) fields Json? /// کاربران شرکت‌کننده؛ فعالیت در لیست آن‌ها هم دیده می‌شود participantIds String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([stageId]) @@index([typeDefId]) @@index([organizationId, status, dueAt]) @@index([organizationId, assigneeId]) @@index([organizationId, createdById]) @@index([reminderAt, reminderSentAt]) @@index([personId]) @@index([companyId]) @@index([dealId]) } model Note { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) title String? body String noteType String @default("general") pinned Boolean @default(false) color String? @default("default") tags String[] @default([]) reminderAt DateTime? /// [{ id: string, text: string, done: boolean }] checklist Json? locationText String? analyzeEnabled Boolean @default(true) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) activityId String? activity Activity? @relation(fields: [activityId], references: [id], onDelete: SetNull) authorId String? author User? @relation("NoteAuthor", fields: [authorId], references: [id], onDelete: SetNull) /// team = قابل‌مشاهده برای تیم/سازمان در محدوده پرونده | personal = فقط نویسنده visibility String @default("team") attachments NoteAttachment[] aiSuggestion NoteAiSuggestion? revisions NoteRevision[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, pinned, updatedAt]) @@index([organizationId, noteType, updatedAt]) @@index([organizationId, reminderAt]) @@index([organizationId, visibility]) @@index([personId]) @@index([companyId]) @@index([dealId]) @@index([activityId]) @@index([authorId]) } /// اسنپ‌شات متن قبل از هر ویرایش معنادار یادداشت model NoteRevision { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) noteId String note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) editorId String? editor User? @relation("NoteRevisionEditor", fields: [editorId], references: [id], onDelete: SetNull) title String? body String noteType String visibility String @default("team") tags String[] @default([]) checklist Json? createdAt DateTime @default(now()) @@index([noteId, createdAt]) @@index([organizationId]) @@index([editorId]) } model NoteAttachment { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) noteId String note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) fileName String mimeType String sizeBytes Int storageKey String uploadedById String? uploadedBy User? @relation("NoteAttachmentUploader", fields: [uploadedById], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @@index([noteId, createdAt]) @@index([organizationId]) @@index([uploadedById]) } model NoteAiSuggestion { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) noteId String @unique note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) status NoteAiSuggestionStatus @default(pending) payload Json? errorMessage String? analyzedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, status]) } model MemoryItem { 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) content String sourceType MemorySourceType @default(manual) sourceId String? confirmedById String? confirmedBy User? @relation("MemoryConfirmedBy", fields: [confirmedById], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, personId, updatedAt]) @@index([personId]) @@index([confirmedById]) } model CustomFieldDefinition { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) entityType CustomFieldEntity key String label String fieldType CustomFieldType @default(text) options String[] @default([]) required Boolean @default(false) position Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, entityType, key]) @@index([organizationId, entityType, position]) } model Product { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) /// product | service kind String @default("product") name String sku String? description String? category String? unit String? @default("عدد") stockQty Int? brand String? model String? country String? tags String[] @default([]) unitPrice Decimal? @db.Decimal(18, 2) currency String @default("IRR") isActive Boolean @default(true) lineItems DealLineItem[] quoteLineItems QuoteLineItem[] invoiceLineItems InvoiceLineItem[] stockMovements StockMovement[] accountingLinks ProductAccountingLink[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, name]) @@index([organizationId, isActive]) @@index([organizationId, category]) } model DealLineItem { id String @id @default(cuid()) dealId String deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade) productId String? product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) name String quantity Decimal @default(1) @db.Decimal(18, 2) unitPrice Decimal @db.Decimal(18, 2) currency String @default("IRR") position Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([dealId, position]) @@index([productId]) } model Notification { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) type String title String body String? link String? entityType String? entityId String? readAt DateTime? createdAt DateTime @default(now()) @@index([userId, readAt, createdAt]) @@index([organizationId, createdAt]) @@index([userId, type, entityId]) } model LeadForm { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? publicToken String @unique @default(cuid()) isActive Boolean @default(true) /// ثبت به‌عنوان Lead در CRM (مسیر اصلی انتشار فرم) createLead Boolean @default(true) createPerson Boolean @default(false) createDeal Boolean @default(false) pipelineId String? /// مرحله اولیه لید (اختیاری؛ وگرنه اولین مرحله) leadStageId String? /// skip | update | activity duplicateMode String @default("update") source String? thankYouMessage String? redirectUrl String? assigneeIds String[] @default([]) roundRobinIndex Int @default(0) submitLabel String @default("ارسال درخواست") submitColor String @default("#14b8a6") coverStorageKey String? viewCount Int @default(0) startCount Int @default(0) submitCount Int @default(0) leadCreateCount Int @default(0) duplicateCount Int @default(0) fields LeadFormField[] submissions LeadFormSubmission[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, updatedAt]) } model LeadFormField { id String @id @default(cuid()) formId String form LeadForm @relation(fields: [formId], references: [id], onDelete: Cascade) key String label String fieldType String @default("text") required Boolean @default(false) mapTo String position Int @default(0) placeholder String? options Json? @@unique([formId, key]) @@index([formId, position]) } model LeadFormSubmission { id String @id @default(cuid()) formId String form LeadForm @relation(fields: [formId], references: [id], onDelete: Cascade) values Json personId String? leadId String? /// created | updated | duplicate_skipped | error status String @default("created") utm Json? meta Json? createdAt DateTime @default(now()) @@index([formId, createdAt]) @@index([leadId]) } model Quote { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) number String title String status QuoteStatus @default(draft) currency String @default("IRR") notes String? discountAmount Decimal @default(0) @db.Decimal(18, 2) taxPercent Decimal @default(0) @db.Decimal(8, 2) issuedAt DateTime? validUntil DateTime? personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) ownerId String? owner User? @relation("QuoteOwner", fields: [ownerId], references: [id], onDelete: SetNull) lineItems QuoteLineItem[] invoices Invoice[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, number]) @@index([organizationId, status, updatedAt]) @@index([personId]) @@index([companyId]) @@index([dealId]) @@index([ownerId]) } model QuoteLineItem { id String @id @default(cuid()) quoteId String quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade) productId String? product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) name String quantity Decimal @default(1) @db.Decimal(18, 2) unitPrice Decimal @db.Decimal(18, 2) currency String @default("IRR") position Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([quoteId, position]) @@index([productId]) } model Invoice { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) number String title String status InvoiceStatus @default(draft) kind String @default("invoice") // invoice | credit currency String @default("IRR") notes String? discountAmount Decimal @default(0) @db.Decimal(18, 2) taxPercent Decimal @default(0) @db.Decimal(8, 2) issuedAt DateTime? dueDate DateTime? personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) quoteId String? quote Quote? @relation(fields: [quoteId], references: [id], onDelete: SetNull) ownerId String? owner User? @relation("InvoiceOwner", fields: [ownerId], references: [id], onDelete: SetNull) lineItems InvoiceLineItem[] payments Payment[] installments InvoiceInstallment[] stockMovements StockMovement[] commissionPercent Decimal? @db.Decimal(8, 2) commissionFixedAmount Decimal? @db.Decimal(18, 2) commissionEntry CommissionEntry? cheques Cheque[] routineTasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, number]) @@index([organizationId, status, updatedAt]) @@index([personId]) @@index([companyId]) @@index([dealId]) @@index([quoteId]) @@index([ownerId]) } model InvoiceLineItem { id String @id @default(cuid()) invoiceId String invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) productId String? product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) name String quantity Decimal @default(1) @db.Decimal(18, 2) unitPrice Decimal @db.Decimal(18, 2) currency String @default("IRR") position Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([invoiceId, position]) @@index([productId]) } model Payment { id String @id @default(cuid()) invoiceId String invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) amount Decimal @db.Decimal(18, 2) currency String @default("IRR") method PaymentMethod @default(transfer) paidAt DateTime @default(now()) notes String? installmentId String? @unique installment InvoiceInstallment? @relation(fields: [installmentId], references: [id], onDelete: SetNull) cheque Cheque? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([invoiceId, paidAt]) } model InvoiceInstallment { id String @id @default(cuid()) invoiceId String invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) position Int @default(0) dueDate DateTime amount Decimal @db.Decimal(18, 2) currency String @default("IRR") status InstallmentStatus @default(pending) paidAt DateTime? payment Payment? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([invoiceId, position]) @@index([invoiceId, dueDate]) @@index([status, dueDate]) } model SalesGoal { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) /// null = هدف سازمانی / تیمی userId String? user User? @relation("SalesGoalUser", fields: [userId], references: [id], onDelete: Cascade) title String /// sales|customer|revenue|deal|activity|quality type String @default("sales") periodStart DateTime periodEnd DateTime targetAmount Decimal @db.Decimal(18, 2) forecastAmount Decimal? @db.Decimal(18, 2) /// 0-100 probability Decimal? @db.Decimal(5, 2) currency String @default("IRR") notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, periodStart, periodEnd]) @@index([userId]) @@index([organizationId, type]) } model ExchangeRate { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) /// ارز مبدأ؛ ۱ واحد این ارز = rateToBase واحد ارز پایه currency String rateToBase Decimal @db.Decimal(24, 6) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) @@unique([organizationId, currency]) @@index([organizationId]) } model Ticket { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) number String subject String description String? status TicketStatus @default(open) priority TicketPriority @default(normal) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) assigneeId String? assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) reporterId String? reporter User? @relation("TicketReporter", fields: [reporterId], references: [id], onDelete: SetNull) resolvedAt DateTime? /// مهلت پاسخ اول بر اساس اولویت slaDueAt DateTime? firstRespondedAt DateTime? csatScore Int? csatComment String? csatAt DateTime? messages TicketMessage[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, number]) @@index([organizationId, status, updatedAt]) @@index([organizationId, slaDueAt]) @@index([assigneeId]) @@index([reporterId]) @@index([personId]) @@index([companyId]) @@index([dealId]) } model TicketMessage { id String @id @default(cuid()) ticketId String ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) authorId String? author User? @relation("TicketMessageAuthor", fields: [authorId], references: [id], onDelete: SetNull) body String isInternal Boolean @default(false) createdAt DateTime @default(now()) @@index([ticketId, createdAt]) @@index([authorId]) } model FaqArticle { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) question String answer String category String? isPublished Boolean @default(true) position Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isPublished, position]) } /// کد یک‌بارمصرف ورود پرتال مشتری (موبایل) model PortalOtp { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) mobile String code String expiresAt DateTime consumedAt DateTime? createdAt DateTime @default(now()) @@index([organizationId, mobile, createdAt]) @@index([expiresAt]) } /// برد کانبان برای کارهای داخلی / پس‌ازفروش (جدا از مسیر فروش) model ProjectBoard { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String isDefault Boolean @default(false) stages ProjectBoardStage[] cards ProjectCard[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId]) @@index([organizationId, isDefault]) } model ProjectBoardStage { id String @id @default(cuid()) boardId String board ProjectBoard @relation(fields: [boardId], references: [id], onDelete: Cascade) name String position Int cards ProjectCard[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([boardId, position]) } model ProjectCard { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) boardId String board ProjectBoard @relation(fields: [boardId], references: [id], onDelete: Cascade) stageId String 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) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? 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 @@index([organizationId]) @@index([boardId, stageId]) @@index([stageId, position]) @@index([personId]) @@index([companyId]) @@index([dealId]) @@index([ownerId]) } model LoyaltyTier { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String minPoints Int @default(0) position Int @default(0) color String? people Person[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, position]) @@index([organizationId, minPoints]) } model LoyaltySettings { id String @id @default(cuid()) organizationId String @unique organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) enabled Boolean @default(true) welcomePoints Int @default(0) notifyOnAdjust Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model LoyaltyLedger { 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) delta Int reason String? source String @default("manual") createdAt DateTime @default(now()) @@index([organizationId, createdAt]) @@index([personId, createdAt]) } model LoyaltyReward { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? pointsCost Int isActive Boolean @default(true) position Int @default(0) color String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt redemptions LoyaltyRedemption[] @@index([organizationId, position]) } model LoyaltyRedemption { 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) rewardId String reward LoyaltyReward @relation(fields: [rewardId], references: [id], onDelete: Restrict) pointsSpent Int createdAt DateTime @default(now()) @@index([organizationId, createdAt]) @@index([personId]) @@index([rewardId]) } model AutomationRule { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String isActive Boolean @default(true) trigger AutomationTrigger action AutomationAction /// عنوان فعالیت/اعلان، assigneeId، notifyUserId، points، activityType config Json @default("{}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive, trigger]) } enum MessageAutomationTrigger { person_created lead_submitted deal_created deal_stage_changed deal_won deal_lost ticket_created ticket_resolved invoice_issued invoice_paid activity_created activity_done sms_link_clicked } enum MessageAutomationDelayUnit { minutes hours days weeks months } enum MessageAutomationChannel { app sms both } enum MessageAutomationTarget { staff person both } enum MessageAutomationJobStatus { pending sent cancelled failed } model MessageAutomationRule { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String isActive Boolean @default(true) trigger MessageAutomationTrigger startDelayValue Int @default(0) startDelayUnit MessageAutomationDelayUnit @default(minutes) repeatCount Int @default(1) intervalValue Int @default(1) intervalUnit MessageAutomationDelayUnit @default(days) notifyTarget MessageAutomationTarget @default(person) channel MessageAutomationChannel @default(sms) title String description String? body String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt jobs MessageAutomationJob[] @@index([organizationId, isActive, trigger]) } model MessageAutomationJob { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) ruleId String rule MessageAutomationRule @relation(fields: [ruleId], references: [id], onDelete: Cascade) trigger MessageAutomationTrigger entityType String entityId String entityTitle String personId String? ownerId String? assigneeId String? attemptIndex Int sendAt DateTime status MessageAutomationJobStatus @default(pending) createdAt DateTime @default(now()) sentAt DateTime? @@unique([ruleId, entityId, attemptIndex]) @@index([status, sendAt]) @@index([organizationId, entityType, entityId, status]) } /// چالش OTP برای ثبت‌نام / ورود پنل model AuthChallenge { id String @id @default(cuid()) /// register | login kind String /// برای login: email | mobile channel String? email String? mobile String? passwordHash String? /// دادهٔ موقت ثبت‌نام (سازمان، نام، زمینه، کد معرف، …) payload Json @default("{}") emailCode String? mobileCode String? emailVerified Boolean @default(false) mobileVerified Boolean @default(false) expiresAt DateTime consumedAt DateTime? createdAt DateTime @default(now()) @@index([kind, createdAt]) @@index([expiresAt]) @@index([email]) @@index([mobile]) } /// اتصال به نرم‌افزار حسابداری خارجی (ته‌حساب) — چند اتصال/DB به‌ازای هر سازمان model AccountingConnection { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) /// فعلاً فقط tahesab provider String @default("tahesab") label String baseUrl String dbName String /// AES-256-GCM ciphertext (base64) tokenCiphertext String tokenIv String tokenTag String /// چهار کاراکتر آخر توکن برای نمایش ماسک‌شده tokenLast4 String @default("") enabled Boolean @default(true) isDefault Boolean @default(false) /// اگر true باشد verify SSL نادیده گرفته می‌شود (فقط برای نصب‌های داخلی با گواهی خودامضا) allowInsecureTls Boolean @default(false) lastHealthAt DateTime? lastHealthOk Boolean? lastError String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt personLinks PersonAccountingLink[] productLinks ProductAccountingLink[] documentLogs AccountingDocumentLog[] balanceCaches AccountingBalanceCache[] @@unique([organizationId, baseUrl, dbName]) @@index([organizationId, enabled]) @@index([organizationId, isDefault]) } model PersonAccountingLink { id String @id @default(cuid()) personId String person Person @relation(fields: [personId], references: [id], onDelete: Cascade) connectionId String connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) /// کد مشتری در ته‌حساب (Moshtari_Code) moshtariCode Int /// manual | mobile | name | created matchMethod String linkedAt DateTime @default(now()) lastBalanceSyncedAt DateTime? balanceCache AccountingBalanceCache? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([personId, connectionId]) @@unique([connectionId, moshtariCode]) @@index([personId]) @@index([connectionId]) } /// لینک محصول CRM ↔ نام کارساخته (یا معادل) در ته‌حساب model ProductAccountingLink { id String @id @default(cuid()) productId String product Product @relation(fields: [productId], references: [id], onDelete: Cascade) connectionId String connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) /// نام کارساخته در ته‌حساب (DoListNameKarsakhte.Name) externalName String ayar Int? vaznSabet Float? /// import | manual matchMethod String @default("import") linkedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([productId, connectionId]) @@unique([connectionId, externalName]) @@index([productId]) @@index([connectionId]) } model AccountingDocumentLog { id String @id @default(cuid()) connectionId String connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) /// مثلاً DoNewSanadBuySaleGOLD docType String /// کد یکتای ≥۲۰ کاراکتر — idempotency factorCode String requestPayload Json responsePayload Json? /// pending | success | failed status String @default("pending") tahesabOk String? tahesabFactorId Int? errorMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, factorCode]) @@index([connectionId, status]) @@index([personId]) @@index([docType]) } model AccountingBalanceCache { id String @id @default(cuid()) linkId String @unique link PersonAccountingLink @relation(fields: [linkId], references: [id], onDelete: Cascade) connectionId String connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) goldBalance Float? rialBalance Float? currencyBalance Float? /// مانده هم‌ارز طلا (MandeyeKolBeTALA) در صورت وجود totalGoldEquiv Float? /// جزئیات گروه‌بندی‌شده برای UI mandehDetail Json @default("[]") rawResponse Json? fetchedAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([connectionId]) } model AccountingSyncJob { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) connectionId String? /// search | import | match kind String status String @default("pending") request Json @default("{}") result Json? errorMessage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, createdAt]) } /// انبار داخلی — وقتی ته‌حساب وصل نیست model Warehouse { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String isDefault Boolean @default(true) movements StockMovement[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isDefault]) } /// گردش کالا؛ مقدار مثبت ورود، منفی خروج model StockMovement { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) warehouseId String warehouse Warehouse @relation(fields: [warehouseId], references: [id], onDelete: Cascade) productId String product Product @relation(fields: [productId], references: [id], onDelete: Cascade) /// opening | purchase | sale | credit | adjustment type String quantity Decimal @db.Decimal(18, 2) note String? invoiceId String? invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: Cascade) occurredAt DateTime @default(now()) createdAt DateTime @default(now()) @@index([organizationId, productId, occurredAt]) @@index([invoiceId]) @@index([warehouseId]) } /// پورسانت کارشناس بعد از صدور فاکتور model CommissionEntry { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) invoiceId String @unique invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) percent Decimal @db.Decimal(8, 2) amount Decimal @db.Decimal(18, 2) currency String @default("IRR") /// posted | void status String @default("posted") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, userId, createdAt]) @@index([status]) } /// چک دریافتی؛ مستقل یا وصل به پرداخت فاکتور model Cheque { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) paymentId String? @unique payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Cascade) invoiceId String? invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: Cascade) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) ownerId String? owner User? @relation("ChequeOwner", fields: [ownerId], references: [id], onDelete: SetNull) amount Decimal @db.Decimal(18, 2) currency String @default("IRR") number String bank String dueDate DateTime /// pending | cleared | bounced status String @default("pending") /// in = دریافتی، out = پرداختی direction String @default("in") reminderNotifiedAt DateTime? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, status, dueDate]) @@index([organizationId, direction, dueDate]) @@index([invoiceId]) @@index([personId]) @@index([ownerId]) } /// مانده اول دوره طرف‌حساب (مثبت = بدهکار ما) model PartyOpeningBalance { 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) amount Decimal @db.Decimal(18, 2) currency String @default("IRR") note String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@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 UserTablePreference { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) organizationId String tableKey String /// ستون جدیدی که بعداً اضافه شود به‌صورت پیش‌فرض نمایش داده می‌شود hiddenColumns String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([userId, organizationId, tableKey]) @@index([userId, organizationId]) } 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 | reports scope String filters Json @default("{}") isShared Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, scope]) @@index([organizationId, ownerUserId]) } /// کلید API عمومی برای یکپارچه‌سازی خارجی model OrganizationApiKey { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String /// پیشوند قابل‌نمایش (مثلاً mk_ab12) prefix String /// SHA-256 هش کلید کامل keyHash String lastUsedAt DateTime? revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([keyHash]) @@index([organizationId, revokedAt]) @@index([prefix]) } /// وب‌هوک خروجی سازمان model WebhookEndpoint { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String url String secret String events String[] @default([]) isActive Boolean @default(true) deliveries WebhookDelivery[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive]) } model WebhookDelivery { id String @id @default(cuid()) organizationId String endpointId String endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade) event String payload Json status String @default("pending") statusCode Int? attempts Int @default(0) lastError String? nextAttemptAt DateTime @default(now()) deliveredAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, createdAt]) @@index([status, nextAttemptAt]) @@index([endpointId, createdAt]) } /// کمپین پیامک گروهی model SmsCampaign { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String body String /// draft | queued | sending | done | cancelled status String @default("draft") scheduledAt DateTime? totalCount Int @default(0) sentCount Int @default(0) failedCount Int @default(0) skippedCount Int @default(0) createdById String? recipients SmsCampaignRecipient[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, status, scheduledAt]) } model SmsCampaignRecipient { id String @id @default(cuid()) campaignId String campaign SmsCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) personId String? /// برای پشتیبانی آینده از Lead (فعلاً کمپین روی Person است) leadId String? mobile String? /// pending | sent | failed | skipped status String @default("pending") error String? sentAt DateTime? createdAt DateTime @default(now()) trackingLinks TrackingLink[] @@index([campaignId, status]) @@index([personId]) @@index([leadId]) } /// لینک ردیابی یکتا — کانال‌خنثی (SMS / Email / …) model TrackingLink { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) /// توکن امن و غیرقابل‌حدس (بدون PII) token String @unique originalUrl String /// sms | email | whatsapp | notification | other channel String @default("sms") /// sms_campaign | … sourceType String sourceId String /// sms_campaign_recipient | … recipientType String? recipientId String? smsRecipientId String? smsRecipient SmsCampaignRecipient? @relation(fields: [smsRecipientId], references: [id], onDelete: SetNull) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) leadId String? lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull) clickCount Int @default(0) firstClickedAt DateTime? lastClickedAt DateTime? createdAt DateTime @default(now()) events TrackingEvent[] @@index([organizationId, sourceType, sourceId]) @@index([organizationId, personId]) @@index([organizationId, leadId]) @@index([smsRecipientId]) @@index([token]) } /// رویداد رفتاری عمومی — آمادهٔ Automation model TrackingEvent { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) trackingLinkId String? trackingLink TrackingLink? @relation(fields: [trackingLinkId], references: [id], onDelete: SetNull) /// مثلاً sms_link_clicked | sms_delivered | sms_failed | … eventType String channel String @default("sms") sourceType String? sourceId String? recipientType String? recipientId String? personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) leadId String? lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull) occurredAt DateTime @default(now()) /// ua / referrer / ipHash (اختیاری، بدون PII خام) metadata Json? createdAt DateTime @default(now()) @@index([organizationId, eventType, occurredAt]) @@index([organizationId, sourceType, sourceId, eventType]) @@index([organizationId, personId, eventType]) @@index([organizationId, leadId, eventType]) @@index([trackingLinkId, occurredAt]) } /// اتصال فروشگاه ووکامرس model WooCommerceConnection { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) label String storeUrl String consumerKeyCiphertext String consumerKeyIv String consumerKeyTag String consumerKeyLast4 String @default("") consumerSecretCiphertext String consumerSecretIv String consumerSecretTag String webhookSecretCiphertext String? webhookSecretIv String? webhookSecretTag String? enabled Boolean @default(true) lastSyncAt DateTime? lastHealthAt DateTime? lastHealthOk Boolean? lastError String? orders WooCommerceOrderLink[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, enabled]) } model WooCommerceOrderLink { id String @id @default(cuid()) organizationId String connectionId String connection WooCommerceConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) wooOrderId String wooOrderNumber String? invoiceId String status String @default("") total Decimal @default(0) @db.Decimal(18, 2) currency String @default("IRR") rawPayload Json? syncedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, wooOrderId]) @@index([organizationId, syncedAt]) @@index([invoiceId]) } /// اتصال فروشگاه Montify (هر فروشگاه: Base URL + توکن اختصاصی) model MontifyConnection { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) label String baseUrl String storeId String @default("") tokenCiphertext String tokenIv String tokenTag String tokenLast4 String @default("") enabled Boolean @default(true) webhookRemoteId String? webhookSecretCiphertext String? webhookSecretIv String? webhookSecretTag String? /// بیشترین updated_at مشتریان دریافت‌شده (برای updated_after) customersCursor DateTime? /// idle | running | ok | error syncStatus String @default("idle") syncStartedAt DateTime? lastSyncAt DateTime? lastSyncResult Json? lastHealthAt DateTime? lastHealthOk Boolean? lastError String? lastWebhookAt DateTime? links MontifyLink[] orders MontifyOrder[] transactions MontifyTransaction[] deliveries MontifyWebhookDelivery[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, enabled]) } /// پیوند مشتری فروشگاه با شخص CRM (entityType = customer، entityId = Person.id) model MontifyLink { id String @id @default(cuid()) organizationId String connectionId String connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) entityType String externalId String entityId String externalUpdatedAt DateTime? /// آخرین زمان دریافت سفارش‌های این مشتری (updated_after سفارش‌ها) ordersSyncedAt DateTime? /// پروفایل فروشگاهی مشتری: wallet_balance، gold_wallet_balance، summary، engagement، … data Json? syncedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, entityType, externalId]) @@index([organizationId, entityType, entityId]) } /// سفارش فروشگاه (فقط‌خواندنی، نگاشت مستند: code → order_reference، sts → order_status) model MontifyOrder { id String @id @default(cuid()) organizationId String connectionId String connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) externalId String customerExternalId String? personId String? orderReference String? orderStatus String? paymentStatus String? paymentType String? totalPrice Decimal? @db.Decimal(20, 2) discountTotalPrice Decimal? @db.Decimal(20, 2) totalDiscounts Decimal? @db.Decimal(20, 2) totalTax Decimal? @db.Decimal(20, 2) totalQty Decimal? @db.Decimal(14, 3) profit Decimal? @db.Decimal(20, 2) percentProfit Decimal? @db.Decimal(10, 3) sumGold Decimal? @db.Decimal(14, 3) sumSilver Decimal? @db.Decimal(14, 3) address Json? note String? cancelReason String? cancelledAt DateTime? closedAt DateTime? codeDiscount String? items Json @default("[]") externalCreatedAt DateTime? externalUpdatedAt DateTime? deletedAt DateTime? raw Json? syncedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, externalId]) @@index([organizationId, personId]) @@index([organizationId, externalCreatedAt]) } /// تراکنش مالی مشتری در فروشگاه (واریز/برداشت ریالی یا گرم طلا) model MontifyTransaction { id String @id @default(cuid()) organizationId String connectionId String connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) externalId String customerExternalId String? personId String? type String? amount Decimal? @db.Decimal(24, 4) currency String? status String? referenceCode String? externalCreatedAt DateTime? raw Json? syncedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, externalId]) @@index([organizationId, personId]) } model MontifyWebhookDelivery { id String @id @default(cuid()) connectionId String connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) deliveryId String event String /// received | processed | failed status String @default("received") error String? createdAt DateTime @default(now()) @@unique([connectionId, deliveryId]) @@index([connectionId, createdAt]) } /// اتصال تلفن سازمانی / سانترال (وب‌هوک عمومی) model VoipConnection { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) label String enabled Boolean @default(true) webhookSecretCiphertext String? webhookSecretIv String? webhookSecretTag String? /// قالب URL شروع تماس؛ جای {number} شماره می‌نشیند clickToCallUrl String? lastEventAt DateTime? lastError String? calls VoipCallLog[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, enabled]) } model VoipCallLog { id String @id @default(cuid()) organizationId String connectionId String connection VoipConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade) externalCallId String /// inbound | outbound direction String @default("inbound") /// ringing | answered | missed | completed | failed status String @default("completed") fromNumber String? toNumber String? personId String? activityId String? durationSec Int? startedAt DateTime @default(now()) endedAt DateTime? rawPayload Json? /// لینک فایل ضبط از سانترال recordingUrl String? /// متن خام (از وب‌هوک یا STT) transcript String? /// خلاصه فارسی تولیدشده summary String? /// none | pending | processing | done | failed | skipped summaryStatus String @default("none") summaryNoteId String? summaryError String? summaryAttempts Int @default(0) summaryNextAttemptAt DateTime? syncedAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([connectionId, externalCallId]) @@index([organizationId, syncedAt]) @@index([personId]) @@index([activityId]) @@index([summaryStatus, summaryNextAttemptAt]) } /// خدمت قابل رزرو (مثلاً مشاوره ۳۰ دقیقه‌ای) model ReservationService { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String @default("") durationMinutes Int @default(30) /// روزهای کار ۰=یکشنبه … ۶=شنبه workDays Int[] @default([0, 1, 2, 3, 4, 5, 6]) dayStartHour Int @default(9) dayEndHour Int @default(17) isActive Boolean @default(true) appointments ReservationAppointment[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive]) } model ReservationAppointment { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) serviceId String service ReservationService @relation(fields: [serviceId], references: [id], onDelete: Restrict) personId String person Person @relation(fields: [personId], references: [id], onDelete: Cascade) assigneeId String? assignee User? @relation("ReservationAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) startsAt DateTime endsAt DateTime /// pending | confirmed | canceled | done status String @default("pending") notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, startsAt]) @@index([organizationId, status]) @@index([serviceId, startsAt]) @@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 @@unique([organizationId, name]) @@index([organizationId, position]) } /// مخاطب بالقوه — جدا از 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? /// پیج اینستاگرام instagram String? /// لیست آدرس‌ها (JSON array of strings) addresses Json @default("[]") /// صنف trade String? source String? notes String? /// لوگوی فروشگاه / شرکت logoStorageKey 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[] trackingLinks TrackingLink[] trackingEvents TrackingEvent[] routineTasks RoutineTask[] 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]) } /// تنظیمات اتصال به سرویس گفتگوی خارجی (هر محیط CRM) model ConversationSettings { organizationId String @id organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) chatHost String apiKeyCipher String apiKeyIv String apiKeyTag String uploadUrl String @default("") downloadUrl String @default("") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } /// نگاشت roomهای سرویس چت (DM dedup و گروه‌های ساخته‌شده از CRM) model ConversationRoom { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) externalRoomId String kind String name String? dmUserMinId String? dmUserMaxId String? createdByUserId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, externalRoomId]) @@unique([organizationId, dmUserMinId, dmUserMaxId]) @@index([organizationId, kind]) } /// پیام‌های حذف‌شده در CRM (سرویس چت خارجی حذف پایدار ندارد) /// scope=all → برای همه؛ scope=self → فقط برای hiddenByUserId model ConversationHiddenMessage { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) externalRoomId String messageId String hiddenByUserId String /// all = حذف برای همه | self = حذف فقط برای کاربر scope String @default("all") createdAt DateTime @default(now()) @@unique([organizationId, externalRoomId, messageId, scope, hiddenByUserId]) @@index([organizationId, externalRoomId]) } /// یک پیام سنجاق‌شده در هر اتاق (برای همه اعضا) model ConversationPinnedMessage { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) externalRoomId String messageId String preview String @default("") pinnedByUserId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, externalRoomId]) @@index([organizationId, externalRoomId]) } /// سنجاق گفتگو در لیست — فقط برای همان کاربر model ConversationRoomUserPin { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String externalRoomId String createdAt DateTime @default(now()) @@unique([organizationId, userId, externalRoomId]) @@index([organizationId, userId]) } /// اعضای گروه/کانال در CRM (نقش ادمین برای ارسال در کانال و مدیریت اعضا) model ConversationRoomMember { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) externalRoomId String userId String /// admin | member role String @default("member") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, externalRoomId, userId]) @@index([organizationId, externalRoomId]) } /// بی‌صدا کردن گفتگو برای کاربر — با استثنای منشن model ConversationRoomUserMute { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String externalRoomId String /// اگر true باشد منشن همچنان اعلان/خوانده‌نشده می‌سازد allowMentions Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, userId, externalRoomId]) @@index([organizationId, userId]) } /// واکنش ایموجی روی پیام model ConversationMessageReaction { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) externalRoomId String messageId String emoji String userId String createdAt DateTime @default(now()) @@unique([organizationId, externalRoomId, messageId, userId, emoji]) @@index([organizationId, externalRoomId]) } enum RoutineTaskStatus { todo in_progress completed overdue skipped cancelled } enum RoutineRecurrence { daily weekdays weekly biweekly monthly every_n_days specific_dates } enum RoutineTaskSource { routine manual follow_up lead customer deal } /// دسته‌بندی کارهای روتین (فروش، پیگیری، لید، …) model RoutineCategory { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String color String @default("#14b8a6") sortOrder Int @default(0) routines Routine[] tasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, name]) @@index([organizationId, sortOrder]) } /// الگوی کار تکرارشونده؛ هر روز Task مستقل (RoutineTask) تولید می‌کند model Routine { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) title String description String? assigneeId String? assignee User? @relation("RoutineAssignee", fields: [assigneeId], references: [id], onDelete: SetNull) /// اگر ست باشد برای هر عضو تیم یک Task جدا ساخته می‌شود teamId String? team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull) categoryId String? category RoutineCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull) priority ActivityPriority @default(medium) /// HH:mm به وقت تهران timeOfDay String @default("09:00") durationMinutes Int? startDate DateTime @db.Date endDate DateTime? @db.Date recurrence RoutineRecurrence @default(daily) /// ۰ = شنبه … ۶ = جمعه weekdays Int[] @default([]) intervalDays Int? monthDay Int? specificDates DateTime[] @db.Date isMandatory Boolean @default(true) allowSkip Boolean @default(false) skipReasonRequired Boolean @default(true) reportRequired Boolean @default(false) allowAfterDeadline Boolean @default(true) carryOverOverdue Boolean @default(false) /// none | optional | lead | customer entityLink String @default("none") reminderMinutesBefore Int? isActive Boolean @default(true) createdById String? createdBy User? @relation("RoutineCreator", fields: [createdById], references: [id], onDelete: SetNull) deletedAt DateTime? tasks RoutineTask[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive, deletedAt]) @@index([assigneeId]) @@index([teamId]) } /// نمونه روزانه یک کار (از روتین، دستی، پیگیری، لید یا مشتری) model RoutineTask { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) routineId String? routine Routine? @relation(fields: [routineId], references: [id], onDelete: SetNull) assigneeId String assignee User @relation("RoutineTaskAssignee", fields: [assigneeId], references: [id], onDelete: Cascade) title String description String? categoryId String? category RoutineCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull) priority ActivityPriority @default(medium) status RoutineTaskStatus @default(todo) /// روز برنامه به وقت تهران scheduledFor DateTime @db.Date dueAt DateTime? startedAt DateTime? completedAt DateTime? resultNote String? skipReason String? source RoutineTaskSource @default(manual) isMandatory Boolean @default(false) leadId String? lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull) personId String? person Person? @relation(fields: [personId], references: [id], onDelete: SetNull) companyId String? company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) dealId String? deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull) invoiceId String? invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull) activityId String? activity Activity? @relation(fields: [activityId], references: [id], onDelete: SetNull) reminderAt DateTime? reminderSentAt DateTime? overdueNotifiedAt DateTime? createdById String? createdBy User? @relation("RoutineTaskCreator", fields: [createdById], references: [id], onDelete: SetNull) events RoutineTaskEvent[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([routineId, assigneeId, scheduledFor]) @@index([organizationId, assigneeId, scheduledFor]) @@index([organizationId, status, dueAt]) @@index([reminderAt, reminderSentAt]) @@index([leadId]) @@index([personId]) @@index([companyId]) @@index([dealId]) @@index([activityId]) } /// تاریخچه تغییرات هر Task model RoutineTaskEvent { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) taskId String task RoutineTask @relation(fields: [taskId], references: [id], onDelete: Cascade) actorId String? actor User? @relation(fields: [actorId], references: [id], onDelete: SetNull) /// created | status_changed | rescheduled | note | reassigned | updated type String fromStatus RoutineTaskStatus? toStatus RoutineTaskStatus? note String? meta Json @default("{}") createdAt DateTime @default(now()) @@index([taskId, createdAt]) @@index([organizationId, createdAt]) } /// تنظیمات اعلان کارهای روتین هر کاربر model RoutineUserSettings { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) remindBeforeMinutes Int? @default(30) endOfDaySummary Boolean @default(true) overdueAlert Boolean @default(true) summaryHour Int @default(16) lastSummaryOn DateTime? @db.Date createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([organizationId, userId]) } /// ───────── باشگاه امتیازات کارکنان (جدا از باشگاه مشتریان) ───────── model GamificationSettings { id String @id @default(cuid()) organizationId String @unique organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) enabled Boolean @default(true) /// [{ name, minScore }] levels Json @default("[]") monthlyGoal Int @default(1500) showFullLeaderboard Boolean @default(true) /// شروع استفاده — مبنای گزارش ROI enabledAt DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model GamificationRule { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? event String score Int @default(0) coins Int @default(0) /// [{ field, op, value }] conditions Json @default("[]") isActive Boolean @default(true) maxPerDay Int? maxCoinsPerDay Int? maxPerUser Int? cooldownMinutes Int? oncePerEntity Boolean @default(false) firstTimeOnly Boolean @default(false) requiresApproval Boolean @default(false) roles String[] @default([]) teamIds String[] @default([]) position Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, event, isActive]) } /// دفتر کل امتیاز و سکه — هر تغییر کیف پول یک ردیف دارد model GamificationTransaction { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String /// rule | bonus | achievement | adjust | redeem | refund kind String event String? ruleId String? bonusId String? badgeId String? score Int @default(0) coins Int @default(0) /// approved | pending | rejected status String @default("approved") title String entityType String? entityId String? entityTitle String? /// کلید یکتا برای جلوگیری از امتیاز تکراری idempotencyKey String? reason String? createdById String? decidedById String? decidedAt DateTime? createdAt DateTime @default(now()) @@unique([organizationId, idempotencyKey]) @@index([organizationId, createdAt]) @@index([organizationId, userId, createdAt]) @@index([organizationId, ruleId, userId, createdAt]) @@index([organizationId, status]) } /// موجودی تجمیعی هر کاربر — برای خواندن سریع model GamificationWallet { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String score Int @default(0) coins Int @default(0) coinsEarned Int @default(0) bestMonthScore Int @default(0) updatedAt DateTime @updatedAt @@unique([organizationId, userId]) @@index([organizationId, score]) } model GamificationReward { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? cost Int icon String @default("gift") color String @default("#14b8a6") stock Int? isActive Boolean @default(true) requiresApproval Boolean @default(true) position Int @default(0) redemptions GamificationRedemption[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive]) } model GamificationRedemption { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) userId String rewardId String reward GamificationReward @relation(fields: [rewardId], references: [id], onDelete: Cascade) cost Int /// pending | approved | rejected | delivered status String @default("pending") note String? decisionNote String? decidedById String? decidedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, status]) @@index([organizationId, userId]) } model GamificationBadge { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? icon String @default("medal") color String @default("#f59e0b") /// event_count | score_total | routine_streak | single_deal_amount metric String event String? threshold Int coins Int @default(0) isActive Boolean @default(true) position Int @default(0) earned GamificationUserBadge[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive]) } model GamificationUserBadge { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) badgeId String badge GamificationBadge @relation(fields: [badgeId], references: [id], onDelete: Cascade) userId String earnedAt DateTime @default(now()) @@unique([badgeId, userId]) @@index([organizationId, userId]) } model GamificationBonus { id String @id @default(cuid()) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String description String? /// event_count_period | routine_streak | top_of_month kind String event String? threshold Int @default(1) /// day | week | month period String @default("month") score Int @default(0) coins Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([organizationId, isActive]) }