101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
// The stock this project holds: its open reservations, read again after a release. Releasing is for
|
|
// whoever holds `inventory.stock.reserve`; the procedure checks again, and a reservation of another
|
|
// project is not found there.
|
|
|
|
import type { ProjectSlugs } from "@conusai-kit/ui"
|
|
import { Can, describeError, useRead, useWrite } from "@conusai-kit/ui/data"
|
|
import { ConfirmDialog, PageSection, type ResourceColumn, ResourceList, serviceNotice } from "@conusai-kit/ui/patterns"
|
|
import { Button, useUiLabels } from "@conusai-kit/ui/primitives"
|
|
import type { ReactElement } from "react"
|
|
import type { ReservationView } from "../contract/index.js"
|
|
|
|
export interface ReservationListLabels {
|
|
readonly title: string
|
|
readonly caption: string
|
|
readonly empty: string
|
|
readonly item: string
|
|
readonly quantity: string
|
|
readonly outstanding: string
|
|
readonly actions: string
|
|
/** `Release the stock held of {item}`: the button's accessible name. */
|
|
readonly release: string
|
|
readonly releaseShort: string
|
|
readonly releasing: string
|
|
/** What releasing does to the stock held. */
|
|
readonly releaseConfirm: string
|
|
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
|
|
readonly refusals: Readonly<Record<string, string>>
|
|
}
|
|
|
|
type Page = { rows: ReservationView[]; after?: string | undefined }
|
|
|
|
export function ReservationList({
|
|
slugs,
|
|
seed,
|
|
itemLabel,
|
|
listHref,
|
|
labels,
|
|
}: {
|
|
slugs: ProjectSlugs
|
|
seed: Page
|
|
/** What a person reads for an item, by id, for the items the page knows; the id otherwise. */
|
|
itemLabel: Readonly<Record<string, string>>
|
|
listHref: string
|
|
labels: ReservationListLabels
|
|
}): ReactElement {
|
|
const list = useRead<Page>("inventory.reservation.list", { ...slugs, status: "open" }, { initial: seed })
|
|
const release = useWrite<Record<string, unknown>, ReservationView>("inventory.stock.release")
|
|
const failed = useUiLabels()?.failed ?? "{detail}"
|
|
const nameOf = (row: ReservationView): string => itemLabel[row.itemId] ?? row.itemId
|
|
const describe = (error: unknown): string => serviceNotice(error, labels.refusals)?.text ?? describeError(error)
|
|
const columns: ResourceColumn<ReservationView>[] = [
|
|
{ id: "itemId", header: labels.item, wrap: true, cell: nameOf },
|
|
{ id: "quantity", header: labels.quantity, align: "end" },
|
|
{ id: "outstanding", header: labels.outstanding, align: "end" },
|
|
{
|
|
id: "actions",
|
|
header: labels.actions,
|
|
cell: (row) => {
|
|
const name = labels.release.replace("{item}", nameOf(row))
|
|
return (
|
|
<Can permission="inventory.stock.reserve">
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button variant="outline" size="sm" aria-label={name}>
|
|
{labels.releaseShort}
|
|
</Button>
|
|
}
|
|
title={name}
|
|
description={labels.releaseConfirm}
|
|
confirmLabel={labels.releaseShort}
|
|
busyLabel={labels.releasing}
|
|
describeError={describe}
|
|
onConfirm={async () => {
|
|
await release.mutate({ ...slugs, reservationId: row.reservationId, requestId: crypto.randomUUID() })
|
|
await list.refetch()
|
|
}}
|
|
/>
|
|
</Can>
|
|
)
|
|
},
|
|
},
|
|
]
|
|
return (
|
|
<PageSection title={labels.title}>
|
|
<ResourceList
|
|
label={labels.caption}
|
|
columns={columns}
|
|
data={list.data?.rows ?? []}
|
|
rowId={(row) => row.reservationId}
|
|
rowHref={(row) => `${listHref}/${row.itemId}`}
|
|
rowLabel={nameOf}
|
|
empty={{ title: labels.empty }}
|
|
{...(list.error === undefined ? {} : { error: failed.replace("{detail}", describe(list.error)) })}
|
|
onRetry={() => void list.refetch()}
|
|
/>
|
|
</PageSection>
|
|
)
|
|
}
|