ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
// The inventory module's pages and components. `inventoryPage` is what the web shell mounts; the rest
// is exported for an app that composes its own page from the parts.
export { ItemForm, type ItemFormLabels } from "./item-form.js"
export { ItemList, type ItemListLabels } from "./item-list.js"
export {
itemFormLabels,
itemListLabels,
reservationListLabels,
reserveFormLabels,
stockLabels,
type Translate,
} from "./labels.js"
export { type InventoryPage, inventoryPage } from "./pages.js"
export { ReservationList, type ReservationListLabels } from "./reservation-list.js"
export { ReserveForm, type ReserveFormLabels, type ReserveOption } from "./reserve-form.js"
export { Stock, type StockLabels } from "./stock.js"
+82
View File
@@ -0,0 +1,82 @@
"use client"
// Adds an item to the organization's catalogue, then opens its stock page. The service checks the SKU
// and the unit again and answers a taken SKU as a duplicate, 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,
serviceNotice,
TextField,
usePageNavigation,
} from "@conusai-kit/ui/patterns"
import type { ReactElement } from "react"
export interface ItemFormLabels {
readonly heading: string
readonly sku: string
readonly skuHint: string
readonly name: string
readonly unit: string
readonly unitHint: string
readonly description: string
readonly submit: string
readonly busy: string
/** The text of each business refusal by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`). */
readonly refusals: Readonly<Record<string, string>>
}
export function ItemForm({
slugs,
listHref,
labels,
}: {
slugs: ProjectSlugs
listHref: string
labels: ItemFormLabels
}): ReactElement {
const navigation = usePageNavigation()
// No live collection follows a service's rows, so the write resolves with the answer.
const create = useWrite<Record<string, unknown>, { itemId: string }>("inventory.item.create")
const submit = async ({ sku, name, unit, description = "" }: FormValues): Promise<void> => {
const trimmed = description.trim()
const created = await create.mutate({ ...slugs, sku, name, unit, ...(trimmed ? { description: trimmed } : {}) })
navigation.navigate(`${listHref}/${created.itemId}`)
}
return (
<PageSection title={labels.heading}>
<FormLayout
submitLabel={labels.submit}
busyLabel={labels.busy}
onSubmit={submit}
describeError={(error) => serviceNotice(error, labels.refusals)?.text ?? describeError(error)}
>
<TextField
name="sku"
label={labels.sku}
description={labels.skuHint}
errorMessage={labels.skuHint}
required
maxLength={64}
pattern="[A-Za-z0-9][A-Za-z0-9._\-]*"
/>
<TextField name="name" label={labels.name} required maxLength={200} />
<TextField
name="unit"
label={labels.unit}
description={labels.unitHint}
errorMessage={labels.unitHint}
required
maxLength={16}
pattern="[a-z][a-z0-9_]*"
/>
<TextField multiline name="description" label={labels.description} rows={3} maxLength={1000} />
</FormLayout>
</PageSection>
)
}
+65
View File
@@ -0,0 +1,65 @@
"use client"
// The organization's items, a page at a time. The server renders the first page (`seed`); a search and
// "Show more" are reads made on a person's action, through the page's data client.
import type { ProjectSlugs } from "@conusai-kit/ui"
import { useDataClient } from "@conusai-kit/ui/data"
import { type ResourceColumn, ResourceList, useResourceList } from "@conusai-kit/ui/patterns"
import type { ReactElement } from "react"
import type { ItemView } from "../contract/index.js"
export interface ItemListLabels {
readonly caption: string
readonly empty: string
readonly sku: string
readonly name: string
readonly unit: string
readonly active: string
readonly archived: string
/** `Stock of {name}`: the link's accessible name. */
readonly open: string
readonly search: string
}
type Page = { rows: ItemView[]; after?: string | undefined }
export function ItemList({
slugs,
seed,
listHref,
labels,
}: {
slugs: ProjectSlugs
seed: Page
listHref: string
labels: ItemListLabels
}): ReactElement {
const client = useDataClient()
const { list } = useResourceList<ItemView>({
seed,
rowId: (item) => item.itemId,
searchLabel: labels.search,
read: async ({ q, after }) => {
const input = { ...slugs, ...(q ? { q } : {}), ...(after ? { after } : {}) }
return (await client.call<Page>("inventory.item.list", input)).value
},
})
const columns: ResourceColumn<ItemView>[] = [
{ id: "sku", header: labels.sku },
{ id: "name", header: labels.name, wrap: true, minWidth: "10rem" },
{ id: "unit", header: labels.unit },
{ id: "active", header: labels.active, cell: (item) => (item.active ? labels.active : labels.archived) },
]
return (
<ResourceList
{...list}
label={labels.caption}
columns={columns}
rowHref={(item) => `${listHref}/${item.itemId}`}
rowLinkLabel={(item) => labels.open.replace("{name}", item.sku)}
rowLabel={(item) => item.sku}
empty={{ title: labels.empty }}
/>
)
}
+88
View File
@@ -0,0 +1,88 @@
// The labels each component takes, built from a message lookup. Every key this module's pages use is
// named here or in pages.tsx, so `__tests__/i18n.test.ts` can run these builders against the
// catalogues and fail on a key either language lacks.
//
// `t` takes a key relative to the module (`form.submit`); the page binds it to
// `i18n.message("inventory." + key)`. `refusals` is the kit's text for each business refusal in the reader's
// language (`businessRefusalNotices`), which a form or a list shows when the service refuses.
import type { ItemFormLabels } from "./item-form.js"
import type { ItemListLabels } from "./item-list.js"
import type { ReservationListLabels } from "./reservation-list.js"
import type { ReserveFormLabels } from "./reserve-form.js"
import type { StockLabels } from "./stock.js"
export type Translate = (key: string, values?: Record<string, string | number>) => string
export type Refusals = Readonly<Record<string, string>>
export function itemFormLabels(t: Translate, refusals: Refusals): ItemFormLabels {
return {
heading: t("form.heading"),
sku: t("form.sku"),
skuHint: t("form.skuHint"),
name: t("form.name"),
unit: t("form.unit"),
unitHint: t("form.unitHint"),
description: t("form.description"),
submit: t("form.submit"),
busy: t("form.busy"),
refusals,
}
}
export function itemListLabels(t: Translate): ItemListLabels {
return {
caption: t("items.caption"),
empty: t("items.empty"),
sku: t("items.sku"),
name: t("items.name"),
unit: t("items.unit"),
active: t("items.active"),
archived: t("items.archived"),
// The placeholders are the component's to fill, so they are passed through untouched.
open: t("items.open", { name: "{name}" }),
search: t("items.search"),
}
}
export function stockLabels(t: Translate, unit: string, locations: number): StockLabels {
return {
unit: t("item.unit", { unit }),
locations: t("item.locations", { count: locations }),
onHand: t("item.onHand"),
reserved: t("item.reserved"),
available: t("item.available"),
caption: t("item.locationsCaption"),
location: t("item.location"),
none: t("item.none"),
}
}
export function reserveFormLabels(t: Translate, refusals: Refusals): ReserveFormLabels {
return {
heading: t("reserve.heading"),
location: t("reserve.location"),
quantity: t("reserve.quantity"),
submit: t("reserve.submit"),
busy: t("reserve.busy"),
nothing: t("reserve.nothing"),
refusals,
}
}
export function reservationListLabels(t: Translate, refusals: Refusals): ReservationListLabels {
return {
title: t("reservations.title"),
caption: t("reservations.caption"),
empty: t("reservations.empty"),
item: t("reservations.item"),
quantity: t("reservations.quantity"),
outstanding: t("reservations.outstanding"),
actions: t("reservations.actions"),
release: t("reservations.release", { item: "{item}" }),
releaseShort: t("reservations.releaseShort"),
releasing: t("reservations.releasing"),
releaseConfirm: t("reservations.releaseConfirm"),
refusals,
}
}
+200
View File
@@ -0,0 +1,200 @@
// 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 `/<org>/<workspace>/<project>/inventory`, and the view of what it returned.
//
// /inventory the organization's items, a form that adds one, and this project's reservations
// /inventory/<itemId> 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<Record<string, string>>
}
/** 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<TransportCaller, I18n, Container, InventoryPageData>
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<InventoryPageData> {
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<InventoryPageData>): ReactElement {
const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/inventory`
if (data.kind === "state") return <ServiceStatePage title={data.title} notice={data.notice} />
if (data.kind === "list") {
return (
<Page>
<PageHeader title={data.title} description={data.intro} />
<DetailLayout
aside={
<Can permission="inventory.item.write">
<ItemForm slugs={slugs} listHref={listHref} labels={data.itemForm} />
</Can>
}
>
<PageSection title={data.itemsHeading}>
<ItemList slugs={slugs} seed={data.items} listHref={listHref} labels={data.itemList} />
</PageSection>
<ReservationList
slugs={slugs}
seed={data.reservations}
itemLabel={data.itemLabel}
listHref={listHref}
labels={data.reservationList}
/>
</DetailLayout>
</Page>
)
}
return (
<Page>
<PageHeader
title={data.heading}
description={data.description}
breadcrumb={<a href={listHref}>{data.back}</a>}
/>
<DetailLayout
aside={
data.active ? (
<Can permission="inventory.stock.reserve">
<ReserveForm
slugs={slugs}
itemId={data.itemId}
options={data.availability.locations.map((level) => ({
locationId: level.locationId,
label: `${locationName(level.locationId)} (${level.available})`,
available: level.available,
}))}
labels={data.reserve}
/>
</Can>
) : undefined
}
>
<Stock availability={data.availability} locationName={locationName} labels={data.stock} />
</DetailLayout>
</Page>
)
},
})
+100
View File
@@ -0,0 +1,100 @@
"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>
)
}
+84
View File
@@ -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>
)
}
+59
View File
@@ -0,0 +1,59 @@
// An item's stock: the totals and one row per location that holds any. Part of a page's view (F10): it
// holds no state, and every string is passed in.
import { FactList, PageSection, type ResourceColumn, ResourceList } from "@conusai-kit/ui/patterns"
import type { ReactElement } from "react"
import type { AvailabilityView } from "../contract/index.js"
export interface StockLabels {
/** `Counted in {unit}`, already filled. */
readonly unit: string
/** How many locations hold stock, already filled with the plural form. */
readonly locations: string
readonly onHand: string
readonly reserved: string
readonly available: string
readonly caption: string
readonly location: string
readonly none: string
}
type Level = AvailabilityView["locations"][number]
export function Stock({
availability,
locationName,
labels,
}: {
availability: AvailabilityView
/** The location's name for a person; the service knows locations by id. */
locationName: (locationId: string) => string
labels: StockLabels
}): ReactElement {
const columns: ResourceColumn<Level>[] = [
{ id: "locationId", header: labels.location, cell: (level) => locationName(level.locationId) },
{ id: "onHand", header: labels.onHand, align: "end" },
{ id: "reserved", header: labels.reserved, align: "end" },
{ id: "available", header: labels.available, align: "end" },
]
return (
<PageSection title={labels.caption} description={labels.unit}>
<FactList
facts={[
{ label: labels.onHand, value: availability.onHand },
{ label: labels.reserved, value: availability.reserved },
{ label: labels.available, value: availability.available },
]}
/>
<ResourceList
label={labels.caption}
columns={columns}
data={availability.locations}
rowId={(level) => level.locationId}
count={labels.locations}
rowLabel={(level) => locationName(level.locationId)}
empty={{ title: labels.none }}
/>
</PageSection>
)
}