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

153 lines
6.5 KiB
TypeScript

"use client"
import { useState } from "react"
import { ResourcePage } from "@/shared/data-view/resource-page"
import { ColumnHeader } from "@/shared/data-view/table-view"
import FormDialog from "@/shared/components/form-dialog"
import { LeaveRequestForm } from "@/modules/leave-requests/leave-request-form"
import { LEAVE_REQUEST_ROUTES } from "@garage/api"
import type { LeaveRequestsClient } from "@garage/api"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { useAuthApi } from "@/shared/useApi"
import { useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { CheckIcon, XIcon } from "lucide-react"
import { confirm } from "@/shared/components/confirm-dialog"
import { formatEnum } from "@/shared/utils/formatters"
import { usePermissions } from "@/shared/hooks/use-permissions"
type LeaveRow = {
id: number
leave_type: string
start_date?: string
end_date?: string
days?: number
status: string
reason?: string | null
employee?: { id?: number; first_name?: string; last_name?: string; email?: string }
}
function formatDate(value: unknown) {
if (!value || typeof value !== "string") return "—"
const d = new Date(value)
return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()
}
function statusVariant(status: string): "default" | "secondary" | "outline" | "destructive" {
if (status === "approved") return "default"
if (status === "rejected") return "destructive"
if (status === "cancelled") return "outline"
return "secondary"
}
export default function LeaveRequestsPage() {
const api = useAuthApi()
const queryClient = useQueryClient()
const perms = usePermissions()
async function handleDecision(id: number, action: "approve" | "reject") {
const ok = await confirm({
title: action === "approve" ? "Approve request?" : "Reject request?",
description: `This will mark the leave request as ${action}d.`,
confirmLabel: action === "approve" ? "Approve" : "Reject",
variant: action === "approve" ? "default" : "destructive",
})
if (!ok) return
const promise = action === "approve"
? api.leaveRequests.approve(String(id))
: api.leaveRequests.reject(String(id))
toast.promise(promise, {
loading: action === "approve" ? "Approving..." : "Rejecting...",
success: action === "approve" ? "Approved" : "Rejected",
error: "Failed",
})
await promise
queryClient.invalidateQueries({ queryKey: [LEAVE_REQUEST_ROUTES.INDEX] })
}
return (
<ResourcePage<LeaveRequestsClient>
pageTitle="Leave Requests"
routeKey={LEAVE_REQUEST_ROUTES.INDEX}
getClient={(api) => api.leaveRequests}
statusFilter={{
statuses: ["pending", "approved", "rejected", "cancelled"],
paramKey: "status",
allLabel: "All",
}}
headerProps={({ selectedItem, invalidateQuery }) => ({
actions: perms.canCreate("leave_requests") ? (
<FormDialog title="Leave Request">
{(resourceId) => (
<LeaveRequestForm
resourceId={resourceId}
initialData={selectedItem}
onSuccess={invalidateQuery}
/>
)}
</FormDialog>
) : null,
})}
columns={({ actionsColumn }) => [
{
accessorKey: "employee",
header: ({ column }) => <ColumnHeader column={column} title="Employee" />,
cell: ({ row }) => {
const e = (row.original as any).employee
if (!e) return "—"
return `${e.first_name ?? ""} ${e.last_name ?? ""}`.trim() || e.email || "—"
},
},
{
accessorKey: "leave_type",
header: ({ column }) => <ColumnHeader column={column} title="Type" />,
cell: ({ row }) => <Badge variant="outline">{formatEnum((row.original as any).leave_type)}</Badge>,
},
{
accessorKey: "start_date",
header: ({ column }) => <ColumnHeader column={column} title="Start" />,
cell: ({ row }) => formatDate((row.original as any).start_date),
},
{
accessorKey: "end_date",
header: ({ column }) => <ColumnHeader column={column} title="End" />,
cell: ({ row }) => formatDate((row.original as any).end_date),
},
{
accessorKey: "days",
header: ({ column }) => <ColumnHeader column={column} title="Days" />,
cell: ({ row }) => (row.original as any).days ?? "—",
},
{
accessorKey: "status",
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const s = (row.original as any).status as string
return <Badge variant={statusVariant(s)}>{formatEnum(s)}</Badge>
},
},
{
id: "decide",
header: () => <span className="sr-only">Decide</span>,
cell: ({ row }) => {
const r = row.original as LeaveRow
if (r.status !== "pending" || !perms.canUpdate("leave_requests")) return null
return (
<div className="flex justify-end gap-1">
<Button size="icon" variant="ghost" onClick={() => handleDecision(r.id, "approve")}>
<CheckIcon className="size-4 text-green-600" />
</Button>
<Button size="icon" variant="ghost" onClick={() => handleDecision(r.id, "reject")}>
<XIcon className="size-4 text-destructive" />
</Button>
</div>
)
},
},
actionsColumn(),
]}
/>
)
}