"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 (

ورود لازم است

برای دسترسی به این بخش باید وارد حساب workspace شوید.

); } 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 (
} /> {features.length > 0 ? (

قابلیت‌های فعال

{features.map(([k]) => ( {k} ))}
) : null}
); } export function PublicFeatures() { const { capsQ } = useBeautyServiceMeta(); if (capsQ.isLoading) return ; return (
    {Object.entries(capsQ.data?.features ?? {}).map(([key, on]) => (
  • {key} {on ? "فعال" : "آینده"}
  • ))}
); } export function PublicPricing() { const { capsQ, metricsQ } = useBeautyServiceMeta(); if (capsQ.isLoading) return ; return (

{capsQ.data?.commercial_product}

فاز: {capsQ.data?.phase}

حالت یکپارچگی: {capsQ.data?.independence.integration_mode}

{metricsQ.data?.note ? (

{metricsQ.data.note}

) : null}
); } 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 (
{items.map((item) => (

{item.q}

{item.a}

))}
); } export function PublicAbout() { const healthQ = useQuery({ queryKey: ["beauty", "health"], queryFn: () => beautyBusinessApi.health(), }); if (healthQ.isLoading) return ; return (

سرویس: {healthQ.data?.service}

نسخه: {healthQ.data?.version}

وضعیت: {healthQ.data?.status}

); } export function PublicContact() { const { capsQ } = useBeautyServiceMeta(); return (

محصول: {capsQ.data?.commercial_product}

); } 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 ; if (!active || orgsQ.isLoading || branchesQ.isLoading) return ; if (orgsQ.error) return orgsQ.refetch()} />; const orgs = (orgsQ.data ?? []).filter(filter); const branches = (branchesQ.data ?? []).filter((b) => orgs.some((o) => o.id === b.organization_id) ); return (
{orgs.map((o) => ( ))} {branches.map((b) => ( ))}
{orgs.length === 0 && branches.length === 0 ? : null}
); } export function PublicFindSalon() { return ( o.business_format === "salon" || o.business_format === "mixed"} /> ); } export function PublicFindLaser() { return ( o.business_format === "clinic" || o.business_format === "mixed"} /> ); } export function PublicFindSpa() { return ( 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 ; if (!active || orgsQ.isLoading) return ; 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 (
{orgs.map((o) => ( ))} {branches.map((b) => ( ))} {services.map((s) => ( ))} {staff.map((s) => ( ))}
{orgs.length + branches.length + services.length + staff.length === 0 ? ( ) : null}
); } export function PublicSearch() { return ( }> ); } 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 ; if (orgQ.error) return orgQ.refetch()} />; return ( ); } 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 ; if (branchQ.error) return branchQ.refetch()} />; return ( ); } 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 ; if (serviceQ.error) return serviceQ.refetch()} />; return ( ); } 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 ; if (staffQ.error) return staffQ.refetch()} />; return ( ); } 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 ; if (!tenantId || orgsQ.isLoading) return ; return ( {step === 1 ? ( ) : null} {step === 2 ? ( ) : null} {step === 3 ? ( ) : null} {step === 4 ? ( ) : null} {step === 5 ? ( <> setScheduledStart(e.target.value)} /> setScheduledEnd(e.target.value)} /> ) : null}
{step > 1 ? ( ) : ( )} {step < 5 ? ( ) : ( )}
); } function BookSuccessInner() { const sp = useSearchParams(); const id = sp.get("id"); return (

درخواست ثبت شد

وضعیت نوبت را از پورتال مشتری یا tracking پیگیری کنید.

{id ? ( ) : null}
); } export function BookSuccess() { return ( }> ); } 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 ; if (apptQ.error) return 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 (
{steps.map((s) => ( ))}
); } export function PublicReviews() { const authed = !!getStoredToken(); const { campaignsQ, active } = usePublicTenantQueries(authed); if (!authed) return ; if (!active || campaignsQ.isLoading) return ; return ( ); } export function PublicGallery() { const authed = !!getStoredToken(); const { servicesQ, active } = usePublicTenantQueries(authed); if (!authed) return ; if (!active || servicesQ.isLoading) return ; return (
{(servicesQ.data ?? []).map((s) => ( ))}
{(servicesQ.data ?? []).length === 0 ? : null}
); } export function PublicPackages() { const authed = !!getStoredToken(); const { packagesQ, active } = usePublicTenantQueries(authed); if (!authed) return ; if (!active || packagesQ.isLoading) return ; return (
{(packagesQ.data ?? []).map((p) => (

{p.name}

{p.total_sessions} جلسه

))}
{(packagesQ.data ?? []).length === 0 ? : null}
); } export function PublicCampaigns() { const authed = !!getStoredToken(); const { campaignsQ, active } = usePublicTenantQueries(authed); if (!authed) return ; if (!active || campaignsQ.isLoading) return ; return (
{(campaignsQ.data ?? []).map((c) => ( ))}
{(campaignsQ.data ?? []).length === 0 ? : null}
); }