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

159 lines
5.7 KiB
TypeScript

"use client"
import { AlertTriangle, Plus, Save } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextField,
RhfSelectField,
RhfTextareaField,
} from "@/shared/components/form"
import { RhfEmployeeSelectField } from "@/modules/employees/rhf-employee-select-field"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { toRelation, toId } from "@/shared/lib/utils"
import { LEAVE_REQUEST_ROUTES } from "@garage/api"
import { z } from "zod"
const LEAVE_TYPES = ["annual", "sick", "unpaid", "maternity", "paternity", "other"] as const
const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()
const schema = z.object({
employee: relationFieldSchema.refine((v) => !!v?.value, "Employee is required"),
leave_type: z.enum(LEAVE_TYPES),
start_date: z.string().min(1, "Start date is required"),
end_date: z.string().min(1, "End date is required"),
reason: z.string().max(2000).optional(),
})
type LeaveFormValues = z.infer<typeof schema>
const DEFAULT_VALUES: LeaveFormValues = {
employee: null,
leave_type: "annual",
start_date: "",
end_date: "",
reason: "",
}
const TYPE_SELECT_OPTIONS = LEAVE_TYPES.map((v) => ({
value: v,
label: v.replace(/^./, (c) => c.toUpperCase()),
}))
export type LeaveRequestFormProps = {
resourceId?: string | null
initialData?: unknown
onSuccess?: () => void
/** Pre-fill the employee select when opened from an employee detail page. */
presetEmployee?: { id: number | string; label: string } | null
}
function mapToFormValues(data: unknown, preset?: { id: number | string; label: string } | null): LeaveFormValues {
const d = (data as any)?.data ?? data ?? {}
const empLabel = d.employee
? `${d.employee.first_name ?? ""} ${d.employee.last_name ?? ""}`.trim()
: ""
const formatDate = (v: unknown) => {
if (!v || typeof v !== "string") return ""
return v.length >= 10 ? v.slice(0, 10) : v
}
return {
employee: d.employee_id
? toRelation(d.employee_id, empLabel || `Employee #${d.employee_id}`)
: (preset ? toRelation(preset.id, preset.label) : null),
leave_type: d.leave_type || "annual",
start_date: formatDate(d.start_date),
end_date: formatDate(d.end_date),
reason: d.reason || "",
}
}
export function LeaveRequestForm({ resourceId, initialData, onSuccess, presetEmployee }: LeaveRequestFormProps) {
const api = useAuthApi()
const { form, isEditing } = useResourceForm<LeaveFormValues, any>({
schema,
defaultValues: presetEmployee
? { ...DEFAULT_VALUES, employee: toRelation(presetEmployee.id, presetEmployee.label) }
: DEFAULT_VALUES,
resourceId,
initialData,
initialize: (id) => api.leaveRequests.show(id),
queryKey: [LEAVE_REQUEST_ROUTES.BY_ID, resourceId],
mapToFormValues: (data) => mapToFormValues(data, presetEmployee),
})
const { mutate, error, isPending } = useFormMutation(form, {
mutationFn: (values: LeaveFormValues) => {
const payload = {
employee_id: toId(values.employee) ?? 0,
leave_type: values.leave_type,
start_date: values.start_date,
end_date: values.end_date,
reason: values.reason || undefined,
}
const promise = isEditing && resourceId
? api.leaveRequests.update(resourceId, payload)
: api.leaveRequests.create(payload as any)
toast.promise(promise, {
loading: isEditing ? "Updating..." : "Submitting...",
success: isEditing ? "Leave request updated" : "Leave request submitted",
error: isEditing ? "Failed to update" : "Failed to submit",
})
return promise
},
onSuccess: () => {
form.reset()
onSuccess?.()
},
})
return (
<Rhform form={form} onSubmit={(values) => mutate(values)}>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="me-2 h-4 w-4" />
<AlertTitle>{isEditing ? "Failed to update" : "Failed to submit"}</AlertTitle>
{error.message}
</Alert>
)}
<FieldGroup>
<RhfEmployeeSelectField name="employee" label="Employee" required />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfSelectField
name="leave_type"
label="Leave Type"
placeholder="Select type"
options={TYPE_SELECT_OPTIONS}
/>
<div />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="start_date" label="Start Date" type="date" required />
<RhfTextField name="end_date" label="End Date" type="date" required />
</div>
<RhfTextareaField name="reason" label="Reason" rows={3} />
<Button type="submit" variant="default" disabled={isPending}>
{isEditing ? <Save /> : <Plus />}
{isPending ? "Saving..." : (isEditing ? "Update" : "Submit Request")}
</Button>
</FieldGroup>
</Rhform>
)
}