2026-04-07 06:32:40 +03:00

267 lines
9.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,
RhfTextareaField,
RhfSelectField,
RhfAsyncSelectField,
RhfDateField,
} from "@/shared/components/form"
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 { taskFormSchema, type TaskFormValues } from "./task.schema"
import { TASK_ROUTES, TASK_TYPE_ROUTES, TASK_SECTION_ROUTES, EMPLOYEE_ROUTES, DEPARTMENT_ROUTES, JOB_CARD_ROUTES } from "@garage/api"
import { TaskStatus, TaskPriority } from "@garage/api"
import { TaskTypeCrudDialog } from "./task-type-crud-dialog"
import { TaskSectionCrudDialog } from "./task-section-crud-dialog"
// ── Constants ──
const STATUS_OPTIONS = TaskStatus.map((s) => ({
value: s,
label: s.charAt(0).toUpperCase() + s.slice(1),
}))
const PRIORITY_OPTIONS = TaskPriority.map((p,i) => ({
value: i,
label: p.charAt(0).toUpperCase() + p.slice(1),
}))
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
// ── Props ──
export type TaskFormProps = {
resourceId?: string | null
initialData?: unknown
onSuccess?: () => void
}
// ── Default values ──
const DEFAULT_VALUES: TaskFormValues = {
subject: "",
description: "",
task_type: null,
task_section: null,
owner: null,
department: null,
priority: "medium",
due_date: "",
status: "pending",
job_card: null,
}
// ── Mapping helpers ──
function mapToFormValues(data: unknown): TaskFormValues {
const d = (data as any)?.data ?? data ?? {}
return {
subject: d.subject || "",
description: d.description || "",
task_type: toRelation(d.task_type_id, d.task_type?.title),
task_section: toRelation(d.task_section_id, d.task_section?.title),
owner: toRelation(
d.owner_id,
d.owner ? `${d.owner.first_name ?? ""} ${d.owner.last_name ?? ""}`.trim() : undefined,
),
department: toRelation(d.department_id, d.department?.name),
priority: d.priority || "medium",
due_date: d.due_date ? d.due_date.split("T")[0] : "",
status: d.status || "pending",
job_card: toRelation(d.job_card_id, d.job_card?.title),
}
}
function mapFormToPayload(values: TaskFormValues) {
return {
subject: values.subject,
description: values.description || undefined,
task_type_id: toId(values.task_type),
task_section_id: toId(values.task_section),
owner_id: toId(values.owner),
department_id: toId(values.department),
priority: values.priority || undefined,
due_date: values.due_date || undefined,
status: values.status || undefined,
job_card_id: toId(values.job_card),
}
}
// ── Component ──
export function TaskForm({ resourceId, initialData, onSuccess }: TaskFormProps) {
const api = useAuthApi()
const { form, isEditing } = useResourceForm<TaskFormValues, any>({
schema: taskFormSchema,
defaultValues: DEFAULT_VALUES,
resourceId,
initialData,
mapToFormValues,
queryKey: [TASK_ROUTES.BY_ID, resourceId],
})
const { mutate, error, isPending } = useFormMutation(form, {
mutationFn: (values: TaskFormValues) => {
const payload = mapFormToPayload(values)
const promise = isEditing && resourceId
? api.tasks.update(resourceId, payload as any)
: api.tasks.create(payload as any)
toast.promise(promise, {
loading: isEditing ? "Updating task..." : "Creating task...",
success: isEditing ? "Task updated successfully" : "Task created successfully",
error: isEditing ? "Failed to update task" : "Failed to create task",
})
return promise
},
onSuccess: () => {
form.reset()
onSuccess?.()
},
})
return (
<Rhform form={form} onSubmit={(values) => mutate(values)} className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>
{isEditing ? "Failed to update task" : "Failed to create task"}
</AlertTitle>
</Alert>
)}
<FieldGroup>
<RhfTextField
name="subject"
label="Subject"
placeholder="e.g. Inspect brake pads"
required
/>
<RhfTextareaField
name="description"
label="Description"
placeholder="Optional description..."
/>
</FieldGroup>
<FieldGroup>
<div className="grid grid-cols-2 gap-4">
{/* Task Type with inline CRUD */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium">Task Type</span>
<TaskTypeCrudDialog />
</div>
<RhfAsyncSelectField
name="task_type"
label=""
placeholder="Select task type..."
queryKey={[TASK_TYPE_ROUTES.INDEX]}
listFn={() => api.taskTypes.list()}
mapOption={(item: any) => ({ value: String(item.id), label: item.title })}
{...STORE_OBJECT}
/>
</div>
{/* Task Section with inline CRUD */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium">Task Section</span>
<TaskSectionCrudDialog />
</div>
<RhfAsyncSelectField
name="task_section"
label=""
placeholder="Select task section..."
queryKey={[TASK_SECTION_ROUTES.INDEX]}
listFn={() => api.taskSections.list()}
mapOption={(item: any) => ({ value: String(item.id), label: item.title })}
{...STORE_OBJECT}
/>
</div>
</div>
</FieldGroup>
<FieldGroup>
<div className="grid grid-cols-2 gap-4">
<RhfAsyncSelectField
name="owner"
label="Assigned To"
placeholder="Select employee..."
queryKey={[EMPLOYEE_ROUTES.INDEX]}
listFn={() => api.employees.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim(),
})}
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="department"
label="Department"
placeholder="Select department..."
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={(item: any) => ({ value: String(item.id), label: item.name })}
{...STORE_OBJECT}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<RhfSelectField
name="priority"
label="Priority"
placeholder="Select priority..."
options={PRIORITY_OPTIONS as any}
/>
<RhfSelectField
name="status"
label="Status"
placeholder="Select status..."
options={STATUS_OPTIONS}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<RhfDateField name="due_date" label="Due Date" />
<RhfAsyncSelectField
name="job_card"
label="Job Card"
placeholder="Select job card..."
queryKey={[JOB_CARD_ROUTES.INDEX]}
listFn={() => api.jobCards.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.title || `#${item.id}`,
})}
{...STORE_OBJECT}
/>
</div>
</FieldGroup>
<Button type="submit" disabled={isPending} className="w-full">
{isEditing ? <Save className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
{isPending
? isEditing ? "Updating..." : "Creating..."
: isEditing ? "Update Task" : "Add Task"}
</Button>
</Rhform>
)
}