|
0
|
1 class ServerTable {
|
|
|
2 /**
|
|
|
3 * @param {HTMLElement} rootEl - The container element for the table
|
|
|
4 * @param {Object} options - Configuration options
|
|
|
5 * @param {string} options.endpoint - API endpoint for data
|
|
|
6 * @param {Array} options.columns - Array of column configs: [{name, label, ...}]
|
|
|
7 * @param {number} [options.pageSize=10] - Default rows per page
|
|
|
8 * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}]
|
|
|
9 * @param {Object} [options.headers={}] - Additional headers for requests
|
|
|
10 * @param {string} [options.groupBy={}] - Which column to group by
|
|
|
11 * @param {Function} [options.groupRender={}] - Function to render the grouping
|
|
|
12 */
|
|
|
13 constructor(rootEl, options) {
|
|
|
14 this.rootEl = rootEl;
|
|
|
15 this.endpoint = options.endpoint;
|
|
|
16 this.columns = options.columns || [];
|
|
|
17 this.pageSize = options.pageSize || 10;
|
|
|
18 this.sort = options.initialSort || [];
|
|
|
19 this.filters = options.filters || {};
|
|
|
20 this.currentPage = 1;
|
|
|
21 this.headers = options.headers || {};
|
|
|
22 this.groupBy = options.groupBy;
|
|
|
23 this.groupRender =
|
|
|
24 options.groupRender ||
|
|
|
25 ((g, rows) =>
|
|
|
26 `<tr class="st-group-row"><td colspan="${this.columns.length}">Group: ${g}</td></tr>`);
|
|
|
27
|
|
|
28 this.skeleton = options.skeleton || `
|
|
|
29 <div class="st-table-container">
|
|
|
30 <table>
|
|
|
31 <thead>
|
|
|
32 </thead>
|
|
|
33 <tbody>
|
|
|
34 <!-- Data rows will go here -->
|
|
|
35 </tbody>
|
|
|
36 </table>
|
|
|
37 <div class="st-controls">
|
|
|
38 <span class="st-pagination"></span>
|
|
|
39 <span class="st-status"></span>
|
|
|
40 </div>
|
|
|
41 </div>
|
|
|
42 `;
|
|
|
43
|
|
|
44 this.state = {
|
|
|
45 loading: false,
|
|
|
46 error: null,
|
|
|
47 totalRecords: 0,
|
|
|
48 records: [],
|
|
|
49 };
|
|
|
50
|
|
|
51 // Render initial skeleton and fetch initial data
|
|
|
52 this.renderSkeleton();
|
|
|
53 this.fetchData();
|
|
|
54 }
|
|
|
55
|
|
|
56 renderSkeleton() {
|
|
|
57 // Build base structure: table shell, controls area
|
|
|
58 this.rootEl.innerHTML = this.skeleton;
|
|
|
59 // Store references
|
|
|
60 this.head = this.rootEl.querySelector("thead");
|
|
|
61 this.tbody = this.rootEl.querySelector("tbody");
|
|
|
62 this.statusEl = this.rootEl.querySelector(".st-status");
|
|
|
63 this.paginationEl = this.rootEl.querySelector(".st-pagination");
|
|
|
64
|
|
|
65 // Draw the header
|
|
|
66 this.head.innerHTML = `
|
|
|
67 <tr>
|
|
|
68 ${this.columns.map((col) => `<th scope="col">${col.label || col.name}</th>`).join("")}
|
|
|
69 </tr>
|
|
|
70 `;
|
|
|
71 }
|
|
|
72
|
|
|
73 async fetchData() {
|
|
|
74 this.setLoading(true);
|
|
|
75 const payload = {
|
|
|
76 page: this.currentPage,
|
|
|
77 page_size: this.pageSize,
|
|
|
78 sort: this.sort,
|
|
|
79 filters: this.filters,
|
|
|
80 };
|
|
|
81
|
|
|
82 try {
|
|
|
83 const res = await fetch(this.endpoint, {
|
|
|
84 method: "POST",
|
|
|
85 headers: {
|
|
|
86 ...this.headers,
|
|
|
87 "Content-Type": "application/json",
|
|
|
88 },
|
|
|
89 body: JSON.stringify(payload),
|
|
|
90 });
|
|
|
91
|
|
|
92 if (!res.ok) {
|
|
|
93 throw new Error(`Server responded with ${res.status}`);
|
|
|
94 }
|
|
|
95
|
|
|
96 const data = await res.json();
|
|
|
97 // Minimal shape validation
|
|
|
98 if (
|
|
|
99 !Array.isArray(data.records) ||
|
|
|
100 typeof data.total_records !== "number"
|
|
|
101 ) {
|
|
|
102 throw new Error("Malformed server response");
|
|
|
103 }
|
|
|
104
|
|
|
105 // Save data in state
|
|
|
106 this.state.records = data.records;
|
|
|
107 this.state.totalRecords = data.total_records;
|
|
|
108 this.state.error = null;
|
|
|
109
|
|
|
110 this.renderRows();
|
|
|
111 this.updateControls();
|
|
|
112 } catch (err) {
|
|
|
113 this.state.error = err.message;
|
|
|
114 this.renderError();
|
|
|
115 } finally {
|
|
|
116 this.setLoading(false);
|
|
|
117 }
|
|
|
118 }
|
|
|
119
|
|
|
120 setLoading(loading) {
|
|
|
121 this.state.loading = loading;
|
|
|
122 this.statusEl.textContent = loading ? "Loading..." : "";
|
|
|
123 }
|
|
|
124
|
|
|
125 renderRows() {
|
|
|
126 const { records } = this.state;
|
|
|
127 const cols = this.columns;
|
|
|
128 const groupBy = this.groupBy;
|
|
|
129 const groupRender =
|
|
|
130 typeof this.groupRender === "function"
|
|
|
131 ? this.groupRender
|
|
|
132 : (g, rows) =>
|
|
|
133 `<tr class="st-group-row"><td colspan="${cols.length}">Project: ${g}</td></tr>`;
|
|
|
134
|
|
|
135 if (!records || records.length === 0) {
|
|
|
136 this.tbody.innerHTML = `<tr><td colspan="${cols.length}">No data</td></tr>`;
|
|
|
137 return;
|
|
|
138 }
|
|
|
139
|
|
|
140 // Group if needed
|
|
|
141 if (groupBy) {
|
|
|
142 // Find grouping field or function
|
|
|
143 const getGroupValue =
|
|
|
144 typeof groupBy === "function" ? groupBy : (row) => row[groupBy];
|
|
|
145 let lastGroup = undefined;
|
|
|
146 let out = "";
|
|
|
147 let groupRows = [];
|
|
|
148
|
|
|
149 for (let i = 0; i < records.length; i++) {
|
|
|
150 const row = records[i];
|
|
|
151 const groupVal = getGroupValue(row);
|
|
|
152
|
|
|
153 // On group transition, flush previous group
|
|
|
154 if (i === 0 || groupVal !== lastGroup) {
|
|
|
155 if (i > 0) {
|
|
|
156 // Optionally do something with groupRows if groupRender wants it
|
|
|
157 }
|
|
|
158 // Insert group header row
|
|
|
159 out += groupRender(groupVal, []);
|
|
|
160 lastGroup = groupVal;
|
|
|
161 groupRows = [];
|
|
|
162 }
|
|
|
163
|
|
|
164 groupRows.push(row);
|
|
|
165
|
|
|
166 out += `<tr>${cols
|
|
|
167 .map((col, ci) => {
|
|
|
168 // If grouping by this column, suppress repeated values (leave blank except for first row in group)
|
|
|
169 if (
|
|
|
170 typeof groupBy === "string" &&
|
|
|
171 col.name === groupBy &&
|
|
|
172 groupVal === lastGroup &&
|
|
|
173 groupRows.length > 1
|
|
|
174 ) {
|
|
|
175 return `<td></td>`;
|
|
|
176 }
|
|
|
177 return `<td class='${ typeof col.class === "string" ? col.class : '' }' >${
|
|
|
178 typeof col.render === "function"
|
|
|
179 ? col.render(row, col, i)
|
|
|
180 : row[col.name]
|
|
|
181 }</td>`;
|
|
|
182 })
|
|
|
183 .join("")}</tr>`;
|
|
|
184 }
|
|
|
185 this.tbody.innerHTML = out;
|
|
|
186 } else {
|
|
|
187 // No grouping
|
|
|
188 this.tbody.innerHTML = records
|
|
|
189 .map(
|
|
|
190 (row, i) =>
|
|
|
191 `<tr>${cols
|
|
|
192 .map(
|
|
|
193 (col) =>
|
|
|
194 `<td class='${ typeof col.class === "string" ? col.class : '' }' >${
|
|
|
195 typeof col.render === "function"
|
|
|
196 ? col.render(row, col, i)
|
|
|
197 : row[col.name]
|
|
|
198 }</td>`,
|
|
|
199 )
|
|
|
200 .join("")}</tr>`,
|
|
|
201 )
|
|
|
202 .join("");
|
|
|
203 }
|
|
|
204 }
|
|
|
205 renderError() {
|
|
|
206 this.tbody.innerHTML = `<tr><td colspan="${this.columns.length}" style="color:red">${this.state.error}</td></tr>`;
|
|
|
207 }
|
|
|
208
|
|
|
209 updateControls() {
|
|
|
210 // Basic pagination info (full controls to come later)
|
|
|
211 const from = 1 + (this.currentPage - 1) * this.pageSize;
|
|
|
212 const to = Math.min(
|
|
|
213 this.currentPage * this.pageSize,
|
|
|
214 this.state.totalRecords,
|
|
|
215 );
|
|
|
216 this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`;
|
|
|
217 }
|
|
|
218
|
|
|
219 // PUBLIC: force reload
|
|
|
220 reload() {
|
|
|
221 this.fetchData();
|
|
|
222 }
|
|
|
223
|
|
|
224 // PUBLIC: update filters, resets to page 1
|
|
|
225 setFilters(newFilters) {
|
|
|
226 this.filters = newFilters;
|
|
|
227 this.currentPage = 1;
|
|
|
228 this.fetchData();
|
|
|
229 }
|
|
|
230 }
|
|
|
231
|
|
|
232 // Example usage (not part of module export):
|
|
|
233 /*
|
|
|
234 const table = new ServerTable(document.getElementById('my-table'), {
|
|
|
235 endpoint: '/tickets/get_data',
|
|
|
236 columns: [
|
|
|
237 {name: 'id', label: 'ID'},
|
|
|
238 {name: 'subject', label: 'Subject'},
|
|
|
239 {name: 'project', label: 'Project'},
|
|
|
240 {name: 'created_at', label: 'Created At'},
|
|
|
241 ],
|
|
|
242 pageSize: 10,
|
|
|
243 initialSort: [{col: 'created_at', dir: 'desc'}]
|
|
|
244 });
|
|
|
245 */
|
|
|
246
|
|
|
247 window.ServerTable = ServerTable;
|