#!/usr/bin/env node /** * Scaffolds Torbat Food (Hospitality) frontend module: * - Feature pages from PAGE_REGISTRY * - Thin app/hospitality routes * - BFF proxy */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); const MOD = path.join(ROOT, "modules", "hospitality"); const APP = path.join(ROOT, "app", "hospitality"); const FEATURES = path.join(MOD, "features"); /** [routePath, exportName, featureFile, apiKey, title, bundleFeature|null] */ const PAGE_REGISTRY = [ ["", "ExecutiveDashboard", "dashboard", null, "داشبورد", null], ["hub", "HospitalityHub", "hub", null, "مرکز تربت فود", null], ["branches", "BranchesPage", "venues", "venues", "شعب و مکان‌ها", null], ["branches/list", "BranchListPage", "branches", "branches", "شعب", null], ["dining-areas", "DiningAreasPage", "diningAreas", "diningAreas", "سالن‌ها", null], ["tables", "TablesPage", "tables", "tables", "میزها", null], ["floor-map", "FloorMapPage", "posFloorPlans", "posFloorPlans", "نقشه سالن", "pos_pro"], ["reservations", "ReservationsPage", "reservations", "reservations", "رزروها", "reservation"], ["queue", "QueuePage", "waitlist", "waitlist", "صف انتظار", "table_service"], ["customers", "CustomersPage", "connectorRegistrationsCrm", "connectorRegistrations", "مشتریان", "crm_connector"], ["guests", "GuestsPage", "qrMenuSessions", "qrMenuSessions", "پروفایل مهمان", "qr_menu"], ["menu", "DigitalMenuPage", "menus", "menus", "منوی دیجیتال", "digital_menu"], ["menu/categories", "MenuCategoriesPage", "menuCategories", "menuCategories", "دسته‌بندی منو", "digital_menu"], ["menu/items", "MenuItemsPage", "menuItems", "menuItems", "آیتم‌های منو", "digital_menu"], ["menu/modifiers", "ModifierGroupsPage", "modifierGroups", "modifierGroups", "گروه افزودنی", "digital_menu"], ["menu/modifier-options", "ModifierOptionsPage", "modifierOptions", "modifierOptions", "گزینه افزودنی", "digital_menu"], ["menu/availability", "AvailabilityPage", "availabilityWindows", "availabilityWindows", "قوانین دسترسی", "digital_menu"], ["menu/media", "MediaLibraryPage", "menuMediaRefs", "menuMediaRefs", "کتابخانه رسانه", "digital_menu"], ["qr/menu", "QrMenuPage", "qrCodes", "qrCodes", "منوی QR", "qr_menu"], ["qr/ordering", "QrOrderingPage", "qrOrderingSessions", "qrOrderingSessions", "سفارش QR", "qr_ordering"], ["ordering/cart", "CartPage", "carts", "carts", "سبد خرید", "qr_ordering"], ["ordering/checkout", "CheckoutPage", "cartLines", "cartLines", "تسویه", "qr_ordering"], ["kitchen/display", "KitchenDisplayPage", "kitchenTickets", "kitchenTickets", "نمایشگر آشپزخانه", "kitchen"], ["kitchen/queue", "KitchenQueuePage", "kitchenTicketItems", "kitchenTicketItems", "صف آشپزخانه", "kitchen"], ["kitchen/preparation", "KitchenPrepPage", "kitchenTicketsPrep", "kitchenTickets", "وضعیت آماده‌سازی", "kitchen"], ["pos/lite", "PosLitePage", "posTickets", "posTickets", "POS Lite", "pos_lite"], ["pos/pro", "PosProPage", "posPayments", "posPayments", "POS Pro", "pos_pro"], ["pos/register", "CashRegisterPage", "posRegisters", "posRegisters", "صندوق", "pos_lite"], ["pos/payments", "PaymentsPage", "posPaymentsList", "posPayments", "پرداخت‌ها", "pos_pro"], ["pos/discounts", "DiscountsPage", "posDiscounts", "posDiscounts", "تخفیف‌ها", "pos_pro"], ["pos/coupons", "CouponsPage", "posDiscountsCoupons", "posDiscounts", "کوپن‌ها", "pos_pro"], ["pos/promotions", "PromotionsPage", "featureTogglesPromo", "featureToggles", "پروموشن‌ها", "pos_pro"], ["integrations/delivery", "DeliveryIntegrationPage", "connectorDelivery", "connectorRegistrations", "پیک", "delivery_connector"], ["orders/pickup", "PickupOrdersPage", "pickupOrders", "qrOrderingSessions", "سفارش بیرون‌بر", "qr_ordering"], ["orders/online", "OnlineOrdersPage", "onlineOrders", "qrOrderingSessions", "سفارش آنلاین", "qr_ordering"], ["orders/timeline", "OrderTimelinePage", "orderTimeline", "kitchenTickets", "خط زمانی سفارش", "kitchen"], ["invoices", "InvoicesPage", "invoices", "posTickets", "فاکتورها", "pos_lite"], ["receipts", "ReceiptsPage", "receipts", "posTickets", "رسیدها", "pos_lite"], ["reports", "ReportsPage", "analyticsReports", "analyticsReports", "گزارش‌ها", "analytics"], ["analytics", "AnalyticsPage", "analyticsSnapshots", "analyticsSnapshots", "آنالیتیکس", "analytics"], ["inventory", "InventoryPage", "inventoryOverview", "analyticsSnapshots", "موجودی", "analytics"], ["integrations/crm", "CrmIntegrationPage", "connectorCrm", "connectorRegistrations", "CRM", "crm_connector"], ["integrations/loyalty", "LoyaltyIntegrationPage", "connectorLoyalty", "connectorRegistrations", "وفاداری", "loyalty_connector"], ["integrations/communication", "CommunicationIntegrationPage", "connectorCommunication", "connectorRegistrations", "ارتباطات", "communication_connector"], ["integrations/website", "WebsiteIntegrationPage", "connectorWebsite", "connectorRegistrations", "وب‌سایت", "website_connector"], ["integrations/marketplace", "MarketplaceIntegrationPage", "connectorDispatches", "connectorDispatches", "مارکت‌پلیس", null], ["settings", "SettingsPage", "settings", "settings", "تنظیمات", null], ["permissions", "PermissionsPage", "permissions", "permissions", "مجوزها", null], ["roles", "RolesPage", "roles", "roles", "نقش‌ها", null], ["audit", "AuditLogsPage", "events", "events", "لاگ ممیزی", null], ["notifications", "NotificationsPage", "notifications", "events", "اعلان‌ها", null], ["health", "HealthPage", "health", null, "سلامت سرویس", null], ["capabilities", "CapabilitiesPage", "capabilities", null, "قابلیت‌ها", null], ["bundles", "BundlesPage", "tenantBundles", "tenantBundles", "بسته‌های فعال", null], ]; function ensureDir(d) { fs.mkdirSync(d, { recursive: true }); } function writeIfMissing(file, content) { if (!fs.existsSync(file)) fs.writeFileSync(file, content); } function writeFeature(name, content) { const file = path.join(FEATURES, `${name}.tsx`); if (!fs.existsSync(file)) { ensureDir(FEATURES); fs.writeFileSync(file, content); } } const listPageTemplate = (exportName, apiKey, title, breadcrumb, bundle) => `"use client"; import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage"; import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api"; export const ${exportName} = createHospitalityListPage({ title: "${title}", breadcrumb: "${breadcrumb}", resourceKey: "${apiKey}", bundleFeature: ${bundle ? `"${bundle}"` : "null"}, permission: "hospitality.view", api: hospitalityApi.${apiKey}, columns: [ { key: "code", header: "کد" }, { key: "name", header: "نام" }, { key: "status", header: "وضعیت", type: "status" }, { key: "created_at", header: "ایجاد", type: "datetime" }, ], createFields: [ { name: "code", label: "کد", required: true }, { name: "name", label: "نام", required: true }, ], }); export default ${exportName}; `; // Generate list-based feature stubs const generatedFeatures = new Set(["dashboard", "hub", "health", "capabilities"]); for (const [, exportName, featureFile, apiKey, title, bundle] of PAGE_REGISTRY) { if (generatedFeatures.has(featureFile) || !apiKey) continue; generatedFeatures.add(featureFile); writeFeature( featureFile, listPageTemplate(exportName, apiKey, title, title, bundle) ); } // Dashboard writeFeature( "dashboard", `"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 { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared"; import { PageHeader, StatCard, Card, CardContent, Button, Badge } from "@/components/ds"; import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs"; export const ExecutiveDashboard = function HospitalityDashboard() { const { tenantId } = useTenantId(); const caps = useHospitalityCapabilities(); const countsQ = useQuery({ queryKey: ["hospitality", tenantId, "dashboard-counts"], queryFn: async () => { const [venues, reservations, tickets, kitchen] = 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 }), ]); return { venues: venues.length, reservations: reservations.length, openOrders: tickets.filter((t) => t.status === "open" || t.status === "draft").length, kitchenActive: kitchen.filter((k) => k.status !== "completed" && k.status !== "cancelled").length, }; }, enabled: !!tenantId, }); if (!tenantId || countsQ.isLoading || caps.isLoading) return ; if (countsQ.error) return countsQ.refetch()} />; const c = countsQ.data!; return (
Torbat Food v{caps.data?.version}} />
); }; export default ExecutiveDashboard; ` ); writeFeature( "hub", `"use client"; import Link from "next/link"; import { useMe } from "@/hooks/useMe"; import { HOSPITALITY_PORTAL } from "@/modules/hospitality/constants/portals"; import { Card, CardContent, Button, LoadingState, ErrorState } from "@/components/ds"; export const HospitalityHub = function HospitalityHubPage() { const { me, loading, error, reload } = useMe(); if (loading) return ; if (error) return ; if (!me?.current_tenant_id) { return ; } return (

تربت فود

پلتفرم مهمان‌نوازی سازمانی

{HOSPITALITY_PORTAL.label}

{HOSPITALITY_PORTAL.description}

); }; export default HospitalityHub; ` ); writeFeature( "health", `"use client"; import { useQuery } from "@tanstack/react-query"; import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api"; import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared"; import { PageHeader, Card, CardContent, Badge } from "@/components/ds"; export const HealthPage = function HospitalityHealthPage() { const q = useQuery({ queryKey: ["hospitality", "health"], queryFn: () => hospitalityApi.health() }); if (q.isLoading) return ; if (q.error) return q.refetch()} />; return (
{q.data?.status}

{q.data?.service} — v{q.data?.version}

); }; export default HealthPage; ` ); writeFeature( "capabilities", `"use client"; import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities"; import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared"; import { PageHeader, Card, CardContent, Badge } from "@/components/ds"; export const CapabilitiesPage = function HospitalityCapabilitiesPage() { const q = useHospitalityCapabilities(); if (q.isLoading) return ; if (q.error) return q.refetch()} />; const features = q.data?.features ?? {}; return (
{Object.entries(features).map(([k, v]) => ( {k} {v ? "فعال" : "غیرفعال"} ))}
); }; export default CapabilitiesPage; ` ); // Routes for (const [routePath, exportName, featureFile] of PAGE_REGISTRY) { const dir = path.join(APP, routePath); ensureDir(dir); const pageContent = `"use client";\n\nexport { ${exportName} as default } from "@/modules/hospitality/features/${featureFile}";\n`; fs.writeFileSync(path.join(dir, "page.tsx"), pageContent); writeIfMissing( path.join(dir, "loading.tsx"), `import { LoadingState } from "@/components/ds";\n\nexport default function Loading() {\n return ;\n}\n` ); writeIfMissing( path.join(dir, "error.tsx"), `"use client";\n\nimport { ErrorState } from "@/components/ds";\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n return ;\n}\n` ); } // Root layout files writeIfMissing( path.join(APP, "layout.tsx"), `/** Hospitality root — portal shell on management routes. */\nexport default function HospitalityRootLayout({ children }: { children: React.ReactNode }) {\n return children;\n}\n` ); writeIfMissing( path.join(APP, "not-found.tsx"), `import Link from "next/link";\nimport { Button, EmptyState } from "@/components/ds";\n\nexport default function HospitalityNotFound() {\n return (\n
\n } />\n
\n );\n}\n` ); // Management layout (all routes except hub use shell) const mgmtLayout = `"use client";\n\nimport { createPortalLayout } from "@/modules/hospitality/components/createPortalLayout";\n\nexport default createPortalLayout("management");\n`; // Apply layout to root management - use a (mgmt) group or layout at app/hospitality level // For simplicity, put layout on main routes via a wrapper layout at hospitality level for non-hub // BFF const bffDir = path.join(ROOT, "app", "api", "hospitality", "[...path]"); ensureDir(bffDir); const bff = `import { NextRequest, NextResponse } from "next/server"; const UPSTREAM = process.env.HOSPITALITY_SERVICE_URL || process.env.NEXT_PUBLIC_HOSPITALITY_API_URL || "http://localhost:8009"; async function proxy(req: NextRequest, pathSegments: string[]) { const p = pathSegments.join("/"); const url = new URL(req.url); const target = \`\${UPSTREAM.replace(/\\/$/, "")}/\${p}\${url.search}\`; const headers = new Headers(); for (const h of ["authorization", "content-type", "x-tenant-id", "accept"]) { const v = req.headers.get(h); if (v) headers.set(h, v); } let body: ArrayBuffer | undefined; if (req.method !== "GET" && req.method !== "HEAD") body = await req.arrayBuffer(); try { const res = await fetch(target, { method: req.method, headers, body, cache: "no-store" }); const out = new Headers(); const ct = res.headers.get("content-type"); if (ct) out.set("content-type", ct); return new NextResponse(await res.arrayBuffer(), { status: res.status, headers: out }); } catch (err) { const detail = err instanceof Error ? err.message : "upstream_unreachable"; return NextResponse.json( { error: { code: "hospitality_proxy_error", message: \`پروکسی تربت فود — \${detail}\` } }, { status: 502 } ); } } export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path: p } = await ctx.params; return proxy(req, p); } export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path: p } = await ctx.params; return proxy(req, p); } export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path: p } = await ctx.params; return proxy(req, p); } export async function PUT(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path: p } = await ctx.params; return proxy(req, p); } export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path: p } = await ctx.params; return proxy(req, p); } `; fs.writeFileSync(path.join(bffDir, "route.ts"), bff); console.log(`Scaffolded ${PAGE_REGISTRY.length} hospitality routes and feature stubs.`);