135 lines
4.8 KiB
TypeScript
135 lines
4.8 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";
|
||
import { getRegistryPresentation } from "../presentation";
|
||
|
||
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;
|
||
icon: string;
|
||
accentColor: string;
|
||
group: CommercialAdminResourceDef["group"];
|
||
}>
|
||
>([]);
|
||
|
||
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]));
|
||
const presentedEntries = 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;
|
||
const presentation = binding?.presentation || getRegistryPresentation(key);
|
||
return {
|
||
key,
|
||
label: presentation.navigationTitle,
|
||
description: presentation.description,
|
||
href: `/admin/commercial/${key}`,
|
||
listPath: path,
|
||
icon: presentation.icon,
|
||
accentColor: presentation.accentColor,
|
||
group: binding?.group || "catalog",
|
||
};
|
||
});
|
||
setEntries(
|
||
Array.from(
|
||
new Map(presentedEntries.map((entry) => [entry.key, entry])).values()
|
||
)
|
||
);
|
||
return;
|
||
}
|
||
setEntries(
|
||
COMMERCIAL_ADMIN_RESOURCES.map((r: CommercialAdminResourceDef) => ({
|
||
key: r.key,
|
||
label: r.presentation.navigationTitle,
|
||
description: r.presentation.description,
|
||
href: `/admin/commercial/${r.key}`,
|
||
listPath: r.listPath,
|
||
icon: r.presentation.icon,
|
||
accentColor: r.presentation.accentColor,
|
||
group: r.group,
|
||
}))
|
||
);
|
||
})();
|
||
}, []);
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
<AdminPageHeader
|
||
title="مدیریت تجاری"
|
||
subtitle="محصولات، بستهها، قیمتها و اشتراکها را از یک فضای ساده مدیریت و منتشر کنید."
|
||
/>
|
||
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{entries.filter((entry) => entry.group !== "tenant").map((r) => (
|
||
<Link
|
||
key={r.key}
|
||
href={r.href}
|
||
className={`${cardClass} group transition-all hover:-translate-y-0.5 hover:shadow-md`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<span className={`grid h-10 w-10 shrink-0 place-items-center rounded-xl text-lg ${r.accentColor}`} aria-hidden="true">
|
||
{r.icon}
|
||
</span>
|
||
<div>
|
||
<p className="font-semibold text-secondary">{r.label}</p>
|
||
<p className="mt-1 text-xs leading-5 text-gray-500">{r.description}</p>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
|
||
<section>
|
||
<h2 className="mb-3 text-sm font-bold text-secondary">مشتریان و فعالسازی</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`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<span className={`grid h-10 w-10 shrink-0 place-items-center rounded-xl text-lg ${r.presentation.accentColor}`} aria-hidden="true">
|
||
{r.presentation.icon}
|
||
</span>
|
||
<div>
|
||
<p className="font-semibold text-secondary">{r.presentation.navigationTitle}</p>
|
||
<p className="mt-1 text-xs leading-5 text-gray-500">{r.presentation.description}</p>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|