TorbatYar/frontend/modules/commercial/features/admin-hub.tsx
Mortezakoohjani 0d424c500a feat(platform): complete commercial runtime consolidation
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>
2026-07-29 11:11:53 +03:30

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>
);
}