Add StockAdjustDialog component for stock adjustment
This commit is contained in:
@@ -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