@operato/gantt 7.0.20 → 7.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/src/draw-time-scale-header.d.ts +5 -0
  3. package/dist/src/draw-time-scale-header.js +154 -0
  4. package/dist/src/draw-time-scale-header.js.map +1 -0
  5. package/dist/src/gantt-chart-controller.d.ts +1 -1
  6. package/dist/src/gantt-chart-controller.js +6 -161
  7. package/dist/src/gantt-chart-controller.js.map +1 -1
  8. package/dist/src/ox-gantt-chart.d.ts +6 -1
  9. package/dist/src/ox-gantt-chart.js +39 -8
  10. package/dist/src/ox-gantt-chart.js.map +1 -1
  11. package/dist/src/ox-gantt-resize-splitter.js +8 -7
  12. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  13. package/dist/src/ox-gantt.d.ts +24 -15
  14. package/dist/src/ox-gantt.js +224 -82
  15. package/dist/src/ox-gantt.js.map +1 -1
  16. package/dist/src/ox-resource-chart.d.ts +42 -0
  17. package/dist/src/ox-resource-chart.js +317 -0
  18. package/dist/src/ox-resource-chart.js.map +1 -0
  19. package/dist/src/types.d.ts +5 -0
  20. package/dist/src/types.js.map +1 -1
  21. package/dist/stories/ox-gantt.stories.js +75 -4
  22. package/dist/stories/ox-gantt.stories.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +2 -2
  25. package/src/draw-time-scale-header.ts +192 -0
  26. package/src/gantt-chart-controller.ts +6 -189
  27. package/src/ox-gantt-chart.ts +46 -9
  28. package/src/ox-gantt-resize-splitter.ts +8 -7
  29. package/src/ox-gantt.ts +231 -86
  30. package/src/ox-resource-chart.ts +328 -0
  31. package/src/types.ts +6 -0
  32. package/stories/ox-gantt.stories.ts +75 -4
  33. package/translations/en.json +4 -1
  34. package/translations/ja.json +3 -1
  35. package/translations/ko.json +4 -1
  36. package/translations/ms.json +3 -1
  37. package/translations/zh.json +3 -1
package/src/ox-gantt.ts CHANGED
@@ -1,13 +1,16 @@
1
- import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
1
+ import { LitElement, html, css, PropertyValues, PropertyValueMap, nothing, TemplateResult } from 'lit'
2
+ import * as d3 from 'd3'
2
3
  import i18next from 'i18next'
3
4
  import { customElement, property, query, state } from 'lit/decorators.js'
4
5
  import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
5
6
  import { ScrollbarStyles } from '@operato/styles'
6
7
 
7
8
  import './ox-gantt-chart.js'
9
+ import './ox-resource-chart.js'
8
10
  import './ox-gantt-resize-splitter.js'
9
11
  import { OxGanttChart } from './ox-gantt-chart.js'
10
- import { ColumnConfig, Task, TimeScale } from './types.js'
12
+ import { OxResourceChart } from './ox-resource-chart.js'
13
+ import { ColumnConfig, Task, TimeScale, Resource } from './types.js'
11
14
 
12
15
  @customElement('ox-gantt')
13
16
  export class OxGantt extends LitElement {
@@ -23,13 +26,24 @@ export class OxGantt extends LitElement {
23
26
  }
24
27
 
25
28
  .container {
29
+ flex: 1;
30
+
26
31
  display: flex;
32
+ flex-direction: column;
27
33
  width: 100%;
28
34
  height: 100%;
29
35
  box-sizing: border-box;
30
36
  }
31
37
 
32
- .task-container {
38
+ .resource-container {
39
+ height: 300px;
40
+
41
+ display: flex;
42
+ overflow: hidden;
43
+ box-sizing: border-box;
44
+ }
45
+
46
+ .resource-table-container {
33
47
  padding: 0;
34
48
  overflow: auto;
35
49
  overflow-y: overlay;
@@ -37,16 +51,34 @@ export class OxGantt extends LitElement {
37
51
  border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
38
52
  }
39
53
 
40
- .chart-container {
54
+ .resource-chart-container {
55
+ flex: 1;
56
+ padding: 0;
57
+ box-sizing: border-box;
58
+ border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
59
+ }
60
+
61
+ .task-container {
41
62
  flex: 1;
63
+
64
+ display: flex;
65
+ overflow: hidden;
66
+ box-sizing: border-box;
67
+ }
68
+
69
+ .task-table-container {
42
70
  padding: 0;
71
+ overflow: auto;
72
+ overflow-y: overlay;
43
73
  box-sizing: border-box;
44
74
  border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
45
75
  }
46
76
 
47
- ox-gantt-resize-splitter {
48
- width: 4px;
49
- cursor: ew-resize;
77
+ .task-chart-container {
78
+ flex: 1;
79
+ padding: 0;
80
+ box-sizing: border-box;
81
+ border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-on-surface));
50
82
  }
51
83
 
52
84
  table {
@@ -77,6 +109,7 @@ export class OxGantt extends LitElement {
77
109
  th {
78
110
  height: 40px;
79
111
  box-sizing: border-box;
112
+ text-align: center;
80
113
  }
81
114
 
82
115
  td {
@@ -91,6 +124,7 @@ export class OxGantt extends LitElement {
91
124
  ]
92
125
 
93
126
  @property({ type: Array }) tasks: Task[] = []
127
+ @property({ type: Array }) resources: Resource[] = []
94
128
  @property({ type: String, attribute: 'from-date' })
95
129
  fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
96
130
  @property({ type: String, attribute: 'to-date' })
@@ -136,41 +170,90 @@ export class OxGantt extends LitElement {
136
170
  visible: true,
137
171
  width: 100,
138
172
  order: 9
173
+ },
174
+ {
175
+ name: 'resources',
176
+ label: i18next.t('label.gantt-task-resources') || 'resources',
177
+ visible: true,
178
+ width: 100,
179
+ order: 10
139
180
  }
140
181
  ]
141
182
 
142
183
  @state() selectedTaskId?: string
143
184
 
144
- @query('.container') container!: HTMLDivElement
145
185
  @query('.task-container') taskContainer!: HTMLDivElement
146
- @query('.chart-container') chartContainer!: HTMLDivElement
147
- @query('table') taskTable!: HTMLTableElement
186
+ @query('.task-table-container') taskTableContainer!: HTMLDivElement
187
+ @query('.resource-container') resourceContainer!: HTMLDivElement
188
+ @query('.resource-table-container') resourceTableContainer!: HTMLDivElement
189
+ @query('.task-table') taskTable!: HTMLTableElement
148
190
  @query('ox-gantt-chart') ganttChart!: OxGanttChart
191
+ @query('ox-resource-chart') resourceChart!: OxResourceChart
149
192
 
150
193
  private _startWidth: number = 0
194
+ private _startHeight: number = 0
151
195
  private _isDragging: boolean = false
152
196
 
153
197
  render() {
154
198
  return html`
155
199
  <div class="container md-typescale-body-medium">
156
- <div class="task-container" @scroll=${this.syncScroll}>${this.renderTaskTable()}</div>
200
+ <div class="task-container">
201
+ <div class="task-table-container" @scroll=${this.syncScroll}>${this.renderTaskTable()}</div>
202
+ <ox-gantt-resize-splitter
203
+ vertical
204
+ @splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
205
+ @splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
206
+ @splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
207
+ ></ox-gantt-resize-splitter>
208
+ <ox-gantt-chart
209
+ class="task-chart-container"
210
+ @scroll=${this.syncScroll}
211
+ .fromDate=${this.fromDate}
212
+ .toDate=${this.toDate}
213
+ .tasks=${this.tasks}
214
+ .selectedTaskId=${this.selectedTaskId}
215
+ .axisScale=${this.timeScale}
216
+ ?extend-grid-lines=${this.extendGridLines}
217
+ @task-clicked=${this.handleTaskClicked}
218
+ @timescale-changed=${(e: CustomEvent) => this.syncTimeScale(e)}
219
+ ></ox-gantt-chart>
220
+ </div>
157
221
  <ox-gantt-resize-splitter
158
- vertical
159
- @splitter-dragstart=${(e: CustomEvent) => this.resizeStart(e)}
160
- @splitter-drag=${(e: CustomEvent) => this.resizeDrag(e)}
161
- @splitter-dragend=${(e: CustomEvent) => this.resizeEnd(e)}
222
+ horizontal
223
+ @splitter-dragstart=${(e: CustomEvent) => this.resizeVerticalStart(e)}
224
+ @splitter-drag=${(e: CustomEvent) => this.resizeVerticalDrag(e)}
225
+ @splitter-dragend=${(e: CustomEvent) => this.resizeVerticalEnd(e)}
162
226
  ></ox-gantt-resize-splitter>
163
- <ox-gantt-chart
164
- class="chart-container"
165
- @scroll=${this.syncScroll}
166
- .fromDate=${this.fromDate}
167
- .toDate=${this.toDate}
168
- .tasks=${this.tasks}
169
- .selectedTaskId=${this.selectedTaskId}
170
- .axisScale=${this.timeScale}
171
- ?extend-grid-lines=${this.extendGridLines}
172
- @task-clicked=${this.handleTaskClicked}
173
- ></ox-gantt-chart>
227
+ <div class="resource-container">
228
+ <div class="resource-table-container" @scroll=${this.syncScroll}>
229
+ <table>
230
+ <thead>
231
+ <tr>
232
+ <th style="width: 100px">${i18next.t('label.gantt-resource-type')}</th>
233
+ <th>${i18next.t('label.gantt-resource-allocated')}</th>
234
+ </tr>
235
+ </thead>
236
+ <tbody>
237
+ ${this.resources.map(resource => this.renderResourceRow(resource))}
238
+ </tbody>
239
+ </table>
240
+ </div>
241
+ <ox-gantt-resize-splitter
242
+ vertical
243
+ @splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
244
+ @splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
245
+ @splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
246
+ ></ox-gantt-resize-splitter>
247
+ <ox-resource-chart
248
+ class="resource-chart-container"
249
+ .fromDate=${this.fromDate}
250
+ .toDate=${this.toDate}
251
+ .tasks=${this.tasks}
252
+ .resources=${this.resources}
253
+ .axisScale=${this.timeScale}
254
+ @scroll=${this.syncScroll}
255
+ ></ox-resource-chart>
256
+ </div>
174
257
  </div>
175
258
  `
176
259
  }
@@ -180,42 +263,55 @@ export class OxGantt extends LitElement {
180
263
  return
181
264
  }
182
265
 
183
- const mergedTasks = this.mergeTasksBySection(this.tasks)
184
-
185
266
  // Sort columns by the configured order
186
267
  const sortedColumns = [...this.columnConfig].sort((a, b) => a.order - b.order)
187
268
 
188
269
  return html`
189
- <table>
270
+ <table class="task-table">
190
271
  <thead>
191
272
  <tr>
192
273
  ${sortedColumns.map(col =>
193
- col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : null
274
+ col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : nothing
194
275
  )}
195
276
  </tr>
196
277
  </thead>
197
278
  <tbody>
198
- ${mergedTasks.map(
199
- ({ task, showSection, rowSpan }) => html`
200
- <tr class="${this.selectedTaskId === task.id ? 'selected' : ''}" @click=${() => this.selectTask(task.id)}>
201
- ${sortedColumns.map(col => {
202
- if (col.name === 'section') {
203
- return col.visible && showSection
204
- ? html`<td rowspan=${rowSpan} style="width: ${col.width}px">${task.section?.name || ''}</td>`
205
- : null
206
- }
207
- return col.visible
208
- ? html`<td style="width: ${col.width}px">${this.getTaskFieldValue(task, col.name)}</td>`
209
- : null
210
- })}
211
- </tr>
212
- `
213
- )}
279
+ ${this.renderTaskRows(this.tasks)}
214
280
  </tbody>
215
281
  </table>
216
282
  `
217
283
  }
218
284
 
285
+ renderTaskRows(tasks: Task[], level: number = 0): TemplateResult[] {
286
+ return tasks.flatMap(task => [
287
+ this.renderTaskRow(task, level),
288
+ ...this.renderTaskRows(task.children || [], level + 1)
289
+ ])
290
+ }
291
+
292
+ renderTaskRow(task: Task, level: number) {
293
+ return html`
294
+ <tr class=${this.selectedTaskId === task.id ? 'selected' : ''} @click=${() => this.selectTask(task.id)}>
295
+ ${this.columnConfig.map(col =>
296
+ col.visible
297
+ ? html`<td class="task-row" style="padding-left: ${col.name == 'title' ? level * 8 + 4 : 4}px">
298
+ ${this.getTaskFieldValue(task, col.name)}
299
+ </td>`
300
+ : null
301
+ )}
302
+ </tr>
303
+ `
304
+ }
305
+
306
+ renderResourceRow(resource: Resource) {
307
+ return html`
308
+ <tr>
309
+ <td>${resource.type}</td>
310
+ <td>${resource.allocated}</td>
311
+ </tr>
312
+ `
313
+ }
314
+
219
315
  getTaskFieldValue(task: Task, field: string) {
220
316
  switch (field) {
221
317
  case 'id':
@@ -236,34 +332,13 @@ export class OxGantt extends LitElement {
236
332
  return task.progress || ''
237
333
  case 'dependsOn':
238
334
  return task.dependsOn || ''
335
+ case 'resources':
336
+ return task.resources?.map(res => `${res.type}: ${res.allocated}`).join(', ') || ''
239
337
  default:
240
338
  return ''
241
339
  }
242
340
  }
243
341
 
244
- mergeTasksBySection(tasks: Task[]): Array<{ task: Task; showSection: boolean; rowSpan: number }> {
245
- const mergedTasks = [] as Array<{ task: Task; showSection: boolean; rowSpan: number }>
246
- let currentSection = ''
247
- let rowSpan = 0
248
-
249
- ;(tasks || []).forEach((task, index) => {
250
- if (task.section?.name !== currentSection) {
251
- currentSection = task.section?.name || ''
252
- rowSpan = 1
253
- mergedTasks.push({ task, showSection: true, rowSpan: 1 })
254
- } else {
255
- mergedTasks[mergedTasks.length - rowSpan].rowSpan = ++rowSpan
256
- mergedTasks.push({ task, showSection: false, rowSpan: 0 })
257
- }
258
-
259
- if (index === tasks.length - 1 && rowSpan > 0) {
260
- mergedTasks[mergedTasks.length - rowSpan].rowSpan = rowSpan
261
- }
262
- })
263
-
264
- return mergedTasks
265
- }
266
-
267
342
  selectTask(taskId: string) {
268
343
  this.selectedTaskId = taskId
269
344
  }
@@ -273,48 +348,118 @@ export class OxGantt extends LitElement {
273
348
  }
274
349
 
275
350
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
276
- this.taskContainer.style.width = '254px'
351
+ this.taskTableContainer.style.width = '254px'
352
+ this.resourceTableContainer.style.width = '254px'
353
+ }
354
+
355
+ updated(changedProperties: PropertyValues<this>) {
356
+ if (changedProperties.has('tasks')) {
357
+ this.resources = this.aggregateResources(this.tasks)
358
+ }
277
359
  }
278
360
 
279
- updated(changes: PropertyValues<this>) {}
361
+ aggregateResources(tasks: Task[]): Resource[] {
362
+ const resourceMap: { [key: string]: number } = {}
363
+
364
+ const traverseTasks = (tasks: Task[]) => {
365
+ tasks.forEach(task => {
366
+ if (task.resources) {
367
+ const startDate = new Date(task.startDate)
368
+ const endDate = new Date(task.endDate)
369
+ const durationDays = d3.timeDay.count(startDate, d3.timeDay.offset(endDate, 1))
370
+
371
+ task.resources.forEach(resource => {
372
+ const totalAllocation = resource.allocated * durationDays
373
+ if (!resourceMap[resource.type]) {
374
+ resourceMap[resource.type] = 0
375
+ }
376
+ resourceMap[resource.type] += totalAllocation
377
+ console.log(resource.type, resource.allocated, durationDays, totalAllocation)
378
+ })
379
+ }
380
+
381
+ if (task.children) {
382
+ traverseTasks(task.children)
383
+ }
384
+ })
385
+ }
386
+
387
+ traverseTasks(tasks)
388
+
389
+ return Object.keys(resourceMap).map(type => ({
390
+ type,
391
+ allocated: resourceMap[type]
392
+ }))
393
+ }
280
394
 
281
395
  syncScroll(e: Event) {
282
396
  const target = e.target as HTMLElement
283
- if (target === this.taskContainer) {
284
- // Sync chart scroll with table scroll
285
- const newScrollTop = this.taskContainer.scrollTop
397
+
398
+ if (target === this.taskTableContainer) {
399
+ const newScrollTop = this.taskTableContainer.scrollTop
286
400
  this.ganttChart.setVerticalOffset(newScrollTop)
287
401
 
288
- // Ensure chart scroll matches table scroll
289
402
  if (this.ganttChart.getVerticalOffset() !== newScrollTop) {
290
403
  this.ganttChart.setVerticalOffset(newScrollTop)
291
404
  }
292
405
  } else if (target === this.ganttChart) {
293
- // Sync table scroll with chart scroll
294
406
  const newScrollTop = this.ganttChart.getVerticalOffset()
295
- this.taskContainer.scrollTop = newScrollTop
407
+ this.taskTableContainer.scrollTop = newScrollTop
408
+ this.ganttChart.setVerticalOffset(this.taskTableContainer.scrollTop)
409
+
410
+ const newScrollLeft = this.ganttChart.getHorizontalOffset()
411
+ this.resourceChart.setHorizontalOffset(newScrollLeft)
412
+ } else if (target === this.resourceChart) {
413
+ const newScrollTop = this.ganttChart.getVerticalOffset()
414
+ this.resourceTableContainer.scrollTop = newScrollTop
415
+ this.resourceChart.setVerticalOffset(this.taskTableContainer.scrollTop)
296
416
 
297
- // Ensure table scroll matches chart scroll
298
- this.ganttChart.setVerticalOffset(this.taskContainer.scrollTop)
417
+ const newScrollLeft = this.resourceChart.getHorizontalOffset()
418
+ this.ganttChart.setHorizontalOffset(newScrollLeft)
419
+ } else if (target === this.resourceTableContainer) {
420
+ const newScrollTop = this.taskTableContainer.scrollTop
421
+ this.resourceChart.setVerticalOffset(newScrollTop)
422
+
423
+ if (this.resourceChart.getVerticalOffset() !== newScrollTop) {
424
+ this.resourceChart.setVerticalOffset(newScrollTop)
425
+ }
299
426
  }
300
427
  }
301
428
 
302
- resizeStart(e: CustomEvent) {
429
+ syncTimeScale(e: CustomEvent) {
430
+ const detail = e.detail
431
+ this.resourceChart.syncTimeScale(detail)
432
+ }
433
+
434
+ resizeHorizontalStart(e: CustomEvent) {
303
435
  this._startWidth = ((e.target as HTMLElement)?.previousElementSibling as HTMLElement)?.offsetWidth
304
436
  this._isDragging = true
305
437
  }
306
438
 
307
- resizeDrag(e: CustomEvent) {
439
+ resizeHorizontalDrag(e: CustomEvent) {
308
440
  if (!this._isDragging) return
309
441
  const delta = e.detail
310
- const taskContainer = this.taskContainer
442
+ this.taskTableContainer.style.width = `${this._startWidth + delta.x}px`
443
+ this.resourceTableContainer.style.width = `${this._startWidth + delta.x}px`
444
+ }
311
445
 
312
- if (taskContainer) {
313
- taskContainer.style.width = `${this._startWidth + delta.x}px`
314
- }
446
+ resizeHorizontalEnd(e: CustomEvent) {
447
+ this._isDragging = false
448
+ }
449
+
450
+ resizeVerticalStart(e: CustomEvent) {
451
+ this._startHeight = ((e.target as HTMLElement)?.nextElementSibling as HTMLElement)?.offsetHeight
452
+ this._isDragging = true
453
+ }
454
+
455
+ resizeVerticalDrag(e: CustomEvent) {
456
+ if (!this._isDragging) return
457
+ const delta = e.detail
458
+
459
+ this.resourceContainer.style.height = `${this._startHeight - delta.y}px`
315
460
  }
316
461
 
317
- resizeEnd(e: CustomEvent) {
462
+ resizeVerticalEnd(e: CustomEvent) {
318
463
  this._isDragging = false
319
464
  }
320
465
  }