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>
207 lines
7.6 KiB
TypeScript
207 lines
7.6 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { AuthGuard } from "@/components/AuthGuard";
|
||
import { TenantSwitcher } from "@/components/TenantSwitcher";
|
||
import { useAuth } from "@/hooks/useAuth";
|
||
import { useMe } from "@/hooks/useMe";
|
||
import { api, ApiError, type TenantContext } from "@/lib/api";
|
||
import { Badge, Button, Container, SectionTitle } from "@/components/ui";
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
draft: "پیشنویس",
|
||
pending_activation: "در انتظار فعالسازی",
|
||
active: "فعال",
|
||
suspended: "معلق",
|
||
archived: "بایگانیشده",
|
||
};
|
||
|
||
const ROLE_LABELS: Record<string, string> = {
|
||
platform_admin: "مدیر پلتفرم",
|
||
tenant_owner: "مالک workspace",
|
||
tenant_admin: "مدیر workspace",
|
||
tenant_editor: "ویرایشگر",
|
||
tenant_viewer: "بیننده",
|
||
};
|
||
|
||
const QUICK_LINKS = [
|
||
{ title: "کاربران و دسترسیها", desc: "مدیریت اعضای workspace (بهزودی)" },
|
||
{ title: "برندینگ و ظاهر", desc: "شخصیسازی رنگ و لوگو (بهزودی)" },
|
||
{ title: "دامنهها", desc: "مدیریت زیردامنه و دامنه اختصاصی (بهزودی)" },
|
||
{ title: "اشتراک و پلن", desc: "مشاهده و ارتقاء پلن (بهزودی)" },
|
||
];
|
||
|
||
function DashboardContent() {
|
||
const router = useRouter();
|
||
const { user, signOut } = useAuth();
|
||
const { me, loading: meLoading, error: meError, reload: reloadMe } = useMe();
|
||
|
||
const [ctx, setCtx] = useState<TenantContext | null>(null);
|
||
const [loadingCtx, setLoadingCtx] = useState(true);
|
||
const [ctxError, setCtxError] = useState("");
|
||
|
||
const loadContext = useCallback(async () => {
|
||
setLoadingCtx(true);
|
||
setCtxError("");
|
||
try {
|
||
const data = await api.tenantContext.current();
|
||
setCtx(data);
|
||
} catch (err) {
|
||
setCtxError(
|
||
err instanceof ApiError ? err.message : "دریافت اطلاعات workspace ناموفق بود"
|
||
);
|
||
setCtx(null);
|
||
} finally {
|
||
setLoadingCtx(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (meLoading) return;
|
||
if (!me) return;
|
||
if (me.onboarding_required) {
|
||
router.replace("/onboarding");
|
||
return;
|
||
}
|
||
void loadContext();
|
||
}, [me, meLoading, router, loadContext]);
|
||
|
||
const handleSwitched = async () => {
|
||
await reloadMe();
|
||
await loadContext();
|
||
};
|
||
|
||
if (meLoading || (me && me.onboarding_required)) {
|
||
return (
|
||
<Container className="py-16 text-center">
|
||
<p className="text-sm text-gray-500">در حال بارگذاری...</p>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Container className="py-10">
|
||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-secondary">داشبورد workspace</h1>
|
||
<p className="mt-1 text-sm text-gray-600">
|
||
خوش آمدید {user?.username || user?.email || "کاربر"}
|
||
</p>
|
||
</div>
|
||
<div className="flex flex-wrap items-end gap-4">
|
||
{me && (
|
||
<TenantSwitcher
|
||
memberships={me.memberships}
|
||
currentTenantId={me.current_tenant_id}
|
||
onSwitched={handleSwitched}
|
||
/>
|
||
)}
|
||
<Link href="/dashboard/settings">
|
||
<Button variant="outline">تنظیمات حساب</Button>
|
||
</Link>
|
||
<Button variant="outline" onClick={() => signOut()}>
|
||
خروج
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{(meError || ctxError) && (
|
||
<p className="mt-6 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600">
|
||
{meError || ctxError}
|
||
</p>
|
||
)}
|
||
|
||
{loadingCtx ? (
|
||
<p className="mt-8 text-sm text-gray-500">در حال بارگذاری workspace...</p>
|
||
) : ctx ? (
|
||
<div className="mt-8 space-y-8">
|
||
<section className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-secondary">{ctx.tenant.name}</h2>
|
||
<p dir="ltr" className="mt-1 font-mono text-sm text-gray-500">
|
||
{ctx.primary_domain || ctx.tenant.slug}
|
||
</p>
|
||
</div>
|
||
<Badge variant={ctx.tenant.status === "active" ? "primary" : "default"}>
|
||
{STATUS_LABELS[ctx.tenant.status] || ctx.tenant.status}
|
||
</Badge>
|
||
</div>
|
||
|
||
<dl className="mt-6 grid grid-cols-1 gap-4 text-sm sm:grid-cols-2 lg:grid-cols-4">
|
||
<div className="rounded-xl bg-gray-50 p-4">
|
||
<dt className="text-xs text-gray-500">پلن فعلی</dt>
|
||
<dd className="mt-1 font-semibold text-secondary">
|
||
{ctx.plan_name || "—"}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-4">
|
||
<dt className="text-xs text-gray-500">وضعیت اشتراک</dt>
|
||
<dd className="mt-1 font-semibold text-secondary">
|
||
{ctx.subscription_status || "—"}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-4">
|
||
<dt className="text-xs text-gray-500">نقش شما</dt>
|
||
<dd className="mt-1 font-semibold text-secondary">
|
||
{(ctx.role && ROLE_LABELS[ctx.role]) || ctx.role || "—"}
|
||
</dd>
|
||
</div>
|
||
<div className="rounded-xl bg-gray-50 p-4">
|
||
<dt className="text-xs text-gray-500">وضعیت onboarding</dt>
|
||
<dd className="mt-1 font-semibold text-secondary">
|
||
{ctx.tenant.onboarding_completed ? "تکمیلشده" : "ناتمام"}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
{ctx.domains.length > 0 && (
|
||
<div className="mt-6">
|
||
<p className="text-xs font-medium text-gray-500">دامنهها</p>
|
||
<div className="mt-2 flex flex-wrap gap-2">
|
||
{ctx.domains.map((d) => (
|
||
<Badge key={d.id} variant={d.is_verified ? "primary" : "default"}>
|
||
<span dir="ltr" className="font-mono">
|
||
{d.domain}
|
||
</span>
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section>
|
||
<SectionTitle title="ماژولها" subtitle="دسترسی سریع به بخشهای workspace" />
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
{QUICK_LINKS.map((link) => (
|
||
<div
|
||
key={link.title}
|
||
className="rounded-2xl border border-gray-100 bg-white p-5 shadow-sm"
|
||
>
|
||
<p className="font-semibold text-secondary">{link.title}</p>
|
||
<p className="mt-1 text-xs text-gray-500">{link.desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
) : (
|
||
!ctxError && (
|
||
<p className="mt-8 text-sm text-gray-500">اطلاعات workspace در دسترس نیست.</p>
|
||
)
|
||
)}
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
return (
|
||
<AuthGuard>
|
||
<DashboardContent />
|
||
</AuthGuard>
|
||
);
|
||
}
|