// The crm module's pages, on the shell's F10 contract (docs/reference/app-kit.md): a loader the web shell // runs on the server under `////crm`, and the view of what it returned. // // /crm the organization's parties, and the form that adds one // /crm/ one party: how to reach it, and the form that adds a contact point // // 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, type ServiceNotice, ServiceStatePage, serviceNotice, } from "@conusai-kit/ui/patterns" import type { ReactElement } from "react" import { type ContactView, type PartyView, partyGet, partyList } from "../contract/index.js" import { ContactForm, type ContactFormLabels } from "./contact-form.js" import { Contacts, type ContactsLabels } from "./contacts.js" import { contactFormLabels, contactsLabels, partyFormLabels, partyListLabels, type Translate } from "./labels.js" import { PartyForm, type PartyFormLabels } from "./party-form.js" import { PartyList, type PartyListLabels } from "./party-list.js" /** The parties, and the form that adds one. */ export interface CrmListData { readonly kind: "list" readonly title: string readonly intro: string readonly form: PartyFormLabels readonly list: PartyListLabels readonly parties: { rows: PartyView[]; after?: string | undefined } } /** One party, its kind and country already in a sentence. */ export interface CrmPartyData { readonly kind: "party" readonly title: string readonly back: string readonly partyId: string readonly displayName: string readonly summary: string /** The note that the party is a member's account, when it is. */ readonly linked?: string readonly contacts: readonly ContactView[] readonly contactLabels: ContactsLabels readonly form: ContactFormLabels } /** The service cannot answer: the notice in place of the page. */ export interface CrmStateData { readonly kind: "state" readonly title: string readonly notice: ServiceNotice } export type CrmPageData = CrmListData | CrmPartyData | CrmStateData export type CrmPage = ProjectPage /** A party id as the service mints it: one path segment. */ const PARTY_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/ /** The party a path segment names; a segment that does not decode names none. */ function partyIdOf(segment: string): string { try { return decodeURIComponent(segment) } catch { throw notFound("no such page") } } export const crmPage: CrmPage = projectPage({ async load({ slugs, segments, caller, i18n }): Promise { const t: Translate = (key, values) => i18n.message(`crm.${key}`, values) const title = t("title") const refusals = businessRefusalNotices(i18n.locale) const [segment, ...rest] = segments const partyId = segment === undefined ? undefined : partyIdOf(segment) if (rest.length > 0 || (partyId !== undefined && !PARTY_ID.test(partyId))) throw notFound("no such page") try { if (partyId === undefined) { return { kind: "list", title, intro: t("intro"), form: partyFormLabels(t, refusals), list: partyListLabels(t), parties: await caller.call(partyList, slugs), } } const { party, contacts } = await caller.call(partyGet, { ...slugs, partyId }) return { kind: "party", title, back: t("party.back"), partyId: party.partyId, displayName: party.displayName, summary: `${t(`kinds.${party.kind}`)}, ${party.country}${party.legalName ? `, ${party.legalName}` : ""}`, ...(party.linked ? { linked: t("party.linked") } : {}), contacts, contactLabels: contactsLabels(t, contacts.length), form: contactFormLabels(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}/crm` if (data.kind === "state") return if (data.kind === "list") { return ( } > ) } return ( {data.back}}> {data.linked ? {data.linked} : null} } > ) }, })