Skip to content

TQWeb-ADM Page Development Guide

This guide explains how to add new pages to the tqweb-adm application following the established ES6 module pattern.

Design System Reference: Before creating any new page, read the design system files:

Every new page must use the canonical components and start from the appropriate template.

Overview

The tqweb-adm application uses ES6 modules for all page-specific JavaScript code. Each HTML page has a dedicated JavaScript module that handles all functionality for that page.

Key Principles

  1. One module per page: Each HTML page has exactly one corresponding JavaScript module
  2. Module naming: The module filename matches the HTML filename (e.g., tripmaker-dash.htmltripmaker-dash.js)
  3. No subdirectories: All page modules are placed directly in js/modules/ (not in subdirectories)
  4. ES6 imports: Use ES6 import/export syntax, not global scope
  5. Initialization in HTML: Module initialization happens in $(document).ready() within the HTML file
  6. Authentication: Pages requiring login must call setGlobalHandlers({ requireAuth: true }) to enable OIDC token-based authentication

File Structure

tqweb-adm/
├── my-page.html                          # HTML page
└── js/
    └── modules/
        ├── globals.js                    # Core utilities (tlinq, getUserSession, etc.)
        └── my-page.js                    # Page-specific ES6 module

The application uses lazy-loading for page headers and footers to avoid code duplication. Templates are loaded dynamically based on the CSS framework used.

Available Templates

  • header_bootstrap.html - Bootstrap-based header (for Bootstrap pages)
  • header.html - Foundation-based header (for Foundation pages)
  • header_uikit.html - UIKit-based header (for UIKit pages)

Template Loading

For Bootstrap Pages:

<body style="visibility:hidden">
    <!-- Header loaded from template -->
    <header id="pgheader" data-load-template="header_bootstrap.html"></header>

    <!-- Page content here -->

    <!-- Load page utilities -->
    <script src="js/pageutil.js"></script>

    <!-- In your initialization -->
    <script type="module">
        import * as $page from './js/modules/my-page.js';

        $(document).ready(function() {
            // Load templates (header/footer)
            loadBootstrapTemplates();

            // Initialize your page
            $page.initializePage();
        });
    </script>
</body>

Note: The style="visibility:hidden" on <body> prevents content flash before authentication. For pages with requireAuth: true, setGlobalHandlers() reveals the body after auth passes. For pages without auth requirements, your initializePage() should set document.body.style.visibility = 'visible' after initialization.

For Foundation Pages:

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

    <!-- Page content here -->

    <!-- Load page utilities -->
    <script src="js/pageutil.js"></script>

    <!-- In your initialization -->
    <script>
        $(document).ready(function() {
            // Load templates (header/footer)
            loadPageTemplates();  // Note: Foundation version

            // Initialize your page
            // ...
        });
    </script>
</body>

Template Functions

Two template loading functions are available:

  1. loadBootstrapTemplates() - For Bootstrap-based pages
  2. Loads HTML templates from files
  3. Handles visibility based on login status (data-visible-when attributes)
  4. NO Foundation initialization (Bootstrap doesn't need it)

  5. loadPageTemplates() - For Foundation-based pages

  6. Loads HTML templates from files
  7. Calls .foundation() on loaded content
  8. Handles visibility based on login status

Template Visibility Control

Templates support conditional visibility based on user login status:

  • data-visible-when="guest" - Shown only when NOT logged in
  • data-visible-when="member" - Shown only when logged in
  • data-visible-when="employee" - Shown only for employee users

Example from header_bootstrap.html:

<li class="nav-item" data-visible-when="member">
    <a class="nav-link" href="visamgmt.html">
        <i class="bi bi-passport"></i> Visa Management
    </a>
</li>
<li class="nav-item" data-visible-when="guest">
    <a class="nav-link" href="login.html">
        <i class="bi bi-box-arrow-in-right"></i> Login
    </a>
</li>

Step-by-Step Guide

Step 1: Create the JavaScript Module

Create your module file in js/modules/ with the same name as your HTML page:

File: js/modules/my-page.js

/**
 * My Page Module
 * ES6 module for the My Page functionality
 */

import { tlinq, getUserSession } from './globals.js';

// ==================== API Client ====================

const API = {
    /**
     * Make an API request using tlinq()
     */
    async request(endpoint, data = {}) {
        const session = getUserSession();
        const requestData = { ...data, session };
        return tlinq(`mymodule${endpoint}`, requestData);
    },

    async getData(id) {
        return this.request('/data/get', { id });
    },

    async saveData(data) {
        return this.request('/data/save', data);
    }
};

// ==================== Utility Functions ====================

const Utils = {
    formatDate(dateString) {
        if (!dateString) return '';
        const date = new Date(dateString);
        return date.toLocaleDateString('en-US', {
            month: 'short',
            day: 'numeric',
            year: 'numeric'
        });
    },

    showAlert(type, message, duration = 5000) {
        const alertClass = {
            'success': 'alert-success',
            'error': 'alert-danger',
            'warning': 'alert-warning',
            'info': 'alert-info'
        }[type] || 'alert-info';

        const iconClass = {
            'success': 'bi-check-circle-fill',
            'error': 'bi-exclamation-circle-fill',
            'warning': 'bi-exclamation-triangle-fill',
            'info': 'bi-info-circle-fill'
        }[type] || 'bi-info-circle-fill';

        const alert = $(`
            <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
                <i class="bi ${iconClass} me-2"></i>
                ${message}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        `);

        $('#alertContainer').append(alert);

        if (duration > 0) {
            setTimeout(() => alert.alert('close'), duration);
        }
    },

    sanitizeHtml(html) {
        const temp = document.createElement('div');
        temp.textContent = html;
        return temp.innerHTML;
    }
};

// ==================== Page State ====================

let currentData = null;
let isLoading = false;

// ==================== Exported Functions ====================

/**
 * Initialize the page
 * This is called from $(document).ready() in the HTML
 */
export function initializePage() {
    loadData();
    setupEventListeners();
}

function setupEventListeners() {
    $('#saveButton').on('click', saveData);
    $('#refreshButton').on('click', loadData);
}

async function loadData() {
    if (isLoading) return;

    isLoading = true;
    $('#loadingIndicator').removeClass('d-none');

    try {
        const data = await API.getData(123);
        currentData = data;
        renderData(data);
        Utils.showAlert('success', 'Data loaded successfully');
    } catch (error) {
        Utils.showAlert('error', 'Failed to load data: ' + error.message);
    } finally {
        isLoading = false;
        $('#loadingIndicator').addClass('d-none');
    }
}

function renderData(data) {
    $('#dataContainer').html(`
        <div class="card">
            <div class="card-body">
                <h5>${Utils.sanitizeHtml(data.title)}</h5>
                <p>${Utils.sanitizeHtml(data.description)}</p>
            </div>
        </div>
    `);
}

export async function saveData() {
    const formData = {
        title: $('#titleInput').val(),
        description: $('#descriptionInput').val()
    };

    try {
        await API.saveData(formData);
        Utils.showAlert('success', 'Data saved successfully');
        loadData();
    } catch (error) {
        Utils.showAlert('error', 'Failed to save data: ' + error.message);
    }
}

export function deleteData(id) {
    if (window.confirm('Are you sure you want to delete this item?')) {
        // Delete logic here
        Utils.showAlert('success', 'Item deleted');
    }
}

Step 2: Create the HTML Page

Create your HTML file in the tqweb-adm/ directory:

File: my-page.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body style="visibility:hidden">
    <header id="pgheader" data-load-template="header_bootstrap.html"></header>

    <!-- Your page content here -->
    <div class="container" style="margin-top: 80px;">
        <h1>My Page</h1>

        <div id="dataContainer"></div>

        <div id="loadingIndicator" class="d-none">
            <div class="spinner-border"></div>
            <p>Loading...</p>
        </div>

        <button id="saveButton" class="btn btn-primary" onclick="window.MyPage.saveData()">
            Save
        </button>

        <button id="refreshButton" class="btn btn-secondary">
            Refresh
        </button>
    </div>

    <!-- Alert Container -->
    <div id="alertContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1100;">
        <!-- Alerts will be shown here -->
    </div>

    <!-- Load jQuery and Bootstrap -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="js/pageutil.js"></script>
    <script src="js/tqpro-compat.js"></script>
    <script>loadBootstrapTemplates();</script>

    <!-- Load Page Module -->
    <script type="module">
        import * as $page from './js/modules/my-page.js';

        $(document).ready(function() {
            // Initialize page
            $page.initializePage();

            // Expose functions to window for onclick handlers
            window.MyPage = $page;
        });
    </script>
</body>
</html>

Important: The style="visibility:hidden" on <body> prevents content flash before authentication. The setGlobalHandlers({ requireAuth: true }) call in your module's initializePage() handles the OIDC auth check and reveals the body. See the Authentication section below.

Module Structure Explained

1. Imports

Always import from globals.js at the top:

import { tlinq, getUserSession } from './globals.js';

Available from globals.js: - tlinq(endpoint, data) - Standard async API request function (returns Promise resolving to apiData) - getUserSession() - Get current user session token - setGlobalHandlers(options) - Initialize page handlers. Pass { requireAuth: true } for authenticated pages - createId() - Generate unique IDs - Other utility functions

2. API Client Section

Organize all API calls in a dedicated object:

const API = {
    async request(endpoint, data = {}) {
        const session = getUserSession();
        const requestData = { ...data, session };
        return tlinq(`mymodule${endpoint}`, requestData);
    },

    async getItems() {
        return this.request('/items/list');
    }
};

Important: Always add session to API requests:

const session = getUserSession();
const requestData = { ...data, session };

3. Utility Functions Section

Keep page-specific utilities in the Utils object:

const Utils = {
    formatDate(dateString) { /* ... */ },
    showAlert(type, message) { /* ... */ },
    sanitizeHtml(html) { /* ... */ }
};

4. State Management

Use module-level variables for state (they're private to the module):

let currentData = null;
let selectedItems = [];
let isLoading = false;

5. Exported Functions

Export functions that need to be called from HTML or other modules:

export function initializePage() {
    // Called from $(document).ready()
}

export function saveData() {
    // Called from onclick handlers
}

export function deleteItem(id) {
    // Called with parameters from onclick
}

Internal functions (not exported) are private to the module:

function renderItems(items) {
    // Private function, only used within this module
}

HTML Integration Patterns

Pattern 1: Event Handlers in JavaScript (Preferred)

Set up event handlers in the module:

function setupEventListeners() {
    $('#saveButton').on('click', saveData);
    $('#cancelButton').on('click', cancel);
}

HTML:

<button id="saveButton" class="btn btn-primary">Save</button>

Pattern 2: Inline onclick Handlers

For dynamic content or when you need parameters:

export function deleteItem(id) {
    // Function must be exported
}

HTML (generated dynamically):

function renderItem(item) {
    return `
        <button onclick="window.MyPage.deleteItem(${item.id})">
            Delete
        </button>
    `;
}

Expose in HTML's module script:

window.MyPage = $page;
window.deleteItem = $page.deleteItem;  // Also expose directly

Best Practices

1. Always Use Async/Await for API Calls

Good:

async function loadData() {
    try {
        const data = await API.getData();
        renderData(data);
    } catch (error) {
        Utils.showAlert('error', 'Failed to load data');
    }
}

Avoid:

function loadData() {
    API.getData().then(data => {
        renderData(data);
    }).catch(error => {
        Utils.showAlert('error', 'Failed to load data');
    });
}

2. Always Sanitize User Input in HTML

Good:

$('#output').html(Utils.sanitizeHtml(userInput));

Avoid:

$('#output').html(userInput);  // XSS vulnerability!

3. Always Add Session to API Requests

Good:

async request(endpoint, data = {}) {
    const session = getUserSession();
    const requestData = { ...data, session };
    return tlinq(`mymodule${endpoint}`, requestData);
}

Avoid:

async request(endpoint, data = {}) {
    return tlinq(`mymodule${endpoint}`, data);  // Missing session!
}

4. Use Proper Error Handling

Good:

try {
    const data = await API.getData();
    renderData(data);
    Utils.showAlert('success', 'Data loaded');
} catch (error) {
    console.error('Load error:', error);
    Utils.showAlert('error', 'Failed to load: ' + error.message);
}

5. Show Loading States

Good:

async function loadData() {
    $('#loadingIndicator').removeClass('d-none');
    try {
        const data = await API.getData();
        renderData(data);
    } finally {
        $('#loadingIndicator').addClass('d-none');
    }
}

Common Patterns

export function showEditModal(itemId) {
    const item = findItemById(itemId);

    // Populate form
    $('#itemName').val(item.name);
    $('#itemDescription').val(item.description);

    // Show modal
    const modal = new bootstrap.Modal(document.getElementById('editModal'));
    modal.show();
}

export async function saveModal() {
    const formData = {
        name: $('#itemName').val(),
        description: $('#itemDescription').val()
    };

    try {
        await API.saveItem(formData);
        Utils.showAlert('success', 'Item saved');

        // Hide modal
        const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
        modal.hide();

        // Reload data
        loadItems();
    } catch (error) {
        Utils.showAlert('error', 'Failed to save: ' + error.message);
    }
}

Confirmation Dialogs

export function deleteItem(itemId) {
    if (window.confirm('Are you sure you want to delete this item?')) {
        performDelete(itemId);
    }
}

async function performDelete(itemId) {
    try {
        await API.deleteItem(itemId);
        Utils.showAlert('success', 'Item deleted');
        loadItems();
    } catch (error) {
        Utils.showAlert('error', 'Failed to delete: ' + error.message);
    }
}

Dynamic Content Generation

function renderItems(items) {
    const container = $('#itemsContainer');
    container.empty();

    items.forEach(item => {
        const card = createItemCard(item);
        container.append(card);
    });
}

function createItemCard(item) {
    return `
        <div class="card mb-3">
            <div class="card-body">
                <h5 class="card-title">${Utils.sanitizeHtml(item.name)}</h5>
                <p class="card-text">${Utils.sanitizeHtml(item.description)}</p>
                <button class="btn btn-primary"
                        onclick="window.MyPage.editItem(${item.id})">
                    Edit
                </button>
                <button class="btn btn-danger"
                        onclick="window.MyPage.deleteItem(${item.id})">
                    Delete
                </button>
            </div>
        </div>
    `;
}

Testing the Module Pattern

To verify your module is working correctly:

  1. Check browser console for import errors
  2. Verify getUserSession() works - no "is not a function" errors
  3. Test all onclick handlers - make sure functions are exposed to window
  4. Check API calls include session - inspect network requests

Migration from Old Pattern

If you have old code using global scripts:

Old (Don't use):

<script src="js/my-script.js"></script>
<script>
    // Global scope code
    function myFunction() {
        window.getUserSession();  // Might not be defined yet!
    }
</script>

New (Use this):

<script type="module">
    import * as $page from './js/modules/my-page.js';

    $(document).ready(function() {
        $page.initializePage();
        window.MyPage = $page;
    });
</script>

Authentication (OIDC)

Since TQ-51, the admin site (tqweb-adm) uses native OIDC authentication via Keycloak with the oidc-client-ts library. This replaces the previous oauth2-proxy approach.

Enabling Authentication on a Page

Pages that require authenticated access must call setGlobalHandlers() with the requireAuth option:

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

This triggers the OIDC Authorization Code flow with PKCE. If the user is not authenticated, they are redirected to Keycloak for login and then returned to the page via callback.html.

How It Works

  1. globals.js imports auth-service.js, which manages the OIDC lifecycle
  2. auth-service.js uses oidc-client-ts to perform Authorization Code flow with PKCE against Keycloak
  3. OIDC configuration is loaded from the backend auth/config endpoint at runtime (no hardcoded URLs)
  4. On successful login, the JWT access token is stored both in oidc-client-ts internal state and cached in sessionStorage as ss_access_token
  5. The tlinq() function automatically attaches the JWT as a Bearer token in the Authorization header for all API calls (using ss_access_token fallback on pages where oidc-client-ts is not loaded)
  6. Token refresh is handled silently via silent-renew.html
  7. Pages with requireAuth: true hide their <body> with visibility:hidden to prevent content flash before authentication; setGlobalHandlers() reveals the body after auth passes

Key Files

File Purpose
js/modules/auth-service.js OIDC client wrapper — login, logout, token management, silent renewal error handling
js/modules/oidc-config.js Loads OIDC config from backend auth/config endpoint, builds UserManager settings
callback.html OIDC redirect callback handler
silent-renew.html Silent token renewal iframe
loggedout.html Post-logout page — clears sessionStorage, cookies

Pages Without Authentication

For public-facing pages or pages that should work without login, omit the requireAuth option:

$site.setGlobalHandlers();  // No authentication required

Session Storage Keys

Key Purpose
ss_user_uid Keycloak user UUID (profile.sub) — used for user identification
ss_access_token Cached OIDC JWT access token — used by tlinq() for Bearer auth
ss_id_token OIDC ID token — used for logout
ss_loggedin Login state flag ('true' when logged in)
ss_username User's preferred username or email
ss_greeting Greeting string (e.g., "Hello, John")

Session Token vs JWT

  • Authenticated pages (requireAuth: true): The JWT Bearer token is sent automatically by tlinq(). The session field in API requests can be empty.
  • Guest/public pages: The session field from getUserSession() is used as before. Guest permissions are defined in api-roles.properties.

Binary Download Endpoints (Excel, PDF)

The tlinq() function always parses the response as JSON and extracts apiData. Endpoints that return binary files (application/octet-stream) — such as Excel exports — cannot use tlinq() because the response is a blob, not JSON.

For these endpoints, use raw fetch() with getAuthHeaders() from globals.js:

import { getUserSession, getAuthHeaders } from './globals.js';

async function downloadFile() {
    const session = getUserSession();
    const headers = await getAuthHeaders();
    const response = await fetch('/tlinq-api/mymodule/export', {
        method: 'POST',
        headers,
        body: JSON.stringify({ session })
    });
    if (!response.ok) { /* handle error */ return; }
    const blob = await response.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'export.xlsx';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
}

getAuthHeaders() resolves the OIDC access token using the same logic as tlinq() and returns a headers object with Content-Type and Authorization set correctly. Do not build the Authorization header manually from getUserSession() — in OIDC mode that returns an empty string and the request will be treated as unauthenticated.


CSS Framework Notes

The tqweb-adm application has pages using different CSS frameworks:

  • Foundation Framework: Some older pages (e.g., visamgmt.html) use Foundation and require $(document).foundation(); in initialization
  • Bootstrap: Newer pages (e.g., TripMaker) use Bootstrap and do not need Foundation initialization

Only include $(document).foundation(); if your page actually uses the Foundation framework. Check which CSS framework your page loads before including this line.

Reference Examples

The following pages in tqweb-adm follow this pattern:

  • visamgmt.html / visamgmt.js - Complex multi-step form with file uploads (uses Foundation)
  • tripmaker-dash.html / tripmaker-dash.js - Dashboard with filtering, KPI cards, and CRUD modals (uses Bootstrap, shared module pattern)
  • tripmaker-common.js - Shared API client, utilities, and destination autocomplete for TripMaker pages (uses Bootstrap)

Study these files for real-world examples of the patterns described in this guide.

Troubleshooting

"getUserSession is not a function"

Cause: Module is using window.getUserSession() instead of importing it.

Fix:

// At top of module
import { getUserSession } from './globals.js';

// In API client
const session = getUserSession();  // Not window.getUserSession()

"Module not found" error

Cause: Incorrect import path or file not in js/modules/

Fix:

// Correct - relative to HTML file
import * as $page from './js/modules/my-page.js';

// Wrong
import * as $page from 'js/modules/my-page.js';

onclick handlers not working

Cause: Functions not exposed to window scope

Fix:

// In HTML's module script
window.MyPage = $page;
window.myFunction = $page.myFunction;

Race condition with globals

Cause: Module tries to use globals before they're loaded

Fix: Always wait for $(document).ready() in the HTML script block, and imports will ensure proper loading order.

Summary Checklist

When creating a new page:

  • [ ] Module file in js/modules/ with same name as HTML
  • [ ] Import tlinq and getUserSession from globals.js
  • [ ] API client adds session to all requests
  • [ ] Export initializePage() function
  • [ ] Export functions used in onclick handlers
  • [ ] HTML includes pageutil.js and tqpro-compat.js scripts
  • [ ] HTML uses <body style="visibility:hidden"> for content flash prevention
  • [ ] HTML calls loadBootstrapTemplates() for header/footer loading
  • [ ] Initialize in $(document).ready()
  • [ ] Call $site.setGlobalHandlers({ requireAuth: true }) for authenticated pages
  • [ ] Expose module and functions to window
  • [ ] Use Utils.sanitizeHtml() for user input
  • [ ] Include proper error handling
  • [ ] Show loading states during async operations

CDN Media Browser Module

The cdn-browser.js module provides a reusable modal for browsing S3/CloudFront images and uploading new ones. It can be added to any page that has URL input fields for CDN images.

Quick Integration

  1. Import the module (it auto-binds on DOMContentLoaded):

    import './cdn-browser.js';
    

  2. Add data-cdn-* attributes to your browse buttons:

    <div class="input-group">
      <input type="url" id="my_image_url" class="form-control form-control-sm" ...>
      <button type="button" class="btn btn-outline-secondary btn-sm"
              data-cdn-purpose="hotel-photos"
              data-cdn-allow-upload="true"
              data-cdn-target="#my_image_url">
        <i class="bi bi-folder2-open"></i>
      </button>
    </div>
    

Attributes: | Attribute | Required | Description | |-----------|----------|-------------| | data-cdn-purpose | Yes | Purpose ID from MediaConfig (e.g. hotel-photos, offer-images) | | data-cdn-allow-upload | No | true to show the Upload tab (default: true) | | data-cdn-target | Yes | CSS selector for the input field to receive the CDN URL |

Programmatic API

For cases where auto-bind isn't sufficient:

import { openCdnBrowser } from './cdn-browser.js';

openCdnBrowser({
    purpose: 'hotel-photos',
    allowUpload: true,
    onSelect(cdnUrl) {
        document.getElementById('my_field').value = cdnUrl;
    }
});

Available Purposes

Purposes are defined in config/nts-client.xml under <MediaConfig><Purposes>. Current purposes: - hotel-photos — Hotel package images - offer-images — Offer/deal images - destination-heroes — Destination hero banners - marketing-campaigns — Marketing media (upload-only, no browser)

Backend API

The module calls three Media API endpoints: - POST /media/cdn/browse — List folders and images - POST /media/cdn/upload — Upload with resize/crop/WebP conversion - POST /media/cdn/templates — Get available image templates

See Media API Specification for details.


Version: 1.3 Last Updated: 2026-03-20 Maintainer: Development Team

Revision History: - v1.3 (2026-03-20): Added CDN Media Browser module documentation (TQ-80) - v1.2 (2026-02-21): Updated for TQ-53 — content flash prevention (visibility:hidden), session storage keys table, oidc-config.js, updated HTML template examples - v1.1 (2026-02-19): Added OIDC authentication section, requireAuth pattern, updated checklist (TQ-51) - v1.0 (2025-11-20): Initial version