TorbatYar/frontend/components/TenantSitePage.tsx
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

230 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { AppStoreGrid } from "@/components/AppStoreGrid";
import { Badge, Button, Container } from "@/components/ui";
import { useAuth } from "@/hooks/useAuth";
import { useMe } from "@/hooks/useMe";
import { useTenantHost } from "@/hooks/useTenantHost";
import { api, type PublicTenantSite } from "@/lib/api";
import { applyTheme } from "@/lib/theme";
/**
* صفحه ساب‌دامین tenant:
* - مهمان / غیرعضو → لندینگ تمپلیت برند کسب‌وکار
* - عضو/مالک لاگین‌شده → دسترسی به سرویس‌ها و داشبورد
*/
export function TenantSitePage() {
const host = useTenantHost();
const { isAuthenticated, login, loading: authLoading, signOut, user } = useAuth();
const { me, loading: meLoading } = useMe();
const [site, setSite] = useState<PublicTenantSite | null>(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
(async () => {
setLoading(true);
setError("");
try {
const data = await api.public.tenantSite({
slug: host.slug,
host: host.hostname,
});
if (cancelled) return;
setSite(data);
applyTheme({
site_name: data.name,
primary_color: data.primary_color || "#0284c7",
secondary_color: data.secondary_color || "#0f172a",
logo_url: data.logo_url || "",
support_email: "",
});
if (typeof document !== "undefined") {
document.title = data.name;
}
} catch (e) {
if (!cancelled) {
setError(e instanceof Error ? e.message : "بارگذاری سایت ناموفق بود");
setSite(null);
}
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [host.slug, host.hostname]);
const membership = useMemo(() => {
if (!me || !site) return null;
return me.memberships.find((m) => m.tenant_slug === site.slug) ?? null;
}, [me, site]);
const isMember = Boolean(isAuthenticated && membership);
const isOwner = Boolean(membership?.is_owner);
if (loading || (isAuthenticated && meLoading)) {
return (
<div className="flex min-h-[60vh] items-center justify-center">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
);
}
if (error || !site) {
return (
<Container className="py-20 text-center">
<h1 className="text-2xl font-bold text-secondary">سایت یافت نشد</h1>
<p className="mt-3 text-gray-600">{error || "این دامنه به workspace فعالی وصل نیست."}</p>
<a href={`https://${host.baseDomain}`} className="mt-8 inline-block text-primary underline">
بازگشت به تربتیار
</a>
</Container>
);
}
if (isMember) {
return (
<TenantMemberHome
site={site}
isOwner={isOwner}
role={membership?.role}
userLabel={user?.username || user?.email || me?.mobile || "کاربر"}
onSignOut={signOut}
/>
);
}
return (
<TenantPublicLanding
site={site}
onLogin={() => login()}
authLoading={authLoading}
platformUrl={`https://${host.baseDomain}`}
/>
);
}
function TenantPublicLanding({
site,
onLogin,
authLoading,
platformUrl,
}: {
site: PublicTenantSite;
onLogin: () => void;
authLoading: boolean;
platformUrl: string;
}) {
const initial = site.name.trim().charAt(0) || "ت";
return (
<div className="relative min-h-[calc(100vh-4rem)] overflow-hidden">
<div
className="pointer-events-none absolute inset-0 opacity-90"
style={{
background: `radial-gradient(ellipse 80% 60% at 50% -10%, var(--color-primary-muted), transparent),
linear-gradient(180deg, var(--color-primary-soft) 0%, #fff 55%)`,
}}
/>
<Container className="relative flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center py-16 text-center">
<div className="mb-6 flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-primary text-3xl font-bold text-white shadow-lg shadow-primary/25">
{site.logo_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={site.logo_url} alt="" className="h-full w-full object-cover" />
) : (
initial
)}
</div>
{site.business_type && (
<Badge variant="primary" className="mb-4">
{site.business_type}
</Badge>
)}
<h1 className="max-w-2xl text-4xl font-extrabold leading-tight text-secondary sm:text-5xl">
{site.name}
</h1>
<p className="mt-4 max-w-xl text-base leading-8 text-gray-600 sm:text-lg">
{site.tagline}. بهزودی صفحه عمومی و سرویسهای این کسبوکار اینجا تکمیل میشود.
</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
<Button size="lg" onClick={onLogin} loading={authLoading} loadingText="...">
ورود مالک / همکاران
</Button>
<a href={platformUrl}>
<Button size="lg" variant="outline">
درباره تربتیار
</Button>
</a>
</div>
{site.primary_domain && (
<p dir="ltr" className="mt-10 font-mono text-xs text-gray-400">
{site.primary_domain}
</p>
)}
</Container>
</div>
);
}
function TenantMemberHome({
site,
isOwner,
role,
userLabel,
onSignOut,
}: {
site: PublicTenantSite;
isOwner: boolean;
role?: string;
userLabel: string;
onSignOut: () => void;
}) {
return (
<div className="pb-16">
<section className="border-b border-gray-100 bg-gradient-to-b from-[var(--color-primary-soft)] to-white">
<Container className="py-10 sm:py-12">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<Badge variant="primary" className="mb-3">
{isOwner ? "مالک workspace" : role || "عضو"}
</Badge>
<h1 className="text-3xl font-extrabold text-secondary">{site.name}</h1>
<p className="mt-2 text-sm text-gray-600">
خوش آمدید {userLabel} سرویسهای تربتیار برای این کسبوکار
</p>
{site.primary_domain && (
<p dir="ltr" className="mt-2 font-mono text-xs text-gray-400">
{site.primary_domain}
</p>
)}
</div>
<div className="flex flex-wrap gap-2">
<Link href="/dashboard">
<Button variant="primary">داشبورد workspace</Button>
</Link>
<Link href="/dashboard/settings">
<Button variant="outline">تنظیمات</Button>
</Link>
<Button variant="ghost" onClick={onSignOut}>
خروج
</Button>
</div>
</div>
</Container>
</section>
<Container className="py-10">
<h2 className="mb-2 text-xl font-bold text-secondary">سرویسها و افزونهها</h2>
<p className="mb-8 text-sm text-gray-600">
ماژولهای پلتفرم که بهتدریج برای این tenant فعال میشوند.
</p>
<AppStoreGrid />
</Container>
</div>
);
}