232 lines
10 KiB
TypeScript
232 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { Button, Card, CardContent, PageHeader, Badge } from "@/components/ds";
|
||
import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs";
|
||
import { HospitalityStatGrid } from "@/modules/hospitality/design-system";
|
||
import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared";
|
||
import { HospitalityBundleGate } from "@/modules/hospitality/components/HospitalityBundleGate";
|
||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
|
||
function useOpsCounts(tenantId: string | null) {
|
||
return useQuery({
|
||
queryKey: ["hospitality", tenantId, "ops-counts"],
|
||
queryFn: async () => {
|
||
const [reservations, waitlist, tables, tickets, kitchen, serviceReqs] = await Promise.all([
|
||
hospitalityApi.reservations.list(tenantId!, { page: 1, page_size: 200 }),
|
||
hospitalityApi.waitlist.list(tenantId!, { page: 1, page_size: 200 }),
|
||
hospitalityApi.tables.list(tenantId!, { page: 1, page_size: 200 }),
|
||
hospitalityApi.posTickets.list(tenantId!, { page: 1, page_size: 200 }),
|
||
hospitalityApi.kitchenTickets.list(tenantId!, { page: 1, page_size: 200 }),
|
||
hospitalityApi.serviceRequests.list(tenantId!, { page: 1, page_size: 200 }),
|
||
]);
|
||
return {
|
||
reservations: reservations.filter((r) => ["pending", "confirmed"].includes(String(r.status))).length,
|
||
waitlist: waitlist.filter((w) => String(w.status) !== "seated" && String(w.status) !== "cancelled").length,
|
||
freeTables: tables.filter((t) => String(t.status) === "available").length,
|
||
openTickets: tickets.filter((t) => ["open", "draft"].includes(String(t.status))).length,
|
||
kitchenActive: kitchen.filter((k) => !["completed", "cancelled"].includes(String(k.status))).length,
|
||
serviceOpen: serviceReqs.filter((s) => String(s.status) === "open" || String(s.status) === "pending").length,
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
refetchInterval: 15_000,
|
||
});
|
||
}
|
||
|
||
export function OpsCenterPage() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useOpsCounts(tenantId);
|
||
if (!tenantId || q.isLoading) return <HospitalityPageLoader />;
|
||
if (q.error) return <HospitalityPageError error={q.error} onRetry={() => q.refetch()} />;
|
||
const c = q.data!;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<HospitalityBreadcrumbs current="مرکز عملیات" />
|
||
<PageHeader
|
||
title="مرکز عملیات"
|
||
description="نمای زنده سالن، آشپزخانه و صندوق — داده API."
|
||
actions={
|
||
<Button variant="outline" size="sm" onClick={() => q.refetch()}>
|
||
بروزرسانی
|
||
</Button>
|
||
}
|
||
/>
|
||
<HospitalityStatGrid
|
||
stats={[
|
||
{ label: "رزرو فعال", value: c.reservations },
|
||
{ label: "صف انتظار", value: c.waitlist },
|
||
{ label: "میز آزاد", value: c.freeTables },
|
||
{ label: "بلیت باز POS", value: c.openTickets },
|
||
{ label: "آشپزخانه فعال", value: c.kitchenActive },
|
||
{ label: "درخواست سرویس", value: c.serviceOpen },
|
||
]}
|
||
/>
|
||
<div className="grid gap-3 sm:grid-cols-3">
|
||
<Link href="/hospitality/ops/kitchen"><Button className="w-full">آشپزخانه</Button></Link>
|
||
<Link href="/hospitality/ops/waiter"><Button className="w-full" variant="outline">گارسون</Button></Link>
|
||
<Link href="/hospitality/ops/cashier"><Button className="w-full" variant="outline">صندوقدار</Button></Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function KitchenOpsDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["hospitality", tenantId, "kitchen-ops"],
|
||
queryFn: async () => {
|
||
const [tickets, items] = await Promise.all([
|
||
hospitalityApi.kitchenTickets.list(tenantId!, { page: 1, page_size: 100 }),
|
||
hospitalityApi.kitchenTicketItems.list(tenantId!, { page: 1, page_size: 200 }),
|
||
]);
|
||
const active = tickets.filter((t) => !["completed", "cancelled"].includes(String(t.status)));
|
||
return { active, pendingItems: items.filter((i) => String(i.status) !== "ready" && String(i.status) !== "completed") };
|
||
},
|
||
enabled: !!tenantId,
|
||
refetchInterval: 8_000,
|
||
});
|
||
|
||
return (
|
||
<HospitalityBundleGate feature="kitchen">
|
||
<div className="space-y-6">
|
||
<HospitalityBreadcrumbs current="داشبورد آشپزخانه" />
|
||
<PageHeader
|
||
title="داشبورد آشپزخانه"
|
||
description="بلیتهای فعال و آیتمهای در حال آمادهسازی."
|
||
actions={
|
||
<Link href="/hospitality/kitchen/display">
|
||
<Button size="sm">نمایشگر کامل</Button>
|
||
</Link>
|
||
}
|
||
/>
|
||
{!tenantId || q.isLoading ? (
|
||
<HospitalityPageLoader />
|
||
) : q.error ? (
|
||
<HospitalityPageError error={q.error} onRetry={() => q.refetch()} />
|
||
) : (
|
||
<>
|
||
<HospitalityStatGrid
|
||
stats={[
|
||
{ label: "بلیت فعال", value: q.data!.active.length },
|
||
{ label: "آیتم باز", value: q.data!.pendingItems.length },
|
||
]}
|
||
/>
|
||
<Card>
|
||
<CardContent className="space-y-2 p-4">
|
||
{q.data!.active.length === 0 ? (
|
||
<p className="text-sm text-[var(--muted)]">بلیت فعالی در صف آشپزخانه نیست.</p>
|
||
) : (
|
||
q.data!.active.slice(0, 12).map((t) => (
|
||
<div key={String(t.id)} className="flex items-center justify-between rounded-lg border border-[var(--border)] px-3 py-2 text-sm">
|
||
<span className="font-mono text-xs" dir="ltr">{String(t.id).slice(0, 8)}</span>
|
||
<Badge>{String(t.status)}</Badge>
|
||
</div>
|
||
))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/hospitality/kitchen/queue"><Button variant="outline">صف</Button></Link>
|
||
<Link href="/hospitality/kitchen/preparation"><Button variant="outline">آمادهسازی</Button></Link>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</HospitalityBundleGate>
|
||
);
|
||
}
|
||
|
||
export function WaiterOpsDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useOpsCounts(tenantId);
|
||
|
||
return (
|
||
<HospitalityBundleGate feature="table_service">
|
||
<div className="space-y-6">
|
||
<HospitalityBreadcrumbs current="داشبورد گارسون" />
|
||
<PageHeader title="داشبورد گارسون" description="میز، رزرو، صف و درخواست سرویس." />
|
||
{!tenantId || q.isLoading ? (
|
||
<HospitalityPageLoader />
|
||
) : q.error ? (
|
||
<HospitalityPageError error={q.error} onRetry={() => q.refetch()} />
|
||
) : (
|
||
<>
|
||
<HospitalityStatGrid
|
||
stats={[
|
||
{ label: "میز آزاد", value: q.data!.freeTables },
|
||
{ label: "رزرو", value: q.data!.reservations },
|
||
{ label: "صف", value: q.data!.waitlist },
|
||
{ label: "سرویس", value: q.data!.serviceOpen },
|
||
]}
|
||
/>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/hospitality/tables"><Button>میزها</Button></Link>
|
||
<Link href="/hospitality/reservations"><Button variant="outline">رزرو</Button></Link>
|
||
<Link href="/hospitality/queue"><Button variant="outline">صف انتظار</Button></Link>
|
||
<Link href="/hospitality/floor-map"><Button variant="outline">نقشه سالن</Button></Link>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</HospitalityBundleGate>
|
||
);
|
||
}
|
||
|
||
export function CashierOpsDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const q = useQuery({
|
||
queryKey: ["hospitality", tenantId, "cashier-ops"],
|
||
queryFn: async () => {
|
||
const [tickets, payments, registers] = await Promise.all([
|
||
hospitalityApi.posTickets.list(tenantId!, { page: 1, page_size: 100 }),
|
||
hospitalityApi.posPayments.list(tenantId!, { page: 1, page_size: 100 }),
|
||
hospitalityApi.posRegisters.list(tenantId!, { page: 1, page_size: 50 }),
|
||
]);
|
||
return {
|
||
open: tickets.filter((t) => ["open", "draft"].includes(String(t.status))).length,
|
||
paymentsToday: payments.length,
|
||
registers: registers.length,
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
refetchInterval: 12_000,
|
||
});
|
||
|
||
return (
|
||
<HospitalityBundleGate feature="pos_lite">
|
||
<div className="space-y-6">
|
||
<HospitalityBreadcrumbs current="داشبورد صندوقدار" />
|
||
<PageHeader
|
||
title="داشبورد صندوقدار"
|
||
description="بلیتهای باز و پرداخت محلی POS — PSP خارجی در سرویس Payment."
|
||
/>
|
||
{!tenantId || q.isLoading ? (
|
||
<HospitalityPageLoader />
|
||
) : q.error ? (
|
||
<HospitalityPageError error={q.error} onRetry={() => q.refetch()} />
|
||
) : (
|
||
<>
|
||
<HospitalityStatGrid
|
||
stats={[
|
||
{ label: "بلیت باز", value: q.data!.open },
|
||
{ label: "پرداخت ثبتشده", value: q.data!.paymentsToday },
|
||
{ label: "صندوق", value: q.data!.registers },
|
||
]}
|
||
/>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/hospitality/pos/lite"><Button>POS Lite</Button></Link>
|
||
<Link href="/hospitality/pos/register"><Button variant="outline">صندوق</Button></Link>
|
||
<Link href="/hospitality/pos/payments"><Button variant="outline">پرداختها</Button></Link>
|
||
<Link href="/hospitality/invoices"><Button variant="outline">فاکتور</Button></Link>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</HospitalityBundleGate>
|
||
);
|
||
}
|