TorbatYar/frontend/modules/beauty/pages/customer.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

443 lines
15 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 { useQuery } from "@tanstack/react-query";
import { PageHeader, EmptyState, Button, Badge, DataTable } from "@/components/ds";
import { beautyBusinessApi } from "@/modules/beauty/services/beauty-business-api";
import { useTenantId } from "@/hooks/useTenantId";
import { useBeautyLookups } from "@/modules/beauty/hooks/useBeautyLookups";
import { AppointmentCard, CustomerCard, PackageCard, SalonCard, StaffCard } from "@/modules/beauty/design-system";
import {
AggregateStatsRow,
BeautyPageError,
BeautyPageLoader,
ServiceMetaBadges,
SettingsListPage,
} from "./shared";
export function CustomerDashboard() {
const { tenantId } = useTenantId();
const { customers, staffName, serviceName, isLoading } = useBeautyLookups();
const apptsQ = useQuery({
queryKey: ["beauty", tenantId, "customer-appts"],
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || isLoading || apptsQ.isLoading) return <BeautyPageLoader />;
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
const upcoming = (apptsQ.data ?? []).filter((a) => new Date(a.scheduled_start) >= new Date());
return (
<div>
<PageHeader
title="داشبورد مشتری"
description="نوبت‌ها و پکیج‌ها از API واقعی."
actions={<ServiceMetaBadges />}
/>
<AggregateStatsRow
stats={[
{ label: "مشتریان workspace", value: customers.length },
{ label: "نوبت‌های آینده", value: upcoming.length },
{ label: "کل نوبت‌ها", value: apptsQ.data?.length ?? 0 },
]}
/>
<section className="mt-8 space-y-3">
<h2 className="font-semibold text-secondary">نوبتهای پیشرو</h2>
{upcoming.length === 0 ? (
<EmptyState
title="نوبت آینده‌ای ندارید"
action={
<Link href="/beauty/customer/appointments/book">
<Button>رزرو نوبت</Button>
</Link>
}
/>
) : (
upcoming.slice(0, 5).map((a) => (
<AppointmentCard
key={a.id}
appointment={a}
staffName={staffName(a.staff_ref)}
serviceName={serviceName(a.service_ref)}
/>
))
)}
</section>
</div>
);
}
function CustomerAppointmentsList({ upcoming }: { upcoming: boolean }) {
const { tenantId } = useTenantId();
const { staffName, serviceName } = useBeautyLookups();
const apptsQ = useQuery({
queryKey: ["beauty", tenantId, "customer-appointments", upcoming],
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || apptsQ.isLoading) return <BeautyPageLoader />;
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
const now = new Date();
const items = (apptsQ.data ?? []).filter((a) =>
upcoming ? new Date(a.scheduled_start) >= now : new Date(a.scheduled_start) < now
);
return (
<div>
<PageHeader
title={upcoming ? "نوبت‌های آینده" : "سوابق نوبت"}
description="لیست نوبت‌ها از appointments API."
/>
<div className="mt-6 space-y-3">
{items.map((a) => (
<AppointmentCard
key={a.id}
appointment={a}
staffName={staffName(a.staff_ref)}
serviceName={serviceName(a.service_ref)}
href={`/beauty/site/tracking/${a.id}`}
/>
))}
</div>
{items.length === 0 ? <EmptyState title="نوبتی یافت نشد" /> : null}
</div>
);
}
export function CustomerAppointmentsUpcoming() {
return <CustomerAppointmentsList upcoming />;
}
export function CustomerAppointmentsPast() {
return <CustomerAppointmentsList upcoming={false} />;
}
export function CustomerAppointmentsBook() {
return (
<div>
<PageHeader title="رزرو نوبت" description="رزرو از سایت عمومی یا فرم چندمرحله‌ای." />
<Link href="/beauty/site/book">
<Button>شروع رزرو</Button>
</Link>
</div>
);
}
export function CustomerPackages() {
const { tenantId } = useTenantId();
const pkgsQ = useQuery({
queryKey: ["beauty", tenantId, "customer-packages"],
queryFn: () => beautyBusinessApi.packageDefinitions.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || pkgsQ.isLoading) return <BeautyPageLoader />;
if (pkgsQ.error) return <BeautyPageError error={pkgsQ.error} onRetry={() => pkgsQ.refetch()} />;
return (
<div>
<PageHeader title="پکیج‌های من" description="پکیج‌های تعریف‌شده در workspace." />
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{(pkgsQ.data ?? []).map((p) => (
<PackageCard key={p.id} pkg={p} />
))}
</div>
{(pkgsQ.data ?? []).length === 0 ? <EmptyState title="پکیجی ثبت نشده" /> : null}
</div>
);
}
export function CustomerMemberships() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "customer-memberships"],
queryFn: () => beautyBusinessApi.membershipPlans.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="عضویت‌ها" description="طرح‌های عضویت از membership-plans API." />
<DataTable
columns={[
{ key: "name", header: "نام" },
{ key: "billing_interval_days", header: "دوره (روز)" },
{
key: "price",
header: "قیمت",
render: (r) => (r.price != null ? String(r.price) : "—"),
},
{
key: "status",
header: "وضعیت",
render: (r) => <Badge tone="default">{String(r.status)}</Badge>,
},
]}
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState title="طرح عضویتی ثبت نشده" />}
/>
</div>
);
}
export function CustomerProfile() {
const { customers, isLoading } = useBeautyLookups();
if (isLoading) return <BeautyPageLoader />;
return (
<div>
<PageHeader title="پروفایل" description="پروفایل‌های مشتری workspace." />
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{customers.map((c) => (
<CustomerCard key={c.id} customer={c} />
))}
</div>
{customers.length === 0 ? <EmptyState title="پروفایلی یافت نشد" /> : null}
</div>
);
}
export function CustomerFavorites() {
const { services, isLoading } = useBeautyLookups();
if (isLoading) return <BeautyPageLoader />;
return (
<div>
<PageHeader title="علاقه‌مندی‌ها" description="خدمات ذخیره‌شده — از services API." />
<EmptyState title="لیست علاقه‌مندی خالی است" />
</div>
);
}
export function CustomerReviews() {
return (
<div>
<PageHeader title="نظرات" description="بازخورد خدمات." />
<EmptyState title="نظری ثبت نشده" />
</div>
);
}
export function CustomerNotifications() {
return (
<div>
<PageHeader title="اعلان‌ها" description="اعلان‌های communication integration." />
<EmptyState title="اعلانی وجود ندارد" />
</div>
);
}
export function CustomerSettings() {
return <SettingsListPage title="تنظیمات مشتری" description="تنظیمات tenant از settings API." />;
}
export function CustomerInvoices() {
return (
<div>
<PageHeader title="فاکتورها" description="فاکتورها از سرویس حسابداری workspace خوانده می‌شوند." />
<EmptyState
title="فاکتوری در beauty API نیست"
action={
<Link href="/accounting/sales/invoices">
<Button variant="outline">مشاهده فاکتورها در حسابداری</Button>
</Link>
}
/>
</div>
);
}
export function CustomerPayments() {
return (
<div>
<PageHeader title="پرداخت‌ها" description="سوابق پرداخت از ماژول حسابداری." />
<EmptyState
title="پرداختی ثبت نشده"
action={
<Link href="/accounting/treasury/receipts-payments">
<Button variant="outline">خزانه و دریافت/پرداخت</Button>
</Link>
}
/>
</div>
);
}
export function CustomerWallet() {
return (
<div>
<PageHeader title="کیف پول" description="موجودی از سرویس وفاداری (Loyalty Wallet)." />
<EmptyState
title="کیف پول در beauty API مدیریت نمی‌شود"
action={
<Link href="/dashboard">
<Button variant="outline">بازگشت به workspace</Button>
</Link>
}
/>
</div>
);
}
export function CustomerLoyaltyPoints() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "loyalty-integrations"],
queryFn: () => beautyBusinessApi.loyaltyIntegrations.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="امتیاز وفاداری" description="پیکربندی یکپارچگی Loyalty از beauty API." />
<DataTable
columns={[
{ key: "name", header: "برنامه" },
{ key: "code", header: "کد" },
{
key: "external_program_ref",
header: "ارجاع Loyalty",
render: (r) => String(r.external_program_ref ?? "—"),
},
{
key: "status",
header: "وضعیت",
render: (r) => <Badge tone="default">{String(r.status)}</Badge>,
},
]}
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState title="یکپارچگی Loyalty پیکربندی نشده" />}
/>
</div>
);
}
export function CustomerMessages() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "integration-dispatches"],
queryFn: () => beautyBusinessApi.integrationDispatches.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="پیام‌ها" description="رویدادهای ارسال‌شده به Communication از integration-dispatches." />
<DataTable
columns={[
{ key: "event_type", header: "رویداد" },
{ key: "integration_kind", header: "کانال" },
{
key: "status",
header: "وضعیت",
render: (r) => <Badge tone="default">{String(r.status)}</Badge>,
},
]}
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState title="پیامی ارسال نشده" />}
/>
</div>
);
}
export function CustomerSessions() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "package-sessions"],
queryFn: () => beautyBusinessApi.packageDefinitions.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="جلسات باقی‌مانده" description="پکیج‌های چندجلسه‌ای از package-definitions API." />
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{(q.data ?? []).map((p) => (
<PackageCard key={p.id} pkg={p} />
))}
</div>
{(q.data ?? []).length === 0 ? <EmptyState title="پکیج فعالی ندارید" /> : null}
</div>
);
}
export function CustomerFavoriteCenters() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "favorite-branches"],
queryFn: () => beautyBusinessApi.branches.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="مراکز محبوب" description="شعب workspace — از branches API." />
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{(q.data ?? []).map((b) => (
<SalonCard key={b.id} branch={b} href={`/beauty/site/salon/${b.id}`} />
))}
</div>
{(q.data ?? []).length === 0 ? (
<EmptyState
title="مرکزی یافت نشد"
action={
<Link href="/beauty/site/search">
<Button>جستجوی مراکز</Button>
</Link>
}
/>
) : null}
</div>
);
}
export function CustomerFavoriteStaff() {
const { tenantId } = useTenantId();
const q = useQuery({
queryKey: ["beauty", tenantId, "favorite-staff"],
queryFn: () => beautyBusinessApi.staff.list(tenantId!),
enabled: !!tenantId,
});
if (!tenantId || q.isLoading) return <BeautyPageLoader />;
if (q.error) return <BeautyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<PageHeader title="پرسنل محبوب" description="لیست پرسنل از staff API." />
<DataTable
columns={[
{ key: "display_name", header: "نام" },
{ key: "kind", header: "نقش" },
{
key: "status",
header: "وضعیت",
render: (r) => <Badge tone="default">{String(r.status)}</Badge>,
},
]}
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState title="پرسنلی یافت نشد" />}
/>
</div>
);
}