view resources/js/ServerTable.js @ 2:90296614b7e2 default tip

Adding in the base for the clients table
author luka
date Thu, 28 Aug 2025 20:55:40 -0400
parents 9d7dcd54c677
children
line wrap: on
line source

class ServerTable {
    /**
     * @param {HTMLElement} rootEl - The container element for the table
     * @param {Object} options - Configuration options
     * @param {string} options.endpoint - API endpoint for data
     * @param {Array} options.columns - Array of column configs: [{name, label, ...}]
     * @param {number} [options.pageSize=10] - Default rows per page
     * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}]
     * @param {Object} [options.headers={}] - Additional headers for requests
     * @param {string} [options.groupBy={}] - Which column to group by
     * @param {Function} [options.groupRender={}] - Function to render the grouping
     */
    constructor(rootEl, options) {
        this.rootEl = rootEl;
        this.endpoint = options.endpoint;
        this.columns = options.columns || [];
        this.pageSize = options.pageSize || 10;
        this.sort = options.initialSort || [];
        this.filters = options.filters || {};
        this.currentPage = 1;
        this.headers = options.headers || {};
        this.groupBy = options.groupBy;
        this.groupRender =
            options.groupRender ||
            ((g, rows) =>
                `<tr class="st-group-row"><td colspan="${this.columns.length}">Group: ${g}</td></tr>`);

        this.skeleton = options.skeleton || `
            <div class="st-table-container">
                <table>
                    <thead>
                    </thead>
                    <tbody>
                        <!-- Data rows will go here -->
                    </tbody>
                </table>
                <div class="st-controls">
                    <span class="st-pagination"></span>
                    <span class="st-status"></span>
                </div>
            </div>
        `;

        this.state = {
            loading: false,
            error: null,
            totalRecords: 0,
            records: [],
        };

        // Render initial skeleton and fetch initial data
        this.renderSkeleton();
        this.fetchData();
    }

    renderSkeleton() {
        // Build base structure: table shell, controls area
        this.rootEl.innerHTML = this.skeleton;
        // Store references
        this.head = this.rootEl.querySelector("thead");
        this.tbody = this.rootEl.querySelector("tbody");
        this.statusEl = this.rootEl.querySelector(".st-status");
        this.paginationEl = this.rootEl.querySelector(".st-pagination");

        // Draw the header
        this.head.innerHTML = `
                        <tr>
                            ${this.columns.map((col) => `<th scope="col">${col.label || col.name}</th>`).join("")}
                        </tr>
                        `;
    }

    async fetchData() {
        this.setLoading(true);
        const payload = {
            page: this.currentPage,
            page_size: this.pageSize,
            sort: this.sort,
            filters: this.filters,
        };

        try {
            const res = await fetch(this.endpoint, {
                method: "POST",
                headers: {
                    ...this.headers,
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(payload),
            });

            if (!res.ok) {
                throw new Error(`Server responded with ${res.status}`);
            }

            const data = await res.json();
            // Minimal shape validation
            if (
                !Array.isArray(data.records) ||
                typeof data.total_records !== "number"
            ) {
                throw new Error("Malformed server response");
            }

            // Save data in state
            this.state.records = data.records;
            this.state.totalRecords = data.total_records;
            this.state.error = null;

            this.renderRows();
            this.updateControls();
        } catch (err) {
            this.state.error = err.message;
            this.renderError();
        } finally {
            this.setLoading(false);
        }
    }

    setLoading(loading) {
        this.state.loading = loading;
        this.statusEl.textContent = loading ? "Loading..." : "";
    }

    renderRows() {
        const { records } = this.state;
        const cols = this.columns;
        const groupBy = this.groupBy;
        const groupRender =
            typeof this.groupRender === "function"
                ? this.groupRender
                : (g, rows) =>
                      `<tr class="st-group-row"><td colspan="${cols.length}">Project: ${g}</td></tr>`;

        if (!records || records.length === 0) {
            this.tbody.innerHTML = `<tr><td colspan="${cols.length}">No data</td></tr>`;
            return;
        }

        // Group if needed
        if (groupBy) {
            // Find grouping field or function
            const getGroupValue =
                typeof groupBy === "function" ? groupBy : (row) => row[groupBy];
            let lastGroup = undefined;
            let out = "";
            let groupRows = [];

            for (let i = 0; i < records.length; i++) {
                const row = records[i];
                const groupVal = getGroupValue(row);

                // On group transition, flush previous group
                if (i === 0 || groupVal !== lastGroup) {
                    if (i > 0) {
                        // Optionally do something with groupRows if groupRender wants it
                    }
                    // Insert group header row
                    out += groupRender(groupVal, []);
                    lastGroup = groupVal;
                    groupRows = [];
                }

                groupRows.push(row);

                out += `<tr>${cols
                    .map((col, ci) => {
                        // If grouping by this column, suppress repeated values (leave blank except for first row in group)
                        if (
                            typeof groupBy === "string" &&
                            col.name === groupBy &&
                            groupVal === lastGroup &&
                            groupRows.length > 1
                        ) {
                            return `<td></td>`;
                        }
                        return `<td class='${ typeof col.class === "string" ? col.class : '' }' >${
                            typeof col.render === "function"
                                ? col.render(row, col, i)
                                : row[col.name]
                        }</td>`;
                    })
                    .join("")}</tr>`;
            }
            this.tbody.innerHTML = out;
        } else {
            // No grouping
            this.tbody.innerHTML = records
                .map(
                    (row, i) =>
                        `<tr>${cols
                            .map(
                                (col) =>
                                    `<td class='${ typeof col.class === "string" ? col.class : '' }' >${
                                        typeof col.render === "function"
                                            ? col.render(row, col, i)
                                            : row[col.name]
                                    }</td>`,
                            )
                            .join("")}</tr>`,
                )
                .join("");
        }
    }
    renderError() {
        this.tbody.innerHTML = `<tr><td colspan="${this.columns.length}" style="color:red">${this.state.error}</td></tr>`;
    }

    updateControls() {
        // Basic pagination info (full controls to come later)
        const from = 1 + (this.currentPage - 1) * this.pageSize;
        const to = Math.min(
            this.currentPage * this.pageSize,
            this.state.totalRecords,
        );
        this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`;
    }

    // PUBLIC: force reload
    reload() {
        this.fetchData();
    }

    // PUBLIC: update filters, resets to page 1
    setFilters(newFilters) {
        this.filters = newFilters;
        this.currentPage = 1;
        this.fetchData();
    }
}

// Example usage (not part of module export):
/*
const table = new ServerTable(document.getElementById('my-table'), {
    endpoint: '/tickets/get_data',
    columns: [
        {name: 'id', label: 'ID'},
        {name: 'subject', label: 'Subject'},
        {name: 'project', label: 'Project'},
        {name: 'created_at', label: 'Created At'},
    ],
    pageSize: 10,
    initialSort: [{col: 'created_at', dir: 'desc'}]
});
*/

window.ServerTable = ServerTable;