@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
// Reserves stock of one item for this project, from one location that has some available. The request
|
||||
// id is minted once per submission, so a retried request reserves once; the service answers a
|
||||
// quantity it cannot hold as `insufficient_stock`, shown here in the kit's words for that refusal.
|
||||
|
||||
import type { ProjectSlugs } from "@conusai-kit/ui"
|
||||
import { describeError, useWrite } from "@conusai-kit/ui/data"
|
||||
import {
|
||||
FormLayout,
|
||||
type FormValues,
|
||||
PageSection,
|
||||
SelectField,
|
||||
serviceNotice,
|
||||
TextField,
|
||||
usePageNavigation,
|
||||
} from "@conusai-kit/ui/patterns"
|
||||
import { type ReactElement, useState } from "react"
|
||||
|
||||
export interface ReserveFormLabels {
|
||||
readonly heading: string
|
||||
readonly location: string
|
||||
readonly quantity: string
|
||||
readonly submit: string
|
||||
readonly busy: string
|
||||
readonly nothing: string
|
||||
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
|
||||
readonly refusals: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
export interface ReserveOption {
|
||||
readonly locationId: string
|
||||
/** What a person reads for the location, with what is available there. */
|
||||
readonly label: string
|
||||
readonly available: number
|
||||
}
|
||||
|
||||
export function ReserveForm({
|
||||
slugs,
|
||||
itemId,
|
||||
options,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
itemId: string
|
||||
options: readonly ReserveOption[]
|
||||
labels: ReserveFormLabels
|
||||
}): ReactElement {
|
||||
const navigation = usePageNavigation()
|
||||
const reserve = useWrite<Record<string, unknown>, unknown>("inventory.stock.reserve")
|
||||
const [requestId, setRequestId] = useState(() => crypto.randomUUID())
|
||||
const open = options.filter((option) => option.available > 0)
|
||||
|
||||
const submit = async ({ locationId, quantity }: FormValues): Promise<void> => {
|
||||
await reserve.mutate({ ...slugs, itemId, locationId, quantity: Number(quantity), requestId })
|
||||
setRequestId(crypto.randomUUID())
|
||||
// The page is rendered on the server: a reload shows the new balance and the reservation.
|
||||
await navigation.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection title={labels.heading} variant="card">
|
||||
{open.length === 0 ? (
|
||||
<p>{labels.nothing}</p>
|
||||
) : (
|
||||
<FormLayout
|
||||
submitLabel={labels.submit}
|
||||
busyLabel={labels.busy}
|
||||
onSubmit={submit}
|
||||
describeError={(error) => serviceNotice(error, labels.refusals)?.text ?? describeError(error)}
|
||||
>
|
||||
<SelectField
|
||||
name="locationId"
|
||||
label={labels.location}
|
||||
required
|
||||
defaultValue={open[0]?.locationId}
|
||||
options={open.map((option) => ({ value: option.locationId, label: option.label }))}
|
||||
/>
|
||||
<TextField name="quantity" label={labels.quantity} type="number" required min={1} step={1} defaultValue={1} />
|
||||
</FormLayout>
|
||||
)}
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user