TorbatYar/frontend/app/admin/tenants/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

125 lines
4.4 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 { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { api, ApiError, type Tenant } from "@/lib/api";
import {
AdminPageHeader,
Banner,
Button,
EmptyState,
StatusBadge,
} from "@/components/admin/controls";
export default function AdminTenantsPage() {
const [tenants, setTenants] = useState<Tenant[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [busyId, setBusyId] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const page = await api.platformTenants.list(1, 100);
setTenants(page.items);
} catch (e) {
setError(e instanceof ApiError ? e.message : "خطا در بارگذاری Tenantها");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const toggleStatus = async (t: Tenant) => {
setBusyId(t.id);
setError("");
try {
if (t.status === "suspended") {
await api.platformTenants.activate(t.id);
} else {
await api.platformTenants.suspend(t.id);
}
await load();
} catch (e) {
setError(e instanceof ApiError ? e.message : "تغییر وضعیت ناموفق بود");
} finally {
setBusyId(null);
}
};
return (
<div className="space-y-6">
<AdminPageHeader
title="مدیریت Tenantها"
subtitle="ایجاد، ویرایش و مدیریت وضعیت workspaceها"
action={
<Link href="/admin/tenants/new">
<Button>ایجاد Tenant</Button>
</Link>
}
/>
{error && <Banner variant="error">{error}</Banner>}
<section className="overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm">
{loading ? (
<EmptyState message="در حال بارگذاری..." />
) : tenants.length === 0 ? (
<EmptyState message="هنوز Tenantی ایجاد نشده است." />
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-gray-50 text-right">
<tr>
<th className="px-4 py-3 font-semibold text-secondary">نام</th>
<th className="px-4 py-3 font-semibold text-secondary">Slug</th>
<th className="px-4 py-3 font-semibold text-secondary">وضعیت</th>
<th className="px-4 py-3 font-semibold text-secondary">Onboarding</th>
<th className="px-4 py-3 font-semibold text-secondary">عملیات</th>
</tr>
</thead>
<tbody>
{tenants.map((t) => (
<tr key={t.id} className="border-t border-gray-100">
<td className="px-4 py-3 font-medium text-secondary">{t.name}</td>
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
{t.slug}
</td>
<td className="px-4 py-3">
<StatusBadge status={t.status} />
</td>
<td className="px-4 py-3 text-gray-600">
{t.onboarding_completed ? "تکمیل‌شده" : "ناتمام"}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-2">
<Link href={`/admin/tenants/${t.id}`}>
<Button variant="outline" size="sm">
مدیریت
</Button>
</Link>
<Button
variant={t.status === "suspended" ? "primary" : "ghost"}
size="sm"
loading={busyId === t.id}
onClick={() => toggleStatus(t)}
>
{t.status === "suspended" ? "فعال‌سازی" : "تعلیق"}
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</div>
);
}