Skip to content

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 at Groups.tsx:81
  • Invoices tab (lazy): src/components/groups/GroupInvoicesTab.tsx — loaded at Groups.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: /groups and /groups/:id — gated by groups.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) via getGroupSeatSnapshot (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:

  1. Name + type — tour category picker (same GROUP_TYPE_OPTIONS as the wizard, Groups.tsx:2447)
  2. Sub-typePILGRIMAGE shows the pilgrimage sub-type picker (Groups.tsx:2471); GENERAL shows DOMESTIC / INTERNATIONAL (Groups.tsx:2532)
  3. Geography — Ziyarat countries (Groups.tsx:2499), Indian states (Groups.tsx:2561), or International countries + cities (Groups.tsx:2593)
  4. Dates — Departure + Return via DateInput (Groups.tsx:2653)
  5. Capacity
  6. Auto-generated code previewpreviewGroupCode shows 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 whose departureDate falls 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 displaychainLegs(legs[], fallbackFrom, fallbackTo) (Groups.tsx:3579) chains every leg's origin/destination into a single string: SXR → DEL → JED for 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:
    <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"
    />
    
    Source: Groups.tsx:3644. The styling rules are:
    • Uppercased on input (.toUpperCase())
    • maxLength={8} — IATA PNRs are always 6–8 characters
    • font-mono uppercase tracking-wider — fixed-width visual alignment
    • autoComplete="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). handleSaveGroupFlightPnr calls the backend.

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.tsGroupPricing 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 × qty via recomputeLineAmount (GroupInvoiceDialog.tsx:88); operators can also override the amount column 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), and appliesTo (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).
  • PrintprintGroupInvoice({ 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).