|
16 | 16 | var ganttChart = null; |
17 | 17 | var splitInstance = null; |
18 | 18 | var tasks = []; |
| 19 | + var sortState = { key: null, direction: 'asc' }; |
19 | 20 |
|
20 | 21 | // ── Build DOM ── |
21 | 22 | var wrapper = sf.el('div', { className: 'sf-gantt-split' }); |
|
38 | 39 | var chartHeader = sf.el('div', { className: 'sf-gantt-pane-header' }); |
39 | 40 | chartHeader.appendChild(sf.el('h3', null, config.chartTitle || 'Timeline')); |
40 | 41 |
|
41 | | - // View mode selector |
42 | 42 | var viewControls = sf.el('div', { className: 'sf-gantt-view-controls' }); |
43 | 43 | var viewSelect = sf.el('select', { className: 'sf-gantt-view-select' }); |
44 | 44 | var modes = [ |
|
54 | 54 | viewSelect.appendChild(opt); |
55 | 55 | }); |
56 | 56 | viewSelect.addEventListener('change', function () { |
57 | | - if (ganttChart) { |
58 | | - ganttChart.change_view_mode(viewSelect.value); |
59 | | - } |
| 57 | + if (ganttChart) ganttChart.change_view_mode(viewSelect.value); |
60 | 58 | }); |
61 | 59 | viewControls.appendChild(viewSelect); |
62 | 60 |
|
|
83 | 81 | }; |
84 | 82 |
|
85 | 83 | ctrl.setTasks = function (newTasks) { |
86 | | - tasks = newTasks; |
87 | | - renderGrid(newTasks); |
88 | | - renderChart(newTasks); |
| 84 | + tasks = Array.isArray(newTasks) ? newTasks.slice() : []; |
| 85 | + renderGrid(tasks); |
| 86 | + renderChart(tasks); |
89 | 87 | }; |
90 | 88 |
|
91 | 89 | ctrl.refresh = function () { |
92 | 90 | if (ganttChart && tasks.length > 0) { |
93 | | - var frappeTasks = tasksToFrappe(tasks); |
94 | | - ganttChart.refresh(frappeTasks); |
| 91 | + ganttChart.refresh(tasksToFrappe(tasks)); |
95 | 92 | } |
96 | 93 | }; |
97 | 94 |
|
|
103 | 100 | }; |
104 | 101 |
|
105 | 102 | ctrl.highlightTask = function (taskId) { |
106 | | - // Grid highlight |
107 | 103 | grid.querySelectorAll('.sf-gantt-row').forEach(function (row) { |
108 | 104 | row.classList.toggle('selected', row.dataset.taskId === taskId); |
109 | 105 | }); |
110 | | - // Bar highlight |
111 | 106 | var svg = chartContainer.querySelector('svg'); |
112 | 107 | if (svg) { |
113 | 108 | svg.querySelectorAll('.bar-wrapper').forEach(function (bw) { |
|
119 | 114 | }; |
120 | 115 |
|
121 | 116 | ctrl.destroy = function () { |
122 | | - if (splitInstance) { splitInstance.destroy(); splitInstance = null; } |
| 117 | + if (splitInstance) { |
| 118 | + splitInstance.destroy(); |
| 119 | + splitInstance = null; |
| 120 | + } |
123 | 121 | ganttChart = null; |
124 | 122 | if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper); |
125 | 123 | }; |
126 | 124 |
|
127 | 125 | return ctrl; |
128 | 126 |
|
129 | | - // ── Internal ── |
130 | | - |
131 | 127 | function initSplit() { |
132 | 128 | if (typeof Split !== 'function') return; |
133 | 129 | splitInstance = Split(['#' + gridPaneId, '#' + chartPaneId], { |
|
149 | 145 | return taskList |
150 | 146 | .filter(function (t) { return t.start && t.end; }) |
151 | 147 | .map(function (t) { |
| 148 | + var customClass = t.custom_class || ''; |
| 149 | + if (t.pinned) { |
| 150 | + customClass = customClass ? customClass + ' pinned' : 'pinned'; |
| 151 | + } |
152 | 152 | return { |
153 | 153 | id: t.id, |
154 | 154 | name: t.name || t.label || t.id, |
155 | 155 | start: t.start, |
156 | 156 | end: t.end, |
157 | | - custom_class: t.custom_class || '', |
| 157 | + custom_class: customClass, |
158 | 158 | dependencies: t.dependencies || '', |
159 | 159 | }; |
160 | 160 | }); |
|
188 | 188 | function renderGrid(taskList) { |
189 | 189 | grid.innerHTML = ''; |
190 | 190 | var table = sf.el('table', { className: 'sf-gantt-table' }); |
191 | | - |
192 | | - // Header |
193 | | - var thead = sf.el('thead'); |
194 | | - var headerRow = sf.el('tr'); |
195 | 191 | var columns = config.columns || [ |
196 | 192 | { key: 'name', label: 'Task' }, |
197 | 193 | { key: 'start', label: 'Start' }, |
198 | 194 | { key: 'end', label: 'End' }, |
199 | 195 | ]; |
| 196 | + var sortedTasks = sortTasks(taskList); |
| 197 | + |
| 198 | + var thead = sf.el('thead'); |
| 199 | + var headerRow = sf.el('tr'); |
200 | 200 | columns.forEach(function (col) { |
201 | | - headerRow.appendChild(sf.el('th', null, col.label)); |
| 201 | + headerRow.appendChild(buildHeaderCell(col)); |
202 | 202 | }); |
203 | 203 | thead.appendChild(headerRow); |
204 | 204 | table.appendChild(thead); |
205 | 205 |
|
206 | | - // Body |
207 | 206 | var tbody = sf.el('tbody'); |
208 | | - taskList.forEach(function (task) { |
| 207 | + sortedTasks.forEach(function (task) { |
| 208 | + var rowClasses = ['sf-gantt-row']; |
| 209 | + if (task.custom_class) rowClasses.push(task.custom_class); |
| 210 | + if (task.projectIndex != null) rowClasses.push('sf-project-' + task.projectIndex); |
| 211 | + |
209 | 212 | var tr = sf.el('tr', { |
210 | | - className: 'sf-gantt-row' + (task.custom_class ? ' ' + task.custom_class : ''), |
| 213 | + className: rowClasses.join(' '), |
211 | 214 | dataset: { taskId: task.id }, |
212 | 215 | onClick: function () { |
213 | 216 | ctrl.highlightTask(task.id); |
214 | 217 | if (config.onTaskClick) config.onTaskClick(task); |
215 | 218 | }, |
216 | 219 | }); |
| 220 | + |
217 | 221 | columns.forEach(function (col) { |
218 | 222 | var td = sf.el('td'); |
219 | 223 | if (col.key === 'name') { |
|
230 | 234 | } |
231 | 235 | tr.appendChild(td); |
232 | 236 | }); |
| 237 | + |
233 | 238 | tbody.appendChild(tr); |
234 | 239 | }); |
235 | 240 | table.appendChild(tbody); |
236 | 241 | grid.appendChild(table); |
237 | 242 | } |
238 | 243 |
|
| 244 | + function buildHeaderCell(col) { |
| 245 | + if (!col.sortable) { |
| 246 | + return sf.el('th', null, col.label); |
| 247 | + } |
| 248 | + |
| 249 | + var isCurrent = sortState.key === col.key; |
| 250 | + var th = sf.el('th', { |
| 251 | + className: 'sortable' + (isCurrent ? ' active' : ''), |
| 252 | + role: 'button', |
| 253 | + tabIndex: 0, |
| 254 | + 'aria-sort': isCurrent ? (sortState.direction === 'asc' ? 'ascending' : 'descending') : 'none', |
| 255 | + }); |
| 256 | + th.appendChild(document.createTextNode(col.label)); |
| 257 | + th.appendChild(sf.el('span', { className: 'sort-icon' }, isCurrent ? (sortState.direction === 'asc' ? '▲' : '▼') : '')); |
| 258 | + |
| 259 | + sf.bindActivation(th, function () { |
| 260 | + if (sortState.key === col.key) { |
| 261 | + sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc'; |
| 262 | + } else { |
| 263 | + sortState.key = col.key; |
| 264 | + sortState.direction = 'asc'; |
| 265 | + } |
| 266 | + renderGrid(tasks); |
| 267 | + }); |
| 268 | + |
| 269 | + return th; |
| 270 | + } |
| 271 | + |
| 272 | + function sortTasks(taskList) { |
| 273 | + if (!sortState.key) return taskList.slice(); |
| 274 | + var sorted = taskList.slice(); |
| 275 | + sorted.sort(function (a, b) { |
| 276 | + var aVal = sortValue(a[sortState.key], sortState.key); |
| 277 | + var bVal = sortValue(b[sortState.key], sortState.key); |
| 278 | + if (aVal === bVal) return 0; |
| 279 | + if (sortState.direction === 'asc') return aVal < bVal ? -1 : 1; |
| 280 | + return aVal > bVal ? -1 : 1; |
| 281 | + }); |
| 282 | + return sorted; |
| 283 | + } |
| 284 | + |
| 285 | + function sortValue(value, key) { |
| 286 | + if (value == null) return ''; |
| 287 | + if (key === 'start' || key === 'end') { |
| 288 | + var parsed = Date.parse(value); |
| 289 | + return isNaN(parsed) ? String(value).toLowerCase() : parsed; |
| 290 | + } |
| 291 | + if (typeof value === 'number') return value; |
| 292 | + return String(value).toLowerCase(); |
| 293 | + } |
| 294 | + |
239 | 295 | function defaultPopup(task) { |
240 | 296 | var t = tasks.find(function (x) { return x.id === task.id; }); |
241 | 297 | if (!t) return ''; |
|
0 commit comments