Add frontend/modules/loyalty with types, API client, design system, feature pages and thin App Router routes under app/loyalty/. BFF proxy at app/api/loyalty/. Include loyalty frontend docs. Co-authored-by: Cursor <cursoragent@cursor.com>
213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { PageHeader, EmptyState, Badge, Button } 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, QueueRow } from "@/modules/beauty/design-system";
|
||
import { ReceptionAppointmentRow } from "@/modules/beauty/components/ReceptionAppointmentRow";
|
||
import {
|
||
AggregateStatsRow,
|
||
AnalyticsPage,
|
||
BeautyPageError,
|
||
BeautyPageLoader,
|
||
} from "./shared";
|
||
|
||
export function ReceptionDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-appts"],
|
||
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
const waitQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-waiting"],
|
||
queryFn: () => beautyBusinessApi.waitingList.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || apptsQ.isLoading) return <BeautyPageLoader />;
|
||
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="داشبورد پذیرش" description="صف، نوبتها و لیست انتظار." />
|
||
<AggregateStatsRow
|
||
stats={[
|
||
{ label: "نوبتها", value: apptsQ.data?.length ?? 0 },
|
||
{ label: "لیست انتظار", value: waitQ.data?.length ?? 0 },
|
||
{
|
||
label: "پذیرششده",
|
||
value: (apptsQ.data ?? []).filter((a) => a.status === "checked_in").length,
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionCalendar() {
|
||
const { tenantId } = useTenantId();
|
||
const { staffName, serviceName } = useBeautyLookups();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-calendar"],
|
||
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || apptsQ.isLoading) return <BeautyPageLoader />;
|
||
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="تقویم پذیرش" description="نوبتهای روز." />
|
||
<div className="mt-6 space-y-3">
|
||
{(apptsQ.data ?? []).map((a) => (
|
||
<AppointmentCard
|
||
key={a.id}
|
||
appointment={a}
|
||
staffName={staffName(a.staff_ref)}
|
||
serviceName={serviceName(a.service_ref)}
|
||
/>
|
||
))}
|
||
</div>
|
||
{(apptsQ.data ?? []).length === 0 ? <EmptyState title="نوبتی نیست" /> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionWalkIn() {
|
||
const { tenantId } = useTenantId();
|
||
const { customers, services } = useBeautyLookups();
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="مراجعه حضوری" description="ثبت walk-in در waiting-list." />
|
||
<p className="text-sm text-[var(--muted)]">
|
||
مشتریان: {customers.length} · خدمات: {services.length}
|
||
</p>
|
||
<Link href="/beauty/booking/waiting-list" className="mt-4 inline-block">
|
||
<Button>لیست انتظار</Button>
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionCheckIn() {
|
||
const { tenantId } = useTenantId();
|
||
const { staffName } = useBeautyLookups();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-checkin"],
|
||
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
select: (rows) => rows.filter((a) => a.status === "confirmed" || a.status === "requested"),
|
||
});
|
||
|
||
if (!tenantId || apptsQ.isLoading) return <BeautyPageLoader />;
|
||
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="پذیرش" description="نوبتهای آماده check-in." />
|
||
<div className="mt-6 space-y-3">
|
||
{(apptsQ.data ?? []).map((a) => (
|
||
<ReceptionAppointmentRow
|
||
key={a.id}
|
||
appointment={a}
|
||
staffName={staffName(a.staff_ref)}
|
||
actions={a.status === "requested" ? ["confirm", "cancel"] : ["check-in", "cancel", "no-show"]}
|
||
queryKey={["beauty", tenantId, "reception-checkin"]}
|
||
/>
|
||
))}
|
||
</div>
|
||
{(apptsQ.data ?? []).length === 0 ? <EmptyState title="نوبتی برای پذیرش نیست" /> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionCheckOut() {
|
||
const { tenantId } = useTenantId();
|
||
const { staffName, serviceName } = useBeautyLookups();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-checkout"],
|
||
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
select: (rows) => rows.filter((a) => a.status === "in_progress" || a.status === "checked_in"),
|
||
});
|
||
|
||
if (!tenantId || apptsQ.isLoading) return <BeautyPageLoader />;
|
||
if (apptsQ.error) return <BeautyPageError error={apptsQ.error} onRetry={() => apptsQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="ترخیص" description="نوبتهای در حال انجام." />
|
||
<div className="mt-6 space-y-3">
|
||
{(apptsQ.data ?? []).map((a) => (
|
||
<ReceptionAppointmentRow
|
||
key={a.id}
|
||
appointment={a}
|
||
staffName={staffName(a.staff_ref)}
|
||
serviceName={serviceName(a.service_ref)}
|
||
actions={["complete", "cancel"]}
|
||
queryKey={["beauty", tenantId, "reception-checkout"]}
|
||
/>
|
||
))}
|
||
</div>
|
||
{(apptsQ.data ?? []).length === 0 ? <EmptyState title="موردی برای ترخیص نیست" /> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionQueue() {
|
||
const { tenantId } = useTenantId();
|
||
const { customerName, serviceName } = useBeautyLookups();
|
||
const waitQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "reception-queue"],
|
||
queryFn: () => beautyBusinessApi.waitingList.list(tenantId!),
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || waitQ.isLoading) return <BeautyPageLoader />;
|
||
if (waitQ.error) return <BeautyPageError error={waitQ.error} onRetry={() => waitQ.refetch()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="صف انتظار" description="waiting-list API." />
|
||
<div className="mt-6 space-y-3">
|
||
{(waitQ.data ?? []).map((w, i) => (
|
||
<QueueRow
|
||
key={w.id}
|
||
position={i + 1}
|
||
title={customerName(w.customer_ref)}
|
||
subtitle={serviceName(w.service_ref)}
|
||
status="pending"
|
||
/>
|
||
))}
|
||
</div>
|
||
{(waitQ.data ?? []).length === 0 ? <EmptyState title="صف خالی است" /> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionPayments() {
|
||
return (
|
||
<div>
|
||
<PageHeader title="پرداخت" description="ثبت پرداخت از طریق یکپارچگی حسابداری." />
|
||
<EmptyState
|
||
title="پرداخت از ماژول حسابداری"
|
||
action={
|
||
<Link href="/accounting">
|
||
<Button variant="outline">رفتن به حسابداری</Button>
|
||
</Link>
|
||
}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ReceptionReports() {
|
||
return <AnalyticsPage title="گزارش پذیرش" />;
|
||
}
|