feat(tenders): add Tender Management module (SRS, backend, frontend)

- SRS document: docs/SRS_TENDER_MANAGEMENT.md
- Prisma: Tender, TenderDirective models; Deal.sourceTenderId; Attachment.tenderId/tenderDirectiveId
- Backend: tenders module (CRUD, duplicate check, directives, notifications, file upload, convert-to-deal)
- Frontend: tenders list, detail, create/edit forms, directives, convert to deal, i18n (en/ar), dashboard card
- Seed: tenders permissions for admin and sales positions
- Auth: admin.service findFirst for email check (Prisma compatibility)

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-11 16:57:40 +04:00
parent 18c13cdf7c
commit 4c139429e2
14 changed files with 2623 additions and 17 deletions

View File

@@ -70,7 +70,10 @@ model User {
projectMembers ProjectMember[]
campaigns Campaign[]
userRoles UserRole[]
tendersCreated Tender[]
tenderDirectivesIssued TenderDirective[]
tenderDirectivesCompleted TenderDirective[] @relation("TenderDirectiveCompletedBy")
@@map("users")
}
@@ -199,7 +202,8 @@ model Employee {
purchaseRequests PurchaseRequest[]
leaveEntitlements LeaveEntitlement[]
employeeContracts EmployeeContract[]
tenderDirectivesAssigned TenderDirective[]
@@index([departmentId])
@@index([positionId])
@@index([status])
@@ -610,7 +614,8 @@ model Contact {
deals Deal[]
attachments Attachment[]
notes Note[]
tenders Tender[]
@@index([type])
@@index([status])
@@index([email])
@@ -705,10 +710,14 @@ model Deal {
// Status
status String @default("ACTIVE") // ACTIVE, WON, LOST, CANCELLED, ARCHIVED
// Source (when converted from Tender)
sourceTenderId String? @unique
sourceTender Tender? @relation(fields: [sourceTenderId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
quotes Quote[]
costSheets CostSheet[]
@@ -718,7 +727,7 @@ model Deal {
contracts Contract[]
invoices Invoice[]
commissions Commission[]
@@index([contactId])
@@index([ownerId])
@@index([pipelineId])
@@ -873,6 +882,66 @@ model Invoice {
@@map("invoices")
}
// ============================================
// TENDER MANAGEMENT - إدارة المناقصات
// ============================================
model Tender {
id String @id @default(uuid())
tenderNumber String @unique
issuingBodyName String
title String
termsValue Decimal @db.Decimal(15, 2)
bondValue Decimal @db.Decimal(15, 2)
announcementDate DateTime @db.Date
closingDate DateTime @db.Date
announcementLink String?
source String // GOVERNMENT_SITE, OFFICIAL_GAZETTE, PERSONAL, PARTNER, WHATSAPP_TELEGRAM, PORTAL, EMAIL, MANUAL
sourceOther String? // Free text when source is MANUAL or other
announcementType String // FIRST, RE_ANNOUNCEMENT_2, RE_ANNOUNCEMENT_3, RE_ANNOUNCEMENT_4
notes String?
status String @default("ACTIVE") // ACTIVE, CONVERTED_TO_DEAL, CANCELLED
contactId String? // Optional link to Contact (issuing body)
contact Contact? @relation(fields: [contactId], references: [id])
createdById String
createdBy User @relation(fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
directives TenderDirective[]
attachments Attachment[]
convertedDeal Deal?
@@index([tenderNumber])
@@index([status])
@@index([createdById])
@@index([announcementDate])
@@index([closingDate])
@@map("tenders")
}
model TenderDirective {
id String @id @default(uuid())
tenderId String
tender Tender @relation(fields: [tenderId], references: [id], onDelete: Cascade)
type String // BUY_TERMS, VISIT_CLIENT, MEET_COMMITTEE, PREPARE_TO_BID
notes String?
assignedToEmployeeId String
assignedToEmployee Employee @relation(fields: [assignedToEmployeeId], references: [id])
issuedById String
issuedBy User @relation(fields: [issuedById], references: [id])
status String @default("PENDING") // PENDING, IN_PROGRESS, COMPLETED, CANCELLED
completedAt DateTime?
completionNotes String?
completedById String?
completedBy User? @relation("TenderDirectiveCompletedBy", fields: [completedById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
attachments Attachment[]
@@index([tenderId])
@@index([assignedToEmployeeId])
@@index([status])
@@map("tender_directives")
}
// ============================================
// MODULE 3: INVENTORY & ASSETS
// ============================================
@@ -1420,11 +1489,11 @@ model Note {
model Attachment {
id String @id @default(uuid())
// Related Entity
entityType String
entityId String
// Relations
contactId String?
contact Contact? @relation(fields: [contactId], references: [id])
@@ -1434,7 +1503,11 @@ model Attachment {
project Project? @relation(fields: [projectId], references: [id])
taskId String?
task Task? @relation(fields: [taskId], references: [id])
tenderId String?
tender Tender? @relation(fields: [tenderId], references: [id], onDelete: Cascade)
tenderDirectiveId String?
tenderDirective TenderDirective? @relation(fields: [tenderDirectiveId], references: [id], onDelete: Cascade)
// File Info
fileName String
originalName String
@@ -1442,19 +1515,21 @@ model Attachment {
size Int
path String
url String?
// Metadata
description String?
category String?
uploadedBy String
uploadedAt DateTime @default(now())
@@index([entityType, entityId])
@@index([contactId])
@@index([dealId])
@@index([projectId])
@@index([taskId])
@@index([tenderId])
@@index([tenderDirectiveId])
@@map("attachments")
}