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:
65
frontend/src/app/portal/salaries/page.tsx
Normal file
65
frontend/src/app/portal/salaries/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { portalAPI, type Salary } from '@/lib/api/portal'
|
||||
import LoadingSpinner from '@/components/LoadingSpinner'
|
||||
import { DollarSign } from 'lucide-react'
|
||||
|
||||
const MONTHS_AR = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر']
|
||||
|
||||
export default function PortalSalariesPage() {
|
||||
const [salaries, setSalaries] = useState<Salary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
portalAPI.getSalaries()
|
||||
.then(setSalaries)
|
||||
.catch(() => setSalaries([]))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
if (loading) return <LoadingSpinner />
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">رواتبي</h1>
|
||||
|
||||
{salaries.length === 0 ? (
|
||||
<div className="bg-white rounded-xl shadow p-12 text-center text-gray-500">
|
||||
<DollarSign className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<p>لا توجد سجلات رواتب</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{salaries.map((s) => (
|
||||
<div key={s.id} className="bg-white rounded-xl shadow p-6 border border-gray-100">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">
|
||||
{MONTHS_AR[s.month - 1]} {s.year}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-teal-600 mt-1">
|
||||
{Number(s.netSalary).toLocaleString()} ر.س
|
||||
</p>
|
||||
<div className="mt-2 text-sm text-gray-600 space-y-1">
|
||||
<p>الأساس: {Number(s.basicSalary).toLocaleString()} | البدلات: {Number(s.allowances).toLocaleString()} | الخصومات: {Number(s.deductions).toLocaleString()}</p>
|
||||
<p>عمولة: {Number(s.commissions).toLocaleString()} | إضافي: {Number(s.overtimePay).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
s.status === 'PAID' ? 'bg-green-100 text-green-800' :
|
||||
s.status === 'APPROVED' ? 'bg-blue-100 text-blue-800' : 'bg-amber-100 text-amber-800'
|
||||
}`}>
|
||||
{s.status === 'PAID' ? 'مدفوع' : s.status === 'APPROVED' ? 'معتمد' : 'قيد المعالجة'}
|
||||
</span>
|
||||
</div>
|
||||
{s.paidDate && (
|
||||
<p className="text-xs text-gray-500 mt-2">تاريخ الدفع: {new Date(s.paidDate).toLocaleDateString('ar-SA')}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user