"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}
);
}