128 lines
5.7 KiB
TypeScript
128 lines
5.7 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import Link from "next/link";
|
||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities";
|
||
import { useHospitalityProductBrand } from "@/modules/hospitality/hooks/useHospitalityProductBrand";
|
||
import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared";
|
||
import { PageHeader, Card, CardContent, Button, Badge } from "@/components/ds";
|
||
import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs";
|
||
import { HospitalityStatGrid } from "@/modules/hospitality/design-system";
|
||
|
||
export const ExecutiveDashboard = function HospitalityDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const caps = useHospitalityCapabilities();
|
||
const brand = useHospitalityProductBrand();
|
||
|
||
const countsQ = useQuery({
|
||
queryKey: ["hospitality", tenantId, "dashboard-counts"],
|
||
queryFn: async () => {
|
||
const [venues, reservations, tickets, kitchen, menus, qrSessions] = await Promise.all([
|
||
hospitalityApi.venues.list(tenantId!, { page: 1, page_size: 500 }),
|
||
hospitalityApi.reservations.list(tenantId!, { page: 1, page_size: 500 }),
|
||
hospitalityApi.posTickets.list(tenantId!, { page: 1, page_size: 500 }),
|
||
hospitalityApi.kitchenTickets.list(tenantId!, { page: 1, page_size: 500 }),
|
||
hospitalityApi.menus.list(tenantId!, { page: 1, page_size: 500 }),
|
||
hospitalityApi.qrOrderingSessions.list(tenantId!, { page: 1, page_size: 500 }),
|
||
]);
|
||
return {
|
||
venues: venues.length,
|
||
reservations: reservations.filter((r) => r.status === "confirmed" || r.status === "pending").length,
|
||
openOrders: tickets.filter((t) => t.status === "open" || t.status === "draft").length,
|
||
kitchenActive: kitchen.filter((k) => k.status !== "completed" && k.status !== "cancelled").length,
|
||
menus: menus.filter((m) => m.status === "published" || m.status === "active").length,
|
||
onlineOrders: qrSessions.filter((s) => s.status === "submitted" || s.status === "preparing").length,
|
||
needsOnboarding: venues.length === 0,
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
refetchInterval: 30_000,
|
||
});
|
||
|
||
if (!tenantId || countsQ.isLoading || caps.isLoading) return <HospitalityPageLoader />;
|
||
if (countsQ.error) return <HospitalityPageError error={countsQ.error} onRetry={() => countsQ.refetch()} />;
|
||
|
||
const c = countsQ.data!;
|
||
const productLabel = brand.data?.displayName || "Hospitality";
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<HospitalityBreadcrumbs current="داشبورد" />
|
||
<PageHeader
|
||
title="داشبورد مدیریتی"
|
||
description={`نمای اجرایی ${productLabel} — داده زنده از API.`}
|
||
actions={
|
||
<div className="flex gap-2">
|
||
<Badge tone="success">
|
||
{productLabel}
|
||
{caps.data?.version ? ` · v${caps.data.version}` : ""}
|
||
</Badge>
|
||
<Button variant="outline" size="sm" onClick={() => countsQ.refetch()}>
|
||
بروزرسانی
|
||
</Button>
|
||
</div>
|
||
}
|
||
/>
|
||
|
||
{c.needsOnboarding ? (
|
||
<Card>
|
||
<CardContent className="flex flex-wrap items-center justify-between gap-3 p-4">
|
||
<div>
|
||
<p className="font-medium text-secondary">راهاندازی اولیه لازم است</p>
|
||
<p className="mt-1 text-sm text-[var(--muted)]">
|
||
هنوز مکانی ثبت نشده. ویزارد رستوران را کامل کنید.
|
||
</p>
|
||
</div>
|
||
<Link href="/hospitality/onboarding">
|
||
<Button>شروع راهاندازی</Button>
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
|
||
<HospitalityStatGrid
|
||
stats={[
|
||
{ label: "مکانها", value: c.venues, hint: "Venues" },
|
||
{ label: "رزرو فعال", value: c.reservations, hint: "Reservations" },
|
||
{ label: "سفارش باز POS", value: c.openOrders, hint: "Open tickets" },
|
||
{ label: "آشپزخانه فعال", value: c.kitchenActive, hint: "Kitchen queue" },
|
||
{ label: "منوهای منتشر", value: c.menus, hint: "Menus" },
|
||
{ label: "سفارش آنلاین", value: c.onlineOrders, hint: "QR ordering" },
|
||
]}
|
||
/>
|
||
<Card>
|
||
<CardContent className="space-y-3 p-4">
|
||
<p className="text-sm font-medium text-secondary">عملیات سریع</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/hospitality/ops">
|
||
<Button variant="outline">مرکز عملیات</Button>
|
||
</Link>
|
||
<Link href="/hospitality/reservations">
|
||
<Button variant="outline">رزروها</Button>
|
||
</Link>
|
||
<Link href="/hospitality/ops/kitchen">
|
||
<Button variant="outline">آشپزخانه</Button>
|
||
</Link>
|
||
<Link href="/hospitality/ops/cashier">
|
||
<Button variant="outline">صندوقدار</Button>
|
||
</Link>
|
||
<Link href="/hospitality/menu">
|
||
<Button variant="outline">منو</Button>
|
||
</Link>
|
||
<Link href="/hospitality/analytics">
|
||
<Button variant="outline">آنالیتیکس</Button>
|
||
</Link>
|
||
<Link href="/hospitality/onboarding">
|
||
<Button variant="outline">راهاندازی</Button>
|
||
</Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ExecutiveDashboard;
|