Dev #23
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm, zodResolver, z } from '@/lib/form'
|
||||
import { FormField } from '@/components/shared/form-field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { FieldGroup } from '@/components/ui/field'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { stock as stockApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import type { StockBalanceResponse, UserResponse } from '@/types/api'
|
||||
|
||||
const schema = z.object({
|
||||
quantity: z.number().refine((v) => v !== 0, 'Adjustment quantity cannot be 0'),
|
||||
reasonCode: z.string().min(1, 'Reason is required'),
|
||||
adjustedByUserId: z.string().min(1, 'Adjusted by is required'),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
|
||||
interface StockAdjustDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
balance: StockBalanceResponse | null
|
||||
}
|
||||
|
||||
export function StockAdjustDialog({ open, onClose, onSuccess, balance }: StockAdjustDialogProps) {
|
||||
const [usersList, setUsersList] = useState<UserResponse[]>([])
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { quantity: 0, reasonCode: '', adjustedByUserId: '' },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
form.reset({ quantity: 0, reasonCode: '', adjustedByUserId: '' })
|
||||
usersApi.list({ pageSize: 200 })
|
||||
.then((res) => setUsersList(getPaginatedItems(res)))
|
||||
.catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' }))
|
||||
}, [open])
|
||||
|
||||
if (!balance) return null
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
try {
|
||||
await stockApi.adjust({
|
||||
itemId: balance!.itemId,
|
||||
locationId: balance!.locationId,
|
||||
locationType: balance!.locationType,
|
||||
quantity: values.quantity,
|
||||
reasonCode: values.reasonCode,
|
||||
adjustedByUserId: values.adjustedByUserId,
|
||||
})
|
||||
toast.add({ title: 'Stock adjusted', type: 'success' })
|
||||
onSuccess()
|
||||
onClose()
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to adjust stock', description: getApiError(err), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Adjust Stock — {balance.itemName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a positive quantity to increase, or a negative quantity to decrease, the balance
|
||||
at this location. Current quantity: {balance.quantity}.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
<FieldGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="quantity"
|
||||
label="Adjustment Quantity"
|
||||
required
|
||||
render={({ field, invalid }) => (
|
||||
<Input
|
||||
id="quantity"
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={Number(field.value as number)}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
onBlur={field.onBlur}
|
||||
placeholder="e.g. 10 or -5"
|
||||
aria-invalid={invalid}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="reasonCode"
|
||||
label="Reason"
|
||||
required
|
||||
render={({ field, invalid }) => (
|
||||
<Input
|
||||
id="reasonCode"
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
placeholder="e.g. Physical count correction"
|
||||
aria-invalid={invalid}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="adjustedByUserId"
|
||||
label="Adjusted By"
|
||||
description="Temporary — required until auth resolves the logged-in user automatically."
|
||||
required
|
||||
render={({ field }) => (
|
||||
<Select value={field.value as string} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select user..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="" disabled>Select user...</SelectItem>
|
||||
{usersList.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.fullName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FieldGroup>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? 'Saving…' : 'Apply Adjustment'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user