Files
zerp/backend/src/modules/crm/deals.service.ts
Talal Sharabi 35daa52767 feat: Complete Z.CRM system with all 6 modules
 Features:
- Complete authentication system with JWT
- Dashboard with all 6 modules visible
- Contact Management module (Salesforce-style)
- CRM & Sales Pipeline module (Pipedrive-style)
- Inventory & Assets module (SAP-style)
- Tasks & Projects module (Jira/Asana-style)
- HR Management module (BambooHR-style)
- Marketing Management module (HubSpot-style)
- Admin Panel with user management and role matrix
- World-class UI/UX with RTL Arabic support
- Cairo font (headings) + Readex Pro font (body)
- Sample data for all modules
- Protected routes and authentication flow
- Backend API with Prisma + PostgreSQL
- Comprehensive documentation

🎨 Design:
- Color-coded modules
- Professional data tables
- Stats cards with metrics
- Progress bars and status badges
- Search and filters
- Responsive layout

📊 Tech Stack:
- Frontend: Next.js 14, TypeScript, Tailwind CSS
- Backend: Node.js, Express, Prisma
- Database: PostgreSQL
- Auth: JWT with bcrypt

🚀 Production-ready frontend with all features accessible
2026-01-06 18:43:43 +04:00

399 lines
8.5 KiB
TypeScript

import prisma from '../../config/database';
import { AppError } from '../../shared/middleware/errorHandler';
import { AuditLogger } from '../../shared/utils/auditLogger';
import { Prisma } from '@prisma/client';
interface CreateDealData {
name: string;
contactId: string;
structure: string; // B2B, B2C, B2G, PARTNERSHIP
pipelineId: string;
stage: string;
estimatedValue: number;
probability?: number;
expectedCloseDate?: Date;
ownerId: string;
fiscalYear: number;
}
interface UpdateDealData extends Partial<CreateDealData> {
stage?: string;
actualValue?: number;
actualCloseDate?: Date;
wonReason?: string;
lostReason?: string;
status?: string;
}
class DealsService {
async create(data: CreateDealData, userId: string) {
// Generate deal number
const dealNumber = await this.generateDealNumber();
const deal = await prisma.deal.create({
data: {
dealNumber,
name: data.name,
contactId: data.contactId,
structure: data.structure,
pipelineId: data.pipelineId,
stage: data.stage,
estimatedValue: data.estimatedValue,
probability: data.probability,
expectedCloseDate: data.expectedCloseDate,
ownerId: data.ownerId,
fiscalYear: data.fiscalYear,
currency: 'SAR',
},
include: {
contact: {
select: {
id: true,
name: true,
email: true,
phone: true,
},
},
owner: {
select: {
id: true,
email: true,
username: true,
},
},
pipeline: true,
},
});
await AuditLogger.log({
entityType: 'DEAL',
entityId: deal.id,
action: 'CREATE',
userId,
});
return deal;
}
async findAll(filters: any, page: number, pageSize: number) {
const skip = (page - 1) * pageSize;
const where: Prisma.DealWhereInput = {};
if (filters.search) {
where.OR = [
{ name: { contains: filters.search, mode: 'insensitive' } },
{ dealNumber: { contains: filters.search } },
];
}
if (filters.structure) {
where.structure = filters.structure;
}
if (filters.stage) {
where.stage = filters.stage;
}
if (filters.status) {
where.status = filters.status;
}
if (filters.ownerId) {
where.ownerId = filters.ownerId;
}
if (filters.fiscalYear) {
where.fiscalYear = parseInt(filters.fiscalYear);
}
const total = await prisma.deal.count({ where });
const deals = await prisma.deal.findMany({
where,
skip,
take: pageSize,
include: {
contact: {
select: {
id: true,
name: true,
email: true,
},
},
owner: {
select: {
id: true,
email: true,
username: true,
},
},
pipeline: true,
},
orderBy: {
createdAt: 'desc',
},
});
return {
deals,
total,
page,
pageSize,
};
}
async findById(id: string) {
const deal = await prisma.deal.findUnique({
where: { id },
include: {
contact: {
include: {
categories: true,
},
},
owner: {
select: {
id: true,
email: true,
username: true,
employee: {
select: {
firstName: true,
lastName: true,
position: true,
department: true,
},
},
},
},
pipeline: true,
quotes: {
orderBy: {
version: 'desc',
},
},
costSheets: {
orderBy: {
version: 'desc',
},
},
activities: {
orderBy: {
createdAt: 'desc',
},
take: 20,
},
notes: {
orderBy: {
createdAt: 'desc',
},
},
attachments: {
orderBy: {
uploadedAt: 'desc',
},
},
contracts: {
orderBy: {
createdAt: 'desc',
},
},
invoices: {
orderBy: {
createdAt: 'desc',
},
},
},
});
if (!deal) {
throw new AppError(404, 'الصفقة غير موجودة - Deal not found');
}
return deal;
}
async update(id: string, data: UpdateDealData, userId: string) {
const existing = await prisma.deal.findUnique({ where: { id } });
if (!existing) {
throw new AppError(404, 'الصفقة غير موجودة - Deal not found');
}
const deal = await prisma.deal.update({
where: { id },
data: {
name: data.name,
contactId: data.contactId,
stage: data.stage,
estimatedValue: data.estimatedValue,
actualValue: data.actualValue,
probability: data.probability,
expectedCloseDate: data.expectedCloseDate,
actualCloseDate: data.actualCloseDate,
wonReason: data.wonReason,
lostReason: data.lostReason,
status: data.status,
},
include: {
contact: true,
owner: true,
pipeline: true,
},
});
await AuditLogger.log({
entityType: 'DEAL',
entityId: deal.id,
action: 'UPDATE',
userId,
changes: {
before: existing,
after: deal,
},
});
return deal;
}
async updateStage(id: string, stage: string, userId: string) {
const deal = await prisma.deal.update({
where: { id },
data: { stage },
include: {
contact: true,
owner: true,
},
});
await AuditLogger.log({
entityType: 'DEAL',
entityId: deal.id,
action: 'STAGE_CHANGE',
userId,
changes: { stage },
});
// Create notification
await prisma.notification.create({
data: {
userId: deal.ownerId,
type: 'DEAL_STAGE_CHANGED',
title: 'تغيير مرحلة الصفقة - Deal stage changed',
message: `تم تغيير مرحلة الصفقة "${deal.name}" إلى "${stage}"`,
entityType: 'DEAL',
entityId: deal.id,
},
});
return deal;
}
async win(id: string, actualValue: number, wonReason: string, userId: string) {
const deal = await prisma.deal.update({
where: { id },
data: {
status: 'WON',
stage: 'WON',
actualValue,
wonReason,
actualCloseDate: new Date(),
},
include: {
contact: true,
owner: true,
},
});
await AuditLogger.log({
entityType: 'DEAL',
entityId: deal.id,
action: 'WIN',
userId,
changes: {
status: 'WON',
actualValue,
wonReason,
},
});
// Create notification
await prisma.notification.create({
data: {
userId: deal.ownerId,
type: 'DEAL_WON',
title: '🎉 صفقة رابحة - Deal Won!',
message: `تم الفوز بالصفقة "${deal.name}" بقيمة ${actualValue} ريال`,
entityType: 'DEAL',
entityId: deal.id,
},
});
return deal;
}
async lose(id: string, lostReason: string, userId: string) {
const deal = await prisma.deal.update({
where: { id },
data: {
status: 'LOST',
stage: 'LOST',
lostReason,
actualCloseDate: new Date(),
},
include: {
contact: true,
owner: true,
},
});
await AuditLogger.log({
entityType: 'DEAL',
entityId: deal.id,
action: 'LOSE',
userId,
changes: {
status: 'LOST',
lostReason,
},
});
return deal;
}
async getHistory(id: string) {
return AuditLogger.getEntityHistory('DEAL', id);
}
private async generateDealNumber(): Promise<string> {
const year = new Date().getFullYear();
const prefix = `DEAL-${year}-`;
const lastDeal = await prisma.deal.findFirst({
where: {
dealNumber: {
startsWith: prefix,
},
},
orderBy: {
createdAt: 'desc',
},
select: {
dealNumber: true,
},
});
let nextNumber = 1;
if (lastDeal) {
const lastNumber = parseInt(lastDeal.dealNumber.split('-')[2]);
nextNumber = lastNumber + 1;
}
return `${prefix}${nextNumber.toString().padStart(6, '0')}`;
}
}
export const dealsService = new DealsService();