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

97 lines
4.1 KiB
TypeScript

"use client"
import { ResourcePage } from "@/shared/data-view/resource-page"
import { ColumnHeader } from "@/shared/data-view/table-view"
import { Badge } from "@/shared/components/ui/badge"
import { TIME_SHEET_ROUTES } from "@garage/api"
import type { TimeSheetsClient } from "@garage/api"
import { ClockIcon } from "lucide-react"
import { formatEnum } from "@/shared/utils/formatters"
function formatDate(value: unknown) {
if (!value || typeof value !== "string") return "—"
const d = new Date(value)
return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()
}
function formatTime(value: unknown) {
if (!value || typeof value !== "string") return "—"
return value.length >= 5 ? value.slice(0, 5) : value
}
const ACTIVITY_VARIANT: Record<string, "default" | "secondary" | "outline"> = {
general: "secondary",
order: "default",
task: "outline",
}
export default function TimeSheetsPage() {
return (
<ResourcePage<TimeSheetsClient>
pageTitle="Time Sheets"
routeKey={TIME_SHEET_ROUTES.INDEX}
getClient={(api) => api.timeSheets}
statusFilter={{
statuses: ["general", "order", "task"],
paramKey: "activity_type",
allLabel: "All",
}}
columns={({ actionsColumn }) => [
{
accessorKey: "date",
header: ({ column }) => <ColumnHeader column={column} title="Date" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<ClockIcon className="size-4 text-muted-foreground" />
<span>{formatDate((row.original as any).date)}</span>
</div>
),
},
{
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: "clock_in",
header: ({ column }) => <ColumnHeader column={column} title="Clock In" />,
cell: ({ row }) => <span className="font-mono text-xs">{formatTime((row.original as any).clock_in)}</span>,
},
{
accessorKey: "clock_out",
header: ({ column }) => <ColumnHeader column={column} title="Clock Out" />,
cell: ({ row }) => <span className="font-mono text-xs">{formatTime((row.original as any).clock_out)}</span>,
},
{
accessorKey: "duration",
header: ({ column }) => <ColumnHeader column={column} title="Duration" />,
cell: ({ row }) => <span className="font-mono text-xs">{formatTime((row.original as any).duration)}</span>,
},
{
accessorKey: "activity_type",
header: ({ column }) => <ColumnHeader column={column} title="Activity" />,
cell: ({ row }) => {
const t = (row.original as any).activity_type ?? "general"
return <Badge variant={ACTIVITY_VARIANT[t] ?? "secondary"}>{formatEnum(t)}</Badge>
},
},
{
accessorKey: "calculated_total_cost",
header: ({ column }) => <ColumnHeader column={column} title="Cost" />,
cell: ({ row }) => {
const v = (row.original as any).calculated_total_cost
if (v == null) return "—"
const n = Number(v)
return Number.isFinite(n) ? n.toFixed(2) : "—"
},
},
actionsColumn({ onEdit: undefined }),
]}
/>
)
}