TorbatYar/frontend/modules/beauty/pages/public.tsx
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:28:27 +03:30

754 lines
26 KiB
TypeScript
Raw Permalink 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 { useRouter, useSearchParams, useParams } from "next/navigation";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useEffect, Suspense, useState } from "react";
import {
PageHeader,
Button,
Card,
CardContent,
Badge,
LoadingState,
EmptyState,
Select,
Input,
FormField,
} from "@/components/ds";
import { beautyBusinessApi } from "@/modules/beauty/services/beauty-business-api";
import { useTenantId } from "@/hooks/useTenantId";
import { getStoredToken } from "@/lib/auth";
import {
CenterCard,
SalonCard,
ServiceCard,
StaffCard,
SearchResultRow,
StatWidget,
TimelineItem,
Hero,
PageSection,
} from "@/modules/beauty/design-system";
import { BeautyPageError, BeautyPageLoader, useBeautyServiceMeta } from "./shared";
import { toast } from "sonner";
function AuthGateCTA({ returnPath }: { returnPath: string }) {
return (
<div className="mx-auto max-w-lg py-20 text-center">
<h1 className="text-2xl font-bold text-secondary">ورود لازم است</h1>
<p className="mt-3 text-[var(--muted)]">برای دسترسی به این بخش باید وارد حساب workspace شوید.</p>
<Link href={`/login?returnUrl=${encodeURIComponent(returnPath)}`} className="mt-6 inline-block">
<Button size="lg">ورود / ثبتنام</Button>
</Link>
</div>
);
}
function usePublicTenantQueries(enabled: boolean) {
const { tenantId } = useTenantId();
const active = enabled && !!tenantId;
const orgsQ = useQuery({
queryKey: ["beauty", tenantId, "public-orgs"],
queryFn: () => beautyBusinessApi.organizations.list(tenantId!),
enabled: active,
});
const branchesQ = useQuery({
queryKey: ["beauty", tenantId, "public-branches"],
queryFn: () => beautyBusinessApi.branches.list(tenantId!),
enabled: active,
});
const servicesQ = useQuery({
queryKey: ["beauty", tenantId, "public-services"],
queryFn: () => beautyBusinessApi.services.list(tenantId!),
enabled: active,
});
const staffQ = useQuery({
queryKey: ["beauty", tenantId, "public-staff"],
queryFn: () => beautyBusinessApi.staff.list(tenantId!),
enabled: active,
});
const packagesQ = useQuery({
queryKey: ["beauty", tenantId, "public-packages"],
queryFn: () => beautyBusinessApi.packageDefinitions.list(tenantId!),
enabled: active,
});
const campaignsQ = useQuery({
queryKey: ["beauty", tenantId, "public-campaigns"],
queryFn: () => beautyBusinessApi.marketingCampaigns.list(tenantId!),
enabled: active,
});
return { tenantId, orgsQ, branchesQ, servicesQ, staffQ, packagesQ, campaignsQ, active };
}
export function PublicHome() {
const { capsQ } = useBeautyServiceMeta();
const features = Object.entries(capsQ.data?.features ?? {}).filter(([, v]) => v).slice(0, 6);
return (
<div>
<Hero
badge="SaaS زیبایی — تربت بیوتی"
title="نوبت‌دهی، سالن و خدمات زیبایی"
description="پلتفرم یکپارچه برای مشتری، پذیرش، پرسنل و مدیر — متصل به API واقعی beauty-business."
actions={
<>
<Link href="/login?returnUrl=/beauty/hub">
<Button size="lg">ورود به پورتال</Button>
</Link>
<Link href="/beauty/site/book">
<Button size="lg" variant="outline">
رزرو نوبت
</Button>
</Link>
</>
}
/>
{features.length > 0 ? (
<PageSection>
<h2 className="mb-6 text-center text-xl font-semibold text-secondary">قابلیتهای فعال</h2>
<div className="flex flex-wrap justify-center gap-2">
{features.map(([k]) => (
<Badge key={k} tone="success">
{k}
</Badge>
))}
</div>
</PageSection>
) : null}
</div>
);
}
export function PublicFeatures() {
const { capsQ } = useBeautyServiceMeta();
if (capsQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="امکانات" description={`فاز ${capsQ.data?.phase}${capsQ.data?.commercial_product}`} />
<ul className="mt-8 grid gap-3 sm:grid-cols-2">
{Object.entries(capsQ.data?.features ?? {}).map(([key, on]) => (
<li
key={key}
className="flex items-center justify-between rounded-2xl border border-[var(--border)] bg-[var(--surface)] px-4 py-3"
>
<span className="text-secondary">{key}</span>
<Badge tone={on ? "success" : "default"}>{on ? "فعال" : "آینده"}</Badge>
</li>
))}
</ul>
</PageSection>
);
}
export function PublicPricing() {
const { capsQ, metricsQ } = useBeautyServiceMeta();
if (capsQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="قیمت‌گذاری" description="بر اساس قابلیت‌های فعال سرویس." />
<Card className="mt-8">
<CardContent className="space-y-4 pt-6">
<p className="font-semibold text-secondary">{capsQ.data?.commercial_product}</p>
<p className="text-sm text-[var(--muted)]">فاز: {capsQ.data?.phase}</p>
<p className="text-sm text-[var(--muted)]">
حالت یکپارچگی: {capsQ.data?.independence.integration_mode}
</p>
{metricsQ.data?.note ? (
<p className="text-xs text-[var(--muted)]">{metricsQ.data.note}</p>
) : null}
<Link href="/login?returnUrl=/beauty/hub">
<Button>شروع با workspace</Button>
</Link>
</CardContent>
</Card>
</PageSection>
);
}
export function PublicFaq() {
const { capsQ } = useBeautyServiceMeta();
const items = [
{
q: "آیا standalone کار می‌کند؟",
a: capsQ.data?.independence.standalone ? "بله — standalone فعال است." : "خیر — نیاز به superapp.",
},
{
q: "یکپارچگی‌ها",
a: (capsQ.data?.independence.integrations ?? []).join("، ") || "—",
},
{ q: "نسخه سرویس", a: capsQ.data?.version ?? "—" },
];
return (
<PageSection className="max-w-3xl">
<PageHeader title="سوالات متداول" />
<div className="mt-8 space-y-4">
{items.map((item) => (
<Card key={item.q}>
<CardContent className="pt-5">
<p className="font-semibold text-secondary">{item.q}</p>
<p className="mt-2 text-sm text-[var(--muted)]">{item.a}</p>
</CardContent>
</Card>
))}
</div>
</PageSection>
);
}
export function PublicAbout() {
const healthQ = useQuery({
queryKey: ["beauty", "health"],
queryFn: () => beautyBusinessApi.health(),
});
if (healthQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection className="max-w-3xl">
<PageHeader title="درباره تربت بیوتی" description="سرویس SaaS زیبایی تربت‌یار." />
<Card className="mt-8">
<CardContent className="space-y-3 pt-6 text-sm text-[var(--muted)]">
<p>
سرویس: <strong className="text-secondary">{healthQ.data?.service}</strong>
</p>
<p>
نسخه: <strong className="text-secondary">{healthQ.data?.version}</strong>
</p>
<p>وضعیت: {healthQ.data?.status}</p>
</CardContent>
</Card>
</PageSection>
);
}
export function PublicContact() {
const { capsQ } = useBeautyServiceMeta();
return (
<PageSection className="max-w-3xl">
<PageHeader title="تماس" description="برای دسترسی به workspace با پشتیبانی تربت‌یار تماس بگیرید." />
<Card className="mt-8">
<CardContent className="space-y-3 pt-6">
<p className="text-sm text-[var(--muted)]">محصول: {capsQ.data?.commercial_product}</p>
<Link href="/login?returnUrl=/beauty/site/contact">
<Button>ورود workspace</Button>
</Link>
</CardContent>
</Card>
</PageSection>
);
}
function AuthenticatedFindPage({
title,
filter,
returnPath,
}: {
title: string;
filter: (org: { business_format: string }) => boolean;
returnPath: string;
}) {
const authed = !!getStoredToken();
const { orgsQ, branchesQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath={returnPath} />;
if (!active || orgsQ.isLoading || branchesQ.isLoading) return <BeautyPageLoader />;
if (orgsQ.error) return <BeautyPageError error={orgsQ.error} onRetry={() => orgsQ.refetch()} />;
const orgs = (orgsQ.data ?? []).filter(filter);
const branches = (branchesQ.data ?? []).filter((b) =>
orgs.some((o) => o.id === b.organization_id)
);
return (
<PageSection>
<PageHeader title={title} description="نتایج از API واقعی — پس از ورود." />
<div className="mt-8 grid gap-4 sm:grid-cols-2">
{orgs.map((o) => (
<CenterCard key={o.id} org={o} />
))}
{branches.map((b) => (
<SalonCard key={b.id} branch={b} />
))}
</div>
{orgs.length === 0 && branches.length === 0 ? <EmptyState title="نتیجه‌ای یافت نشد" /> : null}
</PageSection>
);
}
export function PublicFindSalon() {
return (
<AuthenticatedFindPage
title="یافتن سالن"
returnPath="/beauty/site/find-salon"
filter={(o) => o.business_format === "salon" || o.business_format === "mixed"}
/>
);
}
export function PublicFindLaser() {
return (
<AuthenticatedFindPage
title="یافتن مرکز لیزر"
returnPath="/beauty/site/find-laser"
filter={(o) => o.business_format === "clinic" || o.business_format === "mixed"}
/>
);
}
export function PublicFindSpa() {
return (
<AuthenticatedFindPage
title="یافتن اسپا"
returnPath="/beauty/site/find-spa"
filter={(o) => o.business_format === "spa" || o.business_format === "mixed"}
/>
);
}
function PublicSearchInner() {
const sp = useSearchParams();
const q = (sp.get("q") ?? "").trim().toLowerCase();
const authed = !!getStoredToken();
const { orgsQ, branchesQ, servicesQ, staffQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath={`/beauty/site/search?q=${encodeURIComponent(q)}`} />;
if (!active || orgsQ.isLoading) return <BeautyPageLoader />;
const orgs = (orgsQ.data ?? []).filter(
(o) => !q || o.name.toLowerCase().includes(q) || o.code.toLowerCase().includes(q)
);
const branches = (branchesQ.data ?? []).filter(
(b) => !q || b.name.toLowerCase().includes(q) || b.code.toLowerCase().includes(q)
);
const services = (servicesQ.data ?? []).filter(
(s) => !q || s.name.toLowerCase().includes(q) || s.code.toLowerCase().includes(q)
);
const staff = (staffQ.data ?? []).filter(
(s) => !q || s.display_name.toLowerCase().includes(q) || s.code.toLowerCase().includes(q)
);
return (
<PageSection>
<PageHeader title="جستجو" description={q ? `نتایج برای «${q}»` : "همه نتایج"} />
<div className="mt-8 space-y-3">
{orgs.map((o) => (
<SearchResultRow key={o.id} title={o.name} meta={o.code} href={`/beauty/site/center/${o.id}`} />
))}
{branches.map((b) => (
<SearchResultRow key={b.id} title={b.name} meta={b.code} href={`/beauty/site/salon/${b.id}`} />
))}
{services.map((s) => (
<SearchResultRow key={s.id} title={s.name} meta={s.code} href={`/beauty/site/service/${s.id}`} />
))}
{staff.map((s) => (
<SearchResultRow key={s.id} title={s.display_name} meta={s.kind} href={`/beauty/site/staff/${s.id}`} />
))}
</div>
{orgs.length + branches.length + services.length + staff.length === 0 ? (
<EmptyState title="نتیجه‌ای یافت نشد" />
) : null}
</PageSection>
);
}
export function PublicSearch() {
return (
<Suspense fallback={<LoadingState />}>
<PublicSearchInner />
</Suspense>
);
}
export function CenterDetail() {
const params = useParams();
const id = String(params.id);
const { tenantId } = useTenantId();
const orgQ = useQuery({
queryKey: ["beauty", tenantId, "center", id],
queryFn: () => beautyBusinessApi.organizations.get(tenantId!, id),
enabled: !!tenantId && !!id,
});
if (!tenantId || orgQ.isLoading) return <BeautyPageLoader />;
if (orgQ.error) return <BeautyPageError error={orgQ.error} onRetry={() => orgQ.refetch()} />;
return (
<PageSection className="max-w-3xl">
<CenterCard org={orgQ.data!} />
<Link href="/beauty/site/book" className="mt-6 inline-block">
<Button>رزرو نوبت</Button>
</Link>
</PageSection>
);
}
export function SalonDetail() {
const params = useParams();
const id = String(params.id);
const { tenantId } = useTenantId();
const branchQ = useQuery({
queryKey: ["beauty", tenantId, "salon", id],
queryFn: () => beautyBusinessApi.branches.get(tenantId!, id),
enabled: !!tenantId && !!id,
});
if (!tenantId || branchQ.isLoading) return <BeautyPageLoader />;
if (branchQ.error) return <BeautyPageError error={branchQ.error} onRetry={() => branchQ.refetch()} />;
return (
<PageSection className="max-w-3xl">
<SalonCard branch={branchQ.data!} />
</PageSection>
);
}
export function ServiceDetail() {
const params = useParams();
const id = String(params.id);
const { tenantId } = useTenantId();
const serviceQ = useQuery({
queryKey: ["beauty", tenantId, "service", id],
queryFn: () => beautyBusinessApi.services.get(tenantId!, id),
enabled: !!tenantId && !!id,
});
if (!tenantId || serviceQ.isLoading) return <BeautyPageLoader />;
if (serviceQ.error) return <BeautyPageError error={serviceQ.error} onRetry={() => serviceQ.refetch()} />;
return (
<PageSection className="max-w-3xl">
<ServiceCard service={serviceQ.data!} />
<Link href="/beauty/site/book" className="mt-6 inline-block">
<Button>رزرو این خدمت</Button>
</Link>
</PageSection>
);
}
export function StaffDetail() {
const params = useParams();
const id = String(params.id);
const { tenantId } = useTenantId();
const staffQ = useQuery({
queryKey: ["beauty", tenantId, "staff-detail", id],
queryFn: () => beautyBusinessApi.staff.get(tenantId!, id),
enabled: !!tenantId && !!id,
});
if (!tenantId || staffQ.isLoading) return <BeautyPageLoader />;
if (staffQ.error) return <BeautyPageError error={staffQ.error} onRetry={() => staffQ.refetch()} />;
return (
<PageSection className="max-w-3xl">
<StaffCard staff={staffQ.data!} />
<Link href="/beauty/site/book" className="mt-6 inline-block">
<Button>رزرو با این پرسنل</Button>
</Link>
</PageSection>
);
}
export function PublicBook() {
const authed = !!getStoredToken();
const { tenantId } = useTenantId();
const router = useRouter();
const [step, setStep] = useState(1);
const [orgId, setOrgId] = useState("");
const [branchId, setBranchId] = useState("");
const [serviceId, setServiceId] = useState("");
const [staffId, setStaffId] = useState("");
const [scheduledStart, setScheduledStart] = useState("");
const [scheduledEnd, setScheduledEnd] = useState("");
const orgsQ = useQuery({
queryKey: ["beauty", tenantId, "book-orgs"],
queryFn: () => beautyBusinessApi.organizations.list(tenantId!),
enabled: authed && !!tenantId,
});
const branchesQ = useQuery({
queryKey: ["beauty", tenantId, "book-branches", orgId],
queryFn: () => beautyBusinessApi.branches.list(tenantId!),
enabled: authed && !!tenantId && !!orgId,
select: (rows) => rows.filter((b) => b.organization_id === orgId),
});
const servicesQ = useQuery({
queryKey: ["beauty", tenantId, "book-services", orgId],
queryFn: () => beautyBusinessApi.services.list(tenantId!),
enabled: authed && !!tenantId && !!orgId,
select: (rows) => rows.filter((s) => s.organization_id === orgId),
});
const staffQ = useQuery({
queryKey: ["beauty", tenantId, "book-staff", orgId],
queryFn: () => beautyBusinessApi.staff.list(tenantId!),
enabled: authed && !!tenantId && !!orgId,
select: (rows) => rows.filter((s) => s.organization_id === orgId),
});
const createM = useMutation({
mutationFn: () =>
beautyBusinessApi.appointments.create(tenantId!, {
organization_id: orgId,
branch_id: branchId,
code: `WEB-${Date.now()}`,
scheduled_start: scheduledStart,
scheduled_end: scheduledEnd,
service_ref: serviceId || undefined,
staff_ref: staffId || undefined,
status: "requested",
}),
onSuccess: (appt) => {
toast.success("نوبت ثبت شد");
router.push(`/beauty/site/book/success?id=${appt.id}`);
},
onError: (e: Error) => toast.error(e.message),
});
if (!authed) return <AuthGateCTA returnPath="/beauty/site/book" />;
if (!tenantId || orgsQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection className="max-w-2xl">
<PageHeader title="رزرو نوبت" description={`مرحله ${step} از 5 — org → branch → service → staff → confirm`} />
<Card className="mt-8">
<CardContent className="space-y-4 pt-6">
{step === 1 ? (
<FormField label="سازمان">
<Select value={orgId} onChange={(e) => setOrgId(e.target.value)}>
<option value="">انتخاب کنید</option>
{(orgsQ.data ?? []).map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</Select>
</FormField>
) : null}
{step === 2 ? (
<FormField label="شعبه">
<Select value={branchId} onChange={(e) => setBranchId(e.target.value)}>
<option value="">انتخاب کنید</option>
{(branchesQ.data ?? []).map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</Select>
</FormField>
) : null}
{step === 3 ? (
<FormField label="خدمت">
<Select value={serviceId} onChange={(e) => setServiceId(e.target.value)}>
<option value="">انتخاب کنید</option>
{(servicesQ.data ?? []).map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</Select>
</FormField>
) : null}
{step === 4 ? (
<FormField label="پرسنل (اختیاری)">
<Select value={staffId} onChange={(e) => setStaffId(e.target.value)}>
<option value="">هر پرسنل</option>
{(staffQ.data ?? []).map((s) => (
<option key={s.id} value={s.id}>
{s.display_name}
</option>
))}
</Select>
</FormField>
) : null}
{step === 5 ? (
<>
<FormField label="شروع">
<Input type="datetime-local" value={scheduledStart} onChange={(e) => setScheduledStart(e.target.value)} />
</FormField>
<FormField label="پایان">
<Input type="datetime-local" value={scheduledEnd} onChange={(e) => setScheduledEnd(e.target.value)} />
</FormField>
</>
) : null}
<div className="flex justify-between gap-2 pt-2">
{step > 1 ? (
<Button type="button" variant="outline" onClick={() => setStep((s) => s - 1)}>
قبلی
</Button>
) : (
<span />
)}
{step < 5 ? (
<Button
type="button"
onClick={() => setStep((s) => s + 1)}
disabled={
(step === 1 && !orgId) ||
(step === 2 && !branchId) ||
(step === 3 && !serviceId)
}
>
بعدی
</Button>
) : (
<Button
type="button"
disabled={!scheduledStart || !scheduledEnd || createM.isPending}
onClick={() => createM.mutate()}
>
ثبت نوبت
</Button>
)}
</div>
</CardContent>
</Card>
</PageSection>
);
}
function BookSuccessInner() {
const sp = useSearchParams();
const id = sp.get("id");
return (
<PageSection className="max-w-lg text-center">
<h1 className="text-2xl font-bold text-secondary">درخواست ثبت شد</h1>
<p className="mt-3 text-[var(--muted)]">وضعیت نوبت را از پورتال مشتری یا tracking پیگیری کنید.</p>
{id ? (
<Link href={`/beauty/site/tracking/${id}`} className="mt-4 inline-block">
<Button variant="outline">پیگیری</Button>
</Link>
) : null}
<Link href="/beauty/customer/appointments/upcoming" className="mt-6 inline-block">
<Button>نوبتهای من</Button>
</Link>
</PageSection>
);
}
export function BookSuccess() {
return (
<Suspense fallback={<LoadingState />}>
<BookSuccessInner />
</Suspense>
);
}
export function Tracking() {
const params = useParams();
const id = String(params.id);
const { tenantId } = useTenantId();
const apptQ = useQuery({
queryKey: ["beauty", tenantId, "tracking", id],
queryFn: () => beautyBusinessApi.appointments.get(tenantId!, id),
enabled: !!tenantId && !!id,
});
if (!tenantId || apptQ.isLoading) return <BeautyPageLoader />;
if (apptQ.error) return <BeautyPageError error={apptQ.error} onRetry={() => apptQ.refetch()} />;
const a = apptQ.data!;
const steps = [
{ title: "ثبت", time: a.created_at, active: true },
{ title: "زمان‌بندی", time: a.scheduled_start, active: a.status !== "requested" },
{ title: "وضعیت", time: a.updated_at, active: ["confirmed", "completed"].includes(a.status) },
];
return (
<PageSection className="max-w-lg">
<PageHeader title={`پیگیری ${a.code}`} description={`وضعیت: ${a.status}`} />
<div className="mt-8">
{steps.map((s) => (
<TimelineItem
key={s.title}
title={s.title}
time={new Date(s.time).toLocaleString("fa-IR")}
active={s.active}
/>
))}
</div>
</PageSection>
);
}
export function PublicReviews() {
const authed = !!getStoredToken();
const { campaignsQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath="/beauty/site/reviews" />;
if (!active || campaignsQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="نظرات و بازخورد" description="کمپین‌های بازاریابی مرتبط." />
<EmptyState title="ماژول نظرات — از کمپین‌های فعال استفاده کنید" />
</PageSection>
);
}
export function PublicGallery() {
const authed = !!getStoredToken();
const { servicesQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath="/beauty/site/gallery" />;
if (!active || servicesQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="گالری خدمات" description="نمونه خدمات از کاتالوگ." />
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{(servicesQ.data ?? []).map((s) => (
<ServiceCard key={s.id} service={s} />
))}
</div>
{(servicesQ.data ?? []).length === 0 ? <EmptyState title="خدمتی ثبت نشده" /> : null}
</PageSection>
);
}
export function PublicPackages() {
const authed = !!getStoredToken();
const { packagesQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath="/beauty/site/packages" />;
if (!active || packagesQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="پکیج‌ها" description="پکیج‌های فعال سالن." />
<div className="mt-8 grid gap-4 sm:grid-cols-2">
{(packagesQ.data ?? []).map((p) => (
<Card key={p.id}>
<CardContent className="pt-5">
<p className="font-semibold text-secondary">{p.name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{p.total_sessions} جلسه</p>
</CardContent>
</Card>
))}
</div>
{(packagesQ.data ?? []).length === 0 ? <EmptyState title="پکیجی ثبت نشده" /> : null}
</PageSection>
);
}
export function PublicCampaigns() {
const authed = !!getStoredToken();
const { campaignsQ, active } = usePublicTenantQueries(authed);
if (!authed) return <AuthGateCTA returnPath="/beauty/site/campaigns" />;
if (!active || campaignsQ.isLoading) return <BeautyPageLoader />;
return (
<PageSection>
<PageHeader title="کمپین‌ها" description="کمپین‌های بازاریابی فعال." />
<div className="mt-8 space-y-3">
{(campaignsQ.data ?? []).map((c) => (
<SearchResultRow key={c.id} title={c.name} meta={c.channel ?? c.code} href={`/beauty/marketing/campaigns`} />
))}
</div>
{(campaignsQ.data ?? []).length === 0 ? <EmptyState title="کمپینی ثبت نشده" /> : null}
</PageSection>
);
}