Add delivery task deliver dialog
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||||
|
import { toast } from '@/components/ui/toast'
|
||||||
|
import { deliveryTasks as deliveryTasksApi, stock as stockApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||||
|
import type { DeliveryTaskResponse } from '@/types/api'
|
||||||
|
|
||||||
|
interface DeliveryTaskDeliverDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
task: DeliveryTaskResponse | null
|
||||||
|
vehicleId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeliveryTaskDeliverDialog({ open, onClose, onSuccess, task, vehicleId }: DeliveryTaskDeliverDialogProps) {
|
||||||
|
const [quantities, setQuantities] = useState<Record<string, number>>({})
|
||||||
|
const [available, setAvailable] = useState<Record<string, number>>({})
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !task) return
|
||||||
|
setQuantities(Object.fromEntries(task.items.map((i) => [i.itemId, i.quantityFulfilled])))
|
||||||
|
setAvailable({})
|
||||||
|
|
||||||
|
if (!vehicleId) return
|
||||||
|
stockApi.balances({ locationId: vehicleId, locationType: 'Vehicle', pageSize: 200 })
|
||||||
|
.then((res) => {
|
||||||
|
const map = Object.fromEntries(getPaginatedItems(res).map((b) => [b.itemId, b.quantity]))
|
||||||
|
setAvailable(map)
|
||||||
|
})
|
||||||
|
.catch((err) => toast.add({ title: 'Failed to load vehicle stock', description: getApiError(err), type: 'error' }))
|
||||||
|
}, [open, task, vehicleId])
|
||||||
|
|
||||||
|
if (!task) return null
|
||||||
|
|
||||||
|
const hasShortfall = task.items.some((item) => (quantities[item.itemId] ?? 0) > (available[item.itemId] ?? 0))
|
||||||
|
const nothingToDeliver = task.items.every((item) => (quantities[item.itemId] ?? 0) === 0)
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
const items = Object.entries(quantities).map(([itemId, quantityFulfilled]) => ({ itemId, quantityFulfilled }))
|
||||||
|
await deliveryTasksApi.setQuantities(task!.id, items)
|
||||||
|
await deliveryTasksApi.confirm(task!.id)
|
||||||
|
toast.add({ title: 'Delivery confirmed', type: 'success' })
|
||||||
|
onSuccess()
|
||||||
|
onClose()
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({ title: 'Failed to confirm delivery', description: getApiError(err), type: 'error' })
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
|
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Confirm Delivery</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter the quantity actually delivered for each item — you can't hand over more than what's
|
||||||
|
currently loaded on the vehicle.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<FieldGroup>
|
||||||
|
{task.items.map((item) => {
|
||||||
|
const onVehicle = available[item.itemId] ?? 0
|
||||||
|
const requested = quantities[item.itemId] ?? 0
|
||||||
|
const short = requested > onVehicle
|
||||||
|
const cap = Math.min(item.quantity, onVehicle)
|
||||||
|
return (
|
||||||
|
<Field key={item.itemId}>
|
||||||
|
<FieldLabel htmlFor={`qty-${item.itemId}`}>
|
||||||
|
{item.itemName}{' '}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
(ordered {item.quantity}, on vehicle {onVehicle})
|
||||||
|
</span>
|
||||||
|
</FieldLabel>
|
||||||
|
<Input
|
||||||
|
id={`qty-${item.itemId}`}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={cap}
|
||||||
|
aria-invalid={short}
|
||||||
|
value={requested}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setQuantities((prev) => ({ ...prev, [item.itemId]: Number(e.target.value) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{short && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
Only {onVehicle} of this item is loaded on the vehicle.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
|
{nothingToDeliver && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
Nothing is available to deliver — no stock for this order is currently loaded on the
|
||||||
|
vehicle.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onSubmit} disabled={submitting || hasShortfall || nothingToDeliver}>
|
||||||
|
{submitting ? 'Confirming…' : 'Confirm Delivery'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user