- 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.
216 lines
9.2 KiB
TypeScript
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>
|
|
)
|
|
}
|