Unify commercial runtime ownership across backend and frontend so platform, experience, and hospitality modules use the shared commercial source of truth. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { AdminPageHeader, cardClass } from "@/components/admin/controls";
|
|
import { commercialGet } from "../adapters/http";
|
|
import {
|
|
COMMERCIAL_ADMIN_RESOURCES,
|
|
type CommercialAdminResourceDef,
|
|
} from "../admin/resources";
|
|
|
|
type CatalogEntry = {
|
|
kind: string;
|
|
path?: string;
|
|
display_name?: string;
|
|
description?: string | null;
|
|
href?: string;
|
|
generic_href?: string;
|
|
};
|
|
|
|
/**
|
|
* Admin hub — prefers live GET /api/v1/commercial/catalog.
|
|
* Static resource defs are path bindings only (no row data); used as label fallback.
|
|
*/
|
|
export function AdminCommercialHub() {
|
|
const [entries, setEntries] = useState<
|
|
Array<{ key: string; label: string; description: string; href: string; listPath: string }>
|
|
>([]);
|
|
const [source, setSource] = useState<"catalog" | "bindings">("bindings");
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
const res = await commercialGet<{ registries?: CatalogEntry[] }>("/api/v1/commercial/catalog");
|
|
if (res.ok && Array.isArray(res.data.registries) && res.data.registries.length) {
|
|
const byPath = new Map(COMMERCIAL_ADMIN_RESOURCES.map((r) => [r.listPath, r]));
|
|
setEntries(
|
|
res.data.registries.map((r) => {
|
|
const path = r.href || `/api/v1/commercial/${r.path || r.kind}`;
|
|
const binding = byPath.get(path) || byPath.get(`/api/v1/commercial/${r.path}`);
|
|
const key = binding?.key || r.path || r.kind;
|
|
return {
|
|
key,
|
|
label: r.display_name || binding?.label || r.kind,
|
|
description: r.description || binding?.description || "Commercial registry",
|
|
href: `/admin/commercial/${key}`,
|
|
listPath: path,
|
|
};
|
|
})
|
|
);
|
|
setSource("catalog");
|
|
return;
|
|
}
|
|
setError(res.ok ? "" : res.message);
|
|
setEntries(
|
|
COMMERCIAL_ADMIN_RESOURCES.map((r: CommercialAdminResourceDef) => ({
|
|
key: r.key,
|
|
label: r.label,
|
|
description: r.description,
|
|
href: `/admin/commercial/${r.key}`,
|
|
listPath: r.listPath,
|
|
}))
|
|
);
|
|
setSource("bindings");
|
|
})();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<AdminPageHeader
|
|
title="پرتال تجاری (Commercial Admin)"
|
|
subtitle="کشف رجیستری از Commercial Runtime catalog — SoT واحد. تغییرات بلافاصله در tenant دیده میشود."
|
|
/>
|
|
<p className="text-xs text-gray-500" dir="ltr">
|
|
discovery: {source}
|
|
{error ? ` · ${error}` : ""}
|
|
</p>
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{entries.map((r) => (
|
|
<Link
|
|
key={r.key}
|
|
href={r.href}
|
|
className={`${cardClass} transition-shadow hover:shadow-md`}
|
|
>
|
|
<p className="font-semibold text-secondary">{r.label}</p>
|
|
<p className="mt-1 text-xs text-gray-500">{r.description}</p>
|
|
<p className="mt-2 font-mono text-[10px] text-gray-400" dir="ltr">
|
|
{r.listPath}
|
|
</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
<section>
|
|
<h2 className="mb-3 text-sm font-bold text-secondary">Tenant / Activation</h2>
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{COMMERCIAL_ADMIN_RESOURCES.filter((r) => r.group === "tenant").map((r) => (
|
|
<Link
|
|
key={r.key}
|
|
href={`/admin/commercial/${r.key}`}
|
|
className={`${cardClass} transition-shadow hover:shadow-md`}
|
|
>
|
|
<p className="font-semibold text-secondary">{r.label}</p>
|
|
<p className="mt-1 text-xs text-gray-500">{r.description}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|