TorbatYar/frontend/app/admin/page.tsx
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

109 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { api } from "@/lib/api";
import { getStoredToken } from "@/lib/auth";
import { identityApi } from "@/lib/identity";
import { AdminPageHeader, Banner, cardClass } from "@/components/admin/controls";
interface Stats {
tenants: number | null;
users: number | null;
plans: number | null;
features: number | null;
services: number | null;
}
const QUICK_LINKS = [
{ href: "/admin/tenants", title: "مدیریت Tenantها", desc: "ایجاد، ویرایش، تعلیق و فعال‌سازی" },
{ href: "/admin/users", title: "مدیریت کاربران", desc: "لیست و ایجاد کاربر جدید" },
{ href: "/admin/plans", title: "پلن‌ها", desc: "تعریف پلن و اتصال قابلیت‌ها" },
{ href: "/admin/features", title: "قابلیت‌ها", desc: "تعریف قابلیت‌های سرویس‌ها" },
{ href: "/admin/services", title: "سرویس‌ها", desc: "ثبت و مدیریت وضعیت سرویس‌ها" },
];
export default function AdminOverviewPage() {
const [stats, setStats] = useState<Stats>({
tenants: null,
users: null,
plans: null,
features: null,
services: null,
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = getStoredToken();
(async () => {
const [tenants, plans, features, services, users] = await Promise.allSettled([
api.platformTenants.list(1, 1),
api.plans.list(1, 1),
api.features.list(1, 1),
api.services.list(1, 1),
token ? identityApi.listUsers(token, { limit: 100 }) : Promise.resolve([]),
]);
setStats({
tenants: tenants.status === "fulfilled" ? tenants.value.meta.total_items : null,
plans: plans.status === "fulfilled" ? plans.value.meta.total_items : null,
features: features.status === "fulfilled" ? features.value.meta.total_items : null,
services: services.status === "fulfilled" ? services.value.meta.total_items : null,
users: users.status === "fulfilled" ? users.value.length : null,
});
if (tenants.status === "rejected") {
setError(
tenants.reason instanceof Error ? tenants.reason.message : "خطا در بارگذاری آمار"
);
}
setLoading(false);
})();
}, []);
const cards = [
{ label: "Tenantها", value: stats.tenants, href: "/admin/tenants" },
{ label: "کاربران", value: stats.users, href: "/admin/users" },
{ label: "پلن‌ها", value: stats.plans, href: "/admin/plans" },
{ label: "قابلیت‌ها", value: stats.features, href: "/admin/features" },
{ label: "سرویس‌ها", value: stats.services, href: "/admin/services" },
];
return (
<div className="space-y-8">
<AdminPageHeader
title="نمای کلی پلتفرم"
subtitle="خلاصه وضعیت و دسترسی سریع به بخش‌های مدیریتی"
/>
{error && <Banner variant="error">{error}</Banner>}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5">
{cards.map((c) => (
<Link
key={c.label}
href={c.href}
className={`${cardClass} transition-shadow hover:shadow-md`}
>
<p className="text-xs text-gray-500">{c.label}</p>
<p className="mt-2 text-3xl font-bold text-secondary">
{loading ? "…" : c.value ?? "—"}
</p>
</Link>
))}
</div>
<div>
<h2 className="mb-4 text-lg font-bold text-secondary">دسترسی سریع</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{QUICK_LINKS.map((link) => (
<Link key={link.href} href={link.href} className={`${cardClass} transition-shadow hover:shadow-md`}>
<p className="font-semibold text-secondary">{link.title}</p>
<p className="mt-1 text-sm text-gray-500">{link.desc}</p>
</Link>
))}
</div>
</div>
</div>
);
}