"use client";
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { AdminPageHeader, Banner, Button, EmptyState, Modal } from "@/components/admin/controls";
import { useMe } from "@/hooks/useMe";
import type { CommercialAdminResourceDef } from "../admin/resources";
import {
createAdminRegistryItem,
deleteAdminRegistryItem,
listAdminRegistry,
type RegistryRecord,
updateAdminRegistryItem,
} from "../adapters/admin";
import type { AdapterResult } from "../types";
import {
enumLabel,
fieldLabel,
friendlyMessage,
humanizeCommercialCode,
isTechnicalField,
} from "../presentation";
const PAGE_SIZE = 10;
const FIELD_GROUPS = [
{ title: "اطلاعات پایه", keys: ["display_name", "title", "description", "category"] },
{ title: "اطلاعات تجاری", keys: ["role", "pricing_model", "amount_minor", "currency", "interval", "trial_days", "duration_days", "grace_period_days"] },
{ title: "قیمتگذاری", keys: ["pricing_refs", "tax_class_code", "discount_refs", "coupon_code"] },
{ title: "قابلیتها", keys: ["product_codes", "capability_codes", "feature_refs", "required_capabilities"] },
{ title: "نمایش", keys: ["visibility", "display_order", "trial_eligible"] },
{ title: "انتشار", keys: ["status", "published_at", "starts_at", "ends_at"] },
{ title: "اطلاعات تکمیلی", keys: ["metadata", "attributes", "parameters", "usage_limits", "feature_limits"] },
] as const;
function recordLabel(item: RegistryRecord, resource: CommercialAdminResourceDef): string {
const name = item[resource.nameField];
return humanizeCommercialCode(item[resource.codeField], typeof name === "string" ? name : null);
}
function recordCode(item: RegistryRecord, resource: CommercialAdminResourceDef): string {
const code = item[resource.codeField];
return code == null ? "" : String(code);
}
function recordTargetId(item: RegistryRecord, resource: CommercialAdminResourceDef): string {
const target = item.registry_object_id ?? item.id ?? item[resource.codeField];
return target == null ? "" : String(target);
}
function defaultRecord(resource: CommercialAdminResourceDef): RegistryRecord {
const seed: RegistryRecord = {
[resource.codeField]: "",
[resource.nameField]: "",
status: "draft",
};
if (resource.key === "products") Object.assign(seed, { visibility: "public", display_order: 0 });
if (resource.key === "trial-rules") Object.assign(seed, { duration_days: 14, grace_period_days: 3, auto_disable: true });
if (resource.key === "pricing") Object.assign(seed, { pricing_model: "monthly", currency: "IRR", amount_minor: 0 });
if (resource.key === "policies") Object.assign(seed, { effect: "deny", parameters: { custom_domain_allowed: false, subdomain_only: true, ssl_ready: false } });
return seed;
}
function parseFieldValue(key: string, input: string, current: unknown): unknown {
if (Array.isArray(current) || /(_refs|_codes|capabilities)$/.test(key)) {
return input.split("،").flatMap((part) => part.split(",")).map((part) => part.trim()).filter(Boolean);
}
if (typeof current === "number" || /(amount|days|order|percent|limit)$/.test(key)) {
return input === "" ? null : Number(input);
}
return input;
}
function FieldInput({
name,
value,
onChange,
}: {
name: string;
value: unknown;
onChange: (value: unknown) => void;
}) {
if (Array.isArray(value)) {
return (
{fieldLabel(name)}
{value.length ? value.map((item, index) => (
{humanizeCommercialCode(item)}
)) : موردی انتخاب نشده است.}
);
}
if (typeof value === "boolean") {
return (
);
}
if (value && typeof value === "object" && !Array.isArray(value)) {
return (
);
}
const displayValue = value == null ? "" : String(value);
const selectOptions: Record = {
status: ["draft", "active", "published", "inactive"],
visibility: ["public", "private", "hidden"],
pricing_model: ["monthly", "yearly", "trial", "enterprise_quote"],
interval: ["monthly", "yearly"],
currency: ["IRR"],
};
if (selectOptions[name]) {
return (
);
}
return (
);
}
function CommercialRecordForm({
value,
resource,
onChange,
}: {
value: RegistryRecord;
resource: CommercialAdminResourceDef;
onChange: (record: RegistryRecord) => void;
}) {
const visibleKeys = Object.keys(value).filter((key) => !isTechnicalField(key));
const grouped = new Set(FIELD_GROUPS.flatMap((group) => [...group.keys]));
const advanced = visibleKeys.filter((key) => !grouped.has(key) && key !== resource.nameField);
const setField = (key: string, next: unknown) => onChange({ ...value, [key]: next });
return (
{FIELD_GROUPS.map((group) => {
const keys = Array.from(new Set([
...(group.title === "اطلاعات پایه" ? [resource.nameField] : []),
...group.keys,
])).filter((key) => key in value && !isTechnicalField(key));
if (!keys.length) return null;
return (
{group.title}
{keys.map((key) => setField(key, next)} />)}
);
})}
{advanced.length ? (
تنظیمات پیشرفته
این بخش برای تنظیمات کماستفاده است. تغییر آنها ممکن است روی انتشار اثر بگذارد.
{advanced.map((key) => setField(key, next)} />)}
) : null}
);
}
export function AdminRegistryManager({ resource }: { resource: CommercialAdminResourceDef }) {
const { me } = useMe();
const [result, setResult] = useState | null>(null);
const [error, setError] = useState("");
const [msg, setMsg] = useState("");
const [loading, setLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
const [editItem, setEditItem] = useState(null);
const [formBody, setFormBody] = useState({});
const [busy, setBusy] = useState(false);
const [query, setQuery] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [sortAsc, setSortAsc] = useState(true);
const [page, setPage] = useState(1);
const [selected, setSelected] = useState>(new Set());
const load = useCallback(async () => {
setLoading(true);
setError("");
const response = await listAdminRegistry(resource, me?.current_tenant_id);
setResult(response);
if (["unavailable", "permission_denied", "error"].includes(response.availability)) {
setError(friendlyMessage(response.message, "بارگذاری اطلاعات انجام نشد. لطفاً دوباره تلاش کنید."));
}
setLoading(false);
}, [me?.current_tenant_id, resource]);
useEffect(() => { void load(); }, [load]);
const items = useMemo(() => result?.data ?? [], [result]);
const filtered = useMemo(() => {
const normalized = query.trim().toLocaleLowerCase("fa");
return items
.filter((item) => statusFilter === "all" || item.status === statusFilter)
.filter((item) => !normalized || recordLabel(item, resource).toLocaleLowerCase("fa").includes(normalized))
.sort((a, b) => recordLabel(a, resource).localeCompare(recordLabel(b, resource), "fa") * (sortAsc ? 1 : -1));
}, [items, query, resource, sortAsc, statusFilter]);
const pages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const visible = filtered.slice((Math.min(page, pages) - 1) * PAGE_SIZE, Math.min(page, pages) * PAGE_SIZE);
const openCreate = () => {
setFormBody(defaultRecord(resource));
setCreateOpen(true);
setMsg("");
setError("");
};
const openEdit = (item: RegistryRecord) => {
setEditItem(item);
setFormBody({ ...item });
setMsg("");
setError("");
};
const submitCreate = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
setError("");
const body = { ...formBody };
if (!body[resource.codeField]) {
body[resource.codeField] = `${resource.key.replace(/s$/, "")}.${Date.now().toString(36)}`;
}
const response = await createAdminRegistryItem(resource, body);
if (!response.ok) setError(friendlyMessage(response.message, "ثبت اطلاعات انجام نشد. لطفاً فیلدهای ضروری را بررسی کنید."));
else {
setCreateOpen(false);
setMsg("اطلاعات با موفقیت ثبت شد.");
await load();
}
setBusy(false);
};
const submitEdit = async (event: FormEvent) => {
event.preventDefault();
if (!editItem) return;
const targetId = recordTargetId(editItem, resource);
if (!targetId) return setError("شناسه این مورد پیدا نشد. صفحه را تازهسازی کنید.");
const changedFields = Object.fromEntries(
Object.entries(formBody).filter(
([key, value]) =>
![
"registry_object_id",
"id",
"kind",
"slug",
"stable_code",
"created_at",
"updated_at",
"created_by",
"updated_by",
"published_at",
"archived_at",
"is_deleted",
"clone_of_id",
].includes(key) && !Object.is(value, editItem[key])
)
);
if (!Object.keys(changedFields).length) {
setEditItem(null);
setMsg("تغییری برای ذخیره وجود نداشت.");
return;
}
setBusy(true);
const response = await updateAdminRegistryItem(resource, targetId, changedFields);
if (!response.ok) setError(friendlyMessage(response.message, "ذخیره تغییرات انجام نشد. دوباره تلاش کنید."));
else {
setEditItem(null);
setMsg("تغییرات با موفقیت ذخیره شد.");
await load();
}
setBusy(false);
};
const removeItems = async (targets: RegistryRecord[]) => {
if (!targets.length || !window.confirm(`${targets.length.toLocaleString("fa-IR")} مورد حذف شود؟`)) return;
setBusy(true);
for (const item of targets) {
const targetId = recordTargetId(item, resource);
if (targetId) {
const response = await deleteAdminRegistryItem(resource, targetId);
if (!response.ok) {
setError(friendlyMessage(response.message, "حذف بعضی موارد انجام نشد."));
break;
}
}
}
setSelected(new Set());
await load();
setBusy(false);
};
return (
{!resource.readOnly ? : null} }
/>
{error ? {error} : null}
{msg ? {msg} : null}
{selected.size && !resource.readOnly ? : null}
{loading ? (
) : !visible.length ? (
) : (
)}
{filtered.length > PAGE_SIZE ? (
صفحه {Math.min(page, pages).toLocaleString("fa-IR")} از {pages.toLocaleString("fa-IR")}
) : null}
setCreateOpen(false)} title={`افزودن ${resource.label}`}>
setEditItem(null)} title={`ویرایش ${resource.label}`}>
);
}