- Full inventory UI: product list with search/filter, product detail with tabs (details, units, suppliers, stock receipts, price history) - Product filters: category, type (serialized/rental/repair), low stock, active/inactive — all server-side with URL-synced state - Product-supplier junction: link products to multiple suppliers with preferred flag, joined supplier details in UI - Stock receipts: record incoming stock with supplier, qty, cost per unit, invoice number; auto-increments qty_on_hand for non-serialized products - Price history tab on product detail page - categories/all endpoint to avoid pagination limit on dropdown fetches - categoryId filter on product list endpoint - Repair parts and additional inventory items in music store seed data - isDualUseRepair corrected: instruments set to false, strings/parts true - Product-supplier links and stock receipts in seed data - Price history seed data simulating cost increases over past year - 37 API tests covering categories, suppliers, products, units, product-suppliers, and stock receipts - alert-dialog and checkbox UI components - sync-and-deploy.sh script for rsync + remote deploy
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import { useForm } from 'react-hook-form'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
import type { InventoryUnit, UnitCondition, UnitStatus } from '@/types/inventory'
|
|
|
|
const CONDITIONS: { value: UnitCondition; label: string }[] = [
|
|
{ value: 'new', label: 'New' },
|
|
{ value: 'excellent', label: 'Excellent' },
|
|
{ value: 'good', label: 'Good' },
|
|
{ value: 'fair', label: 'Fair' },
|
|
{ value: 'poor', label: 'Poor' },
|
|
]
|
|
|
|
const STATUSES: { value: UnitStatus; label: string }[] = [
|
|
{ value: 'available', label: 'Available' },
|
|
{ value: 'sold', label: 'Sold' },
|
|
{ value: 'rented', label: 'Rented' },
|
|
{ value: 'on_trial', label: 'On Trial' },
|
|
{ value: 'in_repair', label: 'In Repair' },
|
|
{ value: 'layaway', label: 'Layaway' },
|
|
{ value: 'lost', label: 'Lost' },
|
|
{ value: 'retired', label: 'Retired' },
|
|
]
|
|
|
|
interface Props {
|
|
defaultValues?: Partial<InventoryUnit>
|
|
onSubmit: (data: Record<string, unknown>) => void
|
|
loading?: boolean
|
|
}
|
|
|
|
export function InventoryUnitForm({ defaultValues, onSubmit, loading }: Props) {
|
|
const { register, handleSubmit, setValue, watch } = useForm({
|
|
defaultValues: {
|
|
serialNumber: defaultValues?.serialNumber ?? '',
|
|
condition: (defaultValues?.condition ?? 'new') as UnitCondition,
|
|
status: (defaultValues?.status ?? 'available') as UnitStatus,
|
|
purchaseDate: defaultValues?.purchaseDate ?? '',
|
|
purchaseCost: defaultValues?.purchaseCost ?? '',
|
|
notes: defaultValues?.notes ?? '',
|
|
},
|
|
})
|
|
|
|
const condition = watch('condition')
|
|
const status = watch('status')
|
|
|
|
function handleFormSubmit(data: Record<string, unknown>) {
|
|
onSubmit({
|
|
serialNumber: (data.serialNumber as string) || undefined,
|
|
condition: data.condition,
|
|
status: data.status,
|
|
purchaseDate: (data.purchaseDate as string) || undefined,
|
|
purchaseCost: (data.purchaseCost as string) || undefined,
|
|
notes: (data.notes as string) || undefined,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="unit-serial">Serial Number</Label>
|
|
<Input id="unit-serial" {...register('serialNumber')} placeholder="e.g. US22041234" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Condition</Label>
|
|
<Select value={condition} onValueChange={(v) => setValue('condition', v as UnitCondition)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{CONDITIONS.map((c) => (
|
|
<SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Status</Label>
|
|
<Select value={status} onValueChange={(v) => setValue('status', v as UnitStatus)}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STATUSES.map((s) => (
|
|
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="unit-date">Purchase Date</Label>
|
|
<Input id="unit-date" type="date" {...register('purchaseDate')} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="unit-cost">Purchase Cost</Label>
|
|
<Input id="unit-cost" type="number" step="0.01" min="0" {...register('purchaseCost')} placeholder="0.00" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="unit-notes">Notes</Label>
|
|
<textarea
|
|
id="unit-notes"
|
|
{...register('notes')}
|
|
rows={2}
|
|
placeholder="Any notes about this unit..."
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
|
/>
|
|
</div>
|
|
<Button type="submit" disabled={loading} className="w-full">
|
|
{loading ? 'Saving...' : defaultValues?.id ? 'Save Changes' : 'Add Unit'}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|