TorbatYar/frontend/modules/commercial/components/RegistryBrowser.tsx

211 lines
7.1 KiB
TypeScript

"use client";
import type { AdapterResult, RegistryAvailability } from "../types";
import { CommercialEmptyState } from "./CommercialEmptyState";
import {
commercialEntityName,
fieldLabel,
friendlyMessage,
getRegistryPresentation,
isTechnicalField,
presentValue,
registryKindForField,
} from "../presentation";
function availabilityLabel(a: RegistryAvailability): string {
switch (a) {
case "loading":
return "در حال بارگذاری…";
case "empty":
return "خالی";
case "unavailable":
return "در دسترس نیست";
case "permission_denied":
return "بدون دسترسی";
case "error":
return "خطا";
default:
return "";
}
}
function PresentedValue({ value, contextKey }: { value: unknown; contextKey?: string }) {
const presented = presentValue(value);
if (Array.isArray(presented)) {
return (
<div className="grid gap-2">
{presented.map((item, index) => (
typeof item === "string" ? (
<span key={index} className="w-fit rounded-full bg-gray-100 px-2.5 py-1 text-xs text-gray-700">
{item}
</span>
) : (
<div key={index} className="rounded-xl border border-gray-100 bg-white p-2">
<PresentedValue value={item} contextKey={contextKey} />
</div>
)
))}
</div>
);
}
if (presented && typeof presented === "object") {
const entries = Object.entries(presented).filter(([key]) => !isTechnicalField(key));
const referencedKind = registryKindForField(contextKey);
const codeEntry = Object.entries(presented).find(([key]) => /(^code$|_code$)/.test(key));
const explicitName =
typeof presented.display_name === "string"
? presented.display_name
: typeof presented.title === "string"
? presented.title
: null;
const objectTitle =
referencedKind && codeEntry
? commercialEntityName(referencedKind, codeEntry[1], explicitName)
: null;
return (
<details className="rounded-xl border border-gray-100 bg-gray-50 p-3">
<summary className="cursor-pointer text-xs font-semibold text-secondary">
{objectTitle || "مشاهده جزئیات"}
</summary>
<dl className="mt-3 grid gap-3 sm:grid-cols-2">
{entries.map(([key, nested]) => (
<div key={key}>
<dt className="text-[11px] text-gray-500">{fieldLabel(key)}</dt>
<dd className="mt-1 text-xs text-secondary"><PresentedValue value={nested} contextKey={key} /></dd>
</div>
))}
</dl>
</details>
);
}
return <>{presented}</>;
}
/** Generic human-facing catalog list. Implementation identifiers are deliberately hidden. */
export function RegistryBrowser({
kind,
title,
subtitle,
result,
codeKey,
nameKey = "display_name",
emptyTitle,
}: {
kind: string;
title?: string;
subtitle?: string;
result: AdapterResult<any[]> | null;
codeKey: string;
nameKey?: string;
emptyTitle?: string;
}) {
const presentation = getRegistryPresentation(kind);
const sectionTitle = title || presentation.sectionTitle;
if (!result || result.availability === "loading") {
return (
<section>
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
<p className="mt-2 text-xs text-gray-500">{availabilityLabel("loading")}</p>
</section>
);
}
if (result.availability !== "ready" || !result.data.length) {
return (
<section>
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
<div className="mt-2">
<CommercialEmptyState
title={emptyTitle || presentation.emptyStateTitle}
description={friendlyMessage(result.message, presentation.emptyStateDescription)}
/>
</div>
</section>
);
}
return (
<section>
<h3 className="text-sm font-semibold text-secondary">{sectionTitle}</h3>
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
<ul className="mt-3 grid gap-2 sm:grid-cols-2">
{result.data.map((raw, idx) => {
const item = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
const code = String(item[codeKey] ?? idx);
const explicitName = typeof item[nameKey] === "string" ? String(item[nameKey]) : null;
const name = commercialEntityName(kind, code, explicitName);
const meta = item.metadata;
const detailEntries = Object.entries(item).filter(
([key, value]) =>
key !== codeKey &&
key !== nameKey &&
key !== "metadata" &&
!isTechnicalField(key) &&
value != null
);
return (
<li
key={code}
className="rounded-xl border border-gray-100 bg-white px-3 py-2 text-sm shadow-sm"
>
<p className="font-medium text-secondary">{name}</p>
{detailEntries.length ? (
<dl className="mt-3 space-y-2">
{detailEntries.slice(0, 4).map(([key, value]) => (
<div key={key} className="flex items-start justify-between gap-3 text-xs">
<dt className="text-gray-500">{fieldLabel(key)}</dt>
<dd className="text-left text-secondary"><PresentedValue value={value} contextKey={key} /></dd>
</div>
))}
</dl>
) : null}
{meta && typeof meta === "object" && !Array.isArray(meta) ? (
<div className="mt-3"><PresentedValue value={meta} contextKey="metadata" /></div>
) : null}
</li>
);
})}
</ul>
</section>
);
}
/** Renders an arbitrary metadata bag without whitelisting attribute keys. */
export function MetadataBagView({
title,
attributes,
message,
availability,
}: {
title: string;
attributes?: Record<string, unknown> | null;
message?: string;
availability?: RegistryAvailability;
}) {
const entries = attributes ? Object.entries(attributes) : [];
if (!entries.length) {
return (
<CommercialEmptyState
title={title}
description={friendlyMessage(message, availabilityLabel(availability || "empty"))}
/>
);
}
return (
<div className="rounded-2xl border border-gray-100 bg-white p-4">
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
<dl className="mt-3 space-y-2">
{entries.filter(([key]) => !isTechnicalField(key)).map(([key, value]) => (
<div key={key} className="rounded-xl bg-gray-50 p-3 text-xs">
<dt className="text-gray-500">{fieldLabel(key)}</dt>
<dd className="mt-2 text-secondary">
<PresentedValue value={value} contextKey={key} />
</dd>
</div>
))}
</dl>
</div>
);
}