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

@@ -0,0 +1,163 @@
import { api } from '../api'
export interface PortalProfile {
employee: {
id: string
uniqueEmployeeId: string
firstName: string
lastName: string
firstNameAr?: string | null
lastNameAr?: string | null
email: string
department?: { name: string; nameAr?: string | null }
position?: { title: string; titleAr?: string | null }
}
stats: {
activeLoansCount: number
pendingLeavesCount: number
pendingPurchaseRequestsCount: number
leaveBalance: Array<{
leaveType: string
totalDays: number
carriedOver: number
usedDays: number
available: number
}>
}
}
export interface Loan {
id: string
loanNumber: string
type: string
amount: number
currency: string
installments: number
monthlyAmount?: number
reason?: string | null
status: string
approvedBy?: string | null
approvedAt?: string | null
rejectedReason?: string | null
startDate?: string | null
endDate?: string | null
createdAt: string
installmentsList?: Array<{
id: string
installmentNumber: number
dueDate: string
amount: number
paidDate?: string | null
status: string
}>
}
export interface Leave {
id: string
leaveType: string
startDate: string
endDate: string
days: number
reason?: string | null
status: string
approvedBy?: string | null
approvedAt?: string | null
rejectedReason?: string | null
createdAt: string
}
export interface PurchaseRequest {
id: string
requestNumber: string
items: any[]
totalAmount?: number | null
reason?: string | null
priority: string
status: string
approvedBy?: string | null
approvedAt?: string | null
rejectedReason?: string | null
createdAt: string
}
export interface Attendance {
id: string
date: string
checkIn?: string | null
checkOut?: string | null
workHours?: number | null
overtimeHours?: number | null
status: string
}
export interface Salary {
id: string
month: number
year: number
basicSalary: number
allowances: number
deductions: number
commissions: number
overtimePay: number
netSalary: number
status: string
paidDate?: string | null
createdAt: string
}
export const portalAPI = {
getMe: async (): Promise<PortalProfile> => {
const response = await api.get('/hr/portal/me')
return response.data.data
},
getLoans: async (): Promise<Loan[]> => {
const response = await api.get('/hr/portal/loans')
return response.data.data || []
},
submitLoanRequest: async (data: { type: string; amount: number; installments?: number; reason?: string }): Promise<Loan> => {
const response = await api.post('/hr/portal/loans', data)
return response.data.data
},
getLeaveBalance: async (year?: number): Promise<PortalProfile['stats']['leaveBalance']> => {
const params = year ? `?year=${year}` : ''
const response = await api.get(`/hr/portal/leave-balance${params}`)
return response.data.data || []
},
getLeaves: async (): Promise<Leave[]> => {
const response = await api.get('/hr/portal/leaves')
return response.data.data || []
},
submitLeaveRequest: async (data: { leaveType: string; startDate: string; endDate: string; reason?: string }): Promise<Leave> => {
const response = await api.post('/hr/portal/leaves', data)
return response.data.data
},
getPurchaseRequests: async (): Promise<PurchaseRequest[]> => {
const response = await api.get('/hr/portal/purchase-requests')
return response.data.data || []
},
submitPurchaseRequest: async (data: { items: Array<{ description: string; quantity?: number; estimatedPrice?: number }>; reason?: string; priority?: string }): Promise<PurchaseRequest> => {
const response = await api.post('/hr/portal/purchase-requests', data)
return response.data.data
},
getAttendance: async (month?: number, year?: number): Promise<Attendance[]> => {
const params = new URLSearchParams()
if (month) params.append('month', String(month))
if (year) params.append('year', String(year))
const query = params.toString() ? `?${params.toString()}` : ''
const response = await api.get(`/hr/portal/attendance${query}`)
return response.data.data || []
},
getSalaries: async (): Promise<Salary[]> => {
const response = await api.get('/hr/portal/salaries')
return response.data.data || []
},
}