Groups
The Groups module manages group tours — a package that multiple customers travel together on: Umrah batches, Hajj cohorts, Ziyarat trips, domestic and international tour packages. A TravelGroup is the container; Bookings attach to it.
Where it lives
- List + detail page:
src/pages/groups/Groups.tsx:185 - Group-level detail route:
src/pages/groups/GroupDetailPage.tsx - Pricing tab (lazy):
src/components/groups/GroupPricingTab.tsx— loaded atGroups.tsx:81 - Invoices tab (lazy):
src/components/groups/GroupInvoicesTab.tsx— loaded atGroups.tsx:84 - Group invoice dialog:
src/components/invoices/GroupInvoiceDialog.tsx:44 - Math helpers + filters:
src/pages/groups/groupsMath.ts - Readiness panel:
src/components/groups/GroupReadinessPanel.tsx - Movement chart (itinerary preview):
src/components/groups/MovementChart.tsx - Route:
/groupsand/groups/:id— gated bygroups.view(src/App.tsx:154)
1. What a group tour is
A group tour is a single departure with a shared itinerary and a shared rate sheet that multiple bookings can attach to. The group owns:
| Slot | Purpose |
|---|---|
groupCode |
Auto-generated, server-assigned sequential code (displayed mono — Groups.tsx:2704) |
groupType |
HAJ, HAJ_ZIYARAT, UMRAH, UMRAH_ZIYARAT, ZIYARAT, GENERAL_DOMESTIC, GENERAL_INTERNATIONAL |
departureDate / returnDate |
Travel window; constrains inventory search |
totalCapacity / bookedCount |
Seat math — drives the Open / Full / Planning status |
status |
planning, open, full, departed, completed, cancelled (or manual override via the edit dialog — Groups.tsx:105) |
itinerary |
Nested flights[], hotels[], ground[], activities[] (legacy JSON — the new code uses normalized tables) |
pricing (rate sheet) |
Four line items (flight, hotel, visa, groundServices) × three pax tiers + tax lines — lives on the Pricing tab |
| Linked inventory | Airline blocks, FIT entries, hotels, food, ground transfers — joined via dedicated assignment tables |
Group-wide operations (flights, hotels, ground transfers) cascade to every booking in the group; per-passenger overrides are supported via dedicated UIs (PassengerFlightsPanel, PassengerHotelsPanel).
2. List view (/groups)
The landing page is a dual-view list — card grid and virtualized table — toggled from the header (Groups.tsx imports LayoutGrid + Rows3).
Each GroupCard (src/components/groups/GroupCard.tsx) surfaces:
- Name + auto-generated
groupCode - Departure / return dates
- Capacity bar (
bookedCount/totalCapacity) viagetGroupSeatSnapshot(src/pages/groups/groupsMath.ts) - Status pill —
getGroupStatusClass(Groups.tsx:160) maps status to a colour - Quick KPIs (charges / collected / balance)
Filter bar supports search + status filter via filterGroups (groupsMath.ts).
3. Group creation
"Create Group" (gated by <PermissionGate permission="groups.create">, Groups.tsx:2189) opens a multi-section dialog:
- Name + type — tour category picker (same
GROUP_TYPE_OPTIONSas the wizard,Groups.tsx:2447) - Sub-type —
PILGRIMAGEshows the pilgrimage sub-type picker (Groups.tsx:2471);GENERALshowsDOMESTIC/INTERNATIONAL(Groups.tsx:2532) - Geography — Ziyarat countries (
Groups.tsx:2499), Indian states (Groups.tsx:2561), or International countries + cities (Groups.tsx:2593) - Dates — Departure + Return via
DateInput(Groups.tsx:2653) - Capacity
- Auto-generated code preview —
previewGroupCodeshows the pattern; the server assigns the final sequential letter on save (Groups.tsx:2704)
On submit, groupService.createGroup calls POST /groups.
4. Tabs on the group detail view
Once a group is picked, Tabs (Groups.tsx:3416) render seven tabs:
| Tab | Default? | Purpose |
|---|---|---|
| Overview | ✓ | Financial summary + composition, group leader, readiness panel |
| Operations | Flights, Hotels, Ground transfers (see §5) | |
| Itinerary | Editable itinerary editor — legacy flights[] / hotels[] / ground[] / activities[] | |
| Passengers | Table of bookings in the group, with leader / move / drop actions | |
| Pricing | Lazy-loaded (GroupPricingTab, Groups.tsx:81) — the rate sheet |
|
| Invoices | Lazy-loaded (GroupInvoicesTab, Groups.tsx:84) — payer invoices |
|
| Financials | Per-group P&L (charges, collected, balance); drill into supplier payables |
The lazy-load is deliberate: they're only rendered when the user clicks the tab, saving ~8 KB gzip off the main group chunk (Groups.tsx:77).
5. Operations tab — Flights, Hotels, Ground
5.1 Flights panel
Groups.tsx:3557. Renders every airline block or FIT inventory entry linked to the group, one row each.
- Add Flight dialog (
Groups.tsx:2730) — pick type (Airline Block or FIT), then the specific inventory row. Only flights whosedepartureDatefalls within the group's window are shown, and already-linked blocks / FITs (taken by any group, company-wide) are hidden (Groups.tsx:2774). - PNR auto-fill — when an inventory row has a PNR, the dialog prefills it; a green hint appears confirming the source (
Groups.tsx:2879). Operators can override. - Multi-city route display —
chainLegs(legs[], fallbackFrom, fallbackTo)(Groups.tsx:3579) chains every leg's origin/destination into a single string:SXR → DEL → JEDfor a three-leg outbound. If the block is round-trip, the return route is appended:SXR → DEL → JED · JED → DEL → SXR(Groups.tsx:3602). - PNR editing — per-flight PNR input at the right of each row:
Source:
<Input value={pnrEditState[gf.id] || ''} onChange={(e) => setPnrEditState((prev) => ({ ...prev, [gf.id]: e.target.value.toUpperCase() }))} placeholder="PNR" maxLength={8} className="h-8 w-[110px] text-xs font-mono uppercase tracking-wider" autoComplete="off" />Groups.tsx:3644. The styling rules are:- Uppercased on input (
.toUpperCase()) maxLength={8}— IATA PNRs are always 6–8 charactersfont-mono uppercase tracking-wider— fixed-width visual alignmentautoComplete="off"— prevents Chrome from mixing unrelated saved values A dirty-check flag shows a Save button only when the field value differs from the server value (Groups.tsx:3652).handleSaveGroupFlightPnrcalls the backend.
- Uppercased on input (
Per-passenger PNR override
The group-level PNR is the default. Per-booking PNR overrides live on the booking's PassengerFlightsPanel (src/components/bookings/PassengerFlightsPanel.tsx) — used when one passenger's ticket got re-booked on a different PNR. The group shows the master; the passenger's value wins when ticketing runs.
5.2 Hotels panel
Groups.tsx:3682. "Link Hotel" opens an assignment dialog (rooms allocated, check-in / check-out, room type, price per night, meal plan, meal-day calculation). Rooms are assigned at the passenger level from the Passengers tab.
5.3 Ground transfers panel
Analogous to hotels — pick an inventory entry, assign to the group, then per-passenger assignments.
6. Pricing tab (lazy)
The group's rate sheet is the source of truth the wizard's Sync button reads from (see Booking Wizard §5.1).
Shape:
lineItems: {
flight: { adult, child, infant }
hotel: { adult, child, infant }
visa: { adult, child, infant }
groundServices:{ adult, child, infant }
}
taxLines: [ { id, name, rate, mode: 'inclusive' | 'exclusive', appliesTo: LineKey[] } ]
See src/types/index.ts → GroupPricing and the dialog's recompute logic (GroupInvoiceDialog.tsx:86).
Permissions:
| Action | Permission |
|---|---|
| View rate sheet | groups.view |
| Save edits (line items + tax lines) | group_pricing.edit |
| Refresh rates from inventory | group_pricing.edit |
API: PATCH /groups/:id/pricing.
7. Invoices tab (lazy)
Groups bill per payer — often a family head covers multiple bookings. The Invoices tab lists every payer attached to the group and their invoices.
Invoice lifecycle: DRAFT → ISSUED → PAID (or CANCELLED via a reversing credit-note journal).
GroupInvoiceDialog (src/components/invoices/GroupInvoiceDialog.tsx:44) is the edit / issue / cancel surface:
- Draft edits — per-tier rate (adult/child/infant) editing recomputes
amount = rate × qtyviarecomputeLineAmount(GroupInvoiceDialog.tsx:88); operators can also override theamountcolumn directly for flat-fee negotiations (GroupInvoiceDialog.tsx:110). - HSN / SAC codes — India GST compliance;
updateLineCode(GroupInvoiceDialog.tsx:123) clears the other field when one is filled (they are mutually exclusive per line). - Tax lines — array of named taxes, each with
rate,mode(inclusive / exclusive), andappliesTo(which line keys). Preview computes:exclusive = base × rate/100,inclusive = base × rate/(100+rate)(GroupInvoiceDialog.tsx:186). The server recomputes on save. - Issue — posts the revenue + tax journal:
POST /group-invoices/:id/issue(GroupInvoiceDialog.tsx:225). Locks the invoice. - Cancel — posts a reversing credit-note journal:
POST /group-invoices/:id/cancel(GroupInvoiceDialog.tsx:243). The payer can then be re-invoiced. - Supplementary — an additional invoice against an already-issued invoice (e.g. for a price adjustment).
- Print —
printGroupInvoice({ invoice, groupName, payerName })(GroupInvoiceDialog.tsx:258).
Permissions (see docs/PERMISSIONS.md §6.5):
| Action | Permission |
|---|---|
| View payers + invoices tab | group_invoices.view |
| Create draft invoice | group_invoices.create |
| Edit draft (line items, taxes, due date) | group_invoices.edit |
| Issue draft → ISSUED | group_invoices.issue |
| Cancel issued invoice | group_invoices.cancel |
| Create supplementary | group_invoices.create |
8. Group P&L report
The Financials tab (and the Reports module) expose a group-level P&L: revenue (from issued invoices) − cost (from supplier invoices, hotel nights, ground, airline block costs) = margin.
Cross-group P&L lives on the Reports page under "Group Profitability" — gated by finance.reports.group_profit_loss.view and finance.reports.group_profit_loss.export for download. See docs/PERMISSIONS.md §4.4.
9. Group-wide actions + permissions summary
See docs/PERMISSIONS.md §6.5 for the canonical matrix. Quick reference:
| Action | Permission | Where |
|---|---|---|
| View page | groups.view |
Route |
| Create group | groups.create |
"Create Group" button (Groups.tsx:2189) |
| Delete group | groups.delete |
Card / row delete |
| Assign group leader | groups.edit |
Leader picker on Overview tab |
| Move passenger between groups | groups.edit |
Passengers tab |
| Drop passenger from group | groups.edit |
Passengers tab |
| Link / unlink flight | groups.edit |
Operations → Flights |
| Assign hotel rooms | groups.edit |
Operations → Hotels |
| Assign ground transfer | groups.edit |
Operations → Ground |
| Edit rate sheet | group_pricing.edit |
Pricing tab |
| Refresh rates from inventory | group_pricing.edit |
Pricing tab header |
| Create / edit draft invoices | group_invoices.create / .edit |
Invoices tab |
| Issue / cancel invoices | group_invoices.issue / .cancel |
Invoice dialog |
Roles that hold groups.*: CEO, GM, IT_ADMIN, ADMIN_HR (full); SALES_MANAGER, B2B_MANAGER, OPS_MANAGER hold groups.create; others hold groups.view only (docs/PERMISSIONS.md §5).
10. Group lifecycle diagram
stateDiagram-v2
[*] --> planning: Create group
planning --> open: Inventory + pricing ready
open --> full: bookedCount == totalCapacity
full --> open: Capacity increased or booking cancelled
open --> departed: After departureDate (auto)
full --> departed: After departureDate (auto)
departed --> completed: After returnDate (auto)
planning --> cancelled: Manual cancel
open --> cancelled: Manual cancel
full --> cancelled: Manual cancel
completed --> [*]
cancelled --> [*]
Manual status override
The edit dialog exposes a Status override dropdown (Groups.tsx:105). Set to auto to let the server compute status from dates + capacity; pick an explicit value to pin it (e.g. force planning while the rate sheet is being finalised).