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

@@ -0,0 +1,232 @@
import { Response, NextFunction } from 'express';
import { AuthRequest } from '../../shared/middleware/auth';
import { tendersService } from './tenders.service';
import { ResponseFormatter } from '../../shared/utils/responseFormatter';
export class TendersController {
async checkDuplicates(req: AuthRequest, res: Response, next: NextFunction) {
try {
const duplicates = await tendersService.findPossibleDuplicates(req.body);
res.json(ResponseFormatter.success(duplicates));
} catch (error) {
next(error);
}
}
async create(req: AuthRequest, res: Response, next: NextFunction) {
try {
const result = await tendersService.create(req.body, req.user!.id);
res.status(201).json(
ResponseFormatter.success(
result,
'تم إنشاء المناقصة بنجاح - Tender created successfully'
)
);
} catch (error) {
next(error);
}
}
async findAll(req: AuthRequest, res: Response, next: NextFunction) {
try {
const page = parseInt(req.query.page as string) || 1;
const pageSize = parseInt(req.query.pageSize as string) || 20;
const filters = {
search: req.query.search,
status: req.query.status,
source: req.query.source,
announcementType: req.query.announcementType,
};
const result = await tendersService.findAll(filters, page, pageSize);
res.json(
ResponseFormatter.paginated(
result.tenders,
result.total,
result.page,
result.pageSize
)
);
} catch (error) {
next(error);
}
}
async findById(req: AuthRequest, res: Response, next: NextFunction) {
try {
const tender = await tendersService.findById(req.params.id);
res.json(ResponseFormatter.success(tender));
} catch (error) {
next(error);
}
}
async update(req: AuthRequest, res: Response, next: NextFunction) {
try {
const tender = await tendersService.update(
req.params.id,
req.body,
req.user!.id
);
res.json(
ResponseFormatter.success(
tender,
'تم تحديث المناقصة بنجاح - Tender updated successfully'
)
);
} catch (error) {
next(error);
}
}
async createDirective(req: AuthRequest, res: Response, next: NextFunction) {
try {
const directive = await tendersService.createDirective(
req.params.id,
req.body,
req.user!.id
);
res.status(201).json(
ResponseFormatter.success(
directive,
'تم إصدار التوجيه بنجاح - Directive created successfully'
)
);
} catch (error) {
next(error);
}
}
async updateDirective(req: AuthRequest, res: Response, next: NextFunction) {
try {
const directive = await tendersService.updateDirective(
req.params.directiveId,
req.body,
req.user!.id
);
res.json(
ResponseFormatter.success(
directive,
'تم تحديث التوجيه بنجاح - Directive updated successfully'
)
);
} catch (error) {
next(error);
}
}
async getHistory(req: AuthRequest, res: Response, next: NextFunction) {
try {
const history = await tendersService.getHistory(req.params.id);
res.json(ResponseFormatter.success(history));
} catch (error) {
next(error);
}
}
async convertToDeal(req: AuthRequest, res: Response, next: NextFunction) {
try {
const deal = await tendersService.convertToDeal(
req.params.id,
{
contactId: req.body.contactId,
pipelineId: req.body.pipelineId,
ownerId: req.body.ownerId,
},
req.user!.id
);
res.status(201).json(
ResponseFormatter.success(
deal,
'تم تحويل المناقصة إلى فرصة بنجاح - Tender converted to deal successfully'
)
);
} catch (error) {
next(error);
}
}
async getSourceValues(_req: AuthRequest, res: Response, next: NextFunction) {
try {
const values = tendersService.getSourceValues();
res.json(ResponseFormatter.success(values));
} catch (error) {
next(error);
}
}
async getAnnouncementTypeValues(
_req: AuthRequest,
res: Response,
next: NextFunction
) {
try {
const values = tendersService.getAnnouncementTypeValues();
res.json(ResponseFormatter.success(values));
} catch (error) {
next(error);
}
}
async getDirectiveTypeValues(
_req: AuthRequest,
res: Response,
next: NextFunction
) {
try {
const values = tendersService.getDirectiveTypeValues();
res.json(ResponseFormatter.success(values));
} catch (error) {
next(error);
}
}
async uploadTenderAttachment(req: AuthRequest, res: Response, next: NextFunction) {
try {
if (!req.file) {
return res.status(400).json(
ResponseFormatter.error('No file uploaded', 'Missing file')
);
}
const attachment = await tendersService.uploadTenderAttachment(
req.params.id,
req.file,
req.user!.id,
(req.body.category as string) || undefined
);
res.status(201).json(
ResponseFormatter.success(
attachment,
'تم رفع الملف بنجاح - File uploaded successfully'
)
);
} catch (error) {
next(error);
}
}
async uploadDirectiveAttachment(req: AuthRequest, res: Response, next: NextFunction) {
try {
if (!req.file) {
return res.status(400).json(
ResponseFormatter.error('No file uploaded', 'Missing file')
);
}
const attachment = await tendersService.uploadDirectiveAttachment(
req.params.directiveId,
req.file,
req.user!.id,
(req.body.category as string) || undefined
);
res.status(201).json(
ResponseFormatter.success(
attachment,
'تم رفع الملف بنجاح - File uploaded successfully'
)
);
} catch (error) {
next(error);
}
}
}
export const tendersController = new TendersController();