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.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"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 }),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user