garage-erp/apps/dashboard/modules/employees/employee-general-info.tsx
humam kerdiah 3c55be5052 feat: add payroll, time clocks, and leave request modules
- Implemented PayrollPage for managing payroll runs with CRUD operations.
- Created TimeClocksPage for tracking employee clock-ins and clock-outs.
- Developed TimeSheetsPage for displaying employee timesheets.
- Added EmployeeCertificationForm and EmployeeDocumentForm for managing employee certifications and documents.
- Introduced LeaveRequestForm for submitting and managing leave requests.
- Added usePermissions hook for UI-side permission checks.
- Created LeaveRequestsClient and PayrollClient for API interactions.
2026-05-21 13:30:06 +04:00

216 lines
9.2 KiB
TypeScript

import {
User,
Mail,
Phone,
Briefcase,
Building2,
Clock,
MapPin,
Calendar,
BadgeCheck,
CircleDot,
Cake,
Heart,
IdCard,
Home,
PhoneCall,
Landmark,
Wallet,
PercentIcon,
DollarSign,
} from "lucide-react"
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
import { Badge } from "@/shared/components/ui/badge"
import { Separator } from "@/shared/components/ui/separator"
import { formatEnum } from "@/shared/utils/formatters"
type EmployeeData = {
id?: number
type?: string
first_name?: string
last_name?: string
email?: string
phone?: string
position?: string
designation?: string
salary?: string
wage_type?: string
status?: string
track_attendance?: boolean
notify_owner_when_punch_in_out?: boolean
geo_fence_radius?: string | number | null
department?: { id?: number; name?: string } | null
shop_calender?: { id?: number; title?: string } | null
shop_timing?: { id?: number; title?: string } | null
role?: { id?: number; name?: string } | null
avatar_url?: string | null
avatar_path?: string | null
address?: string | null
date_of_birth?: string | null
gender?: string | null
marital_status?: string | null
national_id?: string | null
hire_date?: string | null
emergency_contact_name?: string | null
emergency_contact_phone?: string | null
bank_name?: string | null
bank_account_number?: string | null
bank_iban?: string | null
hourly_rate?: string | number | null
commission_rate?: string | number | null
created_at?: string
updated_at?: string
}
type EmployeeGeneralInfoProps = {
employee: EmployeeData
}
function formatDate(value?: string | null): string | null {
if (!value) return null
const d = new Date(value)
if (Number.isNaN(d.getTime())) return value
return d.toLocaleDateString()
}
function initialsOf(first?: string | null, last?: string | null) {
const f = (first ?? "").trim()[0] ?? ""
const l = (last ?? "").trim()[0] ?? ""
return (f + l).toUpperCase() || "?"
}
function InfoItem({
icon: Icon,
label,
value,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value?: string | number | null
}) {
return (
<div className="flex items-start gap-3">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium">
{value != null && value !== "" ? value : <span className="text-muted-foreground"></span>}
</span>
</div>
</div>
)
}
export function EmployeeGeneralInfo({ employee }: EmployeeGeneralInfoProps) {
const fullName = [employee.first_name, employee.last_name].filter(Boolean).join(" ")
const avatarSrc = employee.avatar_url ?? (employee.avatar_path ? `/storage/${employee.avatar_path}` : null)
return (
<div className="grid gap-6 md:grid-cols-2">
{/* Personal Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="size-4" />
Personal Information
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex items-center gap-3">
<Avatar size="lg">
{avatarSrc ? <AvatarImage src={avatarSrc} alt={fullName} /> : null}
<AvatarFallback>{initialsOf(employee.first_name, employee.last_name)}</AvatarFallback>
</Avatar>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{fullName || "Unknown"}</Badge>
{employee.type && (
<Badge variant="outline">
{formatEnum(employee.type)}
</Badge>
)}
{employee.status && (
<Badge
variant={employee.status === "active" ? "default" : "secondary"}
>
{formatEnum(employee.status)}
</Badge>
)}
</div>
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Mail} label="Email" value={employee.email} />
<InfoItem icon={Phone} label="Phone" value={employee.phone} />
<InfoItem icon={Cake} label="Date of Birth" value={formatDate(employee.date_of_birth)} />
<InfoItem icon={Heart} label="Marital Status" value={employee.marital_status ? formatEnum(employee.marital_status) : null} />
<InfoItem icon={User} label="Gender" value={employee.gender ? formatEnum(employee.gender) : null} />
<InfoItem icon={IdCard} label="National ID" value={employee.national_id} />
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Home} label="Address" value={employee.address} />
<InfoItem icon={PhoneCall} label="Emergency Contact" value={[employee.emergency_contact_name, employee.emergency_contact_phone].filter(Boolean).join(" • ")} />
</div>
</CardContent>
</Card>
{/* Employment + Compensation */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="size-4" />
Employment
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Briefcase} label="Designation" value={employee.designation} />
<InfoItem icon={Calendar} label="Hire Date" value={formatDate(employee.hire_date)} />
<InfoItem icon={Building2} label="Department" value={employee.department?.name} />
<InfoItem icon={BadgeCheck} label="Role" value={employee.role?.name} />
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={DollarSign} label="Salary" value={employee.salary ? Number(employee.salary).toFixed(2) : null} />
<InfoItem icon={Wallet} label="Wage Type" value={employee.wage_type} />
<InfoItem icon={Clock} label="Hourly Rate" value={employee.hourly_rate != null ? Number(employee.hourly_rate).toFixed(2) : null} />
<InfoItem icon={PercentIcon} label="Commission %" value={employee.commission_rate != null ? Number(employee.commission_rate).toFixed(2) : null} />
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Landmark} label="Bank" value={employee.bank_name} />
<InfoItem icon={Wallet} label="Account #" value={employee.bank_account_number} />
<InfoItem icon={Landmark} label="IBAN" value={employee.bank_iban} />
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem icon={Calendar} label="Shop Calendar" value={employee.shop_calender?.title} />
<InfoItem icon={Clock} label="Shop Timing" value={employee.shop_timing?.title} />
<InfoItem icon={MapPin} label="Geo Fence Radius (m)" value={employee.geo_fence_radius != null ? String(employee.geo_fence_radius) : null} />
</div>
<Separator />
<div className="flex flex-wrap gap-2">
<Badge variant={employee.track_attendance ? "default" : "secondary"}>
<CircleDot className="mr-1 size-3" />
{employee.track_attendance ? "Attendance Tracked" : "No Attendance Tracking"}
</Badge>
{employee.notify_owner_when_punch_in_out && (
<Badge variant="outline">
Punch In/Out Notifications
</Badge>
)}
</div>
</CardContent>
</Card>
</div>
)
}