garage-erp/apps/dashboard/modules/employees/employee-document-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

166 lines
5.9 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { Field, FieldLabel, FieldError } from "@/shared/components/ui/field"
import { useAuthApi } from "@/shared/useApi"
const DOCUMENT_TYPES = [
{ value: "contract", label: "Contract" },
{ value: "id", label: "ID / Passport" },
{ value: "ase", label: "ASE" },
{ value: "certification", label: "Certification" },
{ value: "other", label: "Other" },
] as const
const schema = z.object({
name: z.string().trim().min(1, "Name is required").max(150),
type: z.enum(["contract", "id", "ase", "certification", "other"]),
issued_date: z.string().optional(),
expiry_date: z.string().optional(),
notes: z.string().max(2000).optional(),
})
type FormValues = z.infer<typeof schema>
export type EmployeeDocumentFormProps = {
employeeId: string
onSuccess?: () => void
onCancel?: () => void
}
export function EmployeeDocumentForm({ employeeId, onSuccess, onCancel }: EmployeeDocumentFormProps) {
const api = useAuthApi()
const fileRef = useRef<HTMLInputElement | null>(null)
const [file, setFile] = useState<File | null>(null)
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
reset,
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: "", type: "other", issued_date: "", expiry_date: "", notes: "" },
})
const type = watch("type")
const mutation = useMutation({
mutationFn: (values: FormValues) => {
if (!file) throw new Error("Select a file to upload.")
const promise = api.employees.uploadDocument(employeeId, {
file,
name: values.name,
type: values.type,
issued_date: values.issued_date || undefined,
expiry_date: values.expiry_date || undefined,
notes: values.notes || undefined,
})
toast.promise(promise, {
loading: "Uploading document...",
success: "Document uploaded",
error: (err: any) => err?.message ?? "Failed to upload",
})
return promise
},
onSuccess: () => {
reset()
setFile(null)
onSuccess?.()
},
})
return (
<form className="grid gap-4" onSubmit={handleSubmit((v) => mutation.mutate(v))}>
<Field>
<FieldLabel>File</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,.xls,.xlsx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
/>
</div>
{!file && mutation.isError && (
<FieldError>Select a file before uploading.</FieldError>
)}
</Field>
<Field>
<FieldLabel>Name</FieldLabel>
<Input {...register("name")} placeholder="e.g. Employment contract 2026" />
{errors.name && <FieldError>{errors.name.message}</FieldError>}
</Field>
<Field>
<FieldLabel>Type</FieldLabel>
<Select value={type} onValueChange={(v) => setValue("type", v as FormValues["type"], { shouldValidate: true })}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{DOCUMENT_TYPES.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<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>Notes</FieldLabel>
<Textarea rows={3} {...register("notes")} placeholder="Optional" />
</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 ? "Uploading..." : "Upload"}
</Button>
</div>
</form>
)
}