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>
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { PageHeader, Badge, EmptyState, 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 } from "@/modules/beauty/design-system";
|
||
import {
|
||
AggregateStatsRow,
|
||
AnalyticsPage,
|
||
BeautyPageError,
|
||
BeautyPageLoader,
|
||
ExternalProvidersByKind,
|
||
ServiceMetaBadges,
|
||
} from "./shared";
|
||
|
||
export function OwnerDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const { organizations, branches, customers, staff, services, isLoading } = useBeautyLookups();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "owner-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()} />;
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="داشبورد مدیریت سالن"
|
||
description="نمای کلی تربت بیوتی."
|
||
actions={<ServiceMetaBadges />}
|
||
/>
|
||
<AggregateStatsRow
|
||
stats={[
|
||
{ label: "سازمانها", value: organizations.length },
|
||
{ label: "شعب", value: branches.length },
|
||
{ label: "نوبتها", value: apptsQ.data?.length ?? 0 },
|
||
{ label: "مشتریان", value: customers.length },
|
||
{ label: "پرسنل", value: staff.length },
|
||
{ label: "خدمات", value: services.length },
|
||
]}
|
||
/>
|
||
<section className="mt-8">
|
||
<h2 className="mb-3 font-semibold text-secondary">آخرین نوبتها</h2>
|
||
{(apptsQ.data ?? []).slice(0, 5).map((a) => (
|
||
<AppointmentCard key={a.id} appointment={a} href="/beauty/appointments" />
|
||
))}
|
||
{(apptsQ.data ?? []).length === 0 ? <EmptyState title="نوبتی ثبت نشده" /> : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function OwnerAnalytics() {
|
||
return <AnalyticsPage title="آنالیتیکس مالک" />;
|
||
}
|
||
|
||
export function OwnerCalendar() {
|
||
const { tenantId } = useTenantId();
|
||
const { staffName, serviceName } = useBeautyLookups();
|
||
const apptsQ = useQuery({
|
||
queryKey: ["beauty", tenantId, "owner-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 OwnerCrmIntegration() {
|
||
return (
|
||
<ExternalProvidersByKind
|
||
kind="crm"
|
||
title="یکپارچگی CRM"
|
||
description="ارائهدهندگان CRM از external-providers API."
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function OwnerAccountingIntegration() {
|
||
return (
|
||
<ExternalProvidersByKind
|
||
kind="accounting"
|
||
title="یکپارچگی حسابداری"
|
||
description="اتصال به ماژول حسابداری تربتیار."
|
||
accountingLink
|
||
/>
|
||
);
|
||
}
|
||
|
||
export function OwnerMarketing() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["beauty", tenantId, "owner-marketing"],
|
||
queryFn: () => beautyBusinessApi.marketingCampaigns.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="کمپینهای بازاریابی."
|
||
actions={
|
||
<Link href="/beauty/marketing/campaigns">
|
||
<Badge tone="default">مدیریت کمپین</Badge>
|
||
</Link>
|
||
}
|
||
/>
|
||
<DataTable
|
||
columns={[
|
||
{ key: "code", header: "کد" },
|
||
{ key: "name", header: "نام" },
|
||
{ key: "channel", header: "کانال", render: (r) => String(r.channel ?? "—") },
|
||
{
|
||
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 OwnerCoupons() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["beauty", tenantId, "owner-coupons"],
|
||
queryFn: () => beautyBusinessApi.marketingCampaigns.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="کمپینهای بازاریابی — marketing-campaigns API." />
|
||
<DataTable
|
||
columns={[
|
||
{ key: "code", header: "کد" },
|
||
{ key: "name", header: "نام" },
|
||
{ key: "external_campaign_ref", header: "ارجاع خارجی", render: (r) => String(r.external_campaign_ref ?? "—") },
|
||
]}
|
||
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
|
||
empty={<EmptyState title="کوپنی ثبت نشده" />}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|