TorbatYar/frontend/modules/hospitality/features/ops-dashboards.tsx
2026-07-28 20:39:10 +03:30

232 lines
10 KiB
TypeScript
Raw 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 { 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>
);
}