// The inventory module's pages, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web // shell runs on the server under `////inventory`, and the view of what it returned. // // /inventory the organization's items, a form that adds one, and this project's reservations // /inventory/ one item's stock by location, and the form that reserves some for this project // // Every read is a procedure through the same caller the API uses, and every label is translated in the // loader. A refusal that is the platform's -- the service is not enabled for the organization, the // deployment lacks a scope, the service did not answer -- is the page's own state, not an error page, in the // kit's words for it (`businessServiceNotices` of `@conusai-kit/business/refusals`). import { businessRefusalNotices, businessServiceNotices } from "@conusai-kit/business/refusals" import { type Container, notFound, type TransportCaller } from "@conusai-kit/core" import type { I18n } from "@conusai-kit/i18n" import { type ProjectPage, type ProjectPageViewProps, projectPage } from "@conusai-kit/ui" import { Can } from "@conusai-kit/ui/data" import { DetailLayout, Page, PageHeader, PageSection, type ServiceNotice, ServiceStatePage, serviceNotice, } from "@conusai-kit/ui/patterns" import type { ReactElement } from "react" import { type AvailabilityView, type ItemView, itemAvailability, itemList, type ReservationView, reservationList, } from "../contract/index.js" import { ItemForm, type ItemFormLabels } from "./item-form.js" import { ItemList, type ItemListLabels } from "./item-list.js" import { itemFormLabels, itemListLabels, reservationListLabels, reserveFormLabels, stockLabels, type Translate, } from "./labels.js" import { ReservationList, type ReservationListLabels } from "./reservation-list.js" import { ReserveForm, type ReserveFormLabels } from "./reserve-form.js" import { Stock, type StockLabels } from "./stock.js" /** The items, the form that adds one, and this project's open reservations. */ export interface InventoryListData { readonly kind: "list" readonly title: string readonly intro: string readonly itemsHeading: string readonly items: { rows: ItemView[]; after?: string | undefined } readonly itemForm: ItemFormLabels readonly itemList: ItemListLabels readonly reservations: { rows: ReservationView[]; after?: string | undefined } readonly reservationList: ReservationListLabels /** Each reserved item as a person reads it, by item id. */ readonly itemLabel: Readonly> } /** One item's stock, and the form that reserves it when the item is active. */ export interface InventoryItemData { readonly kind: "item" readonly title: string readonly back: string readonly heading: string readonly description?: string readonly itemId: string readonly active: boolean readonly availability: AvailabilityView readonly stock: StockLabels readonly reserve: ReserveFormLabels } /** The service cannot answer: the notice in place of the page. */ export interface InventoryStateData { readonly kind: "state" readonly title: string readonly notice: ServiceNotice } export type InventoryPageData = InventoryListData | InventoryItemData | InventoryStateData export type InventoryPage = ProjectPage const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i /** A location as a person reads it: the service knows locations by id, so the id's first block. */ const locationName = (locationId: string): string => locationId.slice(0, 8) export const inventoryPage: InventoryPage = projectPage({ async load({ slugs, segments, caller, i18n }): Promise { const t: Translate = (key, values) => i18n.message(`inventory.${key}`, values) const title = t("title") const refusals = businessRefusalNotices(i18n.locale) const [itemId, ...rest] = segments if (rest.length > 0 || (itemId !== undefined && !UUID.test(itemId))) throw notFound("no such page") try { if (itemId === undefined) { const [items, reservations] = await Promise.all([ caller.call(itemList, slugs), caller.call(reservationList, { ...slugs, status: "open" }), ]) return { kind: "list", title, intro: t("intro"), itemsHeading: t("items.title"), items, itemForm: itemFormLabels(t, refusals), itemList: itemListLabels(t), reservations, reservationList: reservationListLabels(t, refusals), itemLabel: Object.fromEntries(items.rows.map((item) => [item.itemId, `${item.sku} ${item.name}`])), } } const { item, availability } = await caller.call(itemAvailability, { ...slugs, itemId }) return { kind: "item", title, back: t("item.back"), heading: `${item.sku} ${item.name}`, ...(item.description ? { description: item.description } : {}), itemId: item.itemId, active: item.active, availability, stock: stockLabels(t, availability.unit, availability.locations.length), reserve: reserveFormLabels(t, refusals), } } catch (error) { const notice = serviceNotice(error, businessServiceNotices(i18n.locale)) if (!notice) throw error return { kind: "state", title, notice } } }, View({ slugs, data }: ProjectPageViewProps): ReactElement { const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/inventory` if (data.kind === "state") return if (data.kind === "list") { return ( } > ) } return ( {data.back}} /> ({ locationId: level.locationId, label: `${locationName(level.locationId)} (${level.available})`, available: level.available, }))} labels={data.reserve} /> ) : undefined } > ) }, })