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). Seesrc/pages/hotels/Hotels.tsx. - Group hotel assignments — links a hotel row to a
TravelGroupwith rooms allocated, check-in/check-out, meal plan settings, and an optional price override. Represented by theGroupHotelAssignmenttable. - Food assignments — the counterpart for catering: a group × food item with meal-days and rate.
- Per-passenger room splits — a
BookingPassengerHoteljunction (added insupabase/migrations/20260418020000_passenger_hotel_junction.sql) lets ops split a single hotel assignment across passengers with per-passenger check-in/check-out,roomShareGrouplabel, room number, and meal-plan override. Triggers maintain a derivedroomsUsedcounter 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)
- Ops creates a
HotelInventoryrow withtotalRoomsandavailableRooms. - From the Groups page ("Hotels" tab) ops creates a
GroupHotelAssignmenttying the hotel to the group, withroomsAllocated,checkIn,checkOut, and meal-plan settings. - Individual passengers on the group's bookings can optionally be placed into specific rooms via the
BookingPassengerHoteljunction (room-share groups, meal overrides). - The booking P&L calculation pulls the group's hotel cost and prorates it across bookings by passenger count — see the
hotelCostcomputation insrc/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
SupplierTransactionrecordsleaseDays × totalRooms × pricePerNightas acredit(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.*.
Related tables (Supabase)
HotelInventory— the core row. Created insupabase/migrations/20260113130121_remote_schema.sql; lease-date columns added in20260331130000_hotel_inventory_lease_dates.sql; account FK fixed in20260404180000_fix_hotel_inventory_account_fk.sql.FoodInventory— same page, parallel table.GroupHotelAssignment— links group × hotel. Lives in the original schema migration; meal fields added in20260403010000_hotel_assignment_meals.sql; date fields in20260403000000_hotel_assignment_dates.sql.BookingPassengerHotel— per-passenger junction;20260418020000_passenger_hotel_junction.sql.SupplierTransaction— payable ledger against the supplier.