- 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.
153 lines
6.5 KiB
TypeScript
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(),
|
|
]}
|
|
/>
|
|
)
|
|
}
|