Skip to content

Developer Guide: Adding New Features to TQPRO

Overview

This guide provides a step-by-step approach to adding new features to the TQPRO system, based on the Trip Offer Management feature implementation. It covers the complete development lifecycle from database design to frontend implementation.

Table of Contents

  1. Architecture Overview
  2. Prerequisites
  3. Step 1: Database Design
  4. Step 2: Create Database Entity Classes
  5. Step 3: Create Canonical Entity Classes
  6. Step 4: Create Business Logic Facade
  7. Step 5: Create REST API Endpoints
  8. Step 6: Configure NTS Service Plugin (NEW)
  9. Step 7: Configure Entity Mappings
  10. Step 8: Create Frontend HTML Page
  11. Step 9: Create JavaScript Module
  12. Step 10: Testing
  13. Step 11: Documentation
  14. Best Practices
  15. Common Patterns

Architecture Overview

TQPRO follows a layered architecture:

┌─────────────────────────────────────────┐
│         Frontend Layer                  │
│  HTML + JavaScript + Foundation CSS     │
└─────────────────────────────────────────┘
                  ↓ REST API
┌─────────────────────────────────────────┐
│           API Layer                     │
│  REST Endpoints (TripOfferApi)          │
│  - Parameter extraction                 │
│  - Exception handling                   │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│         Facade Layer                    │
│  Business Logic (TripOfferFacade)       │
│  - Validation                           │
│  - Business rules                       │
│  - Transaction management               │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│      Canonical Entity Layer             │
│  Platform-independent entities          │
│  (CTripPage, CTripSnippet)              │
└─────────────────────────────────────────┘
                  ↓ EntityFacade + Config
┌─────────────────────────────────────────┐    ┌─────────────────────────────────────────┐
│       Native Entity Layer               │    │     Plugin management layer             │
│  Database-specific entities             │ -> │     (integration to external systems    │
│  (TrippageEntity, TripsnippetEntity)    │    │  Payment GW, Ticketing, Airline GDS     │
└─────────────────────────────────────────┘    └─────────────────────────────────────────┘
                  ↓ Hibernate/JPA
┌─────────────────────────────────────────┐
│         Database Layer                  │
│  PostgreSQL (nts schema)                │
└─────────────────────────────────────────┘

Prerequisites

Before starting, ensure you have:

  • ✅ Access to the TQPRO codebase
  • ✅ PostgreSQL database access
  • ✅ Java Development Kit (JDK) 17+
  • ✅ Gradle build tool
  • ✅ Understanding of:
  • Java, JPA/Hibernate
  • REST APIs
  • JavaScript (ES6+)
  • HTML/CSS (Foundation framework)

Step 1: Database Design

1.1 Design Your Schema

Create tables in the nts schema. Consider: - Primary keys (use sequences) - Foreign keys for relationships - Indexes for performance - Column constraints

Example: Trip Offer Management Tables

-- Main entity table
CREATE TABLE IF NOT EXISTS nts.trip_page (
  page_id INTEGER NOT NULL,
  page_name VARCHAR(200) NOT NULL,
  page_desc VARCHAR(1000) NOT NULL,
  CONSTRAINT trip_page_pk PRIMARY KEY(page_id)
);

-- Detail entity table with foreign key
CREATE TABLE IF NOT EXISTS nts.trip_snippet (
  snippet_id INTEGER NOT NULL,
  page_id INTEGER NOT NULL,
  code VARCHAR(20),
  title VARCHAR(50),
  description VARCHAR(1000),
  active INTEGER,
  CONSTRAINT trip_snippet_code_uindex UNIQUE(code),
  CONSTRAINT trip_snippet_pk PRIMARY KEY(snippet_id),
  CONSTRAINT trip_snippet_trip_page_page_id_fk FOREIGN KEY (page_id)
    REFERENCES nts.trip_page(page_id)
);

1.2 Create SQL Script

Location: doc/your_feature_schema.sql

Include: - Table creation statements - Sequence creation (if not managed by Hibernate) - Sample data for testing - Comments for documentation

-- Create sequences
CREATE SEQUENCE IF NOT EXISTS nts.trip_page_seq START 1;
CREATE SEQUENCE IF NOT EXISTS nts.trip_snippet_seq START 1;

-- Add comments
COMMENT ON TABLE nts.trip_page IS 'Stores destination pages';
COMMENT ON COLUMN nts.trip_page.page_id IS 'Unique identifier (sequence-managed)';

1.3 Apply to Database

psql -h hostname -U username -d database -f doc/your_feature_schema.sql

Step 2: Create Database Entity Classes

Database entities map directly to database tables using Hibernate/JPA.

2.1 Create Package Structure

Location: tqapp/src/main/java/com/perun/tlinq/client/nts/db/[feature_name]/

mkdir -p tqapp/src/main/java/com/perun/tlinq/client/nts/db/trip

2.2 Create Entity Class

Naming Convention: [TableName]Entity.java (e.g., TrippageEntity.java)

Template:

package com.perun.tlinq.client.nts.db.trip;

import com.perun.tlinq.client.nts.entity.NTSEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

/**
 * Hibernate entity for nts.trip_page table
 */
@Entity
@Table(name = "trip_page", schema = "nts")
public class TrippageEntity extends NTSEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "trip_page_id_gen")
    @SequenceGenerator(name = "trip_page_id_gen", sequenceName = "nts.trip_page_seq", allocationSize = 1)
    @Column(name = "page_id", nullable = false)
    private Integer pageId;

    @Size(max = 200)
    @NotNull
    @Column(name = "page_name", nullable = false, length = 200)
    private String pageName;

    @Size(max = 1000)
    @NotNull
    @Column(name = "page_desc", nullable = false, length = 1000)
    private String pageDesc;

    // Must override getId() from NTSEntity
    @Override
    public Integer getId() {
        return pageId;
    }

    public void setId(Integer pageId) {
        this.pageId = pageId;
    }

    // Getters and setters for all fields
    public Integer getPageId() {
        return pageId;
    }

    public void setPageId(Integer pageId) {
        this.pageId = pageId;
    }

    public String getPageName() {
        return pageName;
    }

    public void setPageName(String pageName) {
        this.pageName = pageName;
    }

    public String getPageDesc() {
        return pageDesc;
    }

    public void setPageDesc(String pageDesc) {
        this.pageDesc = pageDesc;
    }
}

2.3 Key Points

Extend NTSEntity: All NTS database entities must extend NTSEntityAnnotations: - @Entity - Marks as JPA entity - @Table - Specifies table name and schema - @Id - Primary key field - @GeneratedValue - Auto-generated values - @SequenceGenerator - Sequence configuration - @Column - Column mapping - @Size, @NotNull - Validation constraints

Naming: Database column names (lowercase with underscores) ✅ Override getId(): Required by NTSEntity


Step 3: Create Canonical Entity Classes

Canonical entities are platform-independent business objects.

3.1 Create Package Structure

Location: tqapp/src/main/java/com/perun/tlinq/entity/[feature_name]/

mkdir -p tqapp/src/main/java/com/perun/tlinq/entity/trip

3.2 Create Canonical Class

Naming Convention: C[EntityName].java (e.g., CTripPage.java)

Template:

package com.perun.tlinq.entity.trip;

import com.perun.tlinq.entity.TlinqEntity;
import java.io.Serializable;

/**
 * Canonical entity for trip destination pages
 */
public class CTripPage extends TlinqEntity implements Serializable {

    private Integer pageId;
    private String pageName;
    private String pageDesc;

    // Getters and setters
    public Integer getPageId() {
        return pageId;
    }

    public void setPageId(Integer pageId) {
        this.pageId = pageId;
    }

    public String getPageName() {
        return pageName;
    }

    public void setPageName(String pageName) {
        this.pageName = pageName;
    }

    public String getPageDesc() {
        return pageDesc;
    }

    public void setPageDesc(String pageDesc) {
        this.pageDesc = pageDesc;
    }

    @Override
    public String toString() {
        return "CTripPage{" +
                "pageId=" + pageId +
                ", pageName='" + pageName + '\'' +
                ", pageDesc='" + pageDesc + '\'' +
                '}';
    }
}

3.3 Key Points

Extend TlinqEntity: All canonical entities extend TlinqEntityImplement Serializable: Required for caching and serialization ✅ No Annotations: Plain Java objects (POJOs) ✅ Business Names: Use camelCase, business-friendly names ✅ toString(): Override for debugging


Step 4: Create Business Logic Facade

The facade centralizes all business logic for your feature.

4.1 Create Facade Class

Location: tqapp/src/main/java/com/perun/tlinq/entity/[feature_name]/[Feature]Facade.java

Template:

package com.perun.tlinq.entity.trip;

import com.perun.tlinq.client.nts.service.NTSServiceFactory;
import com.perun.tlinq.config.SelectCriteriaList;
import com.perun.tlinq.config.ServiceFactoryConfig;
import com.perun.tlinq.entity.EntityFacade;
import com.perun.tlinq.entity.TlinqEntity;
import com.perun.tlinq.util.ClientConfig;
import com.perun.tlinq.util.TlinqClientException;
import com.perun.tlinq.util.TlinqErr;
import com.perun.tlinq.util.TypeUtil;

import java.util.*;
import java.util.logging.Logger;

/**
 * Facade class for managing trip offers
 */
public class TripOfferFacade extends EntityFacade {

    private static final String FACTORY = NTSServiceFactory.FACTORY_NAME;
    private static volatile String sysSession;

    static {
        try {
            ServiceFactoryConfig sf = ClientConfig.instance().getDefaultFactory();
            sysSession = sf.getPropertyList().getProperty("system.session");
        } catch (Exception ex) {
            Logger.getLogger(TripOfferFacade.class.getName()).severe("Cannot get default system session ID!");
        }
    }

    public TripOfferFacade(String token) {
        super(TypeUtil.isEmptyString(token) ? sysSession : token);
    }

    @Override
    public Object write(TlinqEntity entity) throws TlinqClientException {
        entity.setFactoryName(FACTORY);
        return super.write(entity);
    }

    // ==================== CRUD OPERATIONS ====================

    /**
     * List all pages with optional pagination
     */
    @SuppressWarnings("unchecked")
    public List<CTripPage> listPages(Integer page, Integer pageSize) throws TlinqClientException {
        SelectCriteriaList criteria = new SelectCriteriaList();
        List<CTripPage> pages = (List<CTripPage>) search(FACTORY, "TripPage", criteria);

        if (pages == null) {
            return Collections.emptyList();
        }

        // Apply pagination if requested
        if (page != null && pageSize != null) {
            int start = page * pageSize;
            int end = Math.min(start + pageSize, pages.size());
            if (start < pages.size()) {
                return pages.subList(start, end);
            } else {
                return Collections.emptyList();
            }
        }

        return pages;
    }

    /**
     * Read a single page by ID
     */
    @SuppressWarnings("unchecked")
    public CTripPage readPage(Integer pageId) throws TlinqClientException {
        if (pageId == null) {
            throw new TlinqClientException(TlinqErr.MISSING_PARAMETER, "Page ID is required");
        }

        List<CTripPage> results = (List<CTripPage>) read(FACTORY, "TripPage", pageId);

        if (results != null && !results.isEmpty()) {
            return results.get(0);
        } else {
            throw new TlinqClientException(TlinqErr.ENTITY_NOT_FOUND, "Page not found");
        }
    }

    /**
     * Create or update a page
     */
    public CTripPage writePage(CTripPage page) throws TlinqClientException {
        // Validate
        if (page == null) {
            throw new TlinqClientException(TlinqErr.MISSING_PARAMETER, "Page entity is required");
        }
        if (TypeUtil.isEmptyString(page.getPageName())) {
            throw new TlinqClientException(TlinqErr.MISSING_PARAMETER, "Page name is required");
        }

        // Business logic here

        return (CTripPage) write(page);
    }

    /**
     * Delete a page
     */
    public void deletePage(Integer pageId) throws TlinqClientException {
        if (pageId == null) {
            throw new TlinqClientException(TlinqErr.MISSING_PARAMETER, "Page ID is required");
        }
        deleteEntity(FACTORY, "TripPage", pageId);
    }

    // Add more methods as needed...
}

4.2 Key Points

Extend EntityFacade: Provides base CRUD operations ✅ System Session: Initialize default session for background tasks ✅ Override write(): Set factory name for entities ✅ Validation: Always validate inputs ✅ Type Safety: Return specific canonical types ✅ Error Handling: Use TlinqClientException with appropriate error codes ✅ Documentation: JavaDoc for all public methods

4.3 Common Methods Pattern

// List with criteria
@SuppressWarnings("unchecked")
public List<CEntity> listEntities(SelectCriteriaList criteria) {
    List<CEntity> results = (List<CEntity>) search(FACTORY, "EntityName", criteria);
    return (results == null ? Collections.emptyList() : results);
}

// Read by ID
@SuppressWarnings("unchecked")
public CEntity readEntity(Integer id) {
    List<CEntity> results = (List<CEntity>) read(FACTORY, "EntityName", id);
    if (results != null && !results.isEmpty()) {
        return results.get(0);
    }
    throw new TlinqClientException(TlinqErr.ENTITY_NOT_FOUND, "Entity not found");
}

// Write (create/update)
public CEntity writeEntity(CEntity entity) {
    // Validation
    // Business logic
    return (CEntity) write(entity);
}

// Delete
public void deleteEntity(Integer id) {
    deleteEntity(FACTORY, "EntityName", id);
}

Step 5: Create REST API Endpoints

The API layer is a thin wrapper that delegates to the facade.

5.1 Create API Class

Location: tqapi/src/main/java/com/perun/tlinq/api/[Feature]Api.java

Template:

package com.perun.tlinq.api;

import com.perun.tlinq.api.entity.TlinqApiResponse;
import com.perun.tlinq.entity.trip.*;
import com.perun.tlinq.util.TlinqClientException;
import com.perun.tlinq.util.TlinqErr;
import com.perun.tlinq.util.TypeUtil;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.*;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * REST API for Trip Offer Management
 */
@Path("/tripoffer")
public class TripOfferApi {

    private static final Logger logger = Logger.getLogger(TripOfferApi.class.getName());

    // ==================== ENDPOINT METHODS ====================

    @POST
    @Path("/page/list")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response listPages(Map reqData, @Context HttpHeaders headers) {
        Response resp;
        TlinqApiResponse ar;
        String session = null;

        try {
            // Extract session from request or header
            session = ApiUtil.gmp(reqData, "session", String.class, false);
            if(TypeUtil.isEmptyString(session)) {
                session = headers.getHeaderString("X-Auth-Request-Access-Token");
                reqData.put("session", session);
            }
            logger.log(Level.INFO, "BEGIN APICALL listPages for {0}",
                      TypeUtil.isEmptyString(session) ? "NULL" : session);

            // Delegate to implementation method
            ar = doListPages(reqData);

        } catch (TlinqClientException e) {
            ar = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
        } catch (Exception ex) {
            ar = new TlinqApiResponse(TlinqErr.GENERAL,
                    ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
        }

        logger.log(Level.INFO, "END APICALL listPages for {0}",
                  TypeUtil.isEmptyString(session) ? "NULL" : session);
        resp = Response.ok(ar).build();
        return resp;
    }

    @POST
    @Path("/page/write")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response writePage(Map reqData) {
        Response resp;
        TlinqApiResponse ar;
        String session = null;

        try {
            session = ApiUtil.gmp(reqData, "session", String.class, false);
            logger.log(Level.INFO, "BEGIN APICALL writePage for {0}",
                      TypeUtil.isEmptyString(session) ? "NULL" : session);
            ar = doWritePage(reqData);
        } catch (TlinqClientException e) {
            ar = new TlinqApiResponse(e.getErrorCode(), e.getMessage());
        } catch (Exception ex) {
            ar = new TlinqApiResponse(TlinqErr.GENERAL,
                    ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
        }

        logger.log(Level.INFO, "END APICALL writePage for {0}",
                  TypeUtil.isEmptyString(session) ? "NULL" : session);
        resp = Response.ok(ar).build();
        return resp;
    }

    // ==================== IMPLEMENTATION METHODS ====================

    private TlinqApiResponse doListPages(Map reqData) throws TlinqClientException {
        // Extract parameters
        String session = ApiUtil.gmp(reqData, "session", String.class, false);
        Integer page = ApiUtil.gmp(reqData, "page", Integer.class, false);
        Integer pageSize = ApiUtil.gmp(reqData, "pageSize", Integer.class, false);

        // Call facade
        TripOfferFacade facade = new TripOfferFacade(session);
        List<CTripPage> results = facade.listPages(page, pageSize);

        // Return response
        return new TlinqApiResponse(results);
    }

    private TlinqApiResponse doWritePage(Map reqData) throws TlinqClientException {
        // Extract session and remove from data
        String session = ApiUtil.gmp(reqData, "session", String.class, false);
        reqData.remove("session");

        // Convert JSON to entity
        String jsonStr = TypeUtil.extractJsonFromMap(reqData);
        CTripPage page = TypeUtil.extractFromJson(jsonStr, CTripPage.class);

        // Call facade
        TripOfferFacade facade = new TripOfferFacade(session);
        CTripPage savedPage = facade.writePage(page);

        // Return response
        return new TlinqApiResponse(savedPage);
    }
}

5.2 Key Points

Path Annotation: Use @Path to define base URL ✅ HTTP Method: Use @POST (TQPRO standard for all operations) ✅ Consumes/Produces: Always JSON ✅ Exception Handling: Catch both TlinqClientException and general exceptions ✅ Logging: Log begin/end of API calls ✅ Authentication: The API layer supports a three-tier auth pipeline (JWT Bearer → proxy headers → guest session). See API Specification for details. ✅ Session Management: Extract from request body or header ✅ Delegation: Endpoint methods delegate to doXXX methods ✅ Parameter Extraction: Use ApiUtil.gmp() helper

5.3 URL Naming Convention

/api/[feature]/[entity]/[action]

Examples:
/api/tripoffer/page/list
/api/tripoffer/page/write
/api/tripoffer/page/read
/api/tripoffer/page/delete
/api/tripoffer/snippet/list

Step 6: Configure NTS Service Plugin

The NTS Service Plugin configuration (nts-client.xml) maps service names to their implementation classes, methods, and entities. This is a critical step that is separate from entity mappings and must be completed for all CRUD operations to work.

6.1 Understanding the Configuration

File: config/nts-client.xml

This file defines how the NTSServiceFactory executes service calls. Each service entry maps: - A service name (referenced in tourlinq-config.xml) - To an implementation class - With a specific method and entity type

<NTSPluginConfig>
    <PluginProperties>
        <property name="dbname" value="lab"/>
        <property name="weekstart" value="SUN"/>
    </PluginProperties>

    <Services>
        <!-- Service definitions here -->
    </Services>
</NTSPluginConfig>

6.2 Standard Service Types

There are four standard service classes for basic CRUD operations:

Service Class Purpose Method Attribute
NTSEntityReadService Read by ID, Search Empty (uses default)
NTSEntityWriteService Create, Update Empty (uses default)
NTSEntityDeleteService Delete by ID Empty (uses default)
NTSCustomService Custom business logic Specific method name

6.3 Add Service Configurations

For each entity, add service entries for read, write, and delete operations:

<!-- Read/Search Service -->
<Service name="readTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityReadService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

<!-- Write (Create/Update) Service -->
<Service name="saveTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityWriteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

<!-- Delete Service -->
<Service name="deleteTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

6.4 Service Attributes Reference

Attribute Description Example
name Service name (must match tourlinq-config.xml) saveTripPage
class Fully qualified service implementation class com.perun.tlinq.client.nts.service.NTSEntityWriteService
method Method to invoke (empty for standard services) "" or "customMethod"
entity Native entity class (JPA entity) com.perun.tlinq.client.nts.db.trip.TrippageEntity
idField Primary key field name in native entity id

6.5 Custom Service Configuration

For operations beyond basic CRUD (complex searches, calculations, bulk operations), use custom services:

<!-- Custom service with specific method -->
<Service name="searchTripOffers"
         class="com.perun.tlinq.client.nts.service.trip.NTSTripService"
         method="searchOffers"
         entity="com.perun.tlinq.client.nts.entity.trip.NTSTripSearchResult"
         idField=""/>

<!-- Custom service for tree/hierarchical data -->
<Service name="getTripTree"
         class="com.perun.tlinq.client.nts.service.trip.NTSTripService"
         method="getTripTree"
         entity="com.perun.tlinq.client.nts.entity.trip.NTSTripTreeNode"
         idField=""/>

6.6 Create Custom Service Class

When using custom services, create the service implementation class:

Location: tqapp/src/main/java/com/perun/tlinq/client/nts/service/[feature]/NTS[Feature]Service.java

package com.perun.tlinq.client.nts.service.trip;

import com.perun.tlinq.client.nts.service.NTSCustomService;
import com.perun.tlinq.client.nts.entity.trip.*;
import jakarta.persistence.EntityManager;
import java.util.*;

/**
 * Custom NTS service for Trip operations
 */
public class NTSTripService extends NTSCustomService {

    /**
     * Custom search method
     * @param params Map of search parameters from the request
     * @return List of search results
     */
    public List<NTSTripSearchResult> searchOffers(Map<String, Object> params) {
        EntityManager em = getEntityManager();

        // Extract parameters
        String destination = (String) params.get("destination");
        Integer minDuration = (Integer) params.get("minDuration");

        // Build and execute query
        // ...

        return results;
    }

    /**
     * Get hierarchical tree data
     */
    public List<NTSTripTreeNode> getTripTree(Map<String, Object> params) {
        boolean includeInactive = params.containsKey("includeInactive")
            && (Boolean) params.get("includeInactive");

        // Build tree structure
        // ...

        return tree;
    }
}

6.7 Create NTS Native Entity Classes (for Custom Services)

Custom services often return data structures different from JPA entities. Create NTS native entity classes:

Location: tqapp/src/main/java/com/perun/tlinq/client/nts/entity/[feature]/

package com.perun.tlinq.client.nts.entity.trip;

import com.perun.tlinq.entity.RemoteEntityI;
import java.util.List;

/**
 * NTS native entity for trip search results
 */
public class NTSTripSearchResult implements RemoteEntityI {

    private Integer tripId;
    private String tripName;
    private String destination;
    private Integer duration;
    private Double lowestPrice;

    // Getters and setters
    public Integer getTripId() { return tripId; }
    public void setTripId(Integer tripId) { this.tripId = tripId; }

    public String getTripName() { return tripName; }
    public void setTripName(String tripName) { this.tripName = tripName; }

    // ... other getters/setters
}

/**
 * NTS native entity for hierarchical tree node
 */
public class NTSTripTreeNode implements RemoteEntityI {

    private Integer nodeId;
    private String nodeName;
    private String nodeType;  // "category", "destination", "trip"
    private List<NTSTripTreeNode> children;

    // Getters and setters
}

6.8 Key Points

Service names must match: The name attribute in nts-client.xml must exactly match the service names in tourlinq-config.xml

Delete services are required: Without a delete service configuration, deleteEntity() calls in the facade will fail

idField for standard services: Use the JPA entity's ID field name (usually id)

idField for custom services: Can be empty if the service doesn't operate on a single entity by ID

Custom service classes: Must extend NTSCustomService and implement methods matching the method attribute

NTS native entities: Must implement RemoteEntityI interface

6.9 Naming Conventions

Component Convention Example
Read service read[EntityName] readTripPage
Write service save[EntityName] saveTripPage
Delete service delete[EntityName] deleteTripPage
List service list[EntityName]s listTripPages
Custom service class NTS[Feature]Service NTSTripService
NTS native entity NTS[EntityName] NTSTripSearchResult

6.10 Complete Example

For a Trip Offer feature with standard CRUD plus custom search:

<!-- nts-client.xml additions -->

<!-- Trip Page - Standard CRUD -->
<Service name="readTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityReadService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

<Service name="saveTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityWriteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

<Service name="deleteTripPage"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TrippageEntity"
         idField="id"/>

<!-- Trip Snippet - Standard CRUD -->
<Service name="readTripSnippet"
         class="com.perun.tlinq.client.nts.service.NTSEntityReadService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TripsnippetEntity"
         idField="id"/>

<Service name="saveTripSnippet"
         class="com.perun.tlinq.client.nts.service.NTSEntityWriteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TripsnippetEntity"
         idField="id"/>

<Service name="deleteTripSnippet"
         class="com.perun.tlinq.client.nts.service.NTSEntityDeleteService"
         method=""
         entity="com.perun.tlinq.client.nts.db.trip.TripsnippetEntity"
         idField="id"/>

<!-- Custom Services -->
<Service name="searchTripOffers"
         class="com.perun.tlinq.client.nts.service.trip.NTSTripService"
         method="searchOffers"
         entity="com.perun.tlinq.client.nts.entity.trip.NTSTripSearchResult"
         idField=""/>

Step 7: Configure Entity Mappings

Entity mappings define how canonical entities map to native entities.

7.1 Locate Configuration File

File: config/tourlinq-config.xml

7.2 Add Entity Mapping

Find an appropriate location (e.g., after related entities) and add:

<!-- Trip Offer Management Entities -->

<Entity name="TripPage"
        class="com.perun.tlinq.entity.trip.CTripPage"
        idField="pageId"
        defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory"
                 nativeEntity="com.perun.tlinq.client.nts.db.trip.TrippageEntity">
            <ServiceList>
                <Service name="saveTripPage" action="create"/>
                <Service name="saveTripPage" action="update"/>
                <Service name="readTripPage" action="read"/>
                <Service name="readTripPage" action="search"/>
                <Service name="deleteTripPage" action="delete"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="pageId" sourceField="pageId" mapping="DirectMapping"/>
                <FieldMapping targetField="pageName" sourceField="pageName" mapping="DirectMapping"/>
                <FieldMapping targetField="pageDesc" sourceField="pageDesc" mapping="DirectMapping"/>
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

<Entity name="TripSnippet"
        class="com.perun.tlinq.entity.trip.CTripSnippet"
        idField="snippetId"
        defaultFactory="NTSServiceFactory">
    <EntityFactoryList>
        <Factory name="NTSServiceFactory"
                 nativeEntity="com.perun.tlinq.client.nts.db.trip.TripsnippetEntity">
            <ServiceList>
                <Service name="saveTripSnippet" action="create"/>
                <Service name="saveTripSnippet" action="update"/>
                <Service name="readTripSnippet" action="read"/>
                <Service name="readTripSnippet" action="search"/>
                <Service name="deleteTripSnippet" action="delete"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="snippetId" sourceField="snippetId" mapping="DirectMapping"/>
                <FieldMapping targetField="pageId" sourceField="pageId" mapping="DirectMapping"/>
                <FieldMapping targetField="code" sourceField="code" mapping="DirectMapping"/>
                <FieldMapping targetField="title" sourceField="title" mapping="DirectMapping"/>
                <FieldMapping targetField="description" sourceField="description" mapping="DirectMapping"/>
                <FieldMapping targetField="active" sourceField="active" mapping="DirectMapping"/>
                <!-- Add all other fields -->
            </FieldMappingList>
        </Factory>
    </EntityFactoryList>
</Entity>

7.3 Key Elements

Entity Attributes: - name: Entity name used in facade methods - class: Fully qualified canonical class name - idField: Primary key field name (canonical) - defaultFactory: Service factory to use

Factory Attributes: - name: Factory name (usually NTSServiceFactory) - nativeEntity: Fully qualified DB entity class name

Service Actions: - create: Insert new record - update: Update existing record - read: Read by ID - search: Search with criteria - delete: Delete by ID

Field Mapping Types: - DirectMapping: Simple field copy - ArrayMapping: Transform arrays - IndexMapping: Pick element from array - NestedMapping: Load related entity - ModelLookup: Search for parent entity - NoMapping: Skip field

7.4 Advanced Mapping Example

<!-- Foreign key with lookup -->
<FieldMapping targetField="partnerName" sourceField="partnerId"
              mapping="ModelLookup" reverseMapping="NoMapping">
    <ModelLookupMapping>
        <ParentEntity name="Customer" idField="customerId" valueField="fullName"/>
    </ModelLookupMapping>
</FieldMapping>

<!-- Array mapping -->
<FieldMapping targetField="items" targetFieldEntity="Item"
              sourceField="itemList" mapping="ArrayMapping"/>

<!-- Index mapping (get first element) -->
<FieldMapping targetField="primaryContact" sourceField="contacts" mapping="IndexMapping">
    <IndexMapping index="0"/>
</FieldMapping>

Step 8: Create Frontend HTML Page

Frontend pages use Foundation CSS framework and follow established design patterns.

8.1 Create HTML File

Location: tqweb-adm/[feature]man.html

Template Structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Feature Management</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- Foundation CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/css/foundation.min.css">
    <link rel="stylesheet" href="css/foundation-datepicker.css">
    <link rel="stylesheet" href="css/foundation-icons.css">
    <link rel="stylesheet" href="css/tqapp.css">
    <link href="css/fontawesome.min.css" rel="stylesheet">
    <link href="css/solid.css" rel="stylesheet">

    <style>
        /* Design system variables */
        :root {
            --primary-color: #362c5d;
            --primary-dark: #2a2149;
            --secondary-color: #FFC166;
            --success-color: #3adb76;
            --warning-color: #ffae00;
            --alert-color: #cc4b37;
            --light-bg: #f8f9fa;
            --border-color: #e1e4e8;
            --shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
            --transition: all 0.3s ease;
        }

        body {
            background-color: var(--light-bg);
            font-family: 'Inter', 'Noto Sans', sans-serif;
        }

        /* Content cards */
        .content-card {
            background: white;
            border-radius: 8px;
            box-shadow: var(--shadow-sm);
            margin-bottom: 20px;
            overflow: hidden;
        }

        .content-card-header {
            padding: 20px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .content-card-title {
            font-size: 1.25rem;
            font-weight: 600;
            color: #2d3748;
            margin: 0;
        }

        /* Add more styles as needed */
    </style>
</head>
<body>

    <!-- Include jQuery and Foundation -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/js/foundation.min.js"></script>
    <script src="js/vendor/notify.js"></script>

    <!-- Header template -->
    <header id="pgheader" data-load-template="header.html"></header>

    <!-- Modal Dialogs -->
    <section role="dialog">
        <div class="small reveal" id="edit_dlg" data-reveal data-close-on-click="false">
            <div class="content-card-header">
                <h4 class="content-card-title"><i class="fi-page"></i> Edit Item</h4>
            </div>
            <div class="content-card-body">
                <div class="grid-x grid-padding-x">
                    <input type="hidden" id="item_id" data-entity-name="itemRecord" data-entity-field="itemId">
                    <div class="cell small-12">
                        <label for="item_name">Name
                            <input type="text" id="item_name"
                                   data-entity-name="itemRecord"
                                   data-entity-field="itemName">
                        </label>
                    </div>
                </div>
            </div>
            <div class="grid-x grid-padding-x" style="padding: 15px;">
                <div class="cell small-6">
                    <button class="button success expanded" id="item_save" data-close>
                        <i class="fi-check"></i> Save
                    </button>
                </div>
                <div class="cell small-6">
                    <button class="button secondary expanded" data-close>
                        <i class="fi-x"></i> Cancel
                    </button>
                </div>
            </div>
        </div>
    </section>

    <!-- Main Content -->
    <div class="grid-container">
        <div class="grid-x grid-margin-x">
            <!-- Master table (left side) -->
            <div class="cell small-12 medium-4">
                <div class="content-card">
                    <div class="content-card-header">
                        <h4 class="content-card-title">Items</h4>
                        <button class="button success" id="item_add">
                            <i class="fi-plus"></i> New
                        </button>
                    </div>
                    <div style="max-height: 500px; overflow-y: auto;">
                        <table class="hover" id="items_table">
                            <thead>
                                <tr>
                                    <th>ID</th>
                                    <th>Name</th>
                                    <th>Actions</th>
                                </tr>
                            </thead>
                            <tbody id="items_tbody">
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>

            <!-- Detail table (right side) -->
            <div class="cell small-12 medium-8">
                <div class="content-card">
                    <div class="content-card-header">
                        <h4 class="content-card-title">Details</h4>
                    </div>
                    <!-- Detail content here -->
                </div>
            </div>
        </div>
    </div>

    <!-- Load page utilities -->
    <script src="js/pageutil.js"></script>
    <script>
        loadPageTemplates();
        $(document).foundation();
    </script>

    <!-- Load module -->
    <script type="module">
        import * as $page from "./js/modules/featureman.js";
        import * as $site from "./js/modules/globals.js";

        $().ready(function() {
            $site.setGlobalHandlers({ requireAuth: true });

            // Event handlers
            $('#item_add').on('click', $page.newItem);
            $('#item_save').on('click', $page.saveItem);
            $('#items_tbody').on('click', 'tr', $page.selectItem);
            $('#items_tbody').on('click', '.item-edit', $page.editItem);
            $('#items_tbody').on('click', '.item-delete', $page.deleteItem);

            // Initialize
            $page.initialize();
        });
    </script>
</body>
</html>

8.2 Design Patterns

Master-Detail Layout: Left panel (1/3) for master, right panel (2/3) for detail ✅ Content Cards: Use .content-card for sections ✅ Foundation Grid: Use .grid-x, .grid-padding-x, .cellModal Dialogs: Use .reveal for add/edit dialogs ✅ Data Binding: Use data-entity-name and data-entity-field attributes ✅ Icons: Use Foundation icons (fi-*) or Font Awesome ✅ Colors: Use CSS variables from design system

8.3 Key Components

Buttons:

<button class="button primary">Primary Action</button>
<button class="button success">Create</button>
<button class="button alert">Delete</button>
<button class="button secondary">Cancel</button>

Tables:

<table class="hover">
    <thead>
        <tr><th>Column</th></tr>
    </thead>
    <tbody id="table_body">
        <!-- Populated by JavaScript -->
    </tbody>
</table>

Forms with Data Binding:

<input type="text"
       id="field_name"
       data-entity-name="entityRecord"
       data-entity-field="fieldName">


Step 9: Create JavaScript Module

JavaScript modules handle all client-side logic.

9.1 Create Module File

Location: tqweb-adm/js/modules/[feature]man.js

Template:

/*
 * Copyright (c) 2024. Perun Consulting Services FZ LLE
 * Feature Management Module
 */

import * as $util from "./utilmod.js";

// Module state
export let currentPage = 0;
export let selectedItemId = null;
export const pageSize = 10;

let items = [];
let itemRecord = {};

/**
 * Initialize the module
 */
export function initialize() {
    loadItems(0);

    // Setup entity data bindings
    $util.setUpEntityData("itemRecord", itemRecord);
}

// ==================== DATA LOADING ====================

/**
 * Load items with pagination
 */
export function loadItems(page) {
    currentPage = page;

    const reqData = {
        session: "",
        page: page,
        pageSize: pageSize
    };

    $.ajax({
        url: '/api/feature/item/list',
        type: 'POST',
        data: JSON.stringify(reqData),
        contentType: 'application/json',
        success: function(response) {
            if (response.success && response.data) {
                items = response.data;
                renderItems();
            } else {
                $.notify(response.message || 'Failed to load items', 'error');
            }
        },
        error: function(xhr, status, error) {
            $.notify('Error loading items: ' + error, 'error');
        }
    });
}

/**
 * Render items table
 */
function renderItems() {
    const tbody = $('#items_tbody');
    tbody.empty();

    if (items.length === 0) {
        tbody.append('<tr><td colspan="3" style="text-align: center;">No items found</td></tr>');
        return;
    }

    items.forEach(item => {
        const row = $('<tr>').attr('data-item-id', item.itemId);
        if (selectedItemId === item.itemId) {
            row.addClass('selected-row');
        }

        row.append($('<td>').text(item.itemId));
        row.append($('<td>').text(item.itemName));

        const actionsCell = $('<td>');
        actionsCell.append(`<button class="button tiny primary item-edit" data-id="${item.itemId}">
                               <i class="fi-pencil"></i>
                           </button>`);
        actionsCell.append(`<button class="button tiny alert item-delete" data-id="${item.itemId}">
                               <i class="fi-x"></i>
                           </button>`);
        row.append(actionsCell);

        tbody.append(row);
    });
}

// ==================== CRUD OPERATIONS ====================

/**
 * Select an item (click handler)
 */
export function selectItem(e) {
    // Ignore if clicking on action buttons
    if ($(e.target).closest('.button').length > 0) {
        return;
    }

    const row = $(e.currentTarget);
    const itemId = row.data('item-id');

    selectedItemId = itemId;

    // Update UI
    $('#items_tbody tr').removeClass('selected-row');
    row.addClass('selected-row');

    // Load details for this item
    // loadDetails(itemId);
}

/**
 * Open new item dialog
 */
export function newItem() {
    itemRecord = {};
    $util.setUpEntityData("itemRecord", itemRecord);

    $('#item_id').val('');
    $('#item_name').val('');

    $('#edit_dlg').foundation('open');
}

/**
 * Open edit item dialog
 */
export function editItem(e) {
    e.stopPropagation();

    const itemId = $(e.currentTarget).data('id');
    const item = items.find(i => i.itemId === itemId);

    if (!item) {
        $.notify('Item not found', 'error');
        return;
    }

    itemRecord = {...item};
    $util.setUpEntityData("itemRecord", itemRecord);

    $('#item_id').val(item.itemId || '');
    $('#item_name').val(item.itemName || '');

    $('#edit_dlg').foundation('open');
}

/**
 * Save item
 */
export function saveItem() {
    $util.extractEntityData("itemRecord", itemRecord);

    // Validate
    if (!itemRecord.itemName) {
        $.notify('Please enter an item name', 'warning');
        return;
    }

    const reqData = {
        session: "",
        ...itemRecord
    };

    $.ajax({
        url: '/api/feature/item/write',
        type: 'POST',
        data: JSON.stringify(reqData),
        contentType: 'application/json',
        success: function(response) {
            if (response.success) {
                $.notify('Item saved successfully', 'success');
                loadItems(currentPage);
            } else {
                $.notify(response.message || 'Failed to save item', 'error');
            }
        },
        error: function(xhr, status, error) {
            $.notify('Error saving item: ' + error, 'error');
        }
    });
}

/**
 * Delete item
 */
export function deleteItem(e) {
    e.stopPropagation();

    const itemId = $(e.currentTarget).data('id');

    if (!confirm('Are you sure you want to delete this item?')) {
        return;
    }

    const reqData = {
        session: "",
        itemId: itemId
    };

    $.ajax({
        url: '/api/feature/item/delete',
        type: 'POST',
        data: JSON.stringify(reqData),
        contentType: 'application/json',
        success: function(response) {
            if (response.success) {
                $.notify('Item deleted successfully', 'success');
                if (selectedItemId === itemId) {
                    selectedItemId = null;
                }
                loadItems(currentPage);
            } else {
                $.notify(response.message || 'Failed to delete item', 'error');
            }
        },
        error: function(xhr, status, error) {
            $.notify('Error deleting item: ' + error, 'error');
        }
    });
}

9.2 Key Points

ES6 Modules: Use import/exportState Management: Module-level variables for state ✅ Data Binding: Use $util.setUpEntityData() and $util.extractEntityData()API Calls: Prefer tlinq() from globals.js over raw $.ajax(). The tlinq() function handles authentication (JWT Bearer token for OIDC, session token for guests) and returns a Promise resolving to apiData. ✅ Error Handling: Always handle success and error cases ✅ User Feedback: Use $.notify() for notifications ✅ Event Delegation: Use event handlers on parent elements

9.3 Common Patterns

AJAX Request Pattern:

$.ajax({
    url: '/api/feature/endpoint',
    type: 'POST',
    data: JSON.stringify({
        session: "",
        ...params
    }),
    contentType: 'application/json',
    success: function(response) {
        if (response.success && response.data) {
            // Handle success
        } else {
            $.notify(response.message || 'Operation failed', 'error');
        }
    },
    error: function(xhr, status, error) {
        $.notify('Error: ' + error, 'error');
    }
});

Data Binding Pattern:

// Setup (before showing dialog)
$util.setUpEntityData("recordName", recordObject);

// Extract (after editing)
$util.extractEntityData("recordName", recordObject);


Step 10: Testing

10.1 Unit Testing

Create tests for facade methods:

@Test
public void testWritePage() throws TlinqClientException {
    TripOfferFacade facade = new TripOfferFacade(testSession);

    CTripPage page = new CTripPage();
    page.setPageName("test-page.html");
    page.setPageDesc("Test page");

    CTripPage saved = facade.writePage(page);

    assertNotNull(saved.getPageId());
    assertEquals("test-page.html", saved.getPageName());
}

10.2 Integration Testing

Test API endpoints:

curl -X POST http://localhost:8080/api/tripoffer/page/list \
  -H "Content-Type: application/json" \
  -d '{"session":"test-session","page":0,"pageSize":10}'

10.3 Frontend Testing

  1. Open browser developer tools
  2. Navigate to your page
  3. Test each operation (add, edit, delete)
  4. Check console for errors
  5. Verify API calls in Network tab

Step 11: Documentation

11.1 Create Feature Documentation

Location: doc/[FEATURE]_MANAGEMENT.md

Include: - Feature overview - Architecture diagram - Database schema - API endpoints - Usage instructions - Configuration details

11.2 Update Main Documentation

Update relevant docs: - README.md (if needed) - API documentation - Configuration guide


Best Practices

Code Quality

Naming Conventions: - DB entities: [TableName]Entity (e.g., TrippageEntity) - Canonical entities: C[EntityName] (e.g., CTripPage) - Facade: [Feature]Facade (e.g., TripOfferFacade) - API: [Feature]Api (e.g., TripOfferApi) - HTML: [feature]man.html (e.g., offerman.html) - JS Module: [feature]man.js (e.g., offerman.js)

Error Handling: - Always validate inputs in facade - Use appropriate TlinqErr codes - Provide meaningful error messages - Log errors appropriately

Thread Safety (TQ-52): - Declare shared static fields as volatile (e.g., private static volatile String sysSession) - Use ConcurrentHashMap instead of HashMap for shared caches - Always close Hibernate sessions in finally blocks to prevent leaks - Use try/finally pattern around beginTransaction() with rollback() in catch/finally - Use ThreadLocal<DateFormat> instead of shared SimpleDateFormat instances

Security: - Validate all user inputs - Use session tokens or JWT Bearer tokens (see API Specification for auth pipeline) - Sanitize data before database operations - Implement proper authorization via api-roles.properties - Use parameterized HQL queries to prevent SQL injection

Performance: - Use pagination for large datasets - Consider caching where appropriate - Optimize database queries - Minimize API calls from frontend

Code Organization

Package Structure:

tqapp/
  src/main/java/com/perun/tlinq/
    client/nts/db/[feature]/         # DB entities
    entity/[feature]/                 # Canonical entities & facade
tqapi/
  src/main/java/com/perun/tlinq/api/ # API classes
tqweb-adm/
  [feature]man.html                   # HTML page
  js/modules/[feature]man.js          # JS module
config/
  tourlinq-config.xml                 # Entity mappings
doc/
  [feature]_schema.sql                # Database schema
  [FEATURE]_MANAGEMENT.md             # Feature docs

Commit Messages: - Use clear, descriptive messages - Reference issue numbers - Explain the "why" not just the "what"


Common Patterns

Master-Detail Pattern

Use Case: Managing related entities (e.g., orders and order items)

Implementation: - Master table on left (1/3 width) - Detail table on right (2/3 width) - Selecting master row loads related details - Both tables support CRUD operations

Example: Trip pages (master) with snippets (detail)

Wizard Pattern

Use Case: Multi-step processes (e.g., visa application)

Implementation: - Progress stepper at top - Step content in cards - Next/Previous buttons - Validation at each step

Example: Visa application workflow

Search and Filter Pattern

Use Case: Finding records in large datasets

Implementation: - Search panel at top - Filter chips for quick filters - Paginated results table - Sort capabilities

Example: Group management search


Troubleshooting

Common Issues

Problem: Entity not found error - Solution: Check entity name in config matches facade calls exactly

Problem: Field mapping not working - Solution: Verify field names match between canonical and DB entities

Problem: API returns 404 - Solution: Check @Path annotations and ensure API is deployed

Problem: JavaScript not loading - Solution: Check module path and ensure type="module" in script tag

Problem: Session errors - Solution: Verify session token is being passed correctly


Checklist

Before considering your feature complete:

  • [ ] Database schema created and applied
  • [ ] DB entity classes created with JPA annotations
  • [ ] Canonical entity classes created
  • [ ] Facade created with all CRUD operations
  • [ ] API created with all endpoints
  • [ ] NTS services configured in nts-client.xml (including delete services)
  • [ ] Entity mappings added to tourlinq-config.xml
  • [ ] HTML page created following design patterns
  • [ ] JavaScript module created with all handlers
  • [ ] Feature tested end-to-end
  • [ ] Documentation created
  • [ ] Code reviewed
  • [ ] Committed with clear messages
  • [ ] Pushed to feature branch

Additional Resources

  • TQCOMMON Documentation: docs/tqcommon/TQCOMMON_EXECUTIVE_SUMMARY.md
  • Refactoring Guide: doc/TRIP_OFFER_REFACTORING.md
  • Trip Offer Example: doc/TRIP_OFFER_MANAGEMENT.md
  • Foundation CSS Docs: https://get.foundation/sites/docs/
  • JPA Documentation: https://jakarta.ee/specifications/persistence/

Example: Complete File List

For reference, here's the complete file list for the Trip Offer Management feature:

Database: - doc/trip_offer_schema.sql

Backend: - tqapp/src/main/java/com/perun/tlinq/client/nts/db/trip/TrippageEntity.java - tqapp/src/main/java/com/perun/tlinq/client/nts/db/trip/TripsnippetEntity.java - tqapp/src/main/java/com/perun/tlinq/client/nts/db/trip/TripskeletonEntity.java - tqapp/src/main/java/com/perun/tlinq/client/nts/db/trip/TripsnippetfileEntity.java - tqapp/src/main/java/com/perun/tlinq/entity/trip/CTripPage.java - tqapp/src/main/java/com/perun/tlinq/entity/trip/CTripSnippet.java - tqapp/src/main/java/com/perun/tlinq/entity/trip/CTripSkeleton.java - tqapp/src/main/java/com/perun/tlinq/entity/trip/CTripSnippetFile.java - tqapp/src/main/java/com/perun/tlinq/entity/trip/TripOfferFacade.java - tqapi/src/main/java/com/perun/tlinq/api/TripOfferApi.java

Configuration: - config/tourlinq-config.xml (entity mappings added)

Frontend: - tqweb-adm/offerman.html - tqweb-adm/js/modules/offerman.js

Documentation: - doc/TRIP_OFFER_MANAGEMENT.md - doc/TRIP_OFFER_REFACTORING.md


Summary

This guide provides a comprehensive workflow for adding new features to TQPRO. Key takeaways:

  1. Follow the layered architecture: Database → Native Entities → Canonical Entities → Facade → API → Frontend
  2. Use established patterns: Look at existing features for reference
  3. Configuration-driven: Entity mappings in XML provide flexibility
  4. Consistent naming: Follow conventions throughout
  5. Test thoroughly: Unit, integration, and frontend testing
  6. Document everything: Help future developers understand your work

By following this guide, you ensure consistency with the TQPRO architecture and create maintainable, high-quality features.


Document Version: 1.2 Last Updated: 2026-02-19 Based on: Trip Offer Management Feature Implementation

Revision History: - v1.2 (2026-02-19): Added OIDC authentication notes (TQ-51), thread safety best practices (TQ-52), updated frontend init pattern with requireAuth, prefer tlinq() over $.ajax() - v1.1 (2025-12-17): Added Step 6 "Configure NTS Service Plugin" covering nts-client.xml configuration, delete services, custom services, and NTS native entity classes - v1.0 (2025-11-12): Initial version