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¶
- System Architecture
- Module Structure
- Data Model Implementation
- API Implementation
- Frontend Implementation
- Business Logic Layer
- User Interface Components
- Data Flow and Interactions
- Code Organization
- 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:
TlinqApiResponsewithapiStatusandapiDatafields - Client Extraction:
tlinq()function resolves promises directly withapiDataand 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 | |
| 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:
/group/loadExpanded - Request:
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:
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:
/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:
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:
/exportSchedule - Request:
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):
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()callingGroupsAPI.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()
- Loads expanded data for all groups in parallel via
- Schedule Excel export:
downloadScheduleExcel()usesfetch()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
sessionStoragefor downstream pages - Configuration:
schedule.default-rangeproperty intourlinq-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, opensquote.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 aMap<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
numPaxcount - Excel import: file upload via FileReader API, base64 encoding,
PaxExcelServicebackend - 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
hotelLookupAPI and datalist - Automatic nights calculation from check-in/check-out dates
- Currency select (AED/EUR/USD) auto-populated from group's
currencyCodeon new hotel - Navigation to rooming list page per hotel
- Visual cues on search input:
.lookup-matched/.lookup-unmatchedCSS 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 viaupdateRoomPaxListAPI
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
companyLookupand 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
currencyto match backendCTripService.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: currencyCode → currencycode
- CTripHotel: currencyCode → currencycode
- CTripTransport: currencyCode → currencycode
- CTripService: currency → currencycode (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:
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.