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
This commit is contained in:
Talal Sharabi
2026-01-06 18:43:43 +04:00
commit 35daa52767
82 changed files with 29445 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../../config';
import { AppError } from './errorHandler';
import prisma from '../../config/database';
export interface AuthRequest extends Request {
user?: {
id: string;
email: string;
employeeId?: string;
employee?: any;
};
}
export const authenticate = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
try {
// Get token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new AppError(401, 'غير مصرح - Unauthorized');
}
const token = authHeader.split(' ')[1];
// Verify token
const decoded = jwt.verify(token, config.jwt.secret) as {
id: string;
email: string;
};
// Get user with employee info
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: {
employee: {
include: {
position: {
include: {
permissions: true,
},
},
department: true,
},
},
},
});
if (!user || !user.isActive) {
throw new AppError(401, 'غير مصرح - Unauthorized');
}
// HR module requirement: User must have an active employee record
if (!user.employee || user.employee.status !== 'ACTIVE') {
throw new AppError(403, 'الوصول مرفوض - Access denied. Active employee record required.');
}
// Attach user to request
req.user = {
id: user.id,
email: user.email,
employeeId: user.employeeId || undefined,
employee: user.employee,
};
next();
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
return next(new AppError(401, 'رمز غير صالح - Invalid token'));
}
next(error);
}
};
// Permission checking middleware
export const authorize = (module: string, resource: string, action: string) => {
return async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
if (!req.user?.employee?.position?.permissions) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}
// Find permission for this module and resource
const permission = req.user.employee.position.permissions.find(
(p: any) => p.module === module && p.resource === resource
);
if (!permission) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}
// Check if action is allowed
const actions = permission.actions as string[];
if (!actions.includes(action) && !actions.includes('*')) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}
next();
} catch (error) {
next(error);
}
};
};

View File

@@ -0,0 +1,66 @@
import { Request, Response, NextFunction } from 'express';
import { Prisma } from '@prisma/client';
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
}
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
) => {
console.error('Error:', err);
// Handle Prisma errors
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
return res.status(409).json({
success: false,
message: 'سجل مكرر - Duplicate record',
error: 'DUPLICATE_RECORD',
});
}
if (err.code === 'P2025') {
return res.status(404).json({
success: false,
message: 'السجل غير موجود - Record not found',
error: 'RECORD_NOT_FOUND',
});
}
}
// Handle validation errors
if (err instanceof Prisma.PrismaClientValidationError) {
return res.status(400).json({
success: false,
message: 'بيانات غير صالحة - Invalid data',
error: 'VALIDATION_ERROR',
});
}
// Handle custom app errors
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
message: err.message,
error: err.message,
});
}
// Default error
res.status(500).json({
success: false,
message: 'خطأ في الخادم - Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : 'INTERNAL_ERROR',
});
};

View File

@@ -0,0 +1,11 @@
import { Request, Response } from 'express';
export const notFoundHandler = (req: Request, res: Response) => {
res.status(404).json({
success: false,
message: 'الصفحة غير موجودة - Route not found',
error: 'NOT_FOUND',
path: req.originalUrl,
});
};

View File

@@ -0,0 +1,16 @@
import { Request, Response, NextFunction } from 'express';
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const { method, originalUrl } = req;
const { statusCode } = res;
console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} ${statusCode} - ${duration}ms`);
});
next();
};

View File

@@ -0,0 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
export const validate = (req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'بيانات غير صالحة - Validation error',
errors: errors.array(),
});
}
next();
};

View File

@@ -0,0 +1,56 @@
import prisma from '../../config/database';
interface AuditLogData {
entityType: string;
entityId: string;
action: string;
userId: string;
changes?: any;
ipAddress?: string;
userAgent?: string;
reason?: string;
}
export class AuditLogger {
static async log(data: AuditLogData): Promise<void> {
try {
await prisma.auditLog.create({
data: {
entityType: data.entityType,
entityId: data.entityId,
action: data.action,
userId: data.userId,
changes: data.changes || {},
ipAddress: data.ipAddress,
userAgent: data.userAgent,
reason: data.reason,
},
});
} catch (error) {
console.error('Failed to create audit log:', error);
// Don't throw - audit logging should not break the main flow
}
}
static async getEntityHistory(entityType: string, entityId: string) {
return prisma.auditLog.findMany({
where: {
entityType,
entityId,
},
include: {
user: {
select: {
id: true,
email: true,
username: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
}
}

View File

@@ -0,0 +1,31 @@
export class ResponseFormatter {
static success(data: any, message?: string) {
return {
success: true,
message: message || 'تم بنجاح - Success',
data,
};
}
static paginated(data: any[], total: number, page: number, pageSize: number) {
return {
success: true,
data,
pagination: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
};
}
static error(message: string, error?: string) {
return {
success: false,
message,
error,
};
}
}