156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import React from "react"
|
|
import { z } from "zod"
|
|
import { AlertTriangle } from "lucide-react"
|
|
import { useForm } from "react-hook-form"
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
|
|
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,
|
|
RhfAsyncSelectField,
|
|
} from "@/shared/components/form"
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { toast } from "sonner"
|
|
import { DEPARTMENT_ROUTES } from "@garage/api"
|
|
|
|
// ── Schema ──
|
|
|
|
const schema = z.object({
|
|
quantity: z.coerce.number().min(1, "Quantity is required"),
|
|
rate: z.string().min(1, "Rate is required"),
|
|
description: z.string().optional(),
|
|
department: z.object({ value: z.string(), label: z.string() }).nullable().optional(),
|
|
|
|
})
|
|
|
|
type FormValues = z.infer<typeof schema>
|
|
|
|
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
|
|
|
export type EstimateExpenseItemConfigFormProps = {
|
|
expenseItem: { id: number; name?: string; purchase_price?: string | number }
|
|
estimateId: string
|
|
onSuccess?: () => void
|
|
onCancel?: () => void
|
|
}
|
|
|
|
export function EstimateExpenseItemConfigForm({
|
|
expenseItem,
|
|
estimateId,
|
|
onSuccess,
|
|
onCancel,
|
|
}: EstimateExpenseItemConfigFormProps) {
|
|
const api = useAuthApi()
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(schema) as any,
|
|
defaultValues: {
|
|
quantity: 1,
|
|
rate: expenseItem.purchase_price != null ? String(expenseItem.purchase_price) : "",
|
|
description: "",
|
|
},
|
|
})
|
|
|
|
const [error, setError] = React.useState<string | null>(null)
|
|
const [isPending, setIsPending] = React.useState(false)
|
|
|
|
async function handleSubmit(values: FormValues) {
|
|
setError(null)
|
|
setIsPending(true)
|
|
try {
|
|
const promise = api.estimates.addExpenseItem(estimateId, {
|
|
expense_item_id: expenseItem.id,
|
|
quantity: values.quantity,
|
|
rate: values.rate,
|
|
description: values.description || undefined,
|
|
department_id: values.department ? Number(values.department.value) : undefined,
|
|
|
|
} as any)
|
|
toast.promise(promise, {
|
|
loading: "Adding expense item...",
|
|
success: "Expense item added successfully",
|
|
error: "Failed to add expense item",
|
|
})
|
|
await promise
|
|
form.reset()
|
|
onSuccess?.()
|
|
} catch (err: any) {
|
|
setError(err?.message ?? "An unexpected error occurred")
|
|
} finally {
|
|
setIsPending(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Rhform form={form} onSubmit={handleSubmit}>
|
|
{error && (
|
|
<Alert variant="destructive" className="mb-4">
|
|
<AlertTriangle className="me-2 h-4 w-4" />
|
|
<AlertTitle>Failed to add expense item</AlertTitle>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<FieldGroup>
|
|
<div className="rounded-md bg-muted px-3 py-2 text-sm font-medium">
|
|
{expenseItem.name ?? `Expense Item #${expenseItem.id}`}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<RhfTextField
|
|
name="quantity"
|
|
label="Quantity"
|
|
type="number"
|
|
placeholder="1"
|
|
required
|
|
/>
|
|
<RhfTextField
|
|
name="rate"
|
|
label="Rate"
|
|
type="number"
|
|
placeholder="0.00"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<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 ?? item.title ?? String(item.id),
|
|
})}
|
|
{...STORE_OBJECT}
|
|
/>
|
|
|
|
<RhfTextareaField
|
|
name="description"
|
|
label="Description"
|
|
placeholder="Optional description"
|
|
rows={2}
|
|
/>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
{onCancel && (
|
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isPending}>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<Button type="submit" disabled={isPending}>
|
|
{isPending ? "Adding..." : "Add Expense Item"}
|
|
</Button>
|
|
</div>
|
|
</FieldGroup>
|
|
</Rhform>
|
|
)
|
|
}
|