Skip to content

Inbound Group Management System - Implementation Documentation

Document Information

Property Value
Document Version 2.4
Last Updated 2026-03-15
Status Current Implementation
Module tqweb-adm, tqapi, tqapp
Technology Stack JavaScript (ES6 Modules), Java 17, Jakarta EE, Bootstrap 5

Table of Contents

  1. System Architecture
  2. Module Structure
  3. Data Model Implementation
  4. API Implementation
  5. Frontend Implementation
  6. Business Logic Layer
  7. User Interface Components
  8. Data Flow and Interactions
  9. Code Organization
  10. Implementation Patterns

1. System Architecture

1.1 Multi-Tier Architecture

The Inbound Group Management system follows a classic three-tier architecture with a multi-page frontend:

┌──────────────────────────────────────────────────────────────────┐
│   Presentation Layer (tqweb-adm)                                │
│   - 8 Dedicated HTML Pages (Bootstrap 5)                        │
│   - 9 ES6 JavaScript Modules                                    │
│   - groups-core.js: shared models, API client, FormBinder       │
│   - jQuery 3.x + Bootstrap 5 components                         │
└──────────────────────────────────────────────────────────────────┘
                    │ AJAX/JSON (via tlinq() function)
┌──────────────────────────────────────────────────────────────────┐
│   API Layer (tqapi)                                              │
│   - Jakarta RESTful Web Services (JAX-RS)                        │
│   - GroupApi.java - REST endpoints                               │
│   - JSON Request/Response via TlinqApiResponse                   │
└──────────────────────────────────────────────────────────────────┘
                    │ Method Calls
┌──────────────────────────────────────────────────────────────────┐
│   Business Logic Layer (tqapp)                                   │
│   - GroupManagerFacade.java                                      │
│   - Entity Classes (CTripGroup, CTripPax, CTripHotel, etc.)      │
│   - PaxExcelService (Excel import/template generation)           │
│   - ScheduleExcelService (schedule Excel export)                │
│   - EntityTransformer (XML-based field mapping)                  │
└──────────────────────────────────────────────────────────────────┘
                    │ JPA / JDBC
┌──────────────────────────────────────────────────────────────────┐
│   Database Layer (NTS Plugin)                                    │
│   - PostgreSQL via NTSEntityReadService / NTSEntityWriteService  │
│   - NTS native entities (TripgroupEntity, TrippaxEntity, etc.)   │
│   - NTSGroupService for custom room-pax operations               │
└──────────────────────────────────────────────────────────────────┘

1.2 Technology Stack

Layer Technology
Frontend Framework Bootstrap 5.3, Bootstrap Icons
Frontend JS ES6 Modules, jQuery 3.7.1
API Jakarta RESTful Web Services (JAX-RS), Jersey
Business Logic Java 17, Jakarta EE
Serialization GSON 2.10.1 (TypeUtil.extractFromJson)
Database PostgreSQL (via NTS Plugin)
Configuration XML with XInclude (tourlinq-config.xml)

1.3 Communication Protocol

  • Protocol: HTTP/HTTPS (port 11080/11079)
  • Data Format: JSON
  • HTTP Method: POST (all endpoints)
  • Content Type: application/json
  • Authentication: Three-tier pipeline — JWT Bearer token (OIDC), proxy headers, or session token. See API Specification.
  • API Response Wrapper: TlinqApiResponse with apiStatus and apiData fields
  • Client Extraction: tlinq() function resolves promises directly with apiData and automatically attaches JWT Bearer tokens for authenticated pages

2. Module Structure

2.1 Frontend Module (tqweb-adm)

The frontend is organized as a multi-page application where each domain area has its own dedicated HTML page and JavaScript module. All pages share a common core module for data models, API client, utilities, and form binding.

HTML Pages:

Page Purpose
groups-list.html Group listing with compact card strip, schedule table with date-grouped events and readiness alerts, filter modal, schedule print/export, group create/edit modal
groups-summary.html Group dashboard with summary cards, financial overview, and navigation hub
groups-calendar.html Monthly calendar view showing hotels, transports, and activities (postponed — schedule table on groups-list provides equivalent functionality)
groups-passengers.html Passenger management table with add/edit modal, autofill, and Excel import
groups-accommodation.html Hotel booking management with search, dates, meal plan, currency
groups-rooming.html Room list per hotel with room cards, pax assignment, auto-add rooms
groups-transport.html Transport management with supplier lookup, vehicle types, driver info
groups-activities.html Activity/excursion cards with product search, pricing, tickets, transfers

JavaScript Modules (js/modules/):

Module Purpose Exports
groups-core.js Shared foundation: data models, API client, FormBinder, utilities, constants GroupsAPI, GroupTrip, TripPax, TripHotel, TripRoom, TripTransport, TripService, GroupUtils, FormBinder, constants
groups-list.js Group listing, schedule table, expanded data loading, alert builders, schedule Excel download initializePage
groups-summary.js Summary dashboard and financial calculations initializePage
groups-calendar.js Calendar rendering and navigation initializePage, showEventDetails
groups-passengers.js Passenger CRUD, autofill, Excel import initializePage, editPax
groups-accommodation.js Hotel CRUD, hotel search, nights calculation initializePage, editHotel, confirmDelete, openRooming
groups-rooming.js Room CRUD, pax-to-room assignment, auto-add initializePage
groups-transport.js Transport CRUD, supplier lookup initializePage, editTransport, confirmDelete
groups-activities.js Activity CRUD, product lookup initializePage, editActivity, confirmDelete

Stylesheet: css/groups.css - Custom Bootstrap 5 stylesheet with CSS variables, card components, calendar styles, and data table styling.

2.2 API Module (tqapi)

Location: tqapi/src/main/java/com/perun/tlinq/api/

Key Files: - GroupApi.java - REST endpoints for all group operations - ApiUtil.java - Parameter extraction and validation utilities - entity/TlinqApiResponse.java - Standard API response wrapper

2.3 Business Logic Module (tqapp)

Location: tqapp/src/main/java/com/perun/tlinq/entity/group/

Key Files:

File Purpose
GroupManagerFacade.java Main business facade for all CRUD and search operations
CTripGroup.java Group entity
CTripPax.java Passenger entity
CTripHotel.java Hotel booking entity
CTripRoom.java Room allocation entity
CTripService.java Activity/service entity
CTripTransport.java Transport entity
CTripRoomPax.java Room-passenger junction entity
CTripDataMap.java Expanded group data container (all sub-entities in one response)
CTripRoomPaxUpdate.java Batch room-passenger update DTO
CTripHotelRoomMap.java Hotel-room relationship wrapper
CTripRoomPaxMap.java Room-passenger relationship wrapper
CServiceExtraCost.java Activity extra cost entity
CServiceTicketReq.java Activity ticket requirement entity
GroupQuotationPdfRenderer.java PDF quotation generator (OpenHTMLToPDF)
PaxExcelService.java Excel template generation and passenger import
ScheduleExcelService.java Schedule Excel export (date-grouped events with status)
PaxImportResult.java Import result DTO with success/error details
PaxRowError.java Individual row error DTO for import reporting

2.4 Configuration

Entity Configuration: config/entities/group-entities.xml - Defines 11 entities: TripGroup, TripPax, TripHotel, TripTransport, TripRoom, TripRoomPax, TripService, GroupItinerary, TripPricing, ServiceExtraCost, ServiceTicketReq - Each entity maps canonical fields to native database fields via FieldMappingList - Uses NTSServiceFactory with NTSEntityReadService and NTSEntityWriteService

Service Configuration: config/nts-client.xml - Defines read/write/delete service implementations for each entity - Custom NTSGroupService for room-pax operations (saveTripRoomPax, getTripRoomList, deleteTripRoomPax, deleteTripRoom) and ticket fulfillment counting (countFulfilledTickets)

PDF Template: config/templates/group/pdf-quotation-template.html - HTML template for group quotation PDF, rendered by GroupQuotationPdfRenderer via OpenHTMLToPDF - Uses %PLACEHOLDER% substitution for company header, group details, accommodation, activities, transport, costs, and schedule - Company info loaded from tourlinq.properties (tqpro.company.* properties)

API Roles: config/api-roles.properties - Maps each endpoint path to allowed roles (guest, agent, admin)

Application Properties: config/tourlinq-config.xml - schedule.default-range — default number of days for the schedule view (default: 30)


3. Data Model Implementation

3.1 Entity Class Hierarchy

All backend entity classes extend TlinqEntity:

TlinqEntity (base class, tqcommon)
    ├── CTripGroup          (group booking)
    ├── CTripPax            (passenger)
    ├── CTripHotel          (hotel booking)
    ├── CTripRoom           (room allocation)
    ├── CTripService        (activity/service)
    ├── CTripTransport      (ground transport)
    ├── CTripRoomPax        (room-passenger junction)
    ├── CTripRoomPaxUpdate  (batch update DTO)
    ├── CServiceExtraCost   (activity extra cost)
    └── CServiceTicketReq   (activity ticket requirement)

Frontend JS model classes mirror the backend entities, defined in groups-core.js:

GroupTrip       ↔ CTripGroup
TripPax         ↔ CTripPax
TripHotel       ↔ CTripHotel
TripRoom        ↔ CTripRoom
TripService     ↔ CTripService
TripTransport   ↔ CTripTransport

3.2 CTripGroup / GroupTrip

Backend (tqapp/src/main/java/com/perun/tlinq/entity/group/CTripGroup.java):

Field Type Description
tripGroupId Integer Primary key
tripGroupName String Group name
tripGroupDesc String Description
partnerId Integer FK to partner/company
partnerName String Denormalized partner name (via ModelLookup)
arrivalDate Date Arrival date/time
departureDate Date Departure date/time
confirmationDate Date Confirmation deadline
quoteByDate Date Quote deadline
numNights Integer Number of nights
numPax Integer Expected passenger count
tripLeader String Group leader name
tripContact String Contact person
status String Booking status
quoteId Integer Reference to quote
invoiceId Integer Reference to invoice
currencyCode String Currency code (AED, EUR, USD)

Status Values: IN PREPARATION, QUOTE SENT, CONFIRMED, CANCELLED, ARRIVED, DEPARTED

Frontend (groups-core.js - GroupTrip class): - Constructor accepts optional object; if null, initializes with defaults - update(obj) method handles both API field names (groupId/groupName) and internal names (tripGroupId/tripGroupName) - Dates stored as JavaScript Date objects - Integers parsed via parseInt()

3.3 CTripPax / TripPax

Backend (CTripPax.java):

Field Type Description
paxId Integer Primary key
paxFirstName String First name (required)
paxLastName String Last name (required)
gender String Gender (M/F)
birthDate Date Date of birth
nationality String Nationality
passportNum String Passport number
groupId Integer FK to group
contactTel String Phone
contactEmail String Email
age Integer Age
note String Notes

Frontend (TripPax class) - Additional fields beyond backend:

Field Type Description
title String Salutation (Mr, Mrs, etc.)
middleName String Middle name
passportIssueDate Date Passport issue date
passportExpiryDate Date Passport expiry date
leadPassenger Boolean Lead passenger flag
roomId Integer Assigned room

Computed Properties: fullName getter returns ${paxFirstName} ${paxLastName}

3.4 CTripHotel / TripHotel

Backend (CTripHotel.java):

Field Type Description
tripHotelId Integer Primary key
groupId Integer FK to group
hotelId Integer FK to hotel master (via ModelLookup)
hotelName String Denormalized hotel name
status String Booking status
checkInDate Date Check-in date
checkOutDate Date Check-out date
confirmationDate Date Confirmation deadline
description String Booking notes
hotelContacts String Hotel contact info
roomRate Double Cost per room/night
saleRate Double Sale price per room/night
currencyCode String Currency code

Frontend (TripHotel class) - Additional/different fields:

Field Type Description
nights Integer Auto-calculated from check-in/check-out
mealPlan String RO, BB, HB, FB, AI
rateType String NET, GROSS, COMMISSIONABLE
confirmationNumber String Booking confirmation number

Auto-calculation: Nights computed automatically when checkInDate and checkOutDate are set.

Status Values: PENDING, REQUESTED, CONFIRMED, CANCELLED

3.5 CTripRoom / TripRoom

Backend (CTripRoom.java):

Field Type Description
roomId Integer Primary key
groupId Integer FK to group
tripHotelId Integer FK to hotel booking
roomType String Room type (KING BED, SPLIT BEDS)
roomNumber Integer Physical room number
roomPax Integer Room capacity
extraBed Integer Extra bed flag (0/1)
roomPrice Double Sale price
roomCost Double Cost price
notes String Room notes

Frontend (TripRoom class) - Additional field:

Field Type Description
roomView String Room view description

3.6 CTripService / TripService

Backend (CTripService.java):

Field Type Description
tripServiceId Integer Primary key
groupId Integer FK to group
serviceName String Activity name
productId String FK to product catalog
startTime Date Activity date/time
duration Double Duration in hours
adultCost Double Cost per adult
childCost Double Cost per child
adultPrice Double Price per adult
childPrice Double Price per child
numAdults Integer Number of adults
numChildren Integer Number of children
guideName String Assigned guide
status Integer Activity status
note String Notes
currency String Currency code (note: currency, not currencyCode)
hasTickets Boolean Requires tickets
ticketsPurchased Boolean Tickets obtained (set automatically when offline tickets are assigned)
transferIncluded Boolean Includes transfers (triggers auto-transfer creation on save)
meetingPoint String Meeting point
extraCosts List\<CServiceExtraCost> Transient: loaded on-demand, flat extra costs for this activity
ticketRequirements List\<CServiceTicketReq> Transient: loaded only if hasTickets=true, with fulfilled counts
extraCostTotal Double Transient: sum of all extra costs

Offline Ticket Integration: When hasTickets is true, tickets can be assigned from the Offline Tickets module via the POST /offline/sale/assign-group API endpoint. This sets ticketsPurchased = true automatically and creates a link from each assigned ticket back to this trip service via the trip_service_id FK on the offline_ticket table. The assignment bypasses the payment/invoice flow — tickets are marked SOLD directly with download codes generated.

Extra Costs: Flat additional costs (e.g., guide fees, parking, tips) stored in the nts.serviceextracost table. Each extra cost has a description, amount, and currency. Managed via the activity details dialog.

Ticket Requirements: Per-attraction ticket requirements stored in nts.serviceticketreq. Each requirement specifies an attraction from the offline tickets module and the quantity needed. The fulfilled field is a transient count populated by the countFulfilledTickets custom service, which queries nts.offlineticket and nts.ticketbatch tables.

Frontend (TripService class) - Computed properties: - totalAdultCost = adultCost * numAdults - totalChildCost = childCost * numChildren - totalAdultPrice = adultPrice * numAdults - totalChildPrice = childPrice * numChildren

3.7 CTripTransport / TripTransport

Backend (CTripTransport.java):

Field Type Description
tripTransportId Integer Primary key
groupId Integer FK to group
partnerId Integer Transport provider (via ModelLookup)
partnerName String Provider name
description String Service description
notes String Additional notes
startTime Date Pickup date/time
endTime Date Drop-off date/time
vehicle String Vehicle type
status String Transport status
contactTel String Driver contact phone
driverName String Driver name
currencyCode String Currency code
cost Double Cost price
price Double Sale price
fromLocation String Pickup location
toLocation String Drop-off location
duration Integer Duration in hours
activityId Integer FK to CTripService (set when auto-created for an activity)
confirmed Boolean Transport confirmation status

Vehicle Types: "Sedan car", "7-Seater (SUV)", "14-Seater", "24-Seater", "30-Seater", "40-Seater", "52-Seater"

Auto-Transfer Creation: When an activity is saved with transferIncluded=true, a linked transport is automatically created by GroupManagerFacade.autoCreateTransferForActivity(). The auto-created transport copies the activity's date/time and description, sets a default duration of 1 hour, and links back to the activity via activityId. If a transport already exists for that activity, no duplicate is created. Auto-created transports are cascade-deleted when the parent activity is deleted.

3.8 CTripRoomPax

Backend (CTripRoomPax.java):

Field Type Description
tripRoomPaxId Integer Primary key
roomId Integer FK to room
paxId Integer FK to passenger
groupId Integer FK to group
tripHotelId Integer FK to hotel booking

Constraints: Unique (roomId, paxId) prevents double-assignment.

3.9 CServiceExtraCost

Backend (CServiceExtraCost.java):

Field Type Description
serviceExtraCostId Integer Primary key
tripServiceId Integer FK to CTripService
description String Description of the extra cost
cost Double Cost amount
currencyCode String Currency code (3 chars)

Native Entity: ServiceExtracostEntity → table nts.serviceextracost

3.10 CServiceTicketReq

Backend (CServiceTicketReq.java):

Field Type Description
serviceTicketReqId Integer Primary key
tripServiceId Integer FK to CTripService
attractionId Integer FK to OfflineAttraction
attractionName String Display name (via ModelLookup)
quantity Integer Number of tickets required
fulfilled Integer Transient: tickets already fulfilled (populated by countFulfilledTickets)

Native Entity: ServiceTicketreqEntity → table nts.serviceticketreq

3.11 CTripDataMap

Purpose: Container for the loadExpanded API response that bundles all group sub-entities in a single response to minimize API round-trips.

public class CTripDataMap {
    private CTripGroup group;
    private CTripPax[] passengers;
    private CTripHotel[] hotels;
    private CTripRoom[] rooms;
    private CTripService[] services;
    private CTripTransport[] transports;
    private Map<Integer, List<Integer>> hotelRoomMap;   // hotelId -> [roomIds]
    private Map<Integer, List<Integer>> roomPaxMap;     // roomId -> [paxIds]
}

4. API Implementation

4.1 API Base Path and Configuration

Base URL: /groups API Class: com.perun.tlinq.api.GroupApi File: tqapi/src/main/java/com/perun/tlinq/api/GroupApi.java

All endpoints: - Accept POST with JSON body - Require session parameter (extracted from body or X-Auth-Request-Access-Token header) - Return TlinqApiResponse wrapper with apiStatus and apiData

4.2 Group Endpoints

Endpoint Purpose
POST /groups/group/write Create or update group
POST /groups/group/list Search groups by date range and/or partner
POST /groups/group/loadExpanded Load complete group data with all sub-entities

/group/write - Request:

{
  "session": "token",
  "tripGroupId": null,
  "tripGroupName": "Dubai Explorer Group",
  "partnerId": 123,
  "arrivalDate": "2026-03-01T10:00:00",
  "departureDate": "2026-03-05T18:00:00",
  "numPax": 25,
  "numNights": 4,
  "status": "IN PREPARATION",
  "currencyCode": "AED"
}

/group/list - Request:

{
  "session": "token",
  "partnerId": 123,
  "arrivalFrom": "2026-01-01",
  "arrivalTo": "2026-12-31"
}

/group/loadExpanded - Request:

{ "session": "token", "groupId": 456 }

Response includes all sub-entities in a CTripDataMap structure.

4.3 Passenger Endpoints

Endpoint Purpose
POST /groups/pax/write Create/update passenger (single or batch via paxList)
POST /groups/pax/read Get single passenger by paxId
POST /groups/pax/list List passengers for a group
POST /groups/pax/delete Delete passenger by tripPaxId
POST /groups/pax/import Import passengers from Excel file
GET /groups/pax/template Download Excel template for passenger import

Bulk creation uses paxList array in request body:

{
  "session": "token",
  "paxList": [
    { "paxFirstName": "Pax 1", "paxLastName": "Passenger 1", "groupId": 456 },
    { "paxFirstName": "Pax 2", "paxLastName": "Passenger 2", "groupId": 456 }
  ]
}

Excel import uses base64-encoded file data:

{
  "session": "token",
  "groupId": 456,
  "fileData": "base64-encoded-xlsx",
  "fileName": "passengers.xlsx"
}

4.4 Accommodation Endpoints

Endpoint Purpose
POST /groups/hotel/write Create/update hotel booking
POST /groups/hotel/list List hotels for a group
POST /groups/hotel/delete Delete hotel booking

4.5 Room Endpoints

Endpoint Purpose
POST /groups/triproom/write Create/update room
POST /groups/triproom/list List rooms for group (optionally filtered by hotelId)
POST /groups/triproom/delete Delete room (requires room object with roomId, groupId, tripHotelId)
POST /groups/triproom/updatepaxlist Batch add/remove passenger assignments

Room delete requires nested object format:

{ "session": "token", "room": { "roomId": 201, "groupId": 456, "tripHotelId": 101 } }

Update pax list:

{
  "session": "token",
  "groupId": 456,
  "roomId": 201,
  "hotelId": 101,
  "paxToAdd": [1001, 1002],
  "paxToRemove": [1003]
}

4.6 Transport Endpoints

Endpoint Purpose
POST /groups/transport/write Create/update transport
POST /groups/transport/list List transports for a group
POST /groups/transport/delete Delete transport by tripTransportId

4.7 Activity/Service Endpoints

Endpoint Purpose
POST /groups/tripservice/write Create/update activity
POST /groups/tripservice/list List activities for a group
POST /groups/tripservice/delete Delete activity by tripServiceId

4.8 Extra Cost Endpoints

Endpoint Purpose
POST /groups/extracost/list List extra costs for a trip service
POST /groups/extracost/write Create/update an extra cost
POST /groups/extracost/delete Delete an extra cost

/extracost/list - Request:

{ "session": "token", "tripServiceId": 789 }

/extracost/write - Request:

{
  "session": "token",
  "tripServiceId": 789,
  "serviceExtraCostId": null,
  "description": "Guide fee",
  "cost": 150.00,
  "currencyCode": "AED"
}

4.9 Ticket Requirement Endpoints

Endpoint Purpose
POST /groups/ticketreq/list List ticket requirements for an activity (with fulfilled counts)
POST /groups/ticketreq/write Create/update a ticket requirement
POST /groups/ticketreq/delete Delete a ticket requirement

/ticketreq/list - Request:

{ "session": "token", "tripServiceId": 789 }

Response includes fulfilled count for each requirement, populated by the countFulfilledTickets custom service which queries nts.offlineticket joined with nts.ticketbatch.

4.10 PDF Quotation Endpoint

Endpoint Purpose
POST /groups/quotation/generatePdf Generate PDF quotation for a group

Request:

{
  "session": "token",
  "groupId": 456,
  "quoteItems": [
    { "description": "Hotel Accommodation", "quantity": 1, "price": 5000.00, "lineTotal": 5000.00 },
    { "description": "City Tour", "quantity": 25, "price": 120.00, "lineTotal": 3000.00 }
  ],
  "quoteCurrency": "AED"
}

Response:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": { "pdf": "base64-encoded-pdf-bytes" }
}

The PDF includes: company header (logo + name + contacts from tqpro.company.* properties), group details, accommodation table, activities table, transport table (duration in hours), cost breakdown, total amount, and a chronological schedule.

4.11 Export Endpoints

Endpoint Purpose
POST /groups/pax/exportExcel Export passengers as Excel file (binary .xlsx stream)
POST /groups/exportSchedule Export schedule events as Excel file (binary .xlsx stream)

/pax/exportExcel - Request:

{ "session": "token", "groupId": 456 }

/exportSchedule - Request:

{ "session": "token", "groupIds": [456, 457, 458], "rangeDays": 30 }

Both endpoints return application/octet-stream with Content-Disposition header on success, or a JSON TlinqApiResponse on error. The frontend uses fetch() with Bearer auth to download the binary blob (the standard tlinq() function expects JSON and cannot handle binary responses).

4.9 Lookup Endpoints (used by group forms)

Endpoint Purpose
POST /customer/companyLookup Search companies/partners by name fragment
POST /hotel/hotelLookup Search hotels by name
POST /product/productLookup Search products/activities by name

All lookup APIs return String[][] arrays where each inner array is [name, id]. The frontend GroupsAPI transforms these into named objects.

4.9 API Response Format

Standard Response (TlinqApiResponse):

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": { ... }
}

The tlinq() function in globals.js extracts apiData and passes it directly to .then() callbacks. Callers receive the data directly, not the wrapper.

Error Response:

{
  "apiStatus": { "errorCode": "INVALID_FORMAT", "errorMessage": "Date format incorrect" },
  "apiData": null
}


5. Frontend Implementation

5.1 Core Module (groups-core.js)

The core module exports all shared components used across pages:

GroupsAPI Object

Centralized API client wrapping all tlinq() calls with session management:

export const GroupsAPI = {
    async listGroups(partnerId, arrivalFrom, arrivalTo) { ... },
    async loadGroupExpanded(groupId) { ... },
    async saveGroup(group) { ... },
    async listPax(groupId) { ... },
    async savePax(pax) { ... },
    async savePaxList(paxList) { ... },
    async deletePax(tripPaxId) { ... },
    async importPax(groupId, fileData, fileName) { ... },
    async listHotels(groupId) { ... },
    async saveHotel(hotel) { ... },
    async deleteHotel(tripHotelId) { ... },
    async saveRoom(room) { ... },
    async deleteRoom(room) { ... },
    async updateRoomPaxList(roomId, groupId, hotelId, paxToAdd, paxToRemove) { ... },
    async listTransport(groupId) { ... },
    async saveTransport(transport) { ... },
    async deleteTransport(tripTransportId) { ... },
    async saveActivity(activity) { ... },
    async deleteActivity(tripServiceId) { ... },
    async companyLookup(searchTerm) { ... },
    async hotelLookup(searchTerm) { ... },
    async productLookup(searchTerm) { ... }
};

FormBinder Class

Declarative data binding for Bootstrap 5 forms using data-entity-name and data-entity-field HTML attributes:

export class FormBinder {
    constructor(containerSelector) { ... }
    populate(entityName, data) { ... }   // Object → form fields
    extract(entityName) { ... }          // Form fields → object
    clear(entityName) { ... }            // Clear all fields
}

HTML binding pattern:

<input type="text" data-entity-name="hotelRecord" data-entity-field="hotelName">
<select data-entity-name="hotelRecord" data-entity-field="mealPlan">
    <option value="BB">Bed & Breakfast</option>
</select>
<input type="checkbox" data-entity-name="activityRecord" data-entity-field="hasTickets">

The FormBinder handles type-specific extraction: checkboxes use prop('checked'), datetime-local inputs use GroupUtils.formatDateTime(), and standard inputs use val().

GroupUtils Object

Utility functions shared across all pages:

Method Purpose
formatDate(date) Format as YYYY-MM-DD
formatDateTime(date) Format for datetime-local inputs
formatDateTimeDisplay(date) Human-readable format (15 JAN 2026 14:30)
formatShortDate(date) Short format (15.01)
formatMoney(amount, currency) Currency-formatted number
getGroupIdFromContext() Get group ID from URL params or sessionStorage
getHotelIdFromContext() Get hotel ID from URL params or sessionStorage
setCurrentGroup(groupId, groupData) Cache group in sessionStorage
getCachedGroup() Get cached group (5-minute TTL)
setHotelContext(hotelId, ...) Store hotel context for rooming page
navigateToSummary(groupId) Navigate with context preservation
showAlert(type, message, duration) Bootstrap 5 toast alert
escapeHtml(text) XSS prevention
showLoading() / hideLoading() Loading spinner toggle
getStatusBadgeClass(status) Status → Bootstrap badge color

Constants

export const GROUP_STATUS = { IN_PREPARATION, QUOTE_SENT, CONFIRMED, CANCELLED, ARRIVED, DEPARTED };
export const HOTEL_STATUS = { OPEN, REQUESTED, QUOTED, CONFIRMED, CANCELLED };
export const VEHICLE_TYPES = ['Sedan car', '7-Seater (SUV)', '14-Seater', '24-Seater', '30-Seater', '40-Seater', '52-Seater'];
export const ROOM_TYPES = [{ value: 'KING BED', label: 'Single King bed' }, { value: 'SPLIT BEDS', label: 'Two queen beds' }];
export const CURRENCIES = [{ code: 'AED', name: 'Emirates Dirham' }, { code: 'EUR', name: 'Euro' }, { code: 'USD', name: 'United States Dollar' }];

5.2 Page Modules

Each page module follows a consistent pattern:

import { GroupsAPI, GroupTrip, ..., GroupUtils, FormBinder } from './groups-core.js';
import { setGlobalHandlers } from './globals.js';

// Page state
let currentGroup = null;
let entityList = [];
const formBinder = new FormBinder('#formContainer');

// Initialization
export function initializePage() {
    setGlobalHandlers({ requireAuth: true });
    const groupId = GroupUtils.getGroupIdFromContext();
    if (!groupId) { /* redirect */ return; }
    setupEventListeners();
    loadGroupData(groupId);
}

// Data loading via GroupsAPI
async function loadGroupData(groupId) { ... }

// Rendering with Bootstrap components
function renderTable() { ... }

// CRUD via FormBinder + GroupsAPI
function save() {
    const data = formBinder.extract('entityRecord');
    // validate, set groupId, call GroupsAPI.saveEntity(data)
}

5.3 Group List Page (groups-list.js)

  • Page title: "Incoming Groups" displayed in the dark plum (#2a2149) navbar via #currentPageTitle
  • Action bar: [New Group], [Filter], [Print Schedule], [Download Schedule] buttons plus schedule range dropdown (2 weeks / 1 month / 2 months / 3 months)
  • Compact card strip: Horizontal scrollable strip of 220px compact group cards with left/right scroll arrows; cards show group name, partner, dates, pax count, and status-colored top border
  • Schedule table: Date-grouped table of upcoming events (hotel check-in/out, transports, activities) across all visible groups:
    • Loads expanded data for all groups in parallel via Promise.all() calling GroupsAPI.loadGroupExpanded()
    • buildScheduleEvents() extracts events within the selected date range
    • Events sorted by date, rendered with date separator rows and indented event rows
    • Color-coded status pills (readiness alerts): green OK, yellow warnings (No confirmation, No driver, Tickets needed), red danger (Not booked)
    • Alert builders: buildHotelAlerts(), buildTransportAlerts(), buildActivityAlerts()
  • Schedule Excel export: downloadScheduleExcel() uses fetch() with Bearer auth to call /groups/exportSchedule, receives binary blob, triggers browser download
  • Filter modal: Partner search (debounced autocomplete via companyLookup), date range, status
  • Print support: Portrait print stylesheet hides action bar, card strip, modals; shows only schedule table with print-only header
  • Create/edit group modal with partner lookup datalist
  • Navigation: clicking a group card goes to groups-summary.html?groupId=...
  • Caches selected group in sessionStorage for downstream pages
  • Configuration: schedule.default-range property in tourlinq-config.xml (default: 30 days)

5.4 Summary Page (groups-summary.js)

  • Dashboard with group header, status badge, navigation action buttons
  • Summary cards: Group Info, Accommodation, Transport, Activities
  • Financial summary table aggregating rooms (grouped by hotel/rate), transports, activities (adult/child separate lines)
  • Include/exclude checkboxes for quote item selection
  • Quote generation: stores selected items in sessionStorage, opens quote.html
  • Navigation hub to all detail pages

5.5 Calendar Page (groups-calendar.js)

  • Monthly calendar grid with day-of-week headers
  • Events from three sources: hotel check-in/check-out, transports, activities
  • buildEventsMap() creates a Map<dateKey, events[]> from all entity lists
  • Visual indicators: trip-day background, trip-start/trip-end markers, today highlight
  • Event click opens detail modal with entity-specific information
  • Month navigation (prev/next/today) with automatic initial month from group arrival date
  • Color-coded event types via CSS classes: .activity, .transport, .hotel-start, .hotel-end

5.6 Passengers Page (groups-passengers.js)

  • Sortable data table with columns: Name, Gender, Nationality, Passport, Phone, Email
  • Add/edit passenger modal with comprehensive personal information fields
  • Autofill: bulk-create placeholder passengers up to group's numPax count
  • Excel import: file upload via FileReader API, base64 encoding, PaxExcelService backend
  • Import result modal showing success count and per-row errors
  • Excel template download link

5.7 Accommodation Page (groups-accommodation.js)

  • Hotel table with columns: Hotel Name, Check-in, Check-out, Nights, Meal Plan, Rate Type, Confirmation #, Status
  • Hotel search with debounced hotelLookup API and datalist
  • Automatic nights calculation from check-in/check-out dates
  • Currency select (AED/EUR/USD) auto-populated from group's currencyCode on new hotel
  • Navigation to rooming list page per hotel
  • Visual cues on search input: .lookup-matched / .lookup-unmatched CSS classes

5.8 Rooming Page (groups-rooming.js)

  • Room cards display with capacity icons, room type, cost/price, assigned passengers
  • Room modal: type, capacity, room number (auto-populated), extra bed, cost, price, view, notes
  • Auto-add rooms: batch create rooms for a hotel based on count and defaults
  • Passenger assignment: select unassigned passengers from dropdown, add/remove with action tracking
  • Room-pax changes tracked via Map<paxId, "A"|"D"> and committed via updateRoomPaxList API

5.9 Transport Page (groups-transport.js)

  • Transport table with columns: Description, Date/Time, From, To, Vehicle, Supplier, Driver, Cost, Price
  • Transport modal: description, start/end time, from/to locations, supplier (with debounced companyLookup and datalist), vehicle type select, driver name/phone, cost/price, currency
  • Currency auto-populated from group on new transport
  • Supplier lookup with visual matched/unmatched feedback

5.10 Activities Page (groups-activities.js)

  • Activity card grid layout (responsive columns)
  • Activity modal: product search (debounced productLookup), date/time, duration, adult/child counts, adult/child cost/price, currency, tickets/transfer checkboxes, meeting point, notes
  • Currency auto-populated from group on new activity (using field name currency to match backend CTripService.currency)
  • Default pax count from group's numPax

6. Business Logic Layer

6.1 GroupManagerFacade

File: tqapp/src/main/java/com/perun/tlinq/entity/group/GroupManagerFacade.java

Key Methods:

Method Purpose
write(TlinqEntity) Create or update any group sub-entity
getPartnerGroups(partnerId, from, to) Search groups by partner and date range
getArrivals(from, to) Search groups by date range
loadTripData(groupId) Load complete CTripDataMap with all sub-entities
getPax(criteria) List passengers
getHotels(criteria) List hotels
getTransports(criteria) List transports
getServices(criteria) List activities/services
getRooms(criteria) List rooms
deleteRoom(room) Delete room via custom service
listRoomPax(groupId, roomId) List room-passenger assignments
updateRoomPaxList(update) Batch add/remove room-pax assignments
exportPaxExcel(groupId) Export passengers as Excel file
exportScheduleExcel(groupIds, rangeDays) Export schedule events as Excel file

loadTripData flow: 1. Creates SelectCriteriaList with tripGroupId criterion to load the group itself 2. Creates a new SelectCriteriaList with groupId criterion for all sub-entities 3. Loads passengers, hotels, transports, services, rooms in parallel using the groupId criterion 4. Loads room-pax assignments per room 5. Builds hotelRoomMap and roomPaxMap relationship maps 6. Returns assembled CTripDataMap

6.2 PaxExcelService

File: tqapp/src/main/java/com/perun/tlinq/entity/group/PaxExcelService.java

Method Purpose
generateTemplate() Create Excel template with headers and example rows
importFromExcel(groupId, fileData, fileName) Parse Excel, validate, create passengers

Template Headers: First Name, Last Name, Gender, DOB, Nationality, Passport #, Phone, Email, Age, Notes

Import Process: Reads rows, validates required fields, creates CTripPax objects, calls write() for each. Returns PaxImportResult with success count and PaxRowError list.

6.3 ScheduleExcelService

File: tqapp/src/main/java/com/perun/tlinq/entity/group/ScheduleExcelService.java

Method Purpose
generateExport(groups, dataMap, rangeDays) Generate Excel file with date-grouped schedule events

Columns: Group, Event Type, Event Description, Date / Time, Status

Event types: Hotel check-in, hotel check-out, transport, activity — extracted from CTripDataMap for each group within the date range.

Status logic (aligned with frontend buildHotelAlerts / buildTransportAlerts / buildActivityAlerts):

Entity Check Status text
Hotel confirmationDate == null No confirmation
Transport supplierName blank AND partnerId == null Not booked
Transport driverName blank No driver
Transport contactTel blank No phone
Activity hasTickets true AND ticketsPurchased false Tickets needed (resolved when offline tickets are assigned via Offline Tickets module)
Activity transferIncluded true Transfer incl.

Styling: Title + date range subtitle, frozen header pane, date separator rows (grey background), conditional cell styling (yellow for warnings, green for OK). Uses Apache POI XSSFWorkbook with manual column widths (headless-server compatible).

6.4 Entity Transformer Configuration

Each entity's field mapping is defined in config/entities/group-entities.xml:

<Entity name="TripHotel" class="com.perun.tlinq.entity.group.CTripHotel"
        idField="tripHotelId" defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory" nativeEntity="com.perun.tlinq.client.nts.db.group.TriphotelEntity">
            <ServiceList>
                <Service name="saveTripHotel" action="create"/>
                <Service name="saveTripHotel" action="update"/>
                <Service name="readTripHotel" action="read"/>
                <Service name="readTripHotel" action="search"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="tripHotelId" sourceField="id" mapping="DirectMapping"/>
                <FieldMapping targetField="hotelName" sourceField="hotelid" mapping="ModelLookup" lookupEntity="Hotel"/>
                <FieldMapping targetField="currencyCode" sourceField="currencycode" mapping="DirectMapping"/>
                <!-- ... more field mappings ... -->
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

Currency field naming: - CTripGroup: currencyCodecurrencycode - CTripHotel: currencyCodecurrencycode - CTripTransport: currencyCodecurrencycode - CTripService: currencycurrencycode (note: canonical field name is currency, not currencyCode) - CTripRoom: no currency field


7. User Interface Components

7.1 Page Structure

All pages follow a consistent Bootstrap 5 layout:

<body class="groups-page">
    <header id="pgheader" data-load-template="header_bootstrap.html"></header>

    <div class="container-fluid groups-container" style="margin-top: 80px;">
        <!-- Breadcrumb navigation -->
        <nav class="groups-breadcrumb" aria-label="breadcrumb">
            <ol class="breadcrumb mb-0">
                <li class="breadcrumb-item"><a href="groups-list.html">Groups</a></li>
                <li class="breadcrumb-item"><a href="#">Group Name</a></li>
                <li class="breadcrumb-item active">Current Page</li>
            </ol>
        </nav>

        <!-- Page header with title and primary action -->
        <div class="content-card mb-4">
            <div class="content-card-header">
                <h4 class="content-card-title">Page Title</h4>
                <button class="btn btn-success btn-sm">Primary Action</button>
            </div>
        </div>

        <!-- Main content area -->
        <!-- ... data tables, card grids, etc. -->

        <!-- Back navigation -->
        <div class="mt-4">
            <a href="#" class="btn btn-outline-secondary" id="btnBack">Back to Summary</a>
        </div>
    </div>

    <!-- Modal dialogs for create/edit -->
    <div class="modal fade" id="entityModal">...</div>

    <!-- Loading indicator -->
    <div id="loadingIndicator" class="d-none">...</div>

    <!-- Alert container (fixed top-right) -->
    <div id="alertContainer" class="position-fixed top-0 end-0 p-3"></div>
</body>

7.2 Navigation Flow

groups-list.html (entry point)
    ├── [Click group card]
groups-summary.html (dashboard hub)
    ├── [Passengers button] → groups-passengers.html
    ├── [Accommodation button] → groups-accommodation.html
    │       │
    │       └── [Rooming List button] → groups-rooming.html
    ├── [Transportation button] → groups-transport.html
    ├── [Activities button] → groups-activities.html
    └── [Calendar button] → groups-calendar.html

Navigation between pages preserves context via: - URL parameters: ?groupId=456 (primary), ?hotelId=101 (for rooming) - sessionStorage: cached group data with 5-minute TTL (groups_currentGroup, groups_currentGroupId) - Hotel context: groups_hotelContext for rooming page

7.3 CSS Framework

Framework: Bootstrap 5.3 with custom groups.css stylesheet

Custom CSS Variables:

:root {
    --groups-primary: #2c3e50;
    --groups-secondary: #34495e;
    --groups-success: #27ae60;
    --groups-warning: #f39c12;
    --groups-danger: #e74c3c;
    --groups-info: #3498db;
    --groups-light-bg: #f8f9fa;
    --groups-border: #dee2e6;
    --groups-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

Custom Components: - .content-card / .content-card-header / .content-card-body - Page section cards - .groups-breadcrumb - Styled breadcrumb navigation - .data-table-wrapper / .data-table - Styled data tables - .calendar-day / .calendar-event - Calendar grid cells and events - .summary-card-row - Summary information rows - .lookup-matched / .lookup-unmatched - Search input visual feedback

7.4 Modal Dialogs

All entity edit dialogs use Bootstrap 5 modals:

// Open modal
const modal = new bootstrap.Modal(document.getElementById('entityModal'));
modal.show();

// Close modal
bootstrap.Modal.getInstance(document.getElementById('entityModal')).hide();

Modal Structure (consistent across all entities): - Header: colored background (bg-primary text-white), icon, title - Body: form with data-entity-name/data-entity-field bound fields - Footer: Delete button (left, hidden on new), Cancel, Save

7.5 Lookup/Autocomplete Pattern

Search fields use HTML5 <datalist> with debounced API calls:

<input type="text" id="hotelSearch" list="hotelList" autocomplete="off">
<datalist id="hotelList"></datalist>
<input type="hidden" id="hotelRefId" data-entity-name="hotelRecord" data-entity-field="hotelId">
<input type="hidden" id="hotelName" data-entity-name="hotelRecord" data-entity-field="hotelName">
// Debounced search (300ms)
$('#hotelSearch').on('input', debounce(onHotelSearch, 300));

async function onHotelSearch() {
    const term = $('#hotelSearch').val();
    if (term.length < 2) return;
    const results = await GroupsAPI.hotelLookup(term);
    // Populate datalist options
    // On selection: set hidden hotelId and hotelName fields
    // Apply .lookup-matched or .lookup-unmatched CSS class
}

7.6 Toast Notifications

Using GroupUtils.showAlert():

GroupUtils.showAlert('success', 'Hotel saved successfully');
GroupUtils.showAlert('error', 'Failed to save hotel: ' + error.message);
GroupUtils.showAlert('warning', 'No items to quote');
GroupUtils.showAlert('info', 'Template downloaded');

Alerts appear as Bootstrap 5 dismissible alerts in the fixed top-right container with auto-dismiss after 5 seconds.


8. Data Flow and Interactions

8.1 Page Load Flow (any detail page)

Page loads → initializePage()
    ├── setGlobalHandlers({ requireAuth: true }) (from globals.js — triggers OIDC auth)
    ├── getGroupIdFromContext() → URL params or sessionStorage
    ├── setupEventListeners() → bind buttons, search inputs, etc.
    └── loadGroupData(groupId)
            ├── getCachedGroup() → try sessionStorage (5-min TTL)
            │   └── [cache miss] → GroupsAPI.listGroups() → find group → cache it
            ├── GroupsAPI.loadGroupExpanded(groupId)
            │   └── POST /groups/group/loadExpanded
            │       └── GroupManagerFacade.loadTripData()
            │           └── Returns CTripDataMap with all sub-entities
            └── Parse response into typed model objects
                └── Render page (table, cards, calendar, etc.)

8.2 Entity Save Flow (any entity)

User clicks Save button
    ├── formBinder.extract('entityRecord') → get form data as object
    ├── Validate required fields
    ├── Set groupId from currentGroup
    ├── GroupsAPI.saveEntity(data)
    │   └── POST /groups/entity/write
    │       └── TypeUtil.extractFromJson() → canonical entity
    │           └── GroupManagerFacade.write()
    │               └── EntityTransformer → native entity
    │                   └── NTSEntityWriteService → DB INSERT/UPDATE
    │                       └── Return saved entity with ID
    ├── Update local list (add new or update existing)
    ├── Re-render display (table row, card, etc.)
    └── GroupUtils.showAlert('success', 'Saved successfully')

8.3 Lookup/Search Flow

User types in search input (e.g., hotel name)
    ├── debounce(300ms)
    ├── GroupsAPI.hotelLookup(searchTerm)
    │   └── POST /hotel/hotelLookup → returns String[][] [name, id]
    │       └── Transform to [{hotelName, hotelId}]
    ├── Populate <datalist> with options
    └── User selects option
        ├── Set hidden ID field (hotelRefId)
        ├── Set hidden name field (hotelName)
        └── Apply .lookup-matched CSS class

8.4 Financial Summary Flow

groups-summary.js → createFinancialSummary()
    ├── Iterate groupRoomList:
    │   └── Group by hotel + cost/price → ROOM-{hotelId}-{cost}-{price}
    ├── Iterate groupTransportList:
    │   └── One line per transport → TXP-{transportId}
    ├── Iterate groupActivityList:
    │   ├── Adult line → ACT-{serviceId}-A
    │   └── Child line → ACT-{serviceId}-C (if numChildren > 0)
    ├── renderFinancialItems()
    │   └── Table rows with: include checkbox, description, qty, cost, price, margin
    └── updateTotals() → sum included items
        ├── totalCost, totalPrice, totalMargin
        └── User toggles include → recalculate

8.5 Room-Passenger Assignment Flow

User opens room modal → existing room loads assigned passengers
    ├── Select unassigned passenger from dropdown → addPaxToRoom()
    │   └── Track in actionMap: paxId → "A" (add)
    ├── Click remove on assigned passenger → removePaxFromRoom()
    │   └── Track in actionMap: paxId → "D" (delete)
    └── User clicks Save Room
        ├── GroupsAPI.saveRoom(room) → save room first
        │   └── Returns roomId (especially important for new rooms)
        ├── GroupsAPI.updateRoomPaxList(roomId, groupId, hotelId, toAdd, toRemove)
        │   └── POST /groups/triproom/updatepaxlist
        │       └── GroupManagerFacade.updateRoomPaxList()
        │           ├── INSERT CTripRoomPax for paxToAdd
        │           └── DELETE CTripRoomPax for paxToRemove
        └── Refresh room cards display

8.6 Calendar Rendering Flow

groups-calendar.js → renderCalendar()
    ├── buildEventsMap() → Map<dateKey, events[]>
    │   ├── Hotel check-ins/check-outs
    │   ├── Transport start times
    │   └── Activity start times
    ├── Render month grid (7 columns)
    │   ├── Day headers (Sun-Sat)
    │   ├── Empty leading cells
    │   ├── Day cells with:
    │   │   ├── Day number
    │   │   ├── Trip-day background (if within arrival-departure range)
    │   │   ├── Today highlight
    │   │   └── Event pills (max 4, then "+N more")
    │   └── Empty trailing cells
    └── Event click → showEventDetails(type, id)
        └── Modal with entity-specific details and "Edit" link to detail page

9. Code Organization

9.1 Directory Structure

tqpro/
├── tqweb-adm/                                    # Frontend
│   ├── groups-list.html                          # Group listing page
│   ├── groups-summary.html                       # Group dashboard
│   ├── groups-calendar.html                      # Calendar view
│   ├── groups-passengers.html                    # Passenger management
│   ├── groups-accommodation.html                 # Hotel management
│   ├── groups-rooming.html                       # Room & pax assignment
│   ├── groups-transport.html                     # Transport management
│   ├── groups-activities.html                    # Activity management
│   ├── quote.html                                # Quote print page
│   ├── css/
│   │   └── groups.css                            # Custom Bootstrap 5 stylesheet
│   └── js/modules/
│       ├── groups-core.js                        # Shared: models, API, FormBinder, utils
│       ├── groups-list.js                        # List page logic
│       ├── groups-summary.js                     # Summary page logic
│       ├── groups-calendar.js                    # Calendar logic
│       ├── groups-passengers.js                  # Passenger page logic
│       ├── groups-accommodation.js               # Hotel page logic
│       ├── groups-rooming.js                     # Rooming page logic
│       ├── groups-transport.js                   # Transport page logic
│       ├── groups-activities.js                  # Activities page logic
│       └── globals.js                            # Global: tlinq(), getUserSession()
├── tqapi/src/main/java/com/perun/tlinq/api/
│   └── GroupApi.java                             # REST endpoints
├── tqapp/src/main/java/com/perun/tlinq/entity/group/
│   ├── GroupManagerFacade.java                   # Business logic facade
│   ├── CTripGroup.java                           # Group entity
│   ├── CTripPax.java                             # Passenger entity
│   ├── CTripHotel.java                           # Hotel entity
│   ├── CTripRoom.java                            # Room entity
│   ├── CTripService.java                         # Activity entity
│   ├── CTripTransport.java                       # Transport entity
│   ├── CTripRoomPax.java                         # Room-pax junction
│   ├── CServiceExtraCost.java                    # Activity extra cost entity
│   ├── CServiceTicketReq.java                    # Activity ticket requirement entity
│   ├── CTripDataMap.java                         # Expanded data container
│   ├── CTripHotelRoomMap.java                    # Hotel-room wrapper
│   ├── CTripRoomPaxMap.java                      # Room-pax wrapper
│   ├── CTripRoomPaxUpdate.java                   # Batch update DTO
│   ├── GroupQuotationPdfRenderer.java            # PDF quotation generator
│   ├── PaxExcelService.java                      # Excel import/template
│   ├── ScheduleExcelService.java                 # Schedule Excel export
│   ├── PaxImportResult.java                      # Import result DTO
│   └── PaxRowError.java                          # Import error DTO
└── config/
    ├── tourlinq-config.xml                        # App properties (schedule.default-range)
    ├── tourlinq.properties                        # Company info (tqpro.company.*)
    ├── entities/group-entities.xml                # Entity-to-DB field mappings (11 entities)
    ├── nts-client.xml                             # NTS service definitions
    └── templates/group/pdf-quotation-template.html # PDF quotation HTML template

9.2 Naming Conventions

Java: - Entity classes: CTrip* prefix (e.g., CTripGroup, CTripPax) - Facades: *Facade suffix (e.g., GroupManagerFacade) - API classes: *Api suffix (e.g., GroupApi) - Methods: camelCase (getPartnerGroups, loadTripData)

JavaScript: - Classes: PascalCase (GroupTrip, TripPax, FormBinder) - API object: GroupsAPI (singleton) - Utility object: GroupUtils (singleton) - Functions: camelCase (initializePage, loadGroupData, saveHotel) - Module state: camelCase (currentGroup, hotelList, formBinder) - Constants: UPPER_SNAKE_CASE (GROUP_STATUS, VEHICLE_TYPES)

HTML: - Page files: groups-*.html (kebab-case) - Element IDs: camelCase (hotelSearch, btnSaveHotel, activityForm) - CSS classes: kebab-case (content-card, calendar-day, data-table) - Data attributes: data-entity-name, data-entity-field

9.3 Module Dependencies

globals.js (tlinq, getUserSession)
groups-core.js (GroupsAPI, models, FormBinder, GroupUtils, constants)
groups-list.js ─────────┐
groups-summary.js ──────┤
groups-calendar.js ─────┤
groups-passengers.js ───┤  (each imports from groups-core.js)
groups-accommodation.js ┤
groups-rooming.js ──────┤
groups-transport.js ────┤
groups-activities.js ───┘

10. Implementation Patterns

10.1 Session Management

Session tokens are managed transparently by GroupsAPI:

// Frontend: GroupsAPI automatically includes session
const data = await GroupsAPI.loadGroupExpanded(groupId);
// Internally: tlinq('groups/group/loadExpanded', { session: getUserSession(), groupId })

// API layer: extracts session from body or header
String session = ApiUtil.gmp(reqData, "session", String.class, false);
if (TypeUtil.isEmptyString(session)) {
    session = headers.getHeaderString("X-Auth-Request-Access-Token");
}

// Business layer: session passed to facade constructor
GroupManagerFacade gmf = new GroupManagerFacade(session);

10.2 Inter-Page State via sessionStorage

Pages share state through sessionStorage with structured keys:

Key Purpose TTL
groups_currentGroupId Active group ID Persistent
groups_currentGroup Serialized GroupTrip object 5 minutes
groups_currentGroupTime Cache timestamp -
groups_hotelContext Hotel context for rooming page Persistent
quoteItems Financial items for quote page Persistent
groupInfo Group info for quote page Persistent

10.3 FormBinder Data Binding

The FormBinder provides declarative two-way binding:

Populate form from object:

const formBinder = new FormBinder('#hotelModal');
formBinder.populate('hotelRecord', selectedHotel);
// Finds all elements with data-entity-name="hotelRecord"
// Sets each element's value from the corresponding data-entity-field

Extract object from form:

const data = formBinder.extract('hotelRecord');
// Returns { tripHotelId: "123", groupId: "456", hotelName: "Grand Hotel", ... }

Clear form:

formBinder.clear('hotelRecord');
// Clears all bound fields, unchecks checkboxes

10.4 Currency Auto-Population

When creating new entities, the currency is automatically set from the group's currencyCode:

// In openAddHotelModal():
if (currentGroup?.currencyCode) {
    $('#hotelCurrency').val(currentGroup.currencyCode);
}

// In openAddTransportModal():
if (currentGroup?.currencyCode) {
    $('#transportCurrency').val(currentGroup.currencyCode);
}

// In openAddActivityModal():
if (currentGroup?.currencyCode) {
    $('#activityCurrency').val(currentGroup.currencyCode);
}

10.5 Debounced Search/Lookup

All lookup inputs use debounced API calls to prevent excessive requests:

import { debounce } from './globals.js';

$('#hotelSearch').on('input', debounce(onHotelSearch, 300));

async function onHotelSearch() {
    const term = $('#hotelSearch').val();
    if (term.length < 2) return;
    if (term === lastHotelSearch) return;  // skip duplicate searches
    lastHotelSearch = term;

    const results = await GroupsAPI.hotelLookup(term);
    hotelLookupResults = results;

    // Populate datalist
    const options = results.map(r => `<option value="${r.hotelName}">`).join('');
    $('#hotelList').html(options);
}

10.6 Visual Lookup Feedback

Search inputs provide visual cues about match status:

.lookup-matched { border-color: var(--groups-success) !important; }
.lookup-unmatched { border-color: var(--groups-danger) !important; }
function onHotelInputCheck() {
    const val = $('#hotelSearch').val();
    const match = hotelLookupResults.find(r => r.hotelName === val);
    if (match) {
        $('#hotelSearch').removeClass('lookup-unmatched').addClass('lookup-matched');
        $('#hotelRefId').val(match.hotelId);
        $('#hotelName').val(match.hotelName);
    } else {
        $('#hotelSearch').removeClass('lookup-matched').addClass('lookup-unmatched');
        $('#hotelRefId').val('');
    }
}

10.7 Error Handling

Consistent error handling across all pages:

try {
    const data = await GroupsAPI.saveHotel(hotelData);
    GroupUtils.showAlert('success', 'Hotel saved successfully');
} catch (error) {
    console.error('Error saving hotel:', error);
    GroupUtils.showAlert('error', 'Failed to save hotel: ' + (error.errorMessage || error.message || 'Unknown error'));
}

Backend wraps all exceptions in TlinqApiResponse:

try {
    ar = doWriteGroup(reqData);
} catch (TlinqClientException e) {
    ar = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
} catch (Exception ex) {
    ar = new TlinqApiResponse(TlinqErr.GENERAL, ex.getMessage());
}

10.8 XSS Prevention

All user-generated content is escaped before rendering:

// GroupUtils.escapeHtml() used everywhere
`<td>${GroupUtils.escapeHtml(hotel.hotelName)}</td>`

// Implementation
escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

11. Appendix

11.1 Migration from Legacy Implementation

The current implementation replaces the legacy monolithic design:

Aspect Legacy (v1.0) Current (v2.0)
HTML Single groupman.html with tabbed UI 8 dedicated pages
CSS Framework Foundation 6.x Bootstrap 5.3
JS Module Single groups.js (1960 lines) 9 focused modules (~100-500 lines each)
Data Binding PageData with data-field-name FormBinder with data-entity-field
Modals Foundation Reveal Bootstrap Modal
Notifications jQuery Notify plugin Bootstrap 5 alerts
Navigation Tab switching within single page Page navigation with URL params + sessionStorage
Icons Font Awesome Bootstrap Icons
Autocomplete jQuery UI Autocomplete HTML5 datalist with debounced API
Excel Import Not available PaxExcelService with template
Calendar Simple day grid (arrival to departure) Full month calendar with navigation

Legacy files (groupman.html, groupman2.html) are retained for reference but are not part of the active application.

11.2 Key Technologies Summary

Technology Version Purpose
JavaScript ES6+ Frontend logic (modules, async/await, classes)
Java 17 Backend logic
Jakarta EE 9+ Enterprise features
JAX-RS / Jersey 3.0+ RESTful API
jQuery 3.7.1 DOM manipulation
Bootstrap 5.3 CSS framework and components
Bootstrap Icons 1.11 Icon library
GSON 2.10.1 JSON serialization (TypeUtil)

11.3 Database Tables

Table Entity NTS Native Entity
trip_group CTripGroup TripgroupEntity
trip_pax CTripPax TrippaxEntity
trip_hotel CTripHotel TriphotelEntity
room_list CTripRoom RoomlistEntity
trip_service CTripService TripserviceEntity
grp_transport CTripTransport GrpxportEntity
trip_room_pax CTripRoomPax NTSTripRoomPax
serviceextracost CServiceExtraCost ServiceExtracostEntity
serviceticketreq CServiceTicketReq ServiceTicketreqEntity

11.4 Entity Configuration Summary

Entity Config File Read Service Write Service Delete Service
TripGroup group-entities.xml readTripGroup saveTripGroup -
TripPax group-entities.xml readTripPax saveTripPax -
TripHotel group-entities.xml readTripHotel saveTripHotel -
TripTransport group-entities.xml readTransport saveTransport -
TripRoom group-entities.xml readTripRoom saveTripRoom deleteTripRoom (custom)
TripRoomPax group-entities.xml readTripRoomPax (custom) saveTripRoomPax (custom) deleteTripRoomPax (custom)
TripService group-entities.xml readTripService saveTripService deleteTripService
ServiceExtraCost group-entities.xml readServiceExtraCost saveServiceExtraCost deleteServiceExtraCost
ServiceTicketReq group-entities.xml readServiceTicketReq saveServiceTicketReq deleteServiceTicketReq

Custom services use NTSGroupService class with specific method implementations. The countFulfilledTickets custom service on ServiceTicketReq queries nts.offlineticket joined with nts.ticketbatch to count fulfilled tickets per attraction.

Thread Safety and Session Management (TQ-52)

The following thread safety improvements were applied to group management components:

Component Improvement
NTSGroupService Hibernate sessions are now always closed in finally blocks to prevent session leaks. Transactions include proper rollback() on exception.
CartHolder Changed from HashMap to ConcurrentHashMap for thread-safe cart storage across concurrent requests.
GroupManagerFacade Session cleanup follows try-finally pattern; shared static fields use volatile for visibility across threads.

Document End

This implementation documentation reflects the Inbound Group Management system as of 2026-02-22. Updated with TQ-55 schedule table, schedule Excel export, and compact card strip on groups-list page. For requirement specifications, see Group_Management_Requirements.md. For API specifications, see API-Spec-Group.md.