Dev #23
+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