This commit is contained in:
yotakii
2026-03-05 11:57:04 +03:00
58 changed files with 5302 additions and 1608 deletions

View File

@@ -83,7 +83,7 @@ async function main() {
console.log('\n🌱 Running seed...\n');
const backendDir = path.resolve(__dirname, '..');
execSync('node prisma/seed-prod.js', {
execSync('node prisma/seed.js', {
stdio: 'inherit',
cwd: backendDir,
env: process.env,

View File

@@ -0,0 +1,12 @@
-- Ensure GM has all module permissions
-- Run: npx prisma db execute --file prisma/ensure-gm-permissions.sql
INSERT INTO position_permissions (id, "positionId", module, resource, actions, "createdAt", "updatedAt")
SELECT gen_random_uuid(), p.id, m.module, '*', '["*"]', NOW(), NOW()
FROM positions p
CROSS JOIN (VALUES ('contacts'), ('crm'), ('inventory'), ('projects'), ('hr'), ('marketing'), ('admin')) AS m(module)
WHERE p.code = 'GM'
AND NOT EXISTS (
SELECT 1 FROM position_permissions pp
WHERE pp."positionId" = p.id AND pp.module = m.module AND pp.resource = '*'
);

View File

@@ -0,0 +1,131 @@
-- AlterTable: Add attendancePin to employees
ALTER TABLE "employees" ADD COLUMN IF NOT EXISTS "attendancePin" TEXT;
-- CreateIndex (unique) on attendancePin - only if column added
CREATE UNIQUE INDEX IF NOT EXISTS "employees_attendancePin_key" ON "employees"("attendancePin");
-- AlterTable: Add ZK Tico fields to attendances
ALTER TABLE "attendances" ADD COLUMN IF NOT EXISTS "sourceDeviceId" TEXT;
ALTER TABLE "attendances" ADD COLUMN IF NOT EXISTS "externalId" TEXT;
ALTER TABLE "attendances" ADD COLUMN IF NOT EXISTS "rawData" JSONB;
-- CreateIndex on sourceDeviceId
CREATE INDEX IF NOT EXISTS "attendances_sourceDeviceId_idx" ON "attendances"("sourceDeviceId");
-- CreateTable: loans
CREATE TABLE "loans" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"loanNumber" TEXT NOT NULL,
"type" TEXT NOT NULL,
"amount" DECIMAL(12,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'SAR',
"installments" INTEGER NOT NULL DEFAULT 1,
"monthlyAmount" DECIMAL(12,2),
"reason" TEXT,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"approvedBy" TEXT,
"approvedAt" TIMESTAMP(3),
"rejectedReason" TEXT,
"startDate" DATE,
"endDate" DATE,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "loans_pkey" PRIMARY KEY ("id")
);
-- CreateTable: loan_installments
CREATE TABLE "loan_installments" (
"id" TEXT NOT NULL,
"loanId" TEXT NOT NULL,
"installmentNumber" INTEGER NOT NULL,
"dueDate" DATE NOT NULL,
"amount" DECIMAL(12,2) NOT NULL,
"paidDate" DATE,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "loan_installments_pkey" PRIMARY KEY ("id")
);
-- CreateTable: purchase_requests
CREATE TABLE "purchase_requests" (
"id" TEXT NOT NULL,
"requestNumber" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"items" JSONB NOT NULL,
"totalAmount" DECIMAL(12,2),
"reason" TEXT,
"priority" TEXT NOT NULL DEFAULT 'NORMAL',
"status" TEXT NOT NULL DEFAULT 'PENDING',
"approvedBy" TEXT,
"approvedAt" TIMESTAMP(3),
"rejectedReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "purchase_requests_pkey" PRIMARY KEY ("id")
);
-- CreateTable: leave_entitlements
CREATE TABLE "leave_entitlements" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"year" INTEGER NOT NULL,
"leaveType" TEXT NOT NULL,
"totalDays" INTEGER NOT NULL DEFAULT 0,
"usedDays" INTEGER NOT NULL DEFAULT 0,
"carriedOver" INTEGER NOT NULL DEFAULT 0,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "leave_entitlements_pkey" PRIMARY KEY ("id")
);
-- CreateTable: employee_contracts
CREATE TABLE "employee_contracts" (
"id" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"contractNumber" TEXT NOT NULL,
"type" TEXT NOT NULL,
"startDate" DATE NOT NULL,
"endDate" DATE,
"salary" DECIMAL(12,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'SAR',
"documentUrl" TEXT,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "employee_contracts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "loans_loanNumber_key" ON "loans"("loanNumber");
CREATE INDEX "loans_employeeId_idx" ON "loans"("employeeId");
CREATE INDEX "loans_status_idx" ON "loans"("status");
CREATE UNIQUE INDEX "loan_installments_loanId_installmentNumber_key" ON "loan_installments"("loanId", "installmentNumber");
CREATE INDEX "loan_installments_loanId_idx" ON "loan_installments"("loanId");
CREATE UNIQUE INDEX "purchase_requests_requestNumber_key" ON "purchase_requests"("requestNumber");
CREATE INDEX "purchase_requests_employeeId_idx" ON "purchase_requests"("employeeId");
CREATE INDEX "purchase_requests_status_idx" ON "purchase_requests"("status");
CREATE UNIQUE INDEX "leave_entitlements_employeeId_year_leaveType_key" ON "leave_entitlements"("employeeId", "year", "leaveType");
CREATE INDEX "leave_entitlements_employeeId_idx" ON "leave_entitlements"("employeeId");
CREATE UNIQUE INDEX "employee_contracts_contractNumber_key" ON "employee_contracts"("contractNumber");
CREATE INDEX "employee_contracts_employeeId_idx" ON "employee_contracts"("employeeId");
CREATE INDEX "employee_contracts_status_idx" ON "employee_contracts"("status");
-- AddForeignKey
ALTER TABLE "loans" ADD CONSTRAINT "loans_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "loan_installments" ADD CONSTRAINT "loan_installments_loanId_fkey" FOREIGN KEY ("loanId") REFERENCES "loans"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "purchase_requests" ADD CONSTRAINT "purchase_requests_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "leave_entitlements" ADD CONSTRAINT "leave_entitlements_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "employee_contracts" ADD CONSTRAINT "employee_contracts_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,59 @@
-- CreateTable
CREATE TABLE "roles" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"nameAr" TEXT,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "role_permissions" (
"id" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"module" TEXT NOT NULL,
"resource" TEXT NOT NULL,
"actions" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_roles" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
-- CreateIndex
CREATE UNIQUE INDEX "role_permissions_roleId_module_resource_key" ON "role_permissions"("roleId", "module", "resource");
-- CreateIndex
CREATE INDEX "user_roles_userId_idx" ON "user_roles"("userId");
-- CreateIndex
CREATE INDEX "user_roles_roleId_idx" ON "user_roles"("roleId");
-- CreateIndex
CREATE UNIQUE INDEX "user_roles_userId_roleId_key" ON "user_roles"("userId", "roleId");
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -69,10 +69,59 @@ model User {
assignedTasks Task[]
projectMembers ProjectMember[]
campaigns Campaign[]
userRoles UserRole[]
@@map("users")
}
// Optional roles - user can belong to multiple permission groups (Phase 3 multi-group)
model Role {
id String @id @default(uuid())
name String @unique
nameAr String?
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
permissions RolePermission[]
userRoles UserRole[]
@@map("roles")
}
model RolePermission {
id String @id @default(uuid())
roleId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
module String
resource String
actions Json // ["read", "create", "update", "delete", ...]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([roleId, module, resource])
@@map("role_permissions")
}
model UserRole {
id String @id @default(uuid())
userId String
roleId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, roleId])
@@index([userId])
@@index([roleId])
@@map("user_roles")
}
model Employee {
id String @id @default(uuid())
uniqueEmployeeId String @unique // رقم الموظف الموحد
@@ -129,6 +178,9 @@ model Employee {
// Documents
documents Json? // Array of document references
// ZK Tico / Attendance device - maps to employee pin on device
attendancePin String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -143,6 +195,10 @@ model Employee {
disciplinaryActions DisciplinaryAction[]
allowances Allowance[]
commissions Commission[]
loans Loan[]
purchaseRequests PurchaseRequest[]
leaveEntitlements LeaveEntitlement[]
employeeContracts EmployeeContract[]
@@index([departmentId])
@@index([positionId])
@@ -221,12 +277,18 @@ model Attendance {
status String // PRESENT, ABSENT, LATE, HALF_DAY, etc.
notes String?
// ZK Tico / External device sync
sourceDeviceId String?
externalId String?
rawData Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([employeeId, date])
@@index([employeeId])
@@index([date])
@@index([sourceDeviceId])
@@map("attendances")
}
@@ -369,6 +431,115 @@ model DisciplinaryAction {
@@map("disciplinary_actions")
}
model Loan {
id String @id @default(uuid())
employeeId String
employee Employee @relation(fields: [employeeId], references: [id])
loanNumber String @unique
type String // SALARY_ADVANCE, EQUIPMENT, PERSONAL, etc.
amount Decimal @db.Decimal(12, 2)
currency String @default("SAR")
installments Int @default(1)
monthlyAmount Decimal? @db.Decimal(12, 2)
reason String?
status String @default("PENDING") // PENDING, APPROVED, REJECTED, ACTIVE, PAID_OFF
approvedBy String?
approvedAt DateTime?
rejectedReason String?
startDate DateTime? @db.Date
endDate DateTime? @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
installmentsList LoanInstallment[]
@@index([employeeId])
@@index([status])
@@map("loans")
}
model LoanInstallment {
id String @id @default(uuid())
loanId String
loan Loan @relation(fields: [loanId], references: [id], onDelete: Cascade)
installmentNumber Int
dueDate DateTime @db.Date
amount Decimal @db.Decimal(12, 2)
paidDate DateTime? @db.Date
status String @default("PENDING") // PENDING, PAID, OVERDUE
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([loanId, installmentNumber])
@@index([loanId])
@@map("loan_installments")
}
model PurchaseRequest {
id String @id @default(uuid())
requestNumber String @unique
employeeId String
employee Employee @relation(fields: [employeeId], references: [id])
items Json // Array of { description, quantity, estimatedPrice, etc. }
totalAmount Decimal? @db.Decimal(12, 2)
reason String?
priority String @default("NORMAL") // LOW, NORMAL, HIGH, URGENT
status String @default("PENDING") // PENDING, APPROVED, REJECTED, ORDERED
approvedBy String?
approvedAt DateTime?
rejectedReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([employeeId])
@@index([status])
@@map("purchase_requests")
}
model LeaveEntitlement {
id String @id @default(uuid())
employeeId String
employee Employee @relation(fields: [employeeId], references: [id])
year Int
leaveType String // ANNUAL, SICK, EMERGENCY, etc.
totalDays Int @default(0)
usedDays Int @default(0)
carriedOver Int @default(0)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([employeeId, year, leaveType])
@@index([employeeId])
@@map("leave_entitlements")
}
model EmployeeContract {
id String @id @default(uuid())
employeeId String
employee Employee @relation(fields: [employeeId], references: [id])
contractNumber String @unique
type String // FIXED, UNLIMITED, PROBATION, etc.
startDate DateTime @db.Date
endDate DateTime? @db.Date
salary Decimal @db.Decimal(12, 2)
currency String @default("SAR")
documentUrl String?
status String @default("ACTIVE") // DRAFT, ACTIVE, EXPIRED, TERMINATED
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([employeeId])
@@index([status])
@@map("employee_contracts")
}
// ============================================
// MODULE 1: CONTACT MANAGEMENT
// ============================================

146
backend/prisma/seed.js Normal file
View File

@@ -0,0 +1,146 @@
/**
* Minimal seed - System Administrator only.
* Run with: node prisma/seed.js
*/
const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcryptjs');
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting database seeding (minimal - System Administrator only)...');
const adminDept = await prisma.department.create({
data: {
name: 'Administration',
nameAr: 'الإدارة',
code: 'ADMIN',
description: 'System administration and configuration',
},
});
const sysAdminPosition = await prisma.position.create({
data: {
title: 'System Administrator',
titleAr: 'مدير النظام',
code: 'SYS_ADMIN',
departmentId: adminDept.id,
level: 1,
description: 'Full system access - configure and manage all modules',
},
});
const modules = ['contacts', 'crm', 'inventory', 'projects', 'hr', 'marketing', 'admin'];
for (const module of modules) {
await prisma.positionPermission.create({
data: {
positionId: sysAdminPosition.id,
module,
resource: '*',
actions: ['*'],
},
});
}
// Create Sales Department and restricted positions
const salesDept = await prisma.department.create({
data: {
name: 'Sales',
nameAr: 'المبيعات',
code: 'SALES',
description: 'Sales and business development',
},
});
const salesRepPosition = await prisma.position.create({
data: {
title: 'Sales Representative',
titleAr: 'مندوب مبيعات',
code: 'SALES_REP',
departmentId: salesDept.id,
level: 3,
description: 'Limited access - Contacts and CRM deals',
},
});
await prisma.positionPermission.createMany({
data: [
{ positionId: salesRepPosition.id, module: 'contacts', resource: '*', actions: ['read', 'create', 'update'] },
{ positionId: salesRepPosition.id, module: 'crm', resource: 'deals', actions: ['read', 'create', 'update'] },
],
});
const accountantPosition = await prisma.position.create({
data: {
title: 'Accountant',
titleAr: 'محاسب',
code: 'ACCOUNTANT',
departmentId: adminDept.id,
level: 2,
description: 'HR read, inventory read, contacts read',
},
});
await prisma.positionPermission.createMany({
data: [
{ positionId: accountantPosition.id, module: 'contacts', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'crm', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'inventory', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'hr', resource: '*', actions: ['read'] },
],
});
console.log('✅ Created position and permissions');
const sysAdminEmployee = await prisma.employee.create({
data: {
uniqueEmployeeId: 'SYS-001',
firstName: 'System',
lastName: 'Administrator',
firstNameAr: 'مدير',
lastNameAr: 'النظام',
email: 'admin@system.local',
mobile: '+966500000000',
dateOfBirth: new Date('1990-01-01'),
gender: 'MALE',
nationality: 'Saudi',
employmentType: 'Full-time',
contractType: 'Unlimited',
hireDate: new Date(),
departmentId: adminDept.id,
positionId: sysAdminPosition.id,
basicSalary: 0,
status: 'ACTIVE',
},
});
const hashedPassword = await bcrypt.hash('Admin@123', 10);
await prisma.user.create({
data: {
email: 'admin@system.local',
username: 'admin',
password: hashedPassword,
employeeId: sysAdminEmployee.id,
isActive: true,
},
});
console.log('✅ Created System Administrator');
console.log('\n🎉 Database seeding completed!\n');
console.log('📋 System Administrator:');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' Email: admin@system.local');
console.log(' Username: admin');
console.log(' Password: Admin@123');
console.log(' Access: Full system access (all modules)');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
}
main()
.catch((e) => {
console.error('❌ Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -4,58 +4,50 @@ import bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting database seeding...');
console.log('🌱 Starting database seeding (minimal - System Administrator only)...');
// Create Departments
// Create Administration Department
const adminDept = await prisma.department.create({
data: {
name: 'Administration',
nameAr: 'الإدارة',
code: 'ADMIN',
description: 'System administration and configuration',
},
});
// Create System Administrator Position
const sysAdminPosition = await prisma.position.create({
data: {
title: 'System Administrator',
titleAr: 'مدير النظام',
code: 'SYS_ADMIN',
departmentId: adminDept.id,
level: 1,
description: 'Full system access - configure and manage all modules',
},
});
// Create full permissions for all modules
const modules = ['contacts', 'crm', 'inventory', 'projects', 'hr', 'marketing', 'admin'];
for (const module of modules) {
await prisma.positionPermission.create({
data: {
positionId: sysAdminPosition.id,
module,
resource: '*',
actions: ['*'],
},
});
}
// Create Sales Department and restricted positions
const salesDept = await prisma.department.create({
data: {
name: 'Sales Department',
nameAr: 'قسم المبيعات',
name: 'Sales',
nameAr: 'المبيعات',
code: 'SALES',
description: 'Sales and Business Development',
},
});
const itDept = await prisma.department.create({
data: {
name: 'IT Department',
nameAr: 'قسم تقنية المعلومات',
code: 'IT',
description: 'Information Technology',
},
});
const hrDept = await prisma.department.create({
data: {
name: 'HR Department',
nameAr: 'قسم الموارد البشرية',
code: 'HR',
description: 'Human Resources',
},
});
console.log('✅ Created departments');
// Create Positions
const gmPosition = await prisma.position.create({
data: {
title: 'General Manager',
titleAr: 'المدير العام',
code: 'GM',
departmentId: salesDept.id,
level: 1,
description: 'Chief Executive - Full Access',
},
});
const salesManagerPosition = await prisma.position.create({
data: {
title: 'Sales Manager',
titleAr: 'مدير المبيعات',
code: 'SALES_MGR',
departmentId: salesDept.id,
level: 2,
description: 'Sales Department Manager',
description: 'Sales and business development',
},
});
@@ -66,342 +58,83 @@ async function main() {
code: 'SALES_REP',
departmentId: salesDept.id,
level: 3,
description: 'Sales Representative',
description: 'Limited access - Contacts and CRM deals',
},
});
console.log('✅ Created positions');
const itSupportPosition = await prisma.position.create({
data: {
title: 'IT Support',
titleAr: 'دعم فني',
code: 'IT_SUPPORT',
departmentId: itDept.id,
level: 4,
description: 'IT Support Technician',
},
});
const itDeveloperPosition = await prisma.position.create({
data: {
title: 'Developer',
titleAr: 'مطور',
code: 'IT_DEV',
departmentId: itDept.id,
level: 4,
description: 'Software Developer',
},
});
// Employee position for ALL departments (added)
const salesEmployeePosition = await prisma.position.create({
data: {
title: 'Employee',
titleAr: 'موظف',
code: 'SALES_EMPLOYEE',
departmentId: salesDept.id,
level: 5,
description: 'General employee - Sales Department',
},
});
const itEmployeePosition = await prisma.position.create({
data: {
title: 'Employee',
titleAr: 'موظف',
code: 'IT_EMPLOYEE',
departmentId: itDept.id,
level: 5,
description: 'General employee - IT Department',
},
});
const hrEmployeePosition = await prisma.position.create({
data: {
title: 'Employee',
titleAr: 'موظف',
code: 'HR_EMPLOYEE',
departmentId: hrDept.id,
level: 5,
description: 'General employee - HR Department',
},
});
// Create Permissions for GM (Full Access)
const modules = ['contacts', 'crm', 'inventory', 'projects', 'hr', 'marketing'];
const resources = ['*'];
const actions = ['*'];
for (const module of modules) {
await prisma.positionPermission.create({
data: {
positionId: gmPosition.id,
module,
resource: resources[0],
actions,
},
});
}
// Admin permission for GM
await prisma.positionPermission.create({
data: {
positionId: gmPosition.id,
module: 'admin',
resource: '*',
actions: ['*'],
},
});
// Create Permissions for Sales Manager
await prisma.positionPermission.createMany({
data: [
{
positionId: salesManagerPosition.id,
module: 'contacts',
resource: 'contacts',
actions: ['create', 'read', 'update', 'merge'],
},
{
positionId: salesManagerPosition.id,
module: 'crm',
resource: 'deals',
actions: ['create', 'read', 'update', 'approve'],
},
{
positionId: salesManagerPosition.id,
module: 'crm',
resource: 'quotes',
actions: ['create', 'read', 'update', 'approve'],
},
{ positionId: salesRepPosition.id, module: 'contacts', resource: '*', actions: ['read', 'create', 'update'] },
{ positionId: salesRepPosition.id, module: 'crm', resource: 'deals', actions: ['read', 'create', 'update'] },
],
});
// Create Permissions for Sales Rep
const accountantPosition = await prisma.position.create({
data: {
title: 'Accountant',
titleAr: 'محاسب',
code: 'ACCOUNTANT',
departmentId: adminDept.id,
level: 2,
description: 'HR read, inventory read, contacts read',
},
});
await prisma.positionPermission.createMany({
data: [
{
positionId: salesRepPosition.id,
module: 'contacts',
resource: 'contacts',
actions: ['create', 'read', 'update'],
},
{
positionId: salesRepPosition.id,
module: 'crm',
resource: 'deals',
actions: ['create', 'read', 'update'],
},
{
positionId: salesRepPosition.id,
module: 'crm',
resource: 'quotes',
actions: ['create', 'read'],
},
{ positionId: accountantPosition.id, module: 'contacts', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'crm', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'inventory', resource: '*', actions: ['read'] },
{ positionId: accountantPosition.id, module: 'hr', resource: '*', actions: ['read'] },
],
});
console.log('✅ Created permissions');
console.log('✅ Created position and permissions');
// Create Employees
const gmEmployee = await prisma.employee.create({
// Create minimal Employee for System Administrator
const sysAdminEmployee = await prisma.employee.create({
data: {
uniqueEmployeeId: 'EMP-2024-0001',
firstName: 'Ahmed',
lastName: 'Al-Mutairi',
firstNameAr: 'أحمد',
lastNameAr: 'المطيري',
email: 'gm@atmata.com',
mobile: '+966500000001',
dateOfBirth: new Date('1980-01-01'),
uniqueEmployeeId: 'SYS-001',
firstName: 'System',
lastName: 'Administrator',
firstNameAr: 'مدير',
lastNameAr: 'النظام',
email: 'admin@system.local',
mobile: '+966500000000',
dateOfBirth: new Date('1990-01-01'),
gender: 'MALE',
nationality: 'Saudi',
employmentType: 'Full-time',
contractType: 'Unlimited',
hireDate: new Date('2020-01-01'),
departmentId: salesDept.id,
positionId: gmPosition.id,
basicSalary: 50000,
hireDate: new Date(),
departmentId: adminDept.id,
positionId: sysAdminPosition.id,
basicSalary: 0,
status: 'ACTIVE',
},
});
const salesManagerEmployee = await prisma.employee.create({
data: {
uniqueEmployeeId: 'EMP-2024-0002',
firstName: 'Fatima',
lastName: 'Al-Zahrani',
firstNameAr: 'فاطمة',
lastNameAr: 'الزهراني',
email: 'sales.manager@atmata.com',
mobile: '+966500000002',
dateOfBirth: new Date('1985-05-15'),
gender: 'FEMALE',
nationality: 'Saudi',
employmentType: 'Full-time',
contractType: 'Unlimited',
hireDate: new Date('2021-06-01'),
departmentId: salesDept.id,
positionId: salesManagerPosition.id,
reportingToId: gmEmployee.id,
basicSalary: 25000,
status: 'ACTIVE',
},
});
const salesRepEmployee = await prisma.employee.create({
data: {
uniqueEmployeeId: 'EMP-2024-0003',
firstName: 'Mohammed',
lastName: 'Al-Qahtani',
firstNameAr: 'محمد',
lastNameAr: 'القحطاني',
email: 'sales.rep@atmata.com',
mobile: '+966500000003',
dateOfBirth: new Date('1992-08-20'),
gender: 'MALE',
nationality: 'Saudi',
employmentType: 'Full-time',
contractType: 'Fixed',
hireDate: new Date('2023-01-15'),
departmentId: salesDept.id,
positionId: salesRepPosition.id,
reportingToId: salesManagerEmployee.id,
basicSalary: 12000,
status: 'ACTIVE',
},
});
console.log('✅ Created employees');
// Create Users
// Create System Administrator User
const hashedPassword = await bcrypt.hash('Admin@123', 10);
const gmUser = await prisma.user.create({
await prisma.user.create({
data: {
email: 'gm@atmata.com',
email: 'admin@system.local',
username: 'admin',
password: hashedPassword,
employeeId: gmEmployee.id,
employeeId: sysAdminEmployee.id,
isActive: true,
},
});
const salesManagerUser = await prisma.user.create({
data: {
email: 'sales.manager@atmata.com',
username: 'salesmanager',
password: hashedPassword,
employeeId: salesManagerEmployee.id,
isActive: true,
},
});
console.log('✅ Created System Administrator');
const salesRepUser = await prisma.user.create({
data: {
email: 'sales.rep@atmata.com',
username: 'salesrep',
password: hashedPassword,
employeeId: salesRepEmployee.id,
isActive: true,
},
});
console.log('✅ Created users');
// Create Contact Categories
await prisma.contactCategory.createMany({
data: [
{ name: 'Customer', nameAr: 'عميل', description: 'Paying customers' },
{ name: 'Supplier', nameAr: 'مورد', description: 'Product/Service suppliers' },
{ name: 'Partner', nameAr: 'شريك', description: 'Business partners' },
{ name: 'Lead', nameAr: 'عميل محتمل', description: 'Potential customers' },
{ name: 'Company Employee', nameAr: 'موظف الشركة', description: 'Internal company staff' },
],
});
console.log('✅ Created contact categories');
// Create Product Categories
await prisma.productCategory.createMany({
data: [
{ name: 'Electronics', nameAr: 'إلكترونيات', code: 'ELEC' },
{ name: 'Software', nameAr: 'برمجيات', code: 'SOFT' },
{ name: 'Services', nameAr: 'خدمات', code: 'SERV' },
],
});
console.log('✅ Created product categories');
// Create Pipelines
await prisma.pipeline.create({
data: {
name: 'B2B Sales Pipeline',
nameAr: 'مسار مبيعات الشركات',
structure: 'B2B',
stages: [
{ name: 'OPEN', nameAr: 'مفتوحة', order: 1 },
{ name: 'QUALIFIED', nameAr: 'مؤهلة', order: 2 },
{ name: 'NEGOTIATION', nameAr: 'تفاوض', order: 3 },
{ name: 'PROPOSAL', nameAr: 'عرض سعر', order: 4 },
{ name: 'WON', nameAr: 'فازت', order: 5 },
{ name: 'LOST', nameAr: 'خسرت', order: 6 },
],
isActive: true,
},
});
await prisma.pipeline.create({
data: {
name: 'B2C Sales Pipeline',
nameAr: 'مسار مبيعات الأفراد',
structure: 'B2C',
stages: [
{ name: 'LEAD', nameAr: 'عميل محتمل', order: 1 },
{ name: 'CONTACTED', nameAr: 'تم التواصل', order: 2 },
{ name: 'QUALIFIED', nameAr: 'مؤهل', order: 3 },
{ name: 'WON', nameAr: 'بيع', order: 4 },
{ name: 'LOST', nameAr: 'خسارة', order: 5 },
],
isActive: true,
},
});
console.log('✅ Created pipelines');
// Create sample warehouse
await prisma.warehouse.create({
data: {
code: 'WH-MAIN',
name: 'Main Warehouse',
nameAr: 'المستودع الرئيسي',
type: 'MAIN',
city: 'Riyadh',
country: 'Saudi Arabia',
isActive: true,
},
});
console.log('✅ Created warehouse');
console.log('\n🎉 Database seeding completed successfully!\n');
console.log('📋 Default Users Created:');
console.log('\n🎉 Database seeding completed!\n');
console.log('📋 System Administrator:');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('1. General Manager');
console.log(' Email: gm@atmata.com');
console.log(' Email: admin@system.local');
console.log(' Username: admin');
console.log(' Password: Admin@123');
console.log(' Access: Full System Access');
console.log('');
console.log('2. Sales Manager');
console.log(' Email: sales.manager@atmata.com');
console.log(' Password: Admin@123');
console.log(' Access: Contacts, CRM with approvals');
console.log('');
console.log('3. Sales Representative');
console.log(' Email: sales.rep@atmata.com');
console.log(' Password: Admin@123');
console.log(' Access: Basic Contacts and CRM');
console.log(' Access: Full system access (all modules)');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
}
@@ -413,4 +146,3 @@ main()
.finally(async () => {
await prisma.$disconnect();
});