add suppliers
This commit is contained in:
@@ -29,6 +29,7 @@ class ContactsController {
|
||||
const filters = {
|
||||
search: req.query.search as string,
|
||||
type: req.query.type as string,
|
||||
specialization: req.query.specialization as string,
|
||||
status: req.query.status as string,
|
||||
category: req.query.category as string,
|
||||
source: req.query.source as string,
|
||||
|
||||
@@ -43,7 +43,7 @@ router.post(
|
||||
authorize('contacts', 'contacts', 'create'),
|
||||
[
|
||||
body('type').isIn(['INDIVIDUAL', 'COMPANY', 'HOLDING', 'GOVERNMENT','ORGANIZATION','EMBASSIES',
|
||||
'BANK','UNIVERSITY','SCHOOL','UN','NGO','INSTITUTION',]),
|
||||
'BANK','UNIVERSITY','SCHOOL','UN','NGO','INSTITUTION','SUPPLIER',]),
|
||||
body('name').notEmpty().trim(),
|
||||
body('email').optional().isEmail(),
|
||||
body('source').notEmpty(),
|
||||
@@ -73,6 +73,7 @@ router.put(
|
||||
'UN',
|
||||
'NGO',
|
||||
'INSTITUTION',
|
||||
'SUPPLIER',
|
||||
]),
|
||||
body('email')
|
||||
.optional({ values: 'falsy' })
|
||||
|
||||
@@ -36,6 +36,7 @@ interface UpdateContactData extends Partial<CreateContactData> {
|
||||
interface SearchFilters {
|
||||
search?: string;
|
||||
type?: string;
|
||||
specialization?: string;
|
||||
status?: string;
|
||||
category?: string;
|
||||
source?: string;
|
||||
@@ -148,6 +149,12 @@ class ContactsService {
|
||||
where.type = filters.type;
|
||||
}
|
||||
|
||||
if (filters.specialization) {
|
||||
where.tags = {
|
||||
has: filters.specialization,
|
||||
};
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,22 @@ if (!fs.existsSync(expenseClaimsUploadDir)) {
|
||||
fs.mkdirSync(expenseClaimsUploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const decodeOriginalFileName = (name: string) => {
|
||||
return Buffer.from(name, 'latin1').toString('utf8');
|
||||
};
|
||||
|
||||
const expenseClaimStorage = multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, expenseClaimsUploadDir),
|
||||
filename: (_req, file, cb) => {
|
||||
const safeName = (file.originalname || 'file').replace(/[^a-zA-Z0-9.-]/g, '_');
|
||||
cb(null, `${crypto.randomUUID()}-${safeName}`);
|
||||
const originalName = decodeOriginalFileName(file.originalname);
|
||||
const ext = path.extname(originalName);
|
||||
const safeBaseName = path
|
||||
.basename(originalName, ext)
|
||||
.replace(/[^a-zA-Z0-9\u0600-\u06FF._-]/g, '_');
|
||||
|
||||
(file as any).decodedOriginalName = originalName;
|
||||
|
||||
cb(null, `${crypto.randomUUID()}-${safeBaseName}${ext}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import prisma from '../../config/database';
|
||||
import { AppError } from '../../shared/middleware/errorHandler';
|
||||
import { AuditLogger } from '../../shared/utils/auditLogger';
|
||||
import { notificationsService } from '../notifications/notifications.service';
|
||||
|
||||
class HRService {
|
||||
// ========== EMPLOYEES ==========
|
||||
@@ -352,15 +353,40 @@ class HRService {
|
||||
},
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LEAVE',
|
||||
entityId: leave.id,
|
||||
action: 'CREATE',
|
||||
userId,
|
||||
});
|
||||
|
||||
const employeeFullName = `${leave.employee.firstName} ${leave.employee.lastName}`;
|
||||
|
||||
await notificationsService.notifyApprovalRecipients({
|
||||
resource: 'leave_requests',
|
||||
fallbackEmployeeId: leave.employeeId,
|
||||
fallbackToManager: true,
|
||||
type: 'LEAVE_REQUEST_SUBMITTED',
|
||||
title: 'طلب إجازة جديد بانتظار الموافقة',
|
||||
message: `قام الموظف ${employeeFullName} بإرسال طلب إجازة جديد.`,
|
||||
entityType: 'LEAVE',
|
||||
entityId: leave.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: leave.employeeId,
|
||||
type: 'LEAVE_REQUEST_CREATED',
|
||||
title: 'تم إرسال طلب الإجازة',
|
||||
message: 'تم إرسال طلب الإجازة الخاص بك بنجاح وهو الآن بانتظار المراجعة.',
|
||||
entityType: 'LEAVE',
|
||||
entityId: leave.id,
|
||||
excludeUserIds: [],
|
||||
});
|
||||
|
||||
return leave;
|
||||
}
|
||||
|
||||
async approveLeave(id: string, approvedBy: string, userId: string) {
|
||||
const leave = await prisma.leave.update({
|
||||
where: { id },
|
||||
@@ -378,16 +404,25 @@ class HRService {
|
||||
const year = new Date(leave.startDate).getFullYear();
|
||||
await this.updateLeaveEntitlementUsed(leave.employeeId, year, leave.leaveType, leave.days);
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LEAVE',
|
||||
entityId: leave.id,
|
||||
action: 'APPROVE',
|
||||
userId,
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: leave.employeeId,
|
||||
type: 'LEAVE_REQUEST_APPROVED',
|
||||
title: 'تمت الموافقة على طلب الإجازة',
|
||||
message: 'تمت الموافقة على طلب الإجازة الخاص بك.',
|
||||
entityType: 'LEAVE',
|
||||
entityId: leave.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return leave;
|
||||
}
|
||||
|
||||
async rejectLeave(id: string, rejectedReason: string, userId: string) {
|
||||
const leave = await prisma.leave.findUnique({ where: { id } });
|
||||
if (!leave) throw new AppError(404, 'طلب الإجازة غير موجود - Leave request not found');
|
||||
@@ -403,13 +438,26 @@ class HRService {
|
||||
include: { employee: true },
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LEAVE',
|
||||
entityId: id,
|
||||
action: 'REJECT',
|
||||
userId,
|
||||
reason: rejectedReason,
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: updated.employeeId,
|
||||
type: 'LEAVE_REQUEST_REJECTED',
|
||||
title: 'تم رفض طلب الإجازة',
|
||||
message: rejectedReason
|
||||
? `تم رفض طلب الإجازة الخاص بك. السبب: ${rejectedReason}`
|
||||
: 'تم رفض طلب الإجازة الخاص بك.',
|
||||
entityType: 'LEAVE',
|
||||
entityId: updated.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -566,13 +614,23 @@ async findManagedLeaves(status?: string) {
|
||||
const year = new Date(updated.startDate).getFullYear();
|
||||
await this.updateLeaveEntitlementUsed(updated.employeeId, year, updated.leaveType, updated.days);
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LEAVE',
|
||||
entityId: updated.id,
|
||||
action: 'MANAGER_APPROVE',
|
||||
userId,
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: updated.employeeId,
|
||||
type: 'LEAVE_REQUEST_APPROVED',
|
||||
title: 'تمت الموافقة على طلب الإجازة',
|
||||
message: 'تمت الموافقة على طلب الإجازة الخاص بك من قبل المدير المباشر.',
|
||||
entityType: 'LEAVE',
|
||||
entityId: updated.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -610,7 +668,7 @@ async findManagedLeaves(status?: string) {
|
||||
},
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LEAVE',
|
||||
entityId: updated.id,
|
||||
action: 'MANAGER_REJECT',
|
||||
@@ -618,6 +676,18 @@ async findManagedLeaves(status?: string) {
|
||||
reason: rejectedReason,
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: updated.employeeId,
|
||||
type: 'LEAVE_REQUEST_REJECTED',
|
||||
title: 'تم رفض طلب الإجازة',
|
||||
message: rejectedReason
|
||||
? `تم رفض طلب الإجازة الخاص بك من قبل المدير المباشر. السبب: ${rejectedReason}`
|
||||
: 'تم رفض طلب الإجازة الخاص بك من قبل المدير المباشر.',
|
||||
entityType: 'LEAVE',
|
||||
entityId: updated.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -843,7 +913,30 @@ private isSystemAdminUser(user: any) {
|
||||
include: { employee: true },
|
||||
});
|
||||
|
||||
await AuditLogger.log({ entityType: 'LOAN', entityId: loan.id, action: 'CREATE', userId });
|
||||
await AuditLogger.log({ entityType: 'LOAN', entityId: loan.id, action: 'CREATE', userId });
|
||||
|
||||
const employeeFullName = `${loan.employee.firstName} ${loan.employee.lastName}`;
|
||||
|
||||
await notificationsService.notifyApprovalRecipients({
|
||||
resource: 'loan_requests',
|
||||
type: 'LOAN_REQUEST_SUBMITTED',
|
||||
title: 'طلب قرض جديد بانتظار الموافقة',
|
||||
message: `قام الموظف ${employeeFullName} بإرسال طلب قرض جديد برقم ${loan.loanNumber}.`,
|
||||
entityType: 'LOAN',
|
||||
entityId: loan.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: loan.employeeId,
|
||||
type: 'LOAN_REQUEST_CREATED',
|
||||
title: 'تم إرسال طلب القرض',
|
||||
message: `تم إرسال طلب القرض الخاص بك برقم ${loan.loanNumber} وهو الآن بانتظار المراجعة.`,
|
||||
entityType: 'LOAN',
|
||||
entityId: loan.id,
|
||||
excludeUserIds: [],
|
||||
});
|
||||
|
||||
return loan;
|
||||
}
|
||||
|
||||
@@ -889,7 +982,7 @@ private isSystemAdminUser(user: any) {
|
||||
const loanAmount = Number(loan.amount || 0);
|
||||
const needsAdminApproval = basicSalary > 0 && loanAmount > basicSalary * 0.5;
|
||||
|
||||
// المرحلة الأولى: HR approval
|
||||
|
||||
if (loan.status === 'PENDING_HR') {
|
||||
if (needsAdminApproval) {
|
||||
const updatedLoan = await prisma.loan.update({
|
||||
@@ -899,13 +992,36 @@ private isSystemAdminUser(user: any) {
|
||||
},
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LOAN',
|
||||
entityId: id,
|
||||
action: 'HR_APPROVE_FORWARD_TO_ADMIN',
|
||||
userId,
|
||||
});
|
||||
|
||||
const fullLoan = await this.findLoanById(id);
|
||||
const employeeFullName = `${fullLoan.employee.firstName} ${fullLoan.employee.lastName}`;
|
||||
|
||||
await notificationsService.notifyApprovalRecipients({
|
||||
resource: 'loan_requests',
|
||||
type: 'LOAN_REQUEST_PENDING_ADMIN',
|
||||
title: 'طلب قرض محال إلى مدير النظام',
|
||||
message: `تمت إحالة طلب القرض رقم ${fullLoan.loanNumber} الخاص بالموظف ${employeeFullName} إلى مدير النظام لاعتماده النهائي.`,
|
||||
entityType: 'LOAN',
|
||||
entityId: id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: fullLoan.employee.id,
|
||||
type: 'LOAN_REQUEST_ESCALATED',
|
||||
title: 'تمت إحالة طلب القرض للاعتماد النهائي',
|
||||
message: `تمت الموافقة المبدئية على طلب القرض رقم ${fullLoan.loanNumber} وإحالته للاعتماد النهائي.`,
|
||||
entityType: 'LOAN',
|
||||
entityId: id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return updatedLoan;
|
||||
}
|
||||
}
|
||||
@@ -954,14 +1070,26 @@ private isSystemAdminUser(user: any) {
|
||||
),
|
||||
]);
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LOAN',
|
||||
entityId: id,
|
||||
action: loan.status === 'PENDING_ADMIN' ? 'ADMIN_APPROVE' : 'APPROVE',
|
||||
userId,
|
||||
});
|
||||
|
||||
return this.findLoanById(id);
|
||||
const approvedLoan = await this.findLoanById(id);
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: approvedLoan.employee.id,
|
||||
type: 'LOAN_REQUEST_APPROVED',
|
||||
title: 'تمت الموافقة على طلب القرض',
|
||||
message: `تمت الموافقة على طلب القرض الخاص بك برقم ${approvedLoan.loanNumber}.`,
|
||||
entityType: 'LOAN',
|
||||
entityId: approvedLoan.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return approvedLoan;
|
||||
}
|
||||
|
||||
async rejectLoan(id: string, rejectedReason: string, userId: string) {
|
||||
@@ -981,7 +1109,7 @@ private isSystemAdminUser(user: any) {
|
||||
include: { employee: true },
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
await AuditLogger.log({
|
||||
entityType: 'LOAN',
|
||||
entityId: id,
|
||||
action: 'REJECT',
|
||||
@@ -989,6 +1117,18 @@ private isSystemAdminUser(user: any) {
|
||||
reason: rejectedReason,
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: loan.employeeId,
|
||||
type: 'LOAN_REQUEST_REJECTED',
|
||||
title: 'تم رفض طلب القرض',
|
||||
message: rejectedReason?.trim()
|
||||
? `تم رفض طلب القرض الخاص بك. السبب: ${rejectedReason.trim()}`
|
||||
: 'تم رفض طلب القرض الخاص بك.',
|
||||
entityType: 'LOAN',
|
||||
entityId: loan.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return loan;
|
||||
}
|
||||
async recordLoanInstallmentPayment(loanId: string, installmentId: string, paidDate: Date, userId: string) {
|
||||
@@ -1063,7 +1203,30 @@ private isSystemAdminUser(user: any) {
|
||||
},
|
||||
include: { employee: true },
|
||||
});
|
||||
await AuditLogger.log({ entityType: 'PURCHASE_REQUEST', entityId: req.id, action: 'CREATE', userId });
|
||||
await AuditLogger.log({ entityType: 'PURCHASE_REQUEST', entityId: req.id, action: 'CREATE', userId });
|
||||
|
||||
const employeeFullName = `${req.employee.firstName} ${req.employee.lastName}`;
|
||||
|
||||
await notificationsService.notifyApprovalRecipients({
|
||||
resource: 'purchase_requests',
|
||||
type: 'PURCHASE_REQUEST_SUBMITTED',
|
||||
title: 'طلب شراء جديد بانتظار الموافقة',
|
||||
message: `قام الموظف ${employeeFullName} بإرسال طلب شراء جديد برقم ${req.requestNumber}.`,
|
||||
entityType: 'PURCHASE_REQUEST',
|
||||
entityId: req.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: req.employeeId,
|
||||
type: 'PURCHASE_REQUEST_CREATED',
|
||||
title: 'تم إرسال طلب الشراء',
|
||||
message: `تم إرسال طلب الشراء الخاص بك برقم ${req.requestNumber} وهو الآن بانتظار المراجعة.`,
|
||||
entityType: 'PURCHASE_REQUEST',
|
||||
entityId: req.id,
|
||||
excludeUserIds: [],
|
||||
});
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
@@ -1073,8 +1236,19 @@ private isSystemAdminUser(user: any) {
|
||||
data: { status: 'APPROVED', approvedBy, approvedAt: new Date(), rejectedReason: null },
|
||||
include: { employee: true },
|
||||
});
|
||||
await AuditLogger.log({ entityType: 'PURCHASE_REQUEST', entityId: id, action: 'APPROVE', userId });
|
||||
return req;
|
||||
await AuditLogger.log({ entityType: 'PURCHASE_REQUEST', entityId: id, action: 'APPROVE', userId });
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: req.employeeId,
|
||||
type: 'PURCHASE_REQUEST_APPROVED',
|
||||
title: 'تمت الموافقة على طلب الشراء',
|
||||
message: `تمت الموافقة على طلب الشراء الخاص بك برقم ${req.requestNumber}.`,
|
||||
entityType: 'PURCHASE_REQUEST',
|
||||
entityId: req.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return req;;
|
||||
}
|
||||
|
||||
async rejectPurchaseRequest(id: string, rejectedReason: string, userId: string) {
|
||||
@@ -1084,6 +1258,19 @@ private isSystemAdminUser(user: any) {
|
||||
include: { employee: true },
|
||||
});
|
||||
await AuditLogger.log({ entityType: 'PURCHASE_REQUEST', entityId: id, action: 'REJECT', userId, reason: rejectedReason });
|
||||
|
||||
await notificationsService.notifyEmployeeUser({
|
||||
employeeId: req.employeeId,
|
||||
type: 'PURCHASE_REQUEST_REJECTED',
|
||||
title: 'تم رفض طلب الشراء',
|
||||
message: rejectedReason?.trim()
|
||||
? `تم رفض طلب الشراء الخاص بك برقم ${req.requestNumber}. السبب: ${rejectedReason.trim()}`
|
||||
: `تم رفض طلب الشراء الخاص بك برقم ${req.requestNumber}.`,
|
||||
entityType: 'PURCHASE_REQUEST',
|
||||
entityId: req.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import prisma from '../../config/database';
|
||||
import { AppError } from '../../shared/middleware/errorHandler';
|
||||
import { AuditLogger } from '../../shared/utils/auditLogger';
|
||||
import { notificationsService } from '../notifications/notifications.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import path from 'path';
|
||||
import fs from 'fs'
|
||||
@@ -208,11 +209,25 @@ class TendersService {
|
||||
return cleanedNotes ? `${cleanedNotes}\n${metaBlock}` : metaBlock;
|
||||
}
|
||||
|
||||
private mapTenderExtraFields<T extends { notes?: string | null; bondValue?: any }>(tender: T) {
|
||||
private getComputedTenderStatus<T extends { status?: string | null; closingDate?: Date | string | null }>(tender: T) {
|
||||
if (tender.status === 'CONVERTED_TO_DEAL' || tender.status === 'CANCELLED') {
|
||||
return tender.status;
|
||||
}
|
||||
|
||||
if (tender.closingDate && new Date(tender.closingDate) < new Date()) {
|
||||
return 'EXPIRED';
|
||||
}
|
||||
|
||||
return tender.status || 'ACTIVE';
|
||||
}
|
||||
|
||||
private mapTenderExtraFields<T extends { notes?: string | null; bondValue?: any; status?: string | null; closingDate?: Date | string | null }>(tender: T) {
|
||||
const { cleanNotes, meta } = this.extractTenderExtraMeta(tender.notes);
|
||||
|
||||
return {
|
||||
...tender,
|
||||
status: this.getComputedTenderStatus(tender),
|
||||
originalStatus: tender.status,
|
||||
notes: cleanNotes || null,
|
||||
initialBondValue: meta.initialBondValue ?? Number(tender.bondValue ?? 0),
|
||||
finalBondValue: meta.finalBondValue ?? null,
|
||||
@@ -345,7 +360,9 @@ class TendersService {
|
||||
{ issuingBodyName: { contains: filters.search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
if (filters.status) where.status = filters.status;
|
||||
if (filters.status && filters.status !== 'EXPIRED') {
|
||||
where.status = filters.status;
|
||||
}
|
||||
if (filters.source) where.source = filters.source;
|
||||
if (filters.announcementType) where.announcementType = filters.announcementType;
|
||||
|
||||
@@ -361,9 +378,15 @@ class TendersService {
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return {
|
||||
tenders: tenders.map((t) => this.mapTenderExtraFields(t)),
|
||||
total,
|
||||
const mappedTenders = tenders.map((t) => this.mapTenderExtraFields(t));
|
||||
const filteredTenders =
|
||||
filters.status === 'EXPIRED'
|
||||
? mappedTenders.filter((t: any) => t.status === 'EXPIRED')
|
||||
: mappedTenders;
|
||||
|
||||
return {
|
||||
tenders: filteredTenders,
|
||||
total: filters.status === 'EXPIRED' ? filteredTenders.length : total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
@@ -517,20 +540,20 @@ class TendersService {
|
||||
},
|
||||
});
|
||||
|
||||
const assignedUser = directive.assignedToEmployee?.user;
|
||||
if (assignedUser?.id) {
|
||||
const typeLabel = this.getDirectiveTypeLabel(data.type);
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: assignedUser.id,
|
||||
type: 'TENDER_DIRECTIVE_ASSIGNED',
|
||||
title: `مهمة مناقصة - Tender task: ${tender.tenderNumber}`,
|
||||
message: `${tender.title} (${tender.tenderNumber}): ${typeLabel}. ${data.notes || ''}`,
|
||||
entityType: 'TENDER_DIRECTIVE',
|
||||
entityId: directive.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
const assignedUser = directive.assignedToEmployee?.user;
|
||||
if (assignedUser?.id) {
|
||||
const typeLabel = this.getDirectiveTypeLabel(data.type);
|
||||
|
||||
await notificationsService.notifyMany({
|
||||
userIds: [assignedUser.id],
|
||||
type: 'TENDER_DIRECTIVE_ASSIGNED',
|
||||
title: 'تم إسناد توجيه مناقصة جديد',
|
||||
message: `تم إسناد توجيه "${typeLabel}" لك في المناقصة ${tender.tenderNumber}${data.notes?.trim() ? ` - ${data.notes.trim()}` : ''}`,
|
||||
entityType: 'TENDER',
|
||||
entityId: tender.id,
|
||||
excludeUserIds: [userId],
|
||||
});
|
||||
}
|
||||
|
||||
await AuditLogger.log({
|
||||
entityType: 'TENDER_DIRECTIVE',
|
||||
@@ -678,7 +701,17 @@ class TendersService {
|
||||
return deal;
|
||||
}
|
||||
|
||||
async uploadTenderAttachment(
|
||||
private decodeUploadedFileName(fileName: string) {
|
||||
if (!fileName) return 'file';
|
||||
|
||||
try {
|
||||
return Buffer.from(fileName, 'latin1').toString('utf8');
|
||||
} catch {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
async uploadTenderAttachment(
|
||||
tenderId: string,
|
||||
file: { path: string; originalname: string; mimetype: string; size: number },
|
||||
userId: string,
|
||||
@@ -686,14 +719,17 @@ class TendersService {
|
||||
) {
|
||||
const tender = await prisma.tender.findUnique({ where: { id: tenderId } });
|
||||
if (!tender) throw new AppError(404, 'Tender not found');
|
||||
|
||||
const fileName = path.basename(file.path);
|
||||
const originalName = this.decodeUploadedFileName(file.originalname);
|
||||
|
||||
const attachment = await prisma.attachment.create({
|
||||
data: {
|
||||
entityType: 'TENDER',
|
||||
entityId: tenderId,
|
||||
tenderId,
|
||||
fileName,
|
||||
originalName: file.originalname,
|
||||
originalName,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
@@ -701,6 +737,7 @@ class TendersService {
|
||||
uploadedBy: userId,
|
||||
},
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
entityType: 'TENDER',
|
||||
entityId: tenderId,
|
||||
@@ -708,6 +745,7 @@ class TendersService {
|
||||
userId,
|
||||
changes: { attachmentUploaded: attachment.id },
|
||||
});
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
@@ -721,8 +759,12 @@ class TendersService {
|
||||
where: { id: directiveId },
|
||||
select: { id: true, tenderId: true },
|
||||
});
|
||||
|
||||
if (!directive) throw new AppError(404, 'Directive not found');
|
||||
|
||||
const fileName = path.basename(file.path);
|
||||
const originalName = this.decodeUploadedFileName(file.originalname);
|
||||
|
||||
const attachment = await prisma.attachment.create({
|
||||
data: {
|
||||
entityType: 'TENDER_DIRECTIVE',
|
||||
@@ -730,7 +772,7 @@ class TendersService {
|
||||
tenderDirectiveId: directiveId,
|
||||
tenderId: directive.tenderId,
|
||||
fileName,
|
||||
originalName: file.originalname,
|
||||
originalName,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
@@ -738,6 +780,7 @@ class TendersService {
|
||||
uploadedBy: userId,
|
||||
},
|
||||
});
|
||||
|
||||
await AuditLogger.log({
|
||||
entityType: 'TENDER_DIRECTIVE',
|
||||
entityId: directiveId,
|
||||
@@ -745,9 +788,9 @@ class TendersService {
|
||||
userId,
|
||||
changes: { attachmentUploaded: attachment.id },
|
||||
});
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
async getAttachmentFile(attachmentId: string): Promise<string> {
|
||||
const attachment = await prisma.attachment.findUnique({
|
||||
where: { id: attachmentId },
|
||||
@@ -765,12 +808,10 @@ class TendersService {
|
||||
|
||||
if (!attachment) throw new AppError(404, 'File not found')
|
||||
|
||||
// حذف من الديسك
|
||||
if (attachment.path && fs.existsSync(attachment.path)) {
|
||||
fs.unlinkSync(attachment.path)
|
||||
}
|
||||
|
||||
// حذف من DB
|
||||
await prisma.attachment.delete({
|
||||
where: { id: attachmentId },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user