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:
@@ -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;
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user