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>
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { PageHeader, EmptyState, 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 } from "@/modules/beauty/design-system";
|
|
import {
|
|
AggregateStatsRow,
|
|
BeautyPageError,
|
|
BeautyPageLoader,
|
|
SettingsListPage,
|
|
} from "./shared";
|
|
|
|
export function StaffPortalDashboard() {
|
|
const { tenantId } = useTenantId();
|
|
const { staff, isLoading } = useBeautyLookups();
|
|
const schedulesQ = useQuery({
|
|
queryKey: ["beauty", tenantId, "staff-schedules"],
|
|
queryFn: () => beautyBusinessApi.staffSchedules.list(tenantId!),
|
|
enabled: !!tenantId,
|
|
});
|
|
const apptsQ = useQuery({
|
|
queryKey: ["beauty", tenantId, "staff-appts"],
|
|
queryFn: () => beautyBusinessApi.appointments.list(tenantId!),
|
|
enabled: !!tenantId,
|
|
});
|
|
|
|
if (!tenantId || isLoading || schedulesQ.isLoading) return <BeautyPageLoader />;
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader title="داشبورد پرسنل" description="برنامه و نوبتهای امروز." />
|
|
<AggregateStatsRow
|
|
stats={[
|
|
{ label: "پرسنل", value: staff.length },
|
|
{ label: "برنامهها", value: schedulesQ.data?.length ?? 0 },
|
|
{ label: "نوبتها", value: apptsQ.data?.length ?? 0 },
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StaffPortalSchedule() {
|
|
const { tenantId } = useTenantId();
|
|
const { staffName } = useBeautyLookups();
|
|
const q = useQuery({
|
|
queryKey: ["beauty", tenantId, "staff-portal-schedule"],
|
|
queryFn: () => beautyBusinessApi.staffSchedules.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-schedules API." />
|
|
<DataTable
|
|
columns={[
|
|
{ key: "code", header: "کد" },
|
|
{
|
|
key: "staff_ref",
|
|
header: "پرسنل",
|
|
render: (r) => staffName(String(r.staff_ref)),
|
|
},
|
|
{ key: "day_of_week", header: "روز" },
|
|
{ key: "start_time", header: "شروع" },
|
|
{ key: "end_time", header: "پایان" },
|
|
]}
|
|
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
|
|
empty={<EmptyState title="برنامهای ثبت نشده" />}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StaffPortalCommission() {
|
|
const { tenantId } = useTenantId();
|
|
const q = useQuery({
|
|
queryKey: ["beauty", tenantId, "staff-commission"],
|
|
queryFn: () => beautyBusinessApi.commissionRules.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="commission-rules API." />
|
|
<DataTable
|
|
columns={[
|
|
{ key: "code", header: "کد" },
|
|
{ key: "name", header: "نام" },
|
|
{ key: "rule_type", header: "نوع" },
|
|
{
|
|
key: "rate_percent",
|
|
header: "درصد",
|
|
render: (r) => (r.rate_percent != null ? `${r.rate_percent}%` : "—"),
|
|
},
|
|
{
|
|
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 StaffPortalAppointments() {
|
|
const { tenantId } = useTenantId();
|
|
const { staffName, serviceName } = useBeautyLookups();
|
|
const q = useQuery({
|
|
queryKey: ["beauty", tenantId, "staff-portal-appts"],
|
|
queryFn: () => beautyBusinessApi.appointments.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="appointments API." />
|
|
<div className="mt-6 space-y-3">
|
|
{(q.data ?? []).map((a) => (
|
|
<AppointmentCard
|
|
key={a.id}
|
|
appointment={a}
|
|
staffName={staffName(a.staff_ref)}
|
|
serviceName={serviceName(a.service_ref)}
|
|
/>
|
|
))}
|
|
</div>
|
|
{(q.data ?? []).length === 0 ? <EmptyState title="نوبتی نیست" /> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StaffPortalNotifications() {
|
|
return (
|
|
<div>
|
|
<PageHeader title="اعلانها" description="communication-integrations." />
|
|
<EmptyState title="اعلانی وجود ندارد" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function StaffPortalSettings() {
|
|
return <SettingsListPage title="تنظیمات پرسنل" description="settings API." />;
|
|
}
|