feat(hr): Complete HR module with Employee Portal, Loans, Leave, Purchase Requests, Contracts

- Database: Add Loan, LoanInstallment, PurchaseRequest, LeaveEntitlement, EmployeeContract models
- Database: Extend Attendance with ZK Tico fields (sourceDeviceId, externalId, rawData)
- Database: Add Employee.attendancePin for device mapping
- Backend: HR admin - Loans, Purchase Requests, Leave entitlements, Employee contracts CRUD
- Backend: Leave reject, bulk attendance sync (ZK Tico ready)
- Backend: Employee Portal API - scoped by employeeId (loans, leaves, purchase-requests, attendance, salaries)
- Frontend: Employee Portal - dashboard, loans, leave, purchase-requests, attendance, salaries
- Frontend: HR Admin - new tabs for Leaves, Loans, Purchase Requests, Contracts (approve/reject)
- Dashboard: Add My Portal link
- No destructive schema changes; additive migrations only

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-04 19:44:09 +04:00
parent ae890ca1c5
commit 72ed9a2ff5
18 changed files with 2649 additions and 8 deletions

View File

@@ -178,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
@@ -192,6 +195,10 @@ model Employee {
disciplinaryActions DisciplinaryAction[]
allowances Allowance[]
commissions Commission[]
loans Loan[]
purchaseRequests PurchaseRequest[]
leaveEntitlements LeaveEntitlement[]
employeeContracts EmployeeContract[]
@@index([departmentId])
@@index([positionId])
@@ -270,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")
}
@@ -418,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
// ============================================