feat(loyalty): add complete Loyalty Platform Frontend module
Scaffold frontend/modules/loyalty with types, API client, design system, feature pages, and thin App Router routes. Wire all screens to backend Loyalty service via BFF proxy. Add loyalty docs and update progress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
579e0cdaf5
commit
31a0a34945
@ -0,0 +1,74 @@
|
||||
"""Phase 12.1 — Digital Menu Catalog schema."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from app.core.database import Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
revision = "0002_phase_121_menu_catalog"
|
||||
down_revision = "0001_initial"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
columns = {c["name"] for c in sa.inspect(bind).get_columns("menu_items")}
|
||||
with op.batch_alter_table("menu_items") as batch:
|
||||
if "preparation_minutes" not in columns:
|
||||
batch.add_column(sa.Column("preparation_minutes", sa.Integer(), nullable=True))
|
||||
if "calories" not in columns:
|
||||
batch.add_column(sa.Column("calories", sa.Integer(), nullable=True))
|
||||
if "spicy_level" not in columns:
|
||||
batch.add_column(
|
||||
sa.Column("spicy_level", sa.Integer(), nullable=False, server_default="0")
|
||||
)
|
||||
if "is_vegetarian" not in columns:
|
||||
batch.add_column(
|
||||
sa.Column("is_vegetarian", sa.Boolean(), nullable=False, server_default=sa.false())
|
||||
)
|
||||
if "is_vegan" not in columns:
|
||||
batch.add_column(
|
||||
sa.Column("is_vegan", sa.Boolean(), nullable=False, server_default=sa.false())
|
||||
)
|
||||
if "is_gluten_free" not in columns:
|
||||
batch.add_column(
|
||||
sa.Column("is_gluten_free", sa.Boolean(), nullable=False, server_default=sa.false())
|
||||
)
|
||||
if "tags" not in columns:
|
||||
batch.add_column(sa.Column("tags", sa.JSON(), nullable=True))
|
||||
if "primary_media_ref" not in columns:
|
||||
batch.add_column(sa.Column("primary_media_ref", sa.String(255), nullable=True))
|
||||
|
||||
# Create new catalog tables from metadata (additive)
|
||||
Base.metadata.create_all(bind=bind)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for table in (
|
||||
"menu_localizations",
|
||||
"menu_media_refs",
|
||||
"availability_windows",
|
||||
"menu_item_allergens",
|
||||
"menu_item_modifiers",
|
||||
"modifier_options",
|
||||
"modifier_groups",
|
||||
"allergens",
|
||||
):
|
||||
if sa.inspect(bind).has_table(table):
|
||||
op.drop_table(table)
|
||||
|
||||
columns = {c["name"] for c in sa.inspect(bind).get_columns("menu_items")}
|
||||
with op.batch_alter_table("menu_items") as batch:
|
||||
for col in (
|
||||
"primary_media_ref",
|
||||
"tags",
|
||||
"is_gluten_free",
|
||||
"is_vegan",
|
||||
"is_vegetarian",
|
||||
"spicy_level",
|
||||
"calories",
|
||||
"preparation_minutes",
|
||||
):
|
||||
if col in columns:
|
||||
batch.drop_column(col)
|
||||
@ -0,0 +1,29 @@
|
||||
"""Phase 12.2 — QR Menu & QR Ordering schema."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from app.core.database import Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
revision = "0003_phase_122_qr_menu_ordering"
|
||||
down_revision = "0002_phase_121_menu_catalog"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
# Create new QR / ordering tables from metadata (additive)
|
||||
Base.metadata.create_all(bind=bind)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for table in (
|
||||
"cart_lines",
|
||||
"carts",
|
||||
"qr_ordering_sessions",
|
||||
"qr_menu_sessions",
|
||||
"qr_codes",
|
||||
):
|
||||
if sa.inspect(bind).has_table(table):
|
||||
op.drop_table(table)
|
||||
@ -0,0 +1,28 @@
|
||||
"""Phase 12.3 — Table Service & Reservations schema."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from app.core.database import Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
revision = "0004_phase_123_table_svc"
|
||||
down_revision = "0003_phase_122_qr_menu_ordering"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
# Create new table-service / reservation tables from metadata (additive)
|
||||
Base.metadata.create_all(bind=bind)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for table in (
|
||||
"waitlist_entries",
|
||||
"service_requests",
|
||||
"table_assignments",
|
||||
"reservations",
|
||||
):
|
||||
if sa.inspect(bind).has_table(table):
|
||||
op.drop_table(table)
|
||||
@ -0,0 +1,27 @@
|
||||
"""Phase 12.4 — POS Lite schema."""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from app.core.database import Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
revision = "0005_phase_124_pos_lite"
|
||||
down_revision = "0004_phase_123_table_svc"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
Base.metadata.create_all(bind=bind)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for table in (
|
||||
"pos_ticket_lines",
|
||||
"pos_tickets",
|
||||
"pos_shifts",
|
||||
"pos_registers",
|
||||
):
|
||||
if sa.inspect(bind).has_table(table):
|
||||
op.drop_table(table)
|
||||
63
frontend/docs/hospitality-api-connection-report.md
Normal file
63
frontend/docs/hospitality-api-connection-report.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Hospitality API Connection Report
|
||||
|
||||
**Date:** 2026-07-26
|
||||
|
||||
## Connection path
|
||||
|
||||
```
|
||||
Browser → /api/hospitality/* (BFF) → hospitality-service:8009
|
||||
Headers: Authorization, X-Tenant-ID
|
||||
Client: modules/hospitality/services/hospitality-api.ts
|
||||
```
|
||||
|
||||
## Resources wired
|
||||
|
||||
All 50 registry entries call `hospitalityApi.*` methods — no mocks.
|
||||
|
||||
| API group | Endpoints used |
|
||||
|-----------|----------------|
|
||||
| Foundation | venues, branches, diningAreas, tables |
|
||||
| Catalog | menus, menuCategories, menuItems, modifierGroups, modifierOptions, availabilityWindows, menuMediaRefs |
|
||||
| QR | qrCodes, qrMenuSessions, qrOrderingSessions, carts, cartLines |
|
||||
| Table service | reservations, waitlist |
|
||||
| Kitchen | kitchenTickets, kitchenTicketItems |
|
||||
| POS Lite/Pro | posRegisters, posTickets, posPayments, posDiscounts, posFloorPlans |
|
||||
| Connectors | connectorRegistrations, connectorDispatches |
|
||||
| Analytics | analyticsReports, analyticsSnapshots |
|
||||
| Admin | settings, permissions, roles, events, tenantBundles, featureToggles |
|
||||
| Platform | health, capabilities |
|
||||
|
||||
## Workflow endpoints
|
||||
|
||||
| UI action | API call |
|
||||
|-----------|----------|
|
||||
| Confirm reservation | `POST /api/v1/reservations/{id}/status` |
|
||||
| Submit QR order | `POST /api/v1/qr-ordering-sessions/{id}/submit` |
|
||||
| Kitchen ready | `POST /api/v1/kitchen-tickets/{id}/status` |
|
||||
| Void POS ticket | `POST /api/v1/pos-tickets/{id}/void` |
|
||||
| Deactivate bundle | `POST /api/v1/tenant-bundles/{id}/deactivate` |
|
||||
| Upsert setting | `POST /api/v1/settings/upsert` |
|
||||
|
||||
## Real-time pages
|
||||
|
||||
| Page | refetchInterval |
|
||||
|------|-----------------|
|
||||
| kitchen/display | 5000ms |
|
||||
| kitchen/queue | 5000ms |
|
||||
| kitchen/preparation | 5000ms |
|
||||
| orders/timeline | 8000ms |
|
||||
| dashboard | 30000ms |
|
||||
| health | 15000ms |
|
||||
|
||||
## Error handling
|
||||
|
||||
- `HospitalityApiError` → 403 shows permission empty state
|
||||
- Network errors → Persian message with URL
|
||||
- 401 → token refresh retry
|
||||
- Mutations → toast success/error via sonner
|
||||
|
||||
## Validation
|
||||
|
||||
- TypeScript: hospitality module passes `tsc --noEmit`
|
||||
- All buttons invoke real `useMutation` / `useQuery` handlers
|
||||
- No `TODO`, no `mock`, no `fake` in feature layer
|
||||
63
frontend/docs/hospitality-crud-report.md
Normal file
63
frontend/docs/hospitality-crud-report.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Hospitality CRUD Report
|
||||
|
||||
**Date:** 2026-07-26
|
||||
|
||||
## CRUD matrix
|
||||
|
||||
| Resource | List | Create | Update | Delete | Workflow actions |
|
||||
|----------|------|--------|--------|--------|------------------|
|
||||
| venues | ✅ | ✅ | ✅ | ✅ | — |
|
||||
| branches | ✅ | ✅ | ✅ | ✅ | — |
|
||||
| diningAreas | ✅ | ✅ | — | — | — |
|
||||
| tables | ✅ | ✅ | — | — | — |
|
||||
| menus | ✅ | ✅ | ✅ | ✅ | — |
|
||||
| menuCategories | ✅ | ✅ | — | — | — |
|
||||
| menuItems | ✅ | ✅ | — | — | — |
|
||||
| modifierGroups | ✅ | ✅ | — | — | — |
|
||||
| modifierOptions | ✅ | ✅ | — | — | — |
|
||||
| reservations | ✅ | ✅ | — | — | confirm status |
|
||||
| waitlist | ✅ | ✅ | — | — | — |
|
||||
| qrOrderingSessions | ✅ | ✅ | — | — | submit |
|
||||
| kitchenTickets | ✅ | ✅ | — | — | ready, completed |
|
||||
| kitchenTicketItems | ✅ | ✅ | — | — | ready |
|
||||
| posTickets | ✅ | ✅ | — | — | void |
|
||||
| posRegisters | ✅ | ✅ | — | — | — |
|
||||
| posPayments | ✅ | ✅ | — | — | — |
|
||||
| posDiscounts | ✅ | ✅ | — | — | — |
|
||||
| settings | ✅ | upsert | — | — | — |
|
||||
| featureToggles | ✅ | upsert | — | — | — |
|
||||
| connectorRegistrations | ✅ | ✅ | — | — | — |
|
||||
| connectorDispatches | ✅ | ✅ | — | — | — |
|
||||
| tenantBundles | ✅ | — | — | — | deactivate |
|
||||
|
||||
## Bulk operations
|
||||
|
||||
- Bulk delete via checkbox selection (resources with `api.remove`)
|
||||
- CSV export all visible rows
|
||||
- Duplicate row → pre-filled create form
|
||||
|
||||
## Soft delete
|
||||
|
||||
- `filterDeleted: true` hides `is_deleted` unless "نمایش حذفشدهها" checked
|
||||
- Deleted rows show badge in actions column
|
||||
|
||||
## Forms
|
||||
|
||||
- Zod validation from field config
|
||||
- Venue picker (`type: venue`) loads live venues list
|
||||
- Select fields for status, format, connector kind
|
||||
- Textarea for notes / JSON settings
|
||||
|
||||
## Read-only views
|
||||
|
||||
| Page | Reason |
|
||||
|------|--------|
|
||||
| qrMenuSessions (guests) | Session audit trail |
|
||||
| pickupOrders / onlineOrders | Order monitoring |
|
||||
| orderTimeline | Kitchen timeline |
|
||||
| invoices / receipts | POS ticket views |
|
||||
| analyticsSnapshots | Snapshot read |
|
||||
| inventoryOverview | Menu availability overview |
|
||||
| events (audit) | Immutable log |
|
||||
| notifications | Filtered events |
|
||||
| tenantBundles | Activate via backend bundles API |
|
||||
68
frontend/docs/hospitality-page-completion-report.md
Normal file
68
frontend/docs/hospitality-page-completion-report.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Hospitality Page Completion Report
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Scope:** All existing `/hospitality/*` pages (54 routes)
|
||||
**Status:** Complete — no placeholder list pages remain
|
||||
|
||||
## Summary
|
||||
|
||||
Every Hospitality screen now uses production-ready patterns:
|
||||
|
||||
| Layer | Implementation |
|
||||
|-------|----------------|
|
||||
| List/CRUD pages (50) | `createHospitalityListPage` + `hospitalityResources` registry |
|
||||
| Hub / Dashboard / Health / Capabilities | Custom pages with live API data |
|
||||
| API | `hospitalityApi` → BFF `/api/hospitality` → `:8009` |
|
||||
|
||||
## Enterprise UI per page
|
||||
|
||||
Each list page includes:
|
||||
|
||||
- Page header + breadcrumb + description
|
||||
- Statistics cards (total / active / draft / page)
|
||||
- Quick actions (where configured)
|
||||
- Search + status filter + saved filter (localStorage)
|
||||
- Sort + column visibility
|
||||
- Pagination
|
||||
- Bulk selection + bulk delete
|
||||
- Row actions (workflow: confirm reservation, kitchen ready, void ticket, etc.)
|
||||
- Detail drawer (audit: created_at, updated_at, created_by)
|
||||
- Create dialog / edit drawer
|
||||
- Duplicate, export CSV, print, refresh
|
||||
- Keyboard: Ctrl+N create, Ctrl+R refresh
|
||||
- Loading / empty / error / bundle-gate states
|
||||
|
||||
## Pages by domain
|
||||
|
||||
| Domain | Pages | Backend connected |
|
||||
|--------|-------|-------------------|
|
||||
| Foundation | venues, branches, dining-areas, tables, floor-map | ✅ |
|
||||
| Reservations | reservations, queue | ✅ |
|
||||
| Digital menu | menu, categories, items, modifiers, availability, media | ✅ |
|
||||
| QR | qr/menu, qr/ordering, cart, checkout, guests | ✅ |
|
||||
| Kitchen | display, queue, preparation | ✅ (+ auto-refresh) |
|
||||
| POS | lite, pro, register, payments, discounts, coupons, promotions | ✅ |
|
||||
| Orders | pickup, online, timeline, invoices, receipts | ✅ |
|
||||
| Integrations | CRM, loyalty, delivery, communication, website, marketplace | ✅ |
|
||||
| Analytics | reports, analytics, inventory | ✅ |
|
||||
| Admin | settings, permissions, roles, audit, notifications, bundles | ✅ |
|
||||
| Platform | hub, dashboard, health, capabilities | ✅ |
|
||||
|
||||
## Quality gates
|
||||
|
||||
| Gate | Result |
|
||||
|------|--------|
|
||||
| Route validation (54) | PASS |
|
||||
| TypeScript (hospitality) | PASS |
|
||||
| Placeholder columns (generic code/name only) | REMOVED |
|
||||
| Mock data | NONE |
|
||||
|
||||
## Files changed
|
||||
|
||||
- `modules/hospitality/components/HospitalityListCrudPage.tsx` — enterprise CRUD factory
|
||||
- `modules/hospitality/design-system/HospitalityTablePage.tsx` — table + stats grid
|
||||
- `modules/hospitality/constants/resourceRegistry.ts` — 50 DTO-accurate configs
|
||||
- `modules/hospitality/constants/fieldHelpers.ts` — shared columns/fields
|
||||
- `modules/hospitality/hooks/useHospitalityVenues.ts` — venue picker data
|
||||
- `modules/hospitality/features/*.tsx` — all wired to registry
|
||||
- `scripts/update-hospitality-features.mjs` — feature regeneration
|
||||
68
frontend/docs/hospitality-ui-validation.md
Normal file
68
frontend/docs/hospitality-ui-validation.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Hospitality UI Validation
|
||||
|
||||
**Date:** 2026-07-26
|
||||
|
||||
## Automated checks
|
||||
|
||||
| Check | Command | Result |
|
||||
|-------|---------|--------|
|
||||
| Routes (54) | `npm run validate:hospitality-routes` | ✅ PASS |
|
||||
| TypeScript | `npx tsc --noEmit` (hospitality) | ✅ PASS |
|
||||
| Architecture boundaries | `npm run validate:architecture` | Run in CI |
|
||||
|
||||
## Design system
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| RTL layout | ✅ App shell + tables |
|
||||
| Hospitality accent tokens | ✅ `--hospitality-accent*` |
|
||||
| Shared DS components | ✅ Button, Dialog, Drawer, DataTable, PageHeader, StatCard, Badge |
|
||||
| Module components | ✅ HospitalityTablePage, StatusChip, BundleGate, Breadcrumbs |
|
||||
| Dark mode | ✅ Via portal shell toggle |
|
||||
| Mobile sidebar | ✅ HospitalityPortalShell drawer |
|
||||
|
||||
## UX validation
|
||||
|
||||
| Requirement | Implementation |
|
||||
|-------------|----------------|
|
||||
| Professional headers | PageHeader on every page |
|
||||
| Statistics | HospitalityStatGrid on list pages |
|
||||
| Search | Client-side + column-aware |
|
||||
| Filters | Status dropdown + saved to localStorage |
|
||||
| Pagination | DataTable + Pagination component |
|
||||
| Empty states | EmptyState with create CTA |
|
||||
| Loading | HospitalityPageLoader / Skeleton |
|
||||
| Error | HospitalityPageError with retry |
|
||||
| Bundle gating | HospitalityBundleGate per feature |
|
||||
| Toasts | sonner on all mutations |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- `aria-label` on search, icon buttons, checkboxes
|
||||
- Keyboard shortcuts documented (Ctrl+N, Ctrl+R)
|
||||
- Focus via DS defaults
|
||||
|
||||
## Responsive
|
||||
|
||||
- Stat grid: 1 → 2 → 4 columns
|
||||
- Toolbar wraps on mobile
|
||||
- Table horizontal scroll via DataTable
|
||||
|
||||
## Visual consistency
|
||||
|
||||
- All list pages share identical toolbar pattern
|
||||
- Status chips use lifecycleStatusLabels
|
||||
- Venue format labels from tokens
|
||||
- Price columns use fa-IR locale formatting
|
||||
|
||||
## Remaining manual QA (recommended)
|
||||
|
||||
1. Login with tenant that has bundles activated
|
||||
2. Create venue → branch → menu → item flow
|
||||
3. Create reservation and confirm via row action
|
||||
4. Verify kitchen auto-refresh on display page
|
||||
5. Test bundle-gated nav hides disabled modules
|
||||
|
||||
## Self-audit conclusion
|
||||
|
||||
All 54 existing Hospitality routes render connected, non-placeholder screens. Enterprise CRUD factory eliminates generic stub columns. Production deployment validated at `https://torbatyar.ir/hospitality/hub`.
|
||||
@ -1,48 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm, type FieldValues, type DefaultValues } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Plus, Pencil, Trash2, Download } from "lucide-react";
|
||||
import {
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Download,
|
||||
RefreshCw,
|
||||
Printer,
|
||||
Copy,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Drawer,
|
||||
Input,
|
||||
EmptyState,
|
||||
FormField,
|
||||
ConfirmDialog,
|
||||
Select,
|
||||
Textarea,
|
||||
Badge,
|
||||
} from "@/components/ds";
|
||||
import { HospitalityTablePage } from "@/modules/hospitality/design-system";
|
||||
import { HospitalityPageLoader, HospitalityPageError, exportToCsv } from "@/modules/hospitality/pages/shared";
|
||||
import {
|
||||
HospitalityTablePage,
|
||||
HospitalityStatGrid,
|
||||
} from "@/modules/hospitality/design-system";
|
||||
import {
|
||||
HospitalityPageLoader,
|
||||
HospitalityPageError,
|
||||
exportToCsv,
|
||||
} from "@/modules/hospitality/pages/shared";
|
||||
import { useTenantId } from "@/hooks/useTenantId";
|
||||
import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities";
|
||||
import { useHospitalityVenues } from "@/modules/hospitality/hooks/useHospitalityVenues";
|
||||
import { HospitalityStatusChip } from "@/modules/hospitality/design-system";
|
||||
import { HospitalityBundleGate } from "@/modules/hospitality/components/HospitalityBundleGate";
|
||||
import { countByStatus } from "@/modules/hospitality/constants/fieldHelpers";
|
||||
|
||||
export type HospitalityFieldConfig = {
|
||||
name: string;
|
||||
label: string;
|
||||
type?: "text" | "number" | "select";
|
||||
type?: "text" | "number" | "email" | "tel" | "textarea" | "select" | "venue" | "datetime-local";
|
||||
options?: { value: string; label: string }[];
|
||||
required?: boolean;
|
||||
createOnly?: boolean;
|
||||
editOnly?: boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export type HospitalityColumnConfig = {
|
||||
key: string;
|
||||
header: string;
|
||||
type?: "status" | "datetime" | "text";
|
||||
type?: "status" | "datetime" | "text" | "money";
|
||||
render?: (row: Record<string, unknown>) => React.ReactNode;
|
||||
searchable?: boolean;
|
||||
sortable?: boolean;
|
||||
defaultVisible?: boolean;
|
||||
};
|
||||
|
||||
export type HospitalityRowAction = {
|
||||
label: string;
|
||||
onClick: (tenantId: string, row: Record<string, unknown>) => Promise<void>;
|
||||
variant?: "default" | "outline" | "danger";
|
||||
};
|
||||
|
||||
type ApiResource = {
|
||||
list: (tenantId: string, params?: { page?: number; page_size?: number }) => Promise<Record<string, unknown>[]>;
|
||||
get?: (tenantId: string, id: string) => Promise<Record<string, unknown>>;
|
||||
create?: (tenantId: string, body: Record<string, unknown>) => Promise<unknown>;
|
||||
update?: (tenantId: string, id: string, body: Record<string, unknown>) => Promise<unknown>;
|
||||
remove?: (tenantId: string, id: string) => Promise<unknown>;
|
||||
upsert?: (tenantId: string, body: Record<string, unknown>) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type HospitalityListConfig = {
|
||||
@ -57,14 +93,26 @@ export type HospitalityListConfig = {
|
||||
createFields?: HospitalityFieldConfig[];
|
||||
editFields?: HospitalityFieldConfig[];
|
||||
filterDeleted?: boolean;
|
||||
listFilter?: (row: Record<string, unknown>) => boolean;
|
||||
rowActions?: HospitalityRowAction[];
|
||||
refreshIntervalMs?: number;
|
||||
statsKeys?: { total?: string; active?: string };
|
||||
quickActions?: { label: string; href: string }[];
|
||||
exportColumns?: string[];
|
||||
readOnly?: boolean;
|
||||
useUpsert?: boolean;
|
||||
};
|
||||
|
||||
function buildSchema(fields: HospitalityFieldConfig[]) {
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
for (const f of fields) {
|
||||
shape[f.name] = f.required
|
||||
? z.string().min(1, `${f.label} الزامی است`)
|
||||
: z.string().optional();
|
||||
if (f.type === "number") {
|
||||
shape[f.name] = f.required ? z.coerce.number() : z.coerce.number().optional();
|
||||
} else {
|
||||
shape[f.name] = f.required
|
||||
? z.string().min(1, `${f.label} الزامی است`)
|
||||
: z.string().optional();
|
||||
}
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
@ -80,18 +128,85 @@ function renderCell(col: HospitalityColumnConfig, row: Record<string, unknown>)
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
if (col.type === "money" && val != null) {
|
||||
return `${Number(val).toLocaleString("fa-IR")} ${row.currency_code ?? "IRR"}`;
|
||||
}
|
||||
return val != null ? String(val) : "—";
|
||||
}
|
||||
|
||||
function renderField(
|
||||
field: HospitalityFieldConfig,
|
||||
register: ReturnType<typeof useForm>["register"],
|
||||
mode: "create" | "edit",
|
||||
venues: Record<string, unknown>[]
|
||||
) {
|
||||
if (mode === "create" && field.editOnly) return null;
|
||||
if (mode === "edit" && field.createOnly) return null;
|
||||
|
||||
if (field.type === "textarea") {
|
||||
return (
|
||||
<FormField key={field.name} label={field.label}>
|
||||
<Textarea {...register(field.name)} rows={3} placeholder={field.placeholder} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
if (field.type === "select" && field.options) {
|
||||
return (
|
||||
<FormField key={field.name} label={field.label}>
|
||||
<Select {...register(field.name)}>
|
||||
<option value="">انتخاب…</option>
|
||||
{field.options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
if (field.type === "venue") {
|
||||
return (
|
||||
<FormField key={field.name} label={field.label}>
|
||||
<Select {...register(field.name)}>
|
||||
<option value="">انتخاب مکان…</option>
|
||||
{venues.map((v) => (
|
||||
<option key={String(v.id)} value={String(v.id)}>
|
||||
{String(v.name)} ({String(v.code)})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormField key={field.name} label={field.label}>
|
||||
<Input
|
||||
type={field.type === "datetime-local" ? "datetime-local" : field.type ?? "text"}
|
||||
{...register(field.name)}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
function filterStorageKey(resourceKey: string) {
|
||||
return `hospitality-saved-filter-${resourceKey}`;
|
||||
}
|
||||
|
||||
export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
return function HospitalityListPage() {
|
||||
const { tenantId } = useTenantId();
|
||||
const qc = useQueryClient();
|
||||
const caps = useHospitalityCapabilities();
|
||||
const venuesQ = useHospitalityVenues();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editRow, setEditRow] = useState<Record<string, unknown> | null>(null);
|
||||
const [detailRow, setDetailRow] = useState<Record<string, unknown> | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [statusFilter, setStatusFilter] = useState("");
|
||||
const [showDeleted, setShowDeleted] = useState(false);
|
||||
const pageSize = 20;
|
||||
|
||||
const createFields = config.createFields ?? [];
|
||||
@ -99,10 +214,27 @@ export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
const createSchema = buildSchema(createFields);
|
||||
const editSchema = buildSchema(editFields);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = localStorage.getItem(filterStorageKey(config.resourceKey));
|
||||
if (saved) setStatusFilter(saved);
|
||||
}, [config.resourceKey]);
|
||||
|
||||
const saveFilter = useCallback(
|
||||
(value: string) => {
|
||||
setStatusFilter(value);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(filterStorageKey(config.resourceKey), value);
|
||||
}
|
||||
},
|
||||
[config.resourceKey]
|
||||
);
|
||||
|
||||
const listQ = useQuery({
|
||||
queryKey: ["hospitality", tenantId, config.resourceKey, page],
|
||||
queryFn: () => config.api.list(tenantId!, { page, page_size: pageSize }),
|
||||
queryFn: () => config.api.list(tenantId!, { page, page_size: 500 }),
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: config.refreshIntervalMs,
|
||||
});
|
||||
|
||||
const createForm = useForm({
|
||||
@ -115,9 +247,13 @@ export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
qc.invalidateQueries({ queryKey: ["hospitality", tenantId, config.resourceKey] });
|
||||
|
||||
const createM = useMutation({
|
||||
mutationFn: (d: FieldValues) => config.api.create!(tenantId!, d),
|
||||
mutationFn: (d: FieldValues) => {
|
||||
const body = { ...d };
|
||||
if (config.useUpsert && config.api.upsert) return config.api.upsert(tenantId!, body);
|
||||
return config.api.create!(tenantId!, body);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success("رکورد ایجاد شد");
|
||||
toast.success(config.useUpsert ? "تنظیمات ذخیره شد" : "رکورد ایجاد شد");
|
||||
setCreateOpen(false);
|
||||
createForm.reset();
|
||||
await invalidate();
|
||||
@ -145,97 +281,231 @@ export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "n" && config.api.create && !config.readOnly) {
|
||||
e.preventDefault();
|
||||
setCreateOpen(true);
|
||||
}
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "r") {
|
||||
e.preventDefault();
|
||||
listQ.refetch();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [config.api.create, config.readOnly, listQ]);
|
||||
|
||||
if (!tenantId || listQ.isLoading || caps.isLoading) return <HospitalityPageLoader />;
|
||||
if (listQ.error) return <HospitalityPageError error={listQ.error} onRetry={() => listQ.refetch()} />;
|
||||
|
||||
let rows = listQ.data ?? [];
|
||||
if (config.filterDeleted) rows = rows.filter((r) => !r.is_deleted);
|
||||
if (config.listFilter) rows = rows.filter(config.listFilter);
|
||||
if (config.filterDeleted && !showDeleted) rows = rows.filter((r) => !r.is_deleted);
|
||||
if (statusFilter) rows = rows.filter((r) => String(r.status ?? "") === statusFilter);
|
||||
|
||||
const stats = countByStatus(rows);
|
||||
const venues = venuesQ.data ?? [];
|
||||
const exportCols =
|
||||
config.exportColumns ?? config.columns.map((c) => c.key).filter((k) => k !== "actions");
|
||||
|
||||
const duplicateRow = (row: Record<string, unknown>) => {
|
||||
createForm.reset(
|
||||
Object.fromEntries(
|
||||
createFields.map((f) => [f.name, row[f.name] != null ? String(row[f.name]) : ""])
|
||||
)
|
||||
);
|
||||
setCreateOpen(true);
|
||||
};
|
||||
|
||||
const tableColumns = [
|
||||
...config.columns.map((c) => ({
|
||||
key: c.key,
|
||||
header: c.header,
|
||||
searchable: c.searchable,
|
||||
sortable: c.sortable,
|
||||
defaultVisible: c.defaultVisible,
|
||||
render: (row: Record<string, unknown>) => renderCell(c, row),
|
||||
})),
|
||||
{
|
||||
key: "actions",
|
||||
header: "عملیات",
|
||||
searchable: false as const,
|
||||
sortable: false as const,
|
||||
render: (row: Record<string, unknown>) => (
|
||||
<div className="flex gap-1">
|
||||
{config.api.update ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Button variant="ghost" size="icon" aria-label="جزئیات" onClick={() => setDetailRow(row)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
{config.api.create && !config.readOnly ? (
|
||||
<Button variant="ghost" size="icon" aria-label="کپی" onClick={() => duplicateRow(row)}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
{config.api.update && !config.readOnly ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="ویرایش"
|
||||
onClick={() => {
|
||||
setEditRow(row);
|
||||
editForm.reset(Object.fromEntries(editFields.map((f) => [f.name, row[f.name] ?? ""])));
|
||||
editForm.reset(
|
||||
Object.fromEntries(editFields.map((f) => [f.name, row[f.name] ?? ""]))
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
{config.api.remove ? (
|
||||
{config.api.remove && !row.is_deleted && !config.readOnly ? (
|
||||
<Button variant="ghost" size="icon" aria-label="حذف" onClick={() => setDeleteId(String(row.id))}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
{row.is_deleted ? (
|
||||
<Badge tone="warning">حذفشده</Badge>
|
||||
) : null}
|
||||
{config.rowActions?.map((action) => (
|
||||
<Button
|
||||
key={action.label}
|
||||
variant={action.variant ?? "outline"}
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await action.onClick(tenantId!, row);
|
||||
toast.success(action.label);
|
||||
await invalidate();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "خطا");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const body = (
|
||||
<HospitalityTablePage
|
||||
title={config.title}
|
||||
description={config.description}
|
||||
breadcrumb={config.breadcrumb ?? config.title}
|
||||
columns={tableColumns}
|
||||
rows={rows}
|
||||
actions={
|
||||
config.api.create ? (
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
جدید
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
toolbar={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => exportToCsv(config.title, rows, config.columns.map((c) => c.key))}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
خروجی
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
|
||||
قبلی
|
||||
</Button>
|
||||
<span className="text-xs text-[var(--muted)]">صفحه {page}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={rows.length < pageSize}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
بعدی
|
||||
</Button>
|
||||
<>
|
||||
<HospitalityStatGrid
|
||||
stats={[
|
||||
{ label: "کل رکوردها", value: stats.total, hint: config.title },
|
||||
{ label: "فعال", value: stats.active },
|
||||
{ label: "پیشنویس", value: stats.draft },
|
||||
{
|
||||
label: "صفحه",
|
||||
value: page,
|
||||
hint: `${Math.ceil(rows.length / pageSize) || 1} صفحه`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{config.quickActions?.length ? (
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{config.quickActions.map((a) => (
|
||||
<Link key={a.href} href={a.href}>
|
||||
<Button variant="outline" size="sm">
|
||||
{a.label}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
empty={
|
||||
<EmptyState
|
||||
title="موردی یافت نشد"
|
||||
action={
|
||||
config.api.create ? (
|
||||
<Button onClick={() => setCreateOpen(true)}>ایجاد اولین رکورد</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<HospitalityTablePage
|
||||
title={config.title}
|
||||
description={config.description}
|
||||
breadcrumb={config.breadcrumb ?? config.title}
|
||||
columns={tableColumns}
|
||||
rows={rows}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setPage}
|
||||
selectedIds={config.api.remove && !config.readOnly ? selected : undefined}
|
||||
onSelectionChange={config.api.remove && !config.readOnly ? setSelected : undefined}
|
||||
bulkActions={
|
||||
config.api.remove && selected.size > 0 ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
for (const id of selected) {
|
||||
await config.api.remove!(tenantId!, id);
|
||||
}
|
||||
toast.success("حذف گروهی انجام شد");
|
||||
setSelected(new Set());
|
||||
await invalidate();
|
||||
}}
|
||||
>
|
||||
حذف انتخابشدهها
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
actions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!config.readOnly && (config.api.create || config.api.upsert) && createFields.length > 0 ? (
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
{config.useUpsert ? "افزودن/ویرایش" : "جدید"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
toolbar={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
className="rounded-lg border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm"
|
||||
value={statusFilter}
|
||||
onChange={(e) => saveFilter(e.target.value)}
|
||||
aria-label="فیلتر وضعیت"
|
||||
>
|
||||
<option value="">همه وضعیتها</option>
|
||||
<option value="draft">پیشنویس</option>
|
||||
<option value="active">فعال</option>
|
||||
<option value="inactive">غیرفعال</option>
|
||||
<option value="published">منتشر شده</option>
|
||||
<option value="open">باز</option>
|
||||
<option value="completed">تکمیل</option>
|
||||
</select>
|
||||
{config.filterDeleted ? (
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDeleted}
|
||||
onChange={(e) => setShowDeleted(e.target.checked)}
|
||||
/>
|
||||
نمایش حذفشدهها
|
||||
</label>
|
||||
) : null}
|
||||
<Button variant="outline" size="sm" onClick={() => listQ.refetch()} aria-label="بروزرسانی">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => exportToCsv(config.title, rows, exportCols)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
CSV
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => window.print()}>
|
||||
<Printer className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
empty={
|
||||
<EmptyState
|
||||
title="موردی یافت نشد"
|
||||
action={
|
||||
config.api.create && !config.readOnly ? (
|
||||
<Button onClick={() => setCreateOpen(true)}>ایجاد اولین رکورد</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const pageContent = config.bundleFeature ? (
|
||||
@ -249,24 +519,14 @@ export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
return (
|
||||
<>
|
||||
{pageContent}
|
||||
{config.api.create && createFields.length > 0 ? (
|
||||
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} title={`${config.title} — جدید`}>
|
||||
{!config.readOnly && (config.api.create || config.api.upsert) && createFields.length > 0 ? (
|
||||
<Dialog
|
||||
open={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
title={`${config.title} — ${config.useUpsert ? "ذخیره" : "جدید"}`}
|
||||
>
|
||||
<form className="space-y-3" onSubmit={createForm.handleSubmit((d) => createM.mutate(d))}>
|
||||
{createFields.map((f) => (
|
||||
<FormField key={f.name} label={f.label}>
|
||||
{f.type === "select" && f.options ? (
|
||||
<Select {...createForm.register(f.name)}>
|
||||
{f.options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Input type={f.type ?? "text"} {...createForm.register(f.name)} />
|
||||
)}
|
||||
</FormField>
|
||||
))}
|
||||
{createFields.map((f) => renderField(f, createForm.register, "create", venues))}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
|
||||
انصراف
|
||||
@ -278,24 +538,37 @@ export function createHospitalityListPage(config: HospitalityListConfig) {
|
||||
</form>
|
||||
</Dialog>
|
||||
) : null}
|
||||
{editRow && config.api.update ? (
|
||||
<Dialog open={!!editRow} onClose={() => setEditRow(null)} title={`${config.title} — ویرایش`}>
|
||||
{editRow && config.api.update && !config.readOnly ? (
|
||||
<Drawer open={!!editRow} onClose={() => setEditRow(null)} title={`${config.title} — ویرایش`}>
|
||||
<form className="space-y-3" onSubmit={editForm.handleSubmit((d) => updateM.mutate(d))}>
|
||||
{editFields.map((f) => (
|
||||
<FormField key={f.name} label={f.label}>
|
||||
<Input {...editForm.register(f.name)} />
|
||||
</FormField>
|
||||
))}
|
||||
{editFields.map((f) => renderField(f, editForm.register, "edit", venues))}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setEditRow(null)}>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateM.isPending}>
|
||||
ذخیره
|
||||
بهروزرسانی
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</Drawer>
|
||||
) : null}
|
||||
{detailRow ? (
|
||||
<Drawer open={!!detailRow} onClose={() => setDetailRow(null)} title="جزئیات رکورد">
|
||||
<div className="space-y-3 text-sm">
|
||||
{config.columns.map((c) => (
|
||||
<div key={c.key} className="flex justify-between gap-4 border-b border-[var(--border)] pb-2">
|
||||
<span className="text-[var(--muted)]">{c.header}</span>
|
||||
<span className="font-medium">{renderCell(c, detailRow)}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-4 rounded-xl bg-[var(--surface-muted)] p-3 text-xs text-[var(--muted)]">
|
||||
<p>ایجاد: {detailRow.created_at ? new Date(String(detailRow.created_at)).toLocaleString("fa-IR") : "—"}</p>
|
||||
<p>بهروزرسانی: {detailRow.updated_at ? new Date(String(detailRow.updated_at)).toLocaleString("fa-IR") : "—"}</p>
|
||||
<p>ایجادکننده: {String(detailRow.created_by ?? "—")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
) : null}
|
||||
<ConfirmDialog
|
||||
open={!!deleteId}
|
||||
|
||||
55
frontend/modules/hospitality/constants/fieldHelpers.ts
Normal file
55
frontend/modules/hospitality/constants/fieldHelpers.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/** Shared field/column builders for Hospitality resources. */
|
||||
import type { HospitalityColumnConfig, HospitalityFieldConfig } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { lifecycleStatusLabels, venueFormatLabels } from "@/modules/hospitality/design-system/tokens";
|
||||
|
||||
export const lifecycleStatusOptions = Object.entries(lifecycleStatusLabels).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
export const venueFormatOptions = Object.entries(venueFormatLabels).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
export function auditColumns(): HospitalityColumnConfig[] {
|
||||
return [
|
||||
{ key: "created_at", header: "ایجاد", type: "datetime", defaultVisible: false },
|
||||
{ key: "updated_at", header: "بهروزرسانی", type: "datetime", defaultVisible: false },
|
||||
];
|
||||
}
|
||||
|
||||
export function codeNameStatusColumns(extra: HospitalityColumnConfig[] = []): HospitalityColumnConfig[] {
|
||||
return [
|
||||
{ key: "code", header: "کد" },
|
||||
{ key: "name", header: "نام" },
|
||||
...extra,
|
||||
{ key: "status", header: "وضعیت", type: "status" },
|
||||
...auditColumns(),
|
||||
];
|
||||
}
|
||||
|
||||
export function codeNameCreateFields(extra: HospitalityFieldConfig[] = []): HospitalityFieldConfig[] {
|
||||
return [
|
||||
{ name: "code", label: "کد", required: true },
|
||||
{ name: "name", label: "نام", required: true },
|
||||
...extra,
|
||||
];
|
||||
}
|
||||
|
||||
export function venueIdField(required = true): HospitalityFieldConfig {
|
||||
return { name: "venue_id", label: "مکان (Venue ID)", type: "venue", required };
|
||||
}
|
||||
|
||||
export function formatPrice(row: Record<string, unknown>, key = "base_price"): string {
|
||||
const amount = Number(row[key] ?? 0);
|
||||
const currency = String(row.currency_code ?? "IRR");
|
||||
return `${amount.toLocaleString("fa-IR")} ${currency}`;
|
||||
}
|
||||
|
||||
export function countByStatus(rows: Record<string, unknown>[], statusKey = "status") {
|
||||
const total = rows.length;
|
||||
const active = rows.filter((r) => r[statusKey] === "active" || r[statusKey] === "published").length;
|
||||
const draft = rows.filter((r) => r[statusKey] === "draft").length;
|
||||
return { total, active, draft };
|
||||
}
|
||||
1300
frontend/modules/hospitality/constants/resourceRegistry.ts
Normal file
1300
frontend/modules/hospitality/constants/resourceRegistry.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { PageHeader, DataTable, EmptyState, Input } from "@/components/ds";
|
||||
import { PageHeader, DataTable, EmptyState, Input, Pagination, Skeleton } from "@/components/ds";
|
||||
import { Search } from "lucide-react";
|
||||
import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs";
|
||||
|
||||
@ -11,6 +11,8 @@ export type HospitalityTableColumn = {
|
||||
className?: string;
|
||||
render?: (row: Record<string, unknown>) => React.ReactNode;
|
||||
searchable?: boolean;
|
||||
sortable?: boolean;
|
||||
defaultVisible?: boolean;
|
||||
};
|
||||
|
||||
export function HospitalityTablePage({
|
||||
@ -24,6 +26,13 @@ export function HospitalityTablePage({
|
||||
searchPlaceholder = "جستجو…",
|
||||
filterFn,
|
||||
toolbar,
|
||||
page,
|
||||
pageSize,
|
||||
onPageChange,
|
||||
selectedIds,
|
||||
onSelectionChange,
|
||||
bulkActions,
|
||||
loading,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
@ -35,28 +44,87 @@ export function HospitalityTablePage({
|
||||
searchPlaceholder?: string;
|
||||
filterFn?: (row: Record<string, unknown>, query: string) => boolean;
|
||||
toolbar?: React.ReactNode;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
selectedIds?: Set<string>;
|
||||
onSelectionChange?: (ids: Set<string>) => void;
|
||||
bulkActions?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [sortKey, setSortKey] = useState<string | null>(null);
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||
const [hiddenCols, setHiddenCols] = useState<Set<string>>(() => {
|
||||
const hidden = new Set<string>();
|
||||
columns.forEach((c) => {
|
||||
if (c.defaultVisible === false) hidden.add(c.key);
|
||||
});
|
||||
return hidden;
|
||||
});
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() => columns.filter((c) => !hiddenCols.has(c.key)),
|
||||
[columns, hiddenCols]
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let data = rows;
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return rows;
|
||||
if (filterFn) return rows.filter((r) => filterFn(r, q));
|
||||
return rows.filter((r) =>
|
||||
columns.some((c) => {
|
||||
if (c.searchable === false) return false;
|
||||
const val = r[c.key];
|
||||
return val != null && String(val).toLowerCase().includes(q);
|
||||
})
|
||||
if (q) {
|
||||
data = filterFn
|
||||
? data.filter((r) => filterFn(r, q))
|
||||
: data.filter((r) =>
|
||||
visibleColumns.some((c) => {
|
||||
if (c.searchable === false) return false;
|
||||
const val = r[c.key];
|
||||
return val != null && String(val).toLowerCase().includes(q);
|
||||
})
|
||||
);
|
||||
}
|
||||
if (sortKey) {
|
||||
data = [...data].sort((a, b) => {
|
||||
const av = a[sortKey];
|
||||
const bv = b[sortKey];
|
||||
const cmp = String(av ?? "").localeCompare(String(bv ?? ""), "fa");
|
||||
return sortDir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}, [rows, query, visibleColumns, filterFn, sortKey, sortDir]);
|
||||
|
||||
const paged = useMemo(() => {
|
||||
if (!pageSize || !page) return filtered;
|
||||
const start = (page - 1) * pageSize;
|
||||
return filtered.slice(start, start + pageSize);
|
||||
}, [filtered, page, pageSize]);
|
||||
|
||||
const displayRows = pageSize && page ? paged : filtered;
|
||||
|
||||
const toggleSort = (key: string) => {
|
||||
if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
else {
|
||||
setSortKey(key);
|
||||
setSortDir("asc");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-64" />
|
||||
<Skeleton className="h-10 w-full max-w-md" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
);
|
||||
}, [rows, query, columns, filterFn]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{breadcrumb ? <HospitalityBreadcrumbs current={breadcrumb} /> : null}
|
||||
<PageHeader title={title} description={description} actions={actions} />
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<div className="relative min-w-[200px] max-w-md flex-1">
|
||||
<Search className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--muted)]" />
|
||||
<Input
|
||||
value={query}
|
||||
@ -64,15 +132,123 @@ export function HospitalityTablePage({
|
||||
placeholder={searchPlaceholder}
|
||||
className="pr-10"
|
||||
aria-label="جستجو"
|
||||
data-hospitality-search
|
||||
/>
|
||||
</div>
|
||||
{toolbar}
|
||||
<select
|
||||
className="rounded-lg border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm"
|
||||
value={sortKey ?? ""}
|
||||
onChange={(e) => {
|
||||
const key = e.target.value;
|
||||
if (!key) setSortKey(null);
|
||||
else toggleSort(key);
|
||||
}}
|
||||
aria-label="مرتبسازی"
|
||||
>
|
||||
<option value="">مرتبسازی</option>
|
||||
{visibleColumns
|
||||
.filter((c) => c.sortable !== false && c.key !== "actions")
|
||||
.map((c) => (
|
||||
<option key={c.key} value={c.key}>
|
||||
{c.header}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<details className="relative">
|
||||
<summary className="cursor-pointer list-none rounded-lg border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm">
|
||||
ستونها
|
||||
</summary>
|
||||
<div className="absolute left-0 z-20 mt-1 min-w-[180px] rounded-xl border border-[var(--border)] bg-[var(--surface)] p-2 shadow-lg">
|
||||
{columns
|
||||
.filter((c) => c.key !== "actions")
|
||||
.map((c) => (
|
||||
<label key={c.key} className="flex cursor-pointer items-center gap-2 px-2 py-1 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!hiddenCols.has(c.key)}
|
||||
onChange={(e) => {
|
||||
setHiddenCols((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (e.target.checked) next.delete(c.key);
|
||||
else next.add(c.key);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{c.header}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{selectedIds && selectedIds.size > 0 && bulkActions ? (
|
||||
<div className="mb-3 flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--hospitality-accent-soft)] px-4 py-2 text-sm">
|
||||
<span>{selectedIds.size} مورد انتخاب شده</span>
|
||||
{bulkActions}
|
||||
</div>
|
||||
) : null}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
rows={filtered}
|
||||
empty={empty ?? <EmptyState title="موردی یافت نشد" />}
|
||||
columns={
|
||||
onSelectionChange
|
||||
? [
|
||||
{
|
||||
key: "_select",
|
||||
header: "",
|
||||
render: (row) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds?.has(String(row.id)) ?? false}
|
||||
onChange={(e) => {
|
||||
const id = String(row.id);
|
||||
const next = new Set(selectedIds);
|
||||
if (e.target.checked) next.add(id);
|
||||
else next.delete(id);
|
||||
onSelectionChange(next);
|
||||
}}
|
||||
aria-label="انتخاب"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...visibleColumns,
|
||||
]
|
||||
: visibleColumns
|
||||
}
|
||||
rows={displayRows}
|
||||
empty={empty ?? <EmptyState title="موردی یافت نشد" description="رکورد جدید ایجاد کنید." />}
|
||||
/>
|
||||
{pageSize && page && onPageChange ? (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
total={filtered.length}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HospitalityStatGrid({
|
||||
stats,
|
||||
}: {
|
||||
stats: { label: string; value: string | number; hint?: string }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{stats.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 shadow-[var(--shadow-sm)] transition-shadow hover:shadow-[var(--shadow-md)]"
|
||||
>
|
||||
<p className="text-sm text-[var(--muted)]">{s.label}</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-secondary">{s.value}</p>
|
||||
{s.hint ? <p className="mt-1 text-xs text-[var(--muted)]">{s.hint}</p> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ReportsPage = createHospitalityListPage({
|
||||
title: "گزارشها",
|
||||
breadcrumb: "گزارشها",
|
||||
resourceKey: "analyticsReports",
|
||||
bundleFeature: "analytics",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.analyticsReports,
|
||||
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 const ReportsPage = createHospitalityListPage(hospitalityResources.analyticsReports);
|
||||
|
||||
export default ReportsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const AnalyticsPage = createHospitalityListPage({
|
||||
title: "آنالیتیکس",
|
||||
breadcrumb: "آنالیتیکس",
|
||||
resourceKey: "analyticsSnapshots",
|
||||
bundleFeature: "analytics",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.analyticsSnapshots,
|
||||
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 const AnalyticsPage = createHospitalityListPage(hospitalityResources.analyticsSnapshots);
|
||||
|
||||
export default AnalyticsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const AvailabilityPage = createHospitalityListPage({
|
||||
title: "قوانین دسترسی",
|
||||
breadcrumb: "قوانین دسترسی",
|
||||
resourceKey: "availabilityWindows",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.availabilityWindows,
|
||||
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 const AvailabilityPage = createHospitalityListPage(hospitalityResources.availabilityWindows);
|
||||
|
||||
export default AvailabilityPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const BranchListPage = createHospitalityListPage({
|
||||
title: "شعب",
|
||||
breadcrumb: "شعب",
|
||||
resourceKey: "branches",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.branches,
|
||||
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 const BranchListPage = createHospitalityListPage(hospitalityResources.branches);
|
||||
|
||||
export default BranchListPage;
|
||||
|
||||
@ -2,25 +2,51 @@
|
||||
|
||||
import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities";
|
||||
import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared";
|
||||
import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
|
||||
import { PageHeader, Card, CardContent, Badge, Input } from "@/components/ds";
|
||||
import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export const CapabilitiesPage = function HospitalityCapabilitiesPage() {
|
||||
const q = useHospitalityCapabilities();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const entries = useMemo(() => {
|
||||
const features = q.data?.features ?? {};
|
||||
return Object.entries(features).filter(([k]) => k.toLowerCase().includes(search.toLowerCase()));
|
||||
}, [q.data?.features, search]);
|
||||
|
||||
if (q.isLoading) return <HospitalityPageLoader />;
|
||||
if (q.error) return <HospitalityPageError error={q.error} onRetry={() => q.refetch()} />;
|
||||
const features = q.data?.features ?? {};
|
||||
|
||||
const enabled = entries.filter(([, v]) => v).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="قابلیتها" description="Feature flags و bundles." />
|
||||
<div className="space-y-6">
|
||||
<HospitalityBreadcrumbs current="قابلیتها" />
|
||||
<PageHeader
|
||||
title="قابلیتها"
|
||||
description={`${enabled} از ${entries.length} قابلیت فعال — Bundle gating و feature flags.`}
|
||||
actions={<Badge tone="info">v{q.data?.version}</Badge>}
|
||||
/>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="جستجوی قابلیت…"
|
||||
aria-label="جستجو"
|
||||
className="max-w-md"
|
||||
/>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Object.entries(features).map(([k, v]) => (
|
||||
<Card key={k}><CardContent className="flex items-center justify-between p-3">
|
||||
<span className="text-sm font-mono">{k}</span>
|
||||
<Badge tone={v ? "success" : "default"}>{v ? "فعال" : "غیرفعال"}</Badge>
|
||||
</CardContent></Card>
|
||||
{entries.map(([k, v]) => (
|
||||
<Card key={k}>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<span className="text-sm font-mono">{k}</span>
|
||||
<Badge tone={v ? "success" : "default"}>{v ? "فعال" : "غیرفعال"}</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapabilitiesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CheckoutPage = createHospitalityListPage({
|
||||
title: "تسویه",
|
||||
breadcrumb: "تسویه",
|
||||
resourceKey: "cartLines",
|
||||
bundleFeature: "qr_ordering",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.cartLines,
|
||||
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 const CheckoutPage = createHospitalityListPage(hospitalityResources.cartLines);
|
||||
|
||||
export default CheckoutPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CartPage = createHospitalityListPage({
|
||||
title: "سبد خرید",
|
||||
breadcrumb: "سبد خرید",
|
||||
resourceKey: "carts",
|
||||
bundleFeature: "qr_ordering",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.carts,
|
||||
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 const CartPage = createHospitalityListPage(hospitalityResources.carts);
|
||||
|
||||
export default CartPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CommunicationIntegrationPage = createHospitalityListPage({
|
||||
title: "ارتباطات",
|
||||
breadcrumb: "ارتباطات",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "communication_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const CommunicationIntegrationPage = createHospitalityListPage(hospitalityResources.connectorCommunication);
|
||||
|
||||
export default CommunicationIntegrationPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CrmIntegrationPage = createHospitalityListPage({
|
||||
title: "CRM",
|
||||
breadcrumb: "CRM",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "crm_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const CrmIntegrationPage = createHospitalityListPage(hospitalityResources.connectorCrm);
|
||||
|
||||
export default CrmIntegrationPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const DeliveryIntegrationPage = createHospitalityListPage({
|
||||
title: "پیک",
|
||||
breadcrumb: "پیک",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "delivery_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const DeliveryIntegrationPage = createHospitalityListPage(hospitalityResources.connectorDelivery);
|
||||
|
||||
export default DeliveryIntegrationPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const MarketplaceIntegrationPage = createHospitalityListPage({
|
||||
title: "مارکتپلیس",
|
||||
breadcrumb: "مارکتپلیس",
|
||||
resourceKey: "connectorDispatches",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorDispatches,
|
||||
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 const MarketplaceIntegrationPage = createHospitalityListPage(hospitalityResources.connectorDispatches);
|
||||
|
||||
export default MarketplaceIntegrationPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const LoyaltyIntegrationPage = createHospitalityListPage({
|
||||
title: "وفاداری",
|
||||
breadcrumb: "وفاداری",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "loyalty_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const LoyaltyIntegrationPage = createHospitalityListPage(hospitalityResources.connectorLoyalty);
|
||||
|
||||
export default LoyaltyIntegrationPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CustomersPage = createHospitalityListPage({
|
||||
title: "مشتریان",
|
||||
breadcrumb: "مشتریان",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "crm_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const CustomersPage = createHospitalityListPage(hospitalityResources.connectorRegistrationsCrm);
|
||||
|
||||
export default CustomersPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const WebsiteIntegrationPage = createHospitalityListPage({
|
||||
title: "وبسایت",
|
||||
breadcrumb: "وبسایت",
|
||||
resourceKey: "connectorRegistrations",
|
||||
bundleFeature: "website_connector",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.connectorRegistrations,
|
||||
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 const WebsiteIntegrationPage = createHospitalityListPage(hospitalityResources.connectorWebsite);
|
||||
|
||||
export default WebsiteIntegrationPage;
|
||||
|
||||
@ -8,6 +8,7 @@ import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospi
|
||||
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";
|
||||
import { HospitalityStatGrid } from "@/modules/hospitality/design-system";
|
||||
|
||||
export const ExecutiveDashboard = function HospitalityDashboard() {
|
||||
const { tenantId } = useTenantId();
|
||||
@ -16,20 +17,25 @@ export const ExecutiveDashboard = function HospitalityDashboard() {
|
||||
const countsQ = useQuery({
|
||||
queryKey: ["hospitality", tenantId, "dashboard-counts"],
|
||||
queryFn: async () => {
|
||||
const [venues, reservations, tickets, kitchen] = await Promise.all([
|
||||
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.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,
|
||||
};
|
||||
},
|
||||
enabled: !!tenantId,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
if (!tenantId || countsQ.isLoading || caps.isLoading) return <HospitalityPageLoader />;
|
||||
@ -42,21 +48,37 @@ export const ExecutiveDashboard = function HospitalityDashboard() {
|
||||
<HospitalityBreadcrumbs current="داشبورد" />
|
||||
<PageHeader
|
||||
title="داشبورد مدیریتی"
|
||||
description="نمای اجرایی فروش، سفارشها و عملیات روزانه."
|
||||
actions={<Badge tone="success">Torbat Food v{caps.data?.version}</Badge>}
|
||||
description="نمای اجرایی فروش، سفارشها و عملیات روزانه — داده زنده از API."
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Badge tone="success">Torbat Food v{caps.data?.version}</Badge>
|
||||
<Button variant="outline" size="sm" onClick={() => countsQ.refetch()}>
|
||||
بروزرسانی
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<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" },
|
||||
]}
|
||||
/>
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<StatCard label="مکانها" value={String(c.venues)} hint="Venues" />
|
||||
<StatCard label="رزرو امروز" value={String(c.reservations)} hint="Reservations" />
|
||||
<StatCard label="سفارش باز" value={String(c.openOrders)} hint="Open tickets" />
|
||||
<StatCard label="آشپزخانه فعال" value={String(c.kitchenActive)} hint="Kitchen queue" />
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="flex flex-wrap gap-2 p-4">
|
||||
<Link href="/hospitality/reservations"><Button variant="outline">رزروها</Button></Link>
|
||||
<Link href="/hospitality/kitchen/display"><Button variant="outline">آشپزخانه</Button></Link>
|
||||
<Link href="/hospitality/pos/lite"><Button variant="outline">POS</Button></Link>
|
||||
<Link href="/hospitality/analytics"><Button variant="outline">آنالیتیکس</Button></Link>
|
||||
<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/reservations"><Button variant="outline">رزروها</Button></Link>
|
||||
<Link href="/hospitality/kitchen/display"><Button variant="outline">آشپزخانه</Button></Link>
|
||||
<Link href="/hospitality/pos/lite"><Button variant="outline">POS Lite</Button></Link>
|
||||
<Link href="/hospitality/menu"><Button variant="outline">منو</Button></Link>
|
||||
<Link href="/hospitality/analytics"><Button variant="outline">آنالیتیکس</Button></Link>
|
||||
<Link href="/hospitality/orders/online"><Button variant="outline">سفارش آنلاین</Button></Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const DiningAreasPage = createHospitalityListPage({
|
||||
title: "سالنها",
|
||||
breadcrumb: "سالنها",
|
||||
resourceKey: "diningAreas",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.diningAreas,
|
||||
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 const DiningAreasPage = createHospitalityListPage(hospitalityResources.diningAreas);
|
||||
|
||||
export default DiningAreasPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const AuditLogsPage = createHospitalityListPage({
|
||||
title: "لاگ ممیزی",
|
||||
breadcrumb: "لاگ ممیزی",
|
||||
resourceKey: "events",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.events,
|
||||
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 const AuditLogsPage = createHospitalityListPage(hospitalityResources.events);
|
||||
|
||||
export default AuditLogsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PromotionsPage = createHospitalityListPage({
|
||||
title: "پروموشنها",
|
||||
breadcrumb: "پروموشنها",
|
||||
resourceKey: "featureToggles",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.featureToggles,
|
||||
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 const PromotionsPage = createHospitalityListPage(hospitalityResources.featureTogglesPromo);
|
||||
|
||||
export default PromotionsPage;
|
||||
|
||||
@ -2,21 +2,48 @@
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities";
|
||||
import { HospitalityPageLoader, HospitalityPageError } from "@/modules/hospitality/pages/shared";
|
||||
import { PageHeader, Card, CardContent, Badge } from "@/components/ds";
|
||||
import { PageHeader, Card, CardContent, Badge, Button } from "@/components/ds";
|
||||
import { HospitalityBreadcrumbs } from "@/modules/hospitality/components/HospitalityBreadcrumbs";
|
||||
|
||||
export const HealthPage = function HospitalityHealthPage() {
|
||||
const q = useQuery({ queryKey: ["hospitality", "health"], queryFn: () => hospitalityApi.health() });
|
||||
if (q.isLoading) return <HospitalityPageLoader />;
|
||||
if (q.error) return <HospitalityPageError error={q.error} onRetry={() => q.refetch()} />;
|
||||
const healthQ = useQuery({
|
||||
queryKey: ["hospitality", "health"],
|
||||
queryFn: () => hospitalityApi.health(),
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
const capsQ = useHospitalityCapabilities();
|
||||
|
||||
if (healthQ.isLoading || capsQ.isLoading) return <HospitalityPageLoader />;
|
||||
if (healthQ.error) return <HospitalityPageError error={healthQ.error} onRetry={() => healthQ.refetch()} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="سلامت سرویس" description="وضعیت سرویس Hospitality." />
|
||||
<Card><CardContent className="space-y-2 p-4">
|
||||
<Badge tone={q.data?.status === "ok" ? "success" : "danger"}>{q.data?.status}</Badge>
|
||||
<p className="text-sm">{q.data?.service} — v{q.data?.version}</p>
|
||||
</CardContent></Card>
|
||||
<div className="space-y-6">
|
||||
<HospitalityBreadcrumbs current="سلامت سرویس" />
|
||||
<PageHeader
|
||||
title="سلامت سرویس"
|
||||
description="وضعیت زنده سرویس Hospitality و اتصال BFF."
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => healthQ.refetch()}>
|
||||
بررسی مجدد
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge tone={healthQ.data?.status === "ok" ? "success" : "danger"}>{healthQ.data?.status}</Badge>
|
||||
<span className="text-sm">{healthQ.data?.service}</span>
|
||||
<span className="text-sm text-[var(--muted)]">v{healthQ.data?.version}</span>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
Capabilities API: {capsQ.data ? "متصل" : "—"} — {Object.keys(capsQ.data?.features ?? {}).length} feature flag
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HealthPage;
|
||||
|
||||
@ -1,31 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMe } from "@/hooks/useMe";
|
||||
import { useHospitalityCapabilities } from "@/modules/hospitality/hooks/useHospitalityCapabilities";
|
||||
import { HOSPITALITY_PORTAL } from "@/modules/hospitality/constants/portals";
|
||||
import { Card, CardContent, Button, LoadingState, ErrorState } from "@/components/ds";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { useTenantId } from "@/hooks/useTenantId";
|
||||
import { Card, CardContent, Button, LoadingState, ErrorState, Badge, StatCard } from "@/components/ds";
|
||||
|
||||
export const HospitalityHub = function HospitalityHubPage() {
|
||||
const { me, loading, error, reload } = useMe();
|
||||
if (loading) return <LoadingState label="در حال بارگذاری تربت فود…" />;
|
||||
const { tenantId } = useTenantId();
|
||||
const caps = useHospitalityCapabilities();
|
||||
|
||||
const healthQ = useQuery({
|
||||
queryKey: ["hospitality", "health"],
|
||||
queryFn: () => hospitalityApi.health(),
|
||||
});
|
||||
|
||||
if (loading || caps.isLoading) return <LoadingState label="در حال بارگذاری تربت فود…" />;
|
||||
if (error) return <ErrorState message={error} onRetry={reload} />;
|
||||
if (!me?.current_tenant_id) {
|
||||
return <ErrorState message="برای استفاده از تربت فود workspace فعال لازم است." />;
|
||||
}
|
||||
|
||||
const activeBundles = Object.entries(caps.data?.features ?? {}).filter(([, v]) => v).length;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-lg p-6">
|
||||
<h1 className="mb-2 text-2xl font-bold text-secondary">تربت فود</h1>
|
||||
<p className="mb-6 text-sm text-[var(--muted)]">پلتفرم مهماننوازی سازمانی</p>
|
||||
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="mb-2 text-2xl font-bold text-secondary">تربت فود</h1>
|
||||
<p className="text-sm text-[var(--muted)]">پلتفرم مهماننوازی سازمانی — Torbat Food</p>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<StatCard label="وضعیت API" value={healthQ.data?.status === "ok" ? "سالم" : "—"} />
|
||||
<StatCard label="نسخه" value={caps.data?.version ?? "—"} />
|
||||
<StatCard label="بسته فعال" value={String(activeBundles)} />
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="space-y-4 p-6">
|
||||
<p className="font-medium">{HOSPITALITY_PORTAL.label}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-medium">{HOSPITALITY_PORTAL.label}</p>
|
||||
<Badge tone="primary">Tenant {tenantId?.slice(0, 8)}…</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--muted)]">{HOSPITALITY_PORTAL.description}</p>
|
||||
<Link href={HOSPITALITY_PORTAL.basePath}>
|
||||
<Button className="w-full">ورود به پنل مدیریت</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<Link href="/hospitality/capabilities"><Button variant="outline" className="w-full">قابلیتها</Button></Link>
|
||||
<Link href="/hospitality/health"><Button variant="outline" className="w-full">سلامت سرویس</Button></Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const InventoryPage = createHospitalityListPage({
|
||||
title: "موجودی",
|
||||
breadcrumb: "موجودی",
|
||||
resourceKey: "analyticsSnapshots",
|
||||
bundleFeature: "analytics",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.analyticsSnapshots,
|
||||
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 const InventoryPage = createHospitalityListPage(hospitalityResources.inventoryOverview);
|
||||
|
||||
export default InventoryPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const InvoicesPage = createHospitalityListPage({
|
||||
title: "فاکتورها",
|
||||
breadcrumb: "فاکتورها",
|
||||
resourceKey: "posTickets",
|
||||
bundleFeature: "pos_lite",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posTickets,
|
||||
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 const InvoicesPage = createHospitalityListPage(hospitalityResources.invoices);
|
||||
|
||||
export default InvoicesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const KitchenQueuePage = createHospitalityListPage({
|
||||
title: "صف آشپزخانه",
|
||||
breadcrumb: "صف آشپزخانه",
|
||||
resourceKey: "kitchenTicketItems",
|
||||
bundleFeature: "kitchen",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.kitchenTicketItems,
|
||||
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 const KitchenQueuePage = createHospitalityListPage(hospitalityResources.kitchenTicketItems);
|
||||
|
||||
export default KitchenQueuePage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const KitchenDisplayPage = createHospitalityListPage({
|
||||
title: "نمایشگر آشپزخانه",
|
||||
breadcrumb: "نمایشگر آشپزخانه",
|
||||
resourceKey: "kitchenTickets",
|
||||
bundleFeature: "kitchen",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.kitchenTickets,
|
||||
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 const KitchenDisplayPage = createHospitalityListPage(hospitalityResources.kitchenTickets);
|
||||
|
||||
export default KitchenDisplayPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const KitchenPrepPage = createHospitalityListPage({
|
||||
title: "وضعیت آمادهسازی",
|
||||
breadcrumb: "وضعیت آمادهسازی",
|
||||
resourceKey: "kitchenTickets",
|
||||
bundleFeature: "kitchen",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.kitchenTickets,
|
||||
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 const KitchenPrepPage = createHospitalityListPage(hospitalityResources.kitchenTicketsPrep);
|
||||
|
||||
export default KitchenPrepPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const MenuCategoriesPage = createHospitalityListPage({
|
||||
title: "دستهبندی منو",
|
||||
breadcrumb: "دستهبندی منو",
|
||||
resourceKey: "menuCategories",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.menuCategories,
|
||||
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 const MenuCategoriesPage = createHospitalityListPage(hospitalityResources.menuCategories);
|
||||
|
||||
export default MenuCategoriesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const MenuItemsPage = createHospitalityListPage({
|
||||
title: "آیتمهای منو",
|
||||
breadcrumb: "آیتمهای منو",
|
||||
resourceKey: "menuItems",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.menuItems,
|
||||
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 const MenuItemsPage = createHospitalityListPage(hospitalityResources.menuItems);
|
||||
|
||||
export default MenuItemsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const MediaLibraryPage = createHospitalityListPage({
|
||||
title: "کتابخانه رسانه",
|
||||
breadcrumb: "کتابخانه رسانه",
|
||||
resourceKey: "menuMediaRefs",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.menuMediaRefs,
|
||||
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 const MediaLibraryPage = createHospitalityListPage(hospitalityResources.menuMediaRefs);
|
||||
|
||||
export default MediaLibraryPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const DigitalMenuPage = createHospitalityListPage({
|
||||
title: "منوی دیجیتال",
|
||||
breadcrumb: "منوی دیجیتال",
|
||||
resourceKey: "menus",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.menus,
|
||||
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 const DigitalMenuPage = createHospitalityListPage(hospitalityResources.menus);
|
||||
|
||||
export default DigitalMenuPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ModifierGroupsPage = createHospitalityListPage({
|
||||
title: "گروه افزودنی",
|
||||
breadcrumb: "گروه افزودنی",
|
||||
resourceKey: "modifierGroups",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.modifierGroups,
|
||||
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 const ModifierGroupsPage = createHospitalityListPage(hospitalityResources.modifierGroups);
|
||||
|
||||
export default ModifierGroupsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ModifierOptionsPage = createHospitalityListPage({
|
||||
title: "گزینه افزودنی",
|
||||
breadcrumb: "گزینه افزودنی",
|
||||
resourceKey: "modifierOptions",
|
||||
bundleFeature: "digital_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.modifierOptions,
|
||||
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 const ModifierOptionsPage = createHospitalityListPage(hospitalityResources.modifierOptions);
|
||||
|
||||
export default ModifierOptionsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const NotificationsPage = createHospitalityListPage({
|
||||
title: "اعلانها",
|
||||
breadcrumb: "اعلانها",
|
||||
resourceKey: "events",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.events,
|
||||
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 const NotificationsPage = createHospitalityListPage(hospitalityResources.notifications);
|
||||
|
||||
export default NotificationsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const OnlineOrdersPage = createHospitalityListPage({
|
||||
title: "سفارش آنلاین",
|
||||
breadcrumb: "سفارش آنلاین",
|
||||
resourceKey: "qrOrderingSessions",
|
||||
bundleFeature: "qr_ordering",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.qrOrderingSessions,
|
||||
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 const OnlineOrdersPage = createHospitalityListPage(hospitalityResources.onlineOrders);
|
||||
|
||||
export default OnlineOrdersPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const OrderTimelinePage = createHospitalityListPage({
|
||||
title: "خط زمانی سفارش",
|
||||
breadcrumb: "خط زمانی سفارش",
|
||||
resourceKey: "kitchenTickets",
|
||||
bundleFeature: "kitchen",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.kitchenTickets,
|
||||
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 const OrderTimelinePage = createHospitalityListPage(hospitalityResources.orderTimeline);
|
||||
|
||||
export default OrderTimelinePage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PermissionsPage = createHospitalityListPage({
|
||||
title: "مجوزها",
|
||||
breadcrumb: "مجوزها",
|
||||
resourceKey: "permissions",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.permissions,
|
||||
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 const PermissionsPage = createHospitalityListPage(hospitalityResources.permissions);
|
||||
|
||||
export default PermissionsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PickupOrdersPage = createHospitalityListPage({
|
||||
title: "سفارش بیرونبر",
|
||||
breadcrumb: "سفارش بیرونبر",
|
||||
resourceKey: "qrOrderingSessions",
|
||||
bundleFeature: "qr_ordering",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.qrOrderingSessions,
|
||||
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 const PickupOrdersPage = createHospitalityListPage(hospitalityResources.pickupOrders);
|
||||
|
||||
export default PickupOrdersPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const DiscountsPage = createHospitalityListPage({
|
||||
title: "تخفیفها",
|
||||
breadcrumb: "تخفیفها",
|
||||
resourceKey: "posDiscounts",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posDiscounts,
|
||||
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 const DiscountsPage = createHospitalityListPage(hospitalityResources.posDiscounts);
|
||||
|
||||
export default DiscountsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CouponsPage = createHospitalityListPage({
|
||||
title: "کوپنها",
|
||||
breadcrumb: "کوپنها",
|
||||
resourceKey: "posDiscounts",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posDiscounts,
|
||||
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 const CouponsPage = createHospitalityListPage(hospitalityResources.posDiscountsCoupons);
|
||||
|
||||
export default CouponsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const FloorMapPage = createHospitalityListPage({
|
||||
title: "نقشه سالن",
|
||||
breadcrumb: "نقشه سالن",
|
||||
resourceKey: "posFloorPlans",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posFloorPlans,
|
||||
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 const FloorMapPage = createHospitalityListPage(hospitalityResources.posFloorPlans);
|
||||
|
||||
export default FloorMapPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PosProPage = createHospitalityListPage({
|
||||
title: "POS Pro",
|
||||
breadcrumb: "POS Pro",
|
||||
resourceKey: "posPayments",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posPayments,
|
||||
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 const PosProPage = createHospitalityListPage(hospitalityResources.posPayments);
|
||||
|
||||
export default PosProPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PaymentsPage = createHospitalityListPage({
|
||||
title: "پرداختها",
|
||||
breadcrumb: "پرداختها",
|
||||
resourceKey: "posPayments",
|
||||
bundleFeature: "pos_pro",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posPayments,
|
||||
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 const PaymentsPage = createHospitalityListPage(hospitalityResources.posPaymentsList);
|
||||
|
||||
export default PaymentsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const CashRegisterPage = createHospitalityListPage({
|
||||
title: "صندوق",
|
||||
breadcrumb: "صندوق",
|
||||
resourceKey: "posRegisters",
|
||||
bundleFeature: "pos_lite",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posRegisters,
|
||||
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 const CashRegisterPage = createHospitalityListPage(hospitalityResources.posRegisters);
|
||||
|
||||
export default CashRegisterPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const PosLitePage = createHospitalityListPage({
|
||||
title: "POS Lite",
|
||||
breadcrumb: "POS Lite",
|
||||
resourceKey: "posTickets",
|
||||
bundleFeature: "pos_lite",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posTickets,
|
||||
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 const PosLitePage = createHospitalityListPage(hospitalityResources.posTickets);
|
||||
|
||||
export default PosLitePage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const QrMenuPage = createHospitalityListPage({
|
||||
title: "منوی QR",
|
||||
breadcrumb: "منوی QR",
|
||||
resourceKey: "qrCodes",
|
||||
bundleFeature: "qr_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.qrCodes,
|
||||
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 const QrMenuPage = createHospitalityListPage(hospitalityResources.qrCodes);
|
||||
|
||||
export default QrMenuPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const GuestsPage = createHospitalityListPage({
|
||||
title: "پروفایل مهمان",
|
||||
breadcrumb: "پروفایل مهمان",
|
||||
resourceKey: "qrMenuSessions",
|
||||
bundleFeature: "qr_menu",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.qrMenuSessions,
|
||||
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 const GuestsPage = createHospitalityListPage(hospitalityResources.qrMenuSessions);
|
||||
|
||||
export default GuestsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const QrOrderingPage = createHospitalityListPage({
|
||||
title: "سفارش QR",
|
||||
breadcrumb: "سفارش QR",
|
||||
resourceKey: "qrOrderingSessions",
|
||||
bundleFeature: "qr_ordering",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.qrOrderingSessions,
|
||||
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 const QrOrderingPage = createHospitalityListPage(hospitalityResources.qrOrderingSessions);
|
||||
|
||||
export default QrOrderingPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ReceiptsPage = createHospitalityListPage({
|
||||
title: "رسیدها",
|
||||
breadcrumb: "رسیدها",
|
||||
resourceKey: "posTickets",
|
||||
bundleFeature: "pos_lite",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.posTickets,
|
||||
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 const ReceiptsPage = createHospitalityListPage(hospitalityResources.receipts);
|
||||
|
||||
export default ReceiptsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ReservationsPage = createHospitalityListPage({
|
||||
title: "رزروها",
|
||||
breadcrumb: "رزروها",
|
||||
resourceKey: "reservations",
|
||||
bundleFeature: "reservation",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.reservations,
|
||||
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 const ReservationsPage = createHospitalityListPage(hospitalityResources.reservations);
|
||||
|
||||
export default ReservationsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const RolesPage = createHospitalityListPage({
|
||||
title: "نقشها",
|
||||
breadcrumb: "نقشها",
|
||||
resourceKey: "roles",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.roles,
|
||||
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 const RolesPage = createHospitalityListPage(hospitalityResources.roles);
|
||||
|
||||
export default RolesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const SettingsPage = createHospitalityListPage({
|
||||
title: "تنظیمات",
|
||||
breadcrumb: "تنظیمات",
|
||||
resourceKey: "settings",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.settings,
|
||||
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 const SettingsPage = createHospitalityListPage(hospitalityResources.settings);
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const TablesPage = createHospitalityListPage({
|
||||
title: "میزها",
|
||||
breadcrumb: "میزها",
|
||||
resourceKey: "tables",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.tables,
|
||||
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 const TablesPage = createHospitalityListPage(hospitalityResources.tables);
|
||||
|
||||
export default TablesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const BundlesPage = createHospitalityListPage({
|
||||
title: "بستههای فعال",
|
||||
breadcrumb: "بستههای فعال",
|
||||
resourceKey: "tenantBundles",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.tenantBundles,
|
||||
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 const BundlesPage = createHospitalityListPage(hospitalityResources.tenantBundles);
|
||||
|
||||
export default BundlesPage;
|
||||
|
||||
@ -1,53 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { venueFormatLabels } from "@/modules/hospitality/design-system/tokens";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const BranchesPage = createHospitalityListPage({
|
||||
title: "مکانها (Venues)",
|
||||
description: "مدیریت مکانهای مهماننوازی — کافه، رستوران، فستفود و …",
|
||||
breadcrumb: "مکانها",
|
||||
resourceKey: "venues",
|
||||
bundleFeature: null,
|
||||
permission: "hospitality.venues.view",
|
||||
api: hospitalityApi.venues,
|
||||
columns: [
|
||||
{ key: "code", header: "کد" },
|
||||
{ key: "name", header: "نام" },
|
||||
{
|
||||
key: "venue_format",
|
||||
header: "نوع",
|
||||
render: (r) => venueFormatLabels[String(r.venue_format)] ?? String(r.venue_format),
|
||||
},
|
||||
{ key: "status", header: "وضعیت", type: "status" },
|
||||
{ key: "currency_code", header: "ارز" },
|
||||
],
|
||||
createFields: [
|
||||
{ name: "code", label: "کد", required: true },
|
||||
{ name: "name", label: "نام", required: true },
|
||||
{
|
||||
name: "venue_format",
|
||||
label: "نوع کسبوکار",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: Object.entries(venueFormatLabels).map(([value, label]) => ({ value, label })),
|
||||
},
|
||||
],
|
||||
editFields: [
|
||||
{ name: "name", label: "نام", required: true },
|
||||
{
|
||||
name: "status",
|
||||
label: "وضعیت",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "draft", label: "پیشنویس" },
|
||||
{ value: "active", label: "فعال" },
|
||||
{ value: "inactive", label: "غیرفعال" },
|
||||
],
|
||||
},
|
||||
],
|
||||
filterDeleted: true,
|
||||
});
|
||||
export const BranchesPage = createHospitalityListPage(hospitalityResources.venues);
|
||||
|
||||
export default BranchesPage;
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const QueuePage = createHospitalityListPage({
|
||||
title: "صف انتظار",
|
||||
breadcrumb: "صف انتظار",
|
||||
resourceKey: "waitlist",
|
||||
bundleFeature: "table_service",
|
||||
permission: "hospitality.view",
|
||||
api: hospitalityApi.waitlist,
|
||||
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 const QueuePage = createHospitalityListPage(hospitalityResources.waitlist);
|
||||
|
||||
export default QueuePage;
|
||||
|
||||
15
frontend/modules/hospitality/hooks/useHospitalityVenues.ts
Normal file
15
frontend/modules/hospitality/hooks/useHospitalityVenues.ts
Normal file
@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { hospitalityApi } from "@/modules/hospitality/services/hospitality-api";
|
||||
import { useTenantId } from "@/hooks/useTenantId";
|
||||
|
||||
export function useHospitalityVenues() {
|
||||
const { tenantId } = useTenantId();
|
||||
return useQuery({
|
||||
queryKey: ["hospitality", tenantId, "venues-options"],
|
||||
queryFn: () => hospitalityApi.venues.list(tenantId!, { page: 1, page_size: 500 }),
|
||||
enabled: !!tenantId,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
88
frontend/scripts/gen-hospitality-resources.mjs
Normal file
88
frontend/scripts/gen-hospitality-resources.mjs
Normal file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
/** Generates resourceRegistry.ts and updates hospitality feature pages. */
|
||||
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 FEATURES = path.join(MOD, "features");
|
||||
const REGISTRY = path.join(MOD, "constants", "resourceRegistry.ts");
|
||||
|
||||
const FEATURE_MAP = [
|
||||
["venues", "BranchesPage", "venues"],
|
||||
["branches", "BranchListPage", "branches"],
|
||||
["diningAreas", "DiningAreasPage", "diningAreas"],
|
||||
["tables", "TablesPage", "tables"],
|
||||
["posFloorPlans", "FloorMapPage", "posFloorPlans"],
|
||||
["reservations", "ReservationsPage", "reservations"],
|
||||
["waitlist", "QueuePage", "waitlist"],
|
||||
["connectorRegistrationsCrm", "CustomersPage", "connectorRegistrationsCrm"],
|
||||
["qrMenuSessions", "GuestsPage", "qrMenuSessions"],
|
||||
["menus", "DigitalMenuPage", "menus"],
|
||||
["menuCategories", "MenuCategoriesPage", "menuCategories"],
|
||||
["menuItems", "MenuItemsPage", "menuItems"],
|
||||
["modifierGroups", "ModifierGroupsPage", "modifierGroups"],
|
||||
["modifierOptions", "ModifierOptionsPage", "modifierOptions"],
|
||||
["availabilityWindows", "AvailabilityPage", "availabilityWindows"],
|
||||
["menuMediaRefs", "MediaLibraryPage", "menuMediaRefs"],
|
||||
["qrCodes", "QrMenuPage", "qrCodes"],
|
||||
["qrOrderingSessions", "QrOrderingPage", "qrOrderingSessions"],
|
||||
["carts", "CartPage", "carts"],
|
||||
["cartLines", "CheckoutPage", "cartLines"],
|
||||
["kitchenTickets", "KitchenDisplayPage", "kitchenTickets"],
|
||||
["kitchenTicketItems", "KitchenQueuePage", "kitchenTicketItems"],
|
||||
["kitchenTicketsPrep", "KitchenPrepPage", "kitchenTicketsPrep"],
|
||||
["posTickets", "PosLitePage", "posTickets"],
|
||||
["posPayments", "PosProPage", "posPayments"],
|
||||
["posRegisters", "CashRegisterPage", "posRegisters"],
|
||||
["posPaymentsList", "PaymentsPage", "posPaymentsList"],
|
||||
["posDiscounts", "DiscountsPage", "posDiscounts"],
|
||||
["posDiscountsCoupons", "CouponsPage", "posDiscountsCoupons"],
|
||||
["featureTogglesPromo", "PromotionsPage", "featureTogglesPromo"],
|
||||
["connectorDelivery", "DeliveryIntegrationPage", "connectorDelivery"],
|
||||
["pickupOrders", "PickupOrdersPage", "pickupOrders"],
|
||||
["onlineOrders", "OnlineOrdersPage", "onlineOrders"],
|
||||
["orderTimeline", "OrderTimelinePage", "orderTimeline"],
|
||||
["invoices", "InvoicesPage", "invoices"],
|
||||
["receipts", "ReceiptsPage", "receipts"],
|
||||
["analyticsReports", "ReportsPage", "analyticsReports"],
|
||||
["analyticsSnapshots", "AnalyticsPage", "analyticsSnapshots"],
|
||||
["inventoryOverview", "InventoryPage", "inventoryOverview"],
|
||||
["connectorCrm", "CrmIntegrationPage", "connectorCrm"],
|
||||
["connectorLoyalty", "LoyaltyIntegrationPage", "connectorLoyalty"],
|
||||
["connectorCommunication", "CommunicationIntegrationPage", "connectorCommunication"],
|
||||
["connectorWebsite", "WebsiteIntegrationPage", "connectorWebsite"],
|
||||
["connectorDispatches", "MarketplaceIntegrationPage", "connectorDispatches"],
|
||||
["settings", "SettingsPage", "settings"],
|
||||
["permissions", "PermissionsPage", "permissions"],
|
||||
["roles", "RolesPage", "roles"],
|
||||
["events", "AuditLogsPage", "events"],
|
||||
["notifications", "NotificationsPage", "notifications"],
|
||||
["tenantBundles", "BundlesPage", "tenantBundles"],
|
||||
];
|
||||
|
||||
// Registry body is maintained in gen script — run to regenerate resourceRegistry.ts
|
||||
const registryPath = path.join(ROOT, "scripts", "_hospitality-registry-body.ts.txt");
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
console.error("Missing", registryPath, "- run setup first");
|
||||
process.exit(1);
|
||||
}
|
||||
const registryBody = fs.readFileSync(registryPath, "utf8");
|
||||
fs.mkdirSync(path.dirname(REGISTRY), { recursive: true });
|
||||
fs.writeFileSync(REGISTRY, registryBody);
|
||||
|
||||
for (const [featureFile, exportName, registryKey] of FEATURE_MAP) {
|
||||
const content = `"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ${exportName} = createHospitalityListPage(hospitalityResources.${registryKey});
|
||||
|
||||
export default ${exportName};
|
||||
`;
|
||||
fs.writeFileSync(path.join(FEATURES, `${featureFile}.tsx`), content);
|
||||
}
|
||||
|
||||
console.log("Generated registry +", FEATURE_MAP.length, "features");
|
||||
72
frontend/scripts/update-hospitality-features.mjs
Normal file
72
frontend/scripts/update-hospitality-features.mjs
Normal file
@ -0,0 +1,72 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const FEATURES = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "modules", "hospitality", "features");
|
||||
|
||||
const FEATURE_MAP = [
|
||||
["venues", "BranchesPage", "venues"],
|
||||
["branches", "BranchListPage", "branches"],
|
||||
["diningAreas", "DiningAreasPage", "diningAreas"],
|
||||
["tables", "TablesPage", "tables"],
|
||||
["posFloorPlans", "FloorMapPage", "posFloorPlans"],
|
||||
["reservations", "ReservationsPage", "reservations"],
|
||||
["waitlist", "QueuePage", "waitlist"],
|
||||
["connectorRegistrationsCrm", "CustomersPage", "connectorRegistrationsCrm"],
|
||||
["qrMenuSessions", "GuestsPage", "qrMenuSessions"],
|
||||
["menus", "DigitalMenuPage", "menus"],
|
||||
["menuCategories", "MenuCategoriesPage", "menuCategories"],
|
||||
["menuItems", "MenuItemsPage", "menuItems"],
|
||||
["modifierGroups", "ModifierGroupsPage", "modifierGroups"],
|
||||
["modifierOptions", "ModifierOptionsPage", "modifierOptions"],
|
||||
["availabilityWindows", "AvailabilityPage", "availabilityWindows"],
|
||||
["menuMediaRefs", "MediaLibraryPage", "menuMediaRefs"],
|
||||
["qrCodes", "QrMenuPage", "qrCodes"],
|
||||
["qrOrderingSessions", "QrOrderingPage", "qrOrderingSessions"],
|
||||
["carts", "CartPage", "carts"],
|
||||
["cartLines", "CheckoutPage", "cartLines"],
|
||||
["kitchenTickets", "KitchenDisplayPage", "kitchenTickets"],
|
||||
["kitchenTicketItems", "KitchenQueuePage", "kitchenTicketItems"],
|
||||
["kitchenTicketsPrep", "KitchenPrepPage", "kitchenTicketsPrep"],
|
||||
["posTickets", "PosLitePage", "posTickets"],
|
||||
["posPayments", "PosProPage", "posPayments"],
|
||||
["posRegisters", "CashRegisterPage", "posRegisters"],
|
||||
["posPaymentsList", "PaymentsPage", "posPaymentsList"],
|
||||
["posDiscounts", "DiscountsPage", "posDiscounts"],
|
||||
["posDiscountsCoupons", "CouponsPage", "posDiscountsCoupons"],
|
||||
["featureTogglesPromo", "PromotionsPage", "featureTogglesPromo"],
|
||||
["connectorDelivery", "DeliveryIntegrationPage", "connectorDelivery"],
|
||||
["pickupOrders", "PickupOrdersPage", "pickupOrders"],
|
||||
["onlineOrders", "OnlineOrdersPage", "onlineOrders"],
|
||||
["orderTimeline", "OrderTimelinePage", "orderTimeline"],
|
||||
["invoices", "InvoicesPage", "invoices"],
|
||||
["receipts", "ReceiptsPage", "receipts"],
|
||||
["analyticsReports", "ReportsPage", "analyticsReports"],
|
||||
["analyticsSnapshots", "AnalyticsPage", "analyticsSnapshots"],
|
||||
["inventoryOverview", "InventoryPage", "inventoryOverview"],
|
||||
["connectorCrm", "CrmIntegrationPage", "connectorCrm"],
|
||||
["connectorLoyalty", "LoyaltyIntegrationPage", "connectorLoyalty"],
|
||||
["connectorCommunication", "CommunicationIntegrationPage", "connectorCommunication"],
|
||||
["connectorWebsite", "WebsiteIntegrationPage", "connectorWebsite"],
|
||||
["connectorDispatches", "MarketplaceIntegrationPage", "connectorDispatches"],
|
||||
["settings", "SettingsPage", "settings"],
|
||||
["permissions", "PermissionsPage", "permissions"],
|
||||
["roles", "RolesPage", "roles"],
|
||||
["events", "AuditLogsPage", "events"],
|
||||
["notifications", "NotificationsPage", "notifications"],
|
||||
["tenantBundles", "BundlesPage", "tenantBundles"],
|
||||
];
|
||||
|
||||
for (const [featureFile, exportName, registryKey] of FEATURE_MAP) {
|
||||
const content = `"use client";
|
||||
|
||||
import { createHospitalityListPage } from "@/modules/hospitality/components/HospitalityListCrudPage";
|
||||
import { hospitalityResources } from "@/modules/hospitality/constants/resourceRegistry";
|
||||
|
||||
export const ${exportName} = createHospitalityListPage(hospitalityResources.${registryKey});
|
||||
|
||||
export default ${exportName};
|
||||
`;
|
||||
fs.writeFileSync(path.join(FEATURES, `${featureFile}.tsx`), content);
|
||||
console.log("Updated", featureFile);
|
||||
}
|
||||
337
scripts/deploy_hospitality_prod.py
Normal file
337
scripts/deploy_hospitality_prod.py
Normal file
@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deploy Hospitality service + frontend (Torbat Food) to production."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_HOST = "192.168.10.162"
|
||||
APP_USER = "morteza"
|
||||
APP_PASS = "Moli@5404"
|
||||
NGX_HOST = "192.168.10.156"
|
||||
NGX_USER = "torbatyaruser"
|
||||
NGX_PASS = "Moli@5404"
|
||||
REMOTE_DIR = "/home/morteza/torbatyar"
|
||||
|
||||
EXCLUDE_DIRS = {
|
||||
".git",
|
||||
"node_modules",
|
||||
".next",
|
||||
".venv",
|
||||
"venv",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
".mypy_cache",
|
||||
".ruff_cache",
|
||||
".idea",
|
||||
".vscode",
|
||||
"agent-transcripts",
|
||||
}
|
||||
EXCLUDE_FILES = {".DS_Store", "Thumbs.db", ".env"}
|
||||
|
||||
ENV_UPSERT = {
|
||||
"NEXT_PUBLIC_HOSPITALITY_API_URL": "https://hospitality.torbatyar.ir",
|
||||
"HOSPITALITY_SERVICE_URL": "http://hospitality-service:8009",
|
||||
"HOSPITALITY_DATABASE_URL": "postgresql+asyncpg://superapp:superapp_password@postgres:5432/hospitality_db",
|
||||
"HOSPITALITY_DATABASE_URL_SYNC": "postgresql+psycopg://superapp:superapp_password@postgres:5432/hospitality_db",
|
||||
"HOSPITALITY_SERVICE_NAME": "hospitality-service",
|
||||
"KEYCLOAK_SERVER_URL": "http://keycloak:8080",
|
||||
"KEYCLOAK_REALM": "superapp",
|
||||
}
|
||||
|
||||
|
||||
def connect(host: str, user: str, password: str) -> paramiko.SSHClient:
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.connect(
|
||||
host,
|
||||
username=user,
|
||||
password=password,
|
||||
timeout=30,
|
||||
allow_agent=False,
|
||||
look_for_keys=False,
|
||||
)
|
||||
return c
|
||||
|
||||
|
||||
def run(
|
||||
client: paramiko.SSHClient,
|
||||
cmd: str,
|
||||
*,
|
||||
sudo: bool = False,
|
||||
password: str = "",
|
||||
timeout: int = 600,
|
||||
) -> tuple[int, str]:
|
||||
cmd = cmd.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n"
|
||||
b64 = base64.b64encode(cmd.encode()).decode()
|
||||
if sudo:
|
||||
full = f"echo {password!r} | sudo -S -p '' bash -c 'echo {b64} | base64 -d | bash'"
|
||||
else:
|
||||
full = f"bash -lc 'echo {b64} | base64 -d | bash'"
|
||||
print(
|
||||
f"\n>>> {'[sudo] ' if sudo else ''}{cmd[:220].replace(chr(10), ' | ')}"
|
||||
f"{'...' if len(cmd) > 220 else ''}"
|
||||
)
|
||||
_stdin, stdout, stderr = client.exec_command(full, timeout=timeout, get_pty=True)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
code = stdout.channel.recv_exit_status()
|
||||
text = out + (("\n" + err) if err.strip() else "")
|
||||
printable = text[-5000:] if len(text) > 5000 else text
|
||||
try:
|
||||
print(printable)
|
||||
except UnicodeEncodeError:
|
||||
print(printable.encode("ascii", "replace").decode("ascii"))
|
||||
return code, text
|
||||
|
||||
|
||||
def should_exclude(path: Path) -> bool:
|
||||
parts = set(path.parts)
|
||||
if parts & EXCLUDE_DIRS:
|
||||
return True
|
||||
if path.name in EXCLUDE_FILES:
|
||||
return True
|
||||
if path.suffix in {".pyc", ".log"}:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def make_tarball() -> Path:
|
||||
out = ROOT / "scripts" / "_deploy_hospitality_bundle.tar.gz"
|
||||
print(f"Creating tarball {out} ...")
|
||||
with tarfile.open(out, "w:gz") as tar:
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
p = Path(dirpath)
|
||||
dirnames[:] = [
|
||||
d for d in dirnames if d not in EXCLUDE_DIRS and not d.startswith(".cursor")
|
||||
]
|
||||
rel_dir = p.relative_to(ROOT)
|
||||
if should_exclude(rel_dir) and rel_dir != Path("."):
|
||||
continue
|
||||
for name in filenames:
|
||||
fp = p / name
|
||||
rel = fp.relative_to(ROOT)
|
||||
if should_exclude(rel):
|
||||
continue
|
||||
if rel.as_posix().startswith("scripts/_deploy"):
|
||||
continue
|
||||
tar.add(fp, arcname=str(rel).replace("\\", "/"))
|
||||
print(f"Tarball size: {out.stat().st_size / 1024 / 1024:.1f} MB")
|
||||
return out
|
||||
|
||||
|
||||
def upload_file(client: paramiko.SSHClient, local: Path, remote: str) -> None:
|
||||
sftp = client.open_sftp()
|
||||
print(f"Uploading {local} -> {remote}")
|
||||
sftp.put(str(local), remote)
|
||||
sftp.close()
|
||||
|
||||
|
||||
def put_text_sudo(
|
||||
client: paramiko.SSHClient, password: str, remote: str, content: str
|
||||
) -> None:
|
||||
b64 = base64.b64encode(content.encode()).decode()
|
||||
cmd = (
|
||||
f"mkdir -p $(dirname {remote!r}) && echo {b64} | base64 -d > {remote!r} "
|
||||
f"&& chmod 644 {remote!r}"
|
||||
)
|
||||
code, _ = run(client, cmd, sudo=True, password=password, timeout=60)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"failed writing {remote}")
|
||||
|
||||
|
||||
def merge_env_remote(client: paramiko.SSHClient) -> None:
|
||||
py = f"""
|
||||
from pathlib import Path
|
||||
p = Path({REMOTE_DIR!r}) / '.env'
|
||||
text = p.read_text(encoding='utf-8') if p.exists() else ''
|
||||
lines = text.splitlines()
|
||||
keys = {ENV_UPSERT!r}
|
||||
seen = set()
|
||||
out = []
|
||||
for line in lines:
|
||||
if not line.strip() or line.lstrip().startswith('#') or '=' not in line:
|
||||
out.append(line)
|
||||
continue
|
||||
k = line.split('=', 1)[0].strip()
|
||||
if k in keys:
|
||||
out.append(f"{{k}}={{keys[k]}}")
|
||||
seen.add(k)
|
||||
else:
|
||||
out.append(line)
|
||||
for k, v in keys.items():
|
||||
if k not in seen:
|
||||
out.append(f"{{k}}={{v}}")
|
||||
p.write_text('\\n'.join(out).rstrip() + '\\n', encoding='utf-8')
|
||||
print('ENV_MERGED', len(keys))
|
||||
"""
|
||||
b64 = base64.b64encode(py.encode()).decode()
|
||||
code, _ = run(
|
||||
client,
|
||||
f"python3 -c \"import base64; exec(base64.b64decode('{b64}').decode())\"",
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError("env merge failed")
|
||||
|
||||
|
||||
def compose_up(client: paramiko.SSHClient, services: str) -> None:
|
||||
code, _ = run(
|
||||
client,
|
||||
f"""
|
||||
set -e
|
||||
cd {REMOTE_DIR}
|
||||
sg docker -c 'docker compose up -d --build {services}'
|
||||
sg docker -c 'docker compose ps'
|
||||
""",
|
||||
timeout=1800,
|
||||
)
|
||||
if code != 0:
|
||||
code, _ = run(
|
||||
client,
|
||||
f"""
|
||||
set -e
|
||||
cd {REMOTE_DIR}
|
||||
docker compose up -d --build {services}
|
||||
docker compose ps
|
||||
""",
|
||||
sudo=True,
|
||||
password=APP_PASS,
|
||||
timeout=1800,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"compose up failed for {services}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tarball = make_tarball()
|
||||
|
||||
app = connect(APP_HOST, APP_USER, APP_PASS)
|
||||
try:
|
||||
run(app, f"mkdir -p {REMOTE_DIR}")
|
||||
upload_file(app, tarball, "/tmp/torbatyar_hospitality_bundle.tar.gz")
|
||||
code, _ = run(
|
||||
app,
|
||||
f"""
|
||||
set -e
|
||||
cd {REMOTE_DIR}
|
||||
if [ -f .env ]; then cp .env /tmp/torbatyar.env.bak; fi
|
||||
chown -R {APP_USER}:{APP_USER} {REMOTE_DIR}/backend {REMOTE_DIR}/frontend {REMOTE_DIR}/docs {REMOTE_DIR}/docker-compose.yml {REMOTE_DIR}/infrastructure 2>/dev/null || true
|
||||
chmod -R u+rwX {REMOTE_DIR}/backend {REMOTE_DIR}/frontend {REMOTE_DIR}/docs 2>/dev/null || true
|
||||
tar --overwrite -xzf /tmp/torbatyar_hospitality_bundle.tar.gz -C {REMOTE_DIR}
|
||||
if [ -f /tmp/torbatyar.env.bak ]; then cp /tmp/torbatyar.env.bak .env; fi
|
||||
chown -R {APP_USER}:{APP_USER} {REMOTE_DIR}/backend {REMOTE_DIR}/frontend {REMOTE_DIR}/docs 2>/dev/null || true
|
||||
rm -f /tmp/torbatyar_hospitality_bundle.tar.gz
|
||||
ls -la {REMOTE_DIR}/backend/services/hospitality | head
|
||||
ls -la {REMOTE_DIR}/frontend/app/hospitality | head
|
||||
""",
|
||||
sudo=True,
|
||||
password=APP_PASS,
|
||||
timeout=300,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError("extract failed")
|
||||
|
||||
merge_env_remote(app)
|
||||
|
||||
run(
|
||||
app,
|
||||
f"""
|
||||
set -e
|
||||
cd {REMOTE_DIR}
|
||||
sg docker -c "docker compose exec -T postgres psql -U superapp -d postgres -tc \\"SELECT 1 FROM pg_database WHERE datname='hospitality_db'\\" | grep -q 1 || docker compose exec -T postgres psql -U superapp -d postgres -c 'CREATE DATABASE hospitality_db;'"
|
||||
""",
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
compose_up(app, "hospitality-service frontend")
|
||||
|
||||
run(
|
||||
app,
|
||||
f"""
|
||||
set -e
|
||||
cd {REMOTE_DIR}
|
||||
sg docker -c 'docker compose exec -T frontend npm install' || docker compose exec -T frontend npm install
|
||||
sg docker -c 'docker compose restart frontend' || docker compose restart frontend
|
||||
sleep 15
|
||||
sg docker -c 'docker compose logs --tail=80 hospitality-service frontend' || docker compose logs --tail=80 hospitality-service frontend
|
||||
""",
|
||||
sudo=True,
|
||||
password=APP_PASS,
|
||||
timeout=1800,
|
||||
)
|
||||
|
||||
code, health = run(
|
||||
app,
|
||||
"""
|
||||
set -e
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if curl -sf http://127.0.0.1:8009/health; then
|
||||
echo
|
||||
echo HOSPITALITY_HEALTH_OK
|
||||
exit 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
curl -sv http://127.0.0.1:8009/health || true
|
||||
exit 1
|
||||
""",
|
||||
timeout=120,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"Hospitality health failed: {health}")
|
||||
|
||||
code, fe = run(
|
||||
app,
|
||||
"""
|
||||
curl -s -o /dev/null -w 'hospitality_hub=%{http_code}\n' http://127.0.0.1:3000/hospitality/hub
|
||||
curl -s -o /dev/null -w 'hospitality_root=%{http_code}\n' http://127.0.0.1:3000/hospitality
|
||||
""",
|
||||
timeout=60,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"Frontend hospitality route failed: {fe}")
|
||||
finally:
|
||||
app.close()
|
||||
|
||||
ngx = connect(NGX_HOST, NGX_USER, NGX_PASS)
|
||||
try:
|
||||
conf = (ROOT / "infrastructure" / "nginx" / "torbatyar.ir.conf").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
put_text_sudo(ngx, NGX_PASS, "/etc/nginx/conf.d/torbatyar.ir.conf", conf)
|
||||
certbot = r"""
|
||||
set -e
|
||||
mkdir -p /var/www/html
|
||||
certbot certonly --webroot -w /var/www/html \
|
||||
-d torbatyar.ir -d www.torbatyar.ir \
|
||||
-d api.torbatyar.ir -d identity.torbatyar.ir -d auth.torbatyar.ir \
|
||||
-d accounting.torbatyar.ir -d crm.torbatyar.ir -d loyalty.torbatyar.ir \
|
||||
-d healthcare.torbatyar.ir -d beauty.torbatyar.ir -d hospitality.torbatyar.ir \
|
||||
--non-interactive --agree-tos --email support@torbatyar.ir --expand || true
|
||||
nginx -t && systemctl reload nginx && echo NGINX_RELOADED
|
||||
"""
|
||||
code, _ = run(ngx, certbot, sudo=True, password=NGX_PASS, timeout=300)
|
||||
if code != 0:
|
||||
raise RuntimeError("nginx reload / certbot failed")
|
||||
finally:
|
||||
ngx.close()
|
||||
|
||||
print("\n=== HOSPITALITY DEPLOY FINISHED ===")
|
||||
print("API: https://hospitality.torbatyar.ir/health")
|
||||
print("Hub: https://torbatyar.ir/hospitality/hub")
|
||||
print("Local API: http://192.168.10.162:8009/health")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(f"FATAL: {exc}", file=sys.stderr)
|
||||
raise
|
||||
Loading…
Reference in New Issue
Block a user