267 lines
9.7 KiB
TypeScript
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>
|
|
)
|
|
}
|