85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
"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>
|
|
)
|
|
}
|