garage-erp/apps/dashboard/modules/employees/employee-certification-form.tsx
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

141 lines
5.1 KiB
TypeScript

"use client"
import { useRef, useState } from "react"
import { useMutation } from "@tanstack/react-query"
import { toast } from "sonner"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { UploadIcon } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Textarea } from "@/shared/components/ui/textarea"
import { Field, FieldLabel, FieldError } from "@/shared/components/ui/field"
import { useAuthApi } from "@/shared/useApi"
const schema = z.object({
name: z.string().trim().min(1, "Name is required").max(150),
issuer: z.string().trim().max(150).optional(),
certificate_number: z.string().trim().max(100).optional(),
issued_date: z.string().optional(),
expiry_date: z.string().optional(),
notes: z.string().max(2000).optional(),
})
type FormValues = z.infer<typeof schema>
export type EmployeeCertificationFormProps = {
employeeId: string
onSuccess?: () => void
onCancel?: () => void
}
export function EmployeeCertificationForm({ employeeId, onSuccess, onCancel }: EmployeeCertificationFormProps) {
const api = useAuthApi()
const fileRef = useRef<HTMLInputElement | null>(null)
const [file, setFile] = useState<File | null>(null)
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
name: "", issuer: "", certificate_number: "",
issued_date: "", expiry_date: "", notes: "",
},
})
const mutation = useMutation({
mutationFn: (values: FormValues) => {
const promise = api.employees.createCertification(employeeId, {
name: values.name,
issuer: values.issuer || undefined,
certificate_number: values.certificate_number || undefined,
issued_date: values.issued_date || undefined,
expiry_date: values.expiry_date || undefined,
notes: values.notes || undefined,
file: file ?? undefined,
})
toast.promise(promise, {
loading: "Saving certification...",
success: "Certification added",
error: (err: any) => err?.message ?? "Failed to save",
})
return promise
},
onSuccess: () => {
reset()
setFile(null)
onSuccess?.()
},
})
return (
<form className="grid gap-4" onSubmit={handleSubmit((v) => mutation.mutate(v))}>
<Field>
<FieldLabel>Name</FieldLabel>
<Input {...register("name")} placeholder="e.g. ASE A1 Engine Repair" />
{errors.name && <FieldError>{errors.name.message}</FieldError>}
</Field>
<div className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>Issuer</FieldLabel>
<Input {...register("issuer")} placeholder="ASE / SAE / ..." />
</Field>
<Field>
<FieldLabel>Certificate Number</FieldLabel>
<Input {...register("certificate_number")} placeholder="Optional" />
</Field>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Field>
<FieldLabel>Issued Date</FieldLabel>
<Input type="date" {...register("issued_date")} />
</Field>
<Field>
<FieldLabel>Expiry Date</FieldLabel>
<Input type="date" {...register("expiry_date")} />
</Field>
</div>
<Field>
<FieldLabel>Attachment (optional)</FieldLabel>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => fileRef.current?.click()}>
<UploadIcon className="size-4" />
Choose file
</Button>
<span className="text-sm text-muted-foreground">{file ? file.name : "No file selected"}</span>
<input
ref={fileRef}
type="file"
className="hidden"
accept=".pdf,.jpg,.jpeg,.png,.webp,.doc,.docx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
</div>
</Field>
<Field>
<FieldLabel>Notes</FieldLabel>
<Textarea rows={3} {...register("notes")} />
</Field>
<div className="flex justify-end gap-2">
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel}>Cancel</Button>
)}
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Saving..." : "Save Certification"}
</Button>
</div>
</form>
)
}