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

180 lines
6.1 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, 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>
);
}