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

@@ -99,12 +99,17 @@ const translations = {
active: 'Active',
inactive: 'Inactive',
archived: 'Archived',
deleted: 'Deleted'
deleted: 'Deleted',
all: 'All',
view: 'View',
showing: 'Showing',
of: 'of'
},
nav: {
dashboard: 'Dashboard',
contacts: 'Contacts',
crm: 'CRM',
tenders: 'Tenders',
projects: 'Projects',
inventory: 'Inventory',
hr: 'HR',
@@ -318,6 +323,36 @@ const translations = {
paidAmount: 'Paid Amount',
paidDate: 'Paid Date'
},
tenders: {
title: 'Tenders',
subtitle: 'Tender Management',
addTender: 'Add Tender',
tenderNumber: 'Tender number',
issuingBody: 'Issuing body',
titleLabel: 'Title',
termsValue: 'Terms booklet value',
bondValue: 'Bond value',
announcementDate: 'Announcement date',
closingDate: 'Closing date',
announcementLink: 'Announcement link',
source: 'Source',
announcementType: 'Announcement type',
searchPlaceholder: 'Search by number, title, issuing body...',
noTenders: 'No tenders found.',
loadError: 'Failed to load tenders',
createSuccess: 'Tender created successfully',
duplicateWarning: 'Possible duplicates found. Please review.',
directives: 'Directives',
addDirective: 'Add directive',
directiveType: 'Directive type',
assignee: 'Assignee',
convertToDeal: 'Convert to Opportunity',
history: 'History',
attachments: 'Attachments',
uploadFile: 'Upload file',
completeTask: 'Complete task',
completionNotes: 'Completion notes'
},
import: {
title: 'Import Contacts',
downloadTemplate: 'Download Excel Template',
@@ -381,6 +416,7 @@ const translations = {
dashboard: 'لوحة التحكم',
contacts: 'جهات الاتصال',
crm: 'إدارة العملاء',
tenders: 'المناقصات',
projects: 'المشاريع',
inventory: 'المخزون',
hr: 'الموارد البشرية',
@@ -388,6 +424,36 @@ const translations = {
settings: 'الإعدادات',
logout: 'تسجيل الخروج'
},
tenders: {
title: 'المناقصات',
subtitle: 'نظام إدارة المناقصات',
addTender: 'إضافة مناقصة',
tenderNumber: 'رقم المناقصة',
issuingBody: 'الجهة الطارحة',
titleLabel: 'عنوان المناقصة',
termsValue: 'قيمة دفتر الشروط',
bondValue: 'قيمة التأمينات',
announcementDate: 'تاريخ الإعلان',
closingDate: 'تاريخ الإغلاق',
announcementLink: 'رابط الإعلان',
source: 'مصدر المناقصة',
announcementType: 'نوع الإعلان',
searchPlaceholder: 'البحث بالرقم أو العنوان أو الجهة الطارحة...',
noTenders: 'لم يتم العثور على مناقصات.',
loadError: 'فشل تحميل المناقصات',
createSuccess: 'تم إنشاء المناقصة بنجاح',
duplicateWarning: 'يوجد مناقصات مشابهة. يرجى المراجعة.',
directives: 'التوجيهات',
addDirective: 'إضافة توجيه',
directiveType: 'نوع التوجيه',
assignee: 'الموظف المسؤول',
convertToDeal: 'تحويل إلى فرصة',
history: 'السجل',
attachments: 'المرفقات',
uploadFile: 'رفع ملف',
completeTask: 'إتمام المهمة',
completionNotes: 'ملاحظات الإنجاز'
},
contacts: {
title: 'جهات الاتصال',
addContact: 'إضافة جهة اتصال',