Skip to content

Hotels

Hotel inventory module — tracks contracted room stock from hotel suppliers and allocates rooms to travel groups. This is where ops records a hotel contract, how many rooms are held, the night rate, and which group consumes them.

Scope

The Hotels module manages contracted/leased room stock for Hajj and Umrah groups (Makkah, Madinah, Jeddah, etc.). It is not a hotel search/booking aggregator — every row is a room block the agency has negotiated with a supplier, not a free-sell inventory feed.

What it manages

  • Hotel records — one row per contract: supplier, city, star rating, distance from Haram, room type, total rooms, available rooms, rate/night, currency + exchange rate, lease date range, low-room threshold, GL account linkage, and optional GST input-credit fields.
  • Food inventory — the same page hosts a parallel "Food" tab that tracks catering contracts (unit meals, quantity/day, price/meal-day, supplier, date window). See src/pages/hotels/Hotels.tsx.
  • Group hotel assignments — links a hotel row to a TravelGroup with rooms allocated, check-in/check-out, meal plan settings, and an optional price override. Represented by the GroupHotelAssignment table.
  • Food assignments — the counterpart for catering: a group × food item with meal-days and rate.
  • Per-passenger room splits — a BookingPassengerHotel junction (added in supabase/migrations/20260418020000_passenger_hotel_junction.sql) lets ops split a single hotel assignment across passengers with per-passenger check-in/check-out, roomShareGroup label, room number, and meal-plan override. Triggers maintain a derived roomsUsed counter on the parent assignment.

Supporting tabs in the UI: city-level stat cards (properties, total rooms, vacant, occupied), low-room threshold dialog, CSV import for food, and Excel export of assignments.

Currency and GST

Hotels are typically priced in SAR. The stored exchangeRate is the contract rate (1 foreign unit = N INR) and is persisted on the hotel row so later recomputes use the contract rate, not today's spot rate. When gstEnabled is true, the purchase journal splits the base amount to the hotel-expense account and the GST portion to 1400 GST Input Credit. See src/lib/api.ts (POST /hotels).

Pages

  • src/pages/hotels/Hotels.tsx — the one and only page for this module. It contains both the Hotels tab and the Food tab, plus all create/edit/assign/threshold/import dialogs. Route: /hotels.
  • No separate detail route — hotels are edited inline via dialog.

Bundle size

Despite being a ~2,600-line file, the Hotels route gzips to roughly 10.7 KB at build time (budget 13 KB). The lean footprint is enforced by scripts/check-bundle-budgets.mjs. Heavy utilities (XLSX export) are lazy-loaded so they don't tax the initial page load.

Relationship to bookings

Hotels attach to groups, not directly to bookings. The chain is:

Booking ──(groupId)──▶ TravelGroup ──(GroupHotelAssignment)──▶ HotelInventory
                              └──▶ BookingPassengerHotel (per-pax room assignment)
  1. Ops creates a HotelInventory row with totalRooms and availableRooms.
  2. From the Groups page ("Hotels" tab) ops creates a GroupHotelAssignment tying the hotel to the group, with roomsAllocated, checkIn, checkOut, and meal-plan settings.
  3. Individual passengers on the group's bookings can optionally be placed into specific rooms via the BookingPassengerHotel junction (room-share groups, meal overrides).
  4. The booking P&L calculation pulls the group's hotel cost and prorates it across bookings by passenger count — see the hotelCost computation in src/lib/api.ts (around lines 1005–1015, 4531–4543).

When an assignment is created, a journal entry posts Dr Hotel Expense / Cr Stock-in-Hand to recognize consumption. When an assignment is deleted, the journal is auto-reversed.

Supplier linkage

Every hotel row must belong to a supplier. handleHotels (POST /hotels in src/lib/api.ts) calls ensureHotelSupplierRecord(body.supplierId) and throws a 400 "Select a hotel supplier first" if omitted. The supplier's name is copied into the hotel row's hotelName field, and the hotel's supplier drives the payable ledger:

  • On hotel creation, a SupplierTransaction records leaseDays × totalRooms × pricePerNight as a credit (payable) against the supplier.
  • If the contract currency isn't INR, the INR-denominated payable is computed using the stored exchangeRate.
  • The supplier view (/suppliers/:id) lists all linked hotel contracts and their payable/payment history.

Supplier is immutable on the hotel row

The Hotels module does not expose an edit path that changes supplierId after creation — once a contract is tied to a supplier, reassigning it means deleting and re-creating. This keeps the ledger trail consistent.

Permissions

Route guard: hotels.view (src/App.tsx:175). All mutating actions on this page are wrapped in <PermissionGate>:

Action Permission Location
View /hotels route hotels.view src/App.tsx
Add Hotel / Add Food hotels.create <PermissionGate> on "Add Hotel" + "Add Food" buttons
Edit hotel fields / assign rooms / set thresholds / assign food / edit food hotels.edit <PermissionGate> on edit + assign buttons, server-side requirePermission('hotels.edit') on PATCH routes
Delete hotel / delete food item / delete assignment hotels.delete <PermissionGate> on delete buttons, server-side requirePermission('hotels.delete') on DELETE routes
Export hotels/food workbook hotels.export <PermissionGate> on export buttons

Server-side enforcement lives in src/lib/api.ts (grep for requirePermission('hotels.). Nav item appears in the sidebar as "Inventory → Hotels & Food" when the user has hotels.view.

See docs/PERMISSIONS.md §6.14 for the authoritative matrix row, and §5 for which roles carry hotels.*.

  • HotelInventory — the core row. Created in supabase/migrations/20260113130121_remote_schema.sql; lease-date columns added in 20260331130000_hotel_inventory_lease_dates.sql; account FK fixed in 20260404180000_fix_hotel_inventory_account_fk.sql.
  • FoodInventory — same page, parallel table.
  • GroupHotelAssignment — links group × hotel. Lives in the original schema migration; meal fields added in 20260403010000_hotel_assignment_meals.sql; date fields in 20260403000000_hotel_assignment_dates.sql.
  • BookingPassengerHotel — per-passenger junction; 20260418020000_passenger_hotel_junction.sql.
  • SupplierTransaction — payable ledger against the supplier.