import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { z } from 'zod'; import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { PartState } from '@vector/shared'; import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from '@vector/ui'; import { listManufacturers } from '../../lib/api/manufacturers.js'; import { listBins } from '../../lib/api/bins.js'; import { createPart, updatePart } from '../../lib/api/parts.js'; import type { Part } from '../../lib/api/types.js'; import { ApiRequestError } from '../../lib/api/client.js'; import { queryKeys } from '../../lib/queryKeys.js'; import { partStateOptions } from './PartStateBadge.js'; // Schema reflects the server's CreatePartRequest but keeps strings for the form, letting the // submit handler coerce to the network shape. const PartFormSchema = z.object({ serialNumber: z.string().min(1, 'Required').max(128), mpn: z.string().min(1, 'Required').max(128), manufacturerId: z.string().uuid('Select a manufacturer'), state: PartState, binId: z.string().optional(), // '' = none price: z.string().optional(), // empty string = null notes: z.string().max(4096).optional(), }); type PartFormValues = z.infer; const UNASSIGNED = '__none__'; interface PartFormDialogProps { open: boolean; onOpenChange: (open: boolean) => void; part?: Part | null; } export function PartFormDialog({ open, onOpenChange, part }: PartFormDialogProps) { const editing = Boolean(part); const queryClient = useQueryClient(); const form = useForm({ resolver: zodResolver(PartFormSchema), defaultValues: { serialNumber: '', mpn: '', manufacturerId: '', state: 'SPARE', binId: '', price: '', notes: '', }, }); useEffect(() => { if (!open) return; form.reset( part ? { serialNumber: part.serialNumber, mpn: part.partModel.mpn, manufacturerId: part.manufacturerId, state: part.state, binId: part.binId ?? '', price: part.price != null ? String(part.price) : '', notes: part.notes ?? '', } : { serialNumber: '', mpn: '', manufacturerId: '', state: 'SPARE', binId: '', price: '', notes: '', }, ); }, [open, part, form]); const manufacturers = useQuery({ queryKey: queryKeys.manufacturers.list({ pageSize: 100 }), queryFn: () => listManufacturers({ pageSize: 100 }), enabled: open, }); const bins = useQuery({ queryKey: queryKeys.bins.list({ pageSize: 100 }), queryFn: () => listBins({ pageSize: 100 }), enabled: open, }); const mutation = useMutation({ mutationFn: async (values: PartFormValues) => { const payload = { serialNumber: values.serialNumber, mpn: values.mpn, manufacturerId: values.manufacturerId, state: values.state, binId: values.binId ? values.binId : null, price: values.price === '' ? null : Number(values.price), notes: values.notes ? values.notes : null, }; return editing && part ? updatePart(part.id, payload) : createPart(payload); }, onSuccess: (saved) => { toast.success(editing ? 'Part updated' : 'Part created'); queryClient.invalidateQueries({ queryKey: queryKeys.parts.all }); if (editing) { queryClient.setQueryData(queryKeys.parts.detail(saved.id), saved); } onOpenChange(false); }, onError: (err) => { const msg = err instanceof ApiRequestError ? err.body.message : 'Could not save part'; toast.error(msg); }, }); const onSubmit = (values: PartFormValues) => { if (values.price !== '' && values.price !== undefined) { const n = Number(values.price); if (!Number.isFinite(n) || n < 0) { form.setError('price', { message: 'Must be a non-negative number' }); return; } } mutation.mutate(values); }; return ( {editing ? 'Edit part' : 'New part'} {editing ? 'Update this part. Changes are logged to its history.' : 'Add a part to inventory. Serial numbers must be unique.'}
( Serial )} /> ( MPN )} />
( Manufacturer )} />
( State )} /> ( Price (USD) )} />
( Location )} /> ( Notes