@basementuniverse/kanbn 1.0.0 → 1.1.0

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.
@@ -23,10 +23,15 @@ async function interactiveCreateTask(taskData, taskIds, columnName, columnNames)
23
23
  'due' in taskData.metadata &&
24
24
  taskData.metadata.due != null
25
25
  );
26
- const postponedDateExists = (
26
+ const plannedStartDateExists = (
27
27
  'metadata' in taskData &&
28
- 'postponed' in taskData.metadata &&
29
- taskData.metadata.postponed != null
28
+ 'plannedStart' in taskData.metadata &&
29
+ taskData.metadata.plannedStart != null
30
+ );
31
+ const plannedFinishDateExists = (
32
+ 'metadata' in taskData &&
33
+ 'plannedFinish' in taskData.metadata &&
34
+ taskData.metadata.plannedFinish != null
30
35
  );
31
36
  const assignedExists = (
32
37
  'metadata' in taskData &&
@@ -83,18 +88,33 @@ async function interactiveCreateTask(taskData, taskIds, columnName, columnNames)
83
88
  },
84
89
  {
85
90
  type: 'confirm',
86
- name: 'setPostponed',
87
- message: 'Set a postponed date?',
91
+ name: 'setPlannedStart',
92
+ message: 'Set a planned start date?',
88
93
  default: false,
89
- when: answers => !postponedDateExists
94
+ when: answers => !plannedStartDateExists
90
95
  },
91
96
  {
92
97
  type: 'datepicker',
93
- name: 'postponed',
94
- message: 'Postponed date:',
95
- default: postponedDateExists ? taskData.metadata.postponed : new Date(),
98
+ name: 'plannedStart',
99
+ message: 'Planned start date:',
100
+ default: plannedStartDateExists ? taskData.metadata.plannedStart : new Date(),
96
101
  format: ['Y', '/', 'MM', '/', 'DD'],
97
- when: answers => answers.setPostponed,
102
+ when: answers => answers.setPlannedStart,
103
+ },
104
+ {
105
+ type: 'confirm',
106
+ name: 'setPlannedFinish',
107
+ message: 'Set a planned finish date?',
108
+ default: false,
109
+ when: answers => !plannedFinishDateExists
110
+ },
111
+ {
112
+ type: 'datepicker',
113
+ name: 'plannedFinish',
114
+ message: 'Planned finish date:',
115
+ default: plannedFinishDateExists ? taskData.metadata.plannedFinish : new Date(),
116
+ format: ['Y', '/', 'MM', '/', 'DD'],
117
+ when: answers => answers.setPlannedFinish,
98
118
  },
99
119
  {
100
120
  type: 'confirm',
@@ -343,11 +363,20 @@ module.exports = async args => {
343
363
  }
344
364
  }
345
365
 
346
- // Postponed date
347
- if (args.postponed) {
348
- taskData.metadata.postponed = chrono.parseDate(utility.strArg(args.postponed));
349
- if (taskData.metadata.postponed === null) {
350
- utility.error('Unable to parse postponed date');
366
+ // Planned start date
367
+ if (args.plannedStart) {
368
+ taskData.metadata.plannedStart = chrono.parseDate(utility.strArg(args.plannedStart));
369
+ if (taskData.metadata.plannedStart === null) {
370
+ utility.error('Unable to parse plannedStart date');
371
+ return;
372
+ }
373
+ }
374
+
375
+ // Planned finish date
376
+ if (args.plannedFinish) {
377
+ taskData.metadata.plannedFinish = chrono.parseDate(utility.strArg(args.plannedFinish));
378
+ if (taskData.metadata.plannedFinish === null) {
379
+ utility.error('Unable to parse plannedFinish date');
351
380
  return;
352
381
  }
353
382
  }
@@ -481,8 +510,11 @@ module.exports = async args => {
481
510
  if ('due' in answers) {
482
511
  taskData.metadata.due = answers.due;
483
512
  }
484
- if ('postponed' in answers) {
485
- taskData.metadata.postponed = answers.postponed;
513
+ if ('plannedStart' in answers) {
514
+ taskData.metadata.plannedStart = answers.plannedStart;
515
+ }
516
+ if ('plannedFinish' in answers) {
517
+ taskData.metadata.plannedFinish = answers.plannedFinish;
486
518
  }
487
519
  if ('assigned' in answers) {
488
520
  taskData.metadata.assigned = answers.assigned;
@@ -23,10 +23,15 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
23
23
  'due' in taskData.metadata &&
24
24
  taskData.metadata.due != null
25
25
  );
26
- const postponedDateExists = (
26
+ const plannedStartDateExists = (
27
27
  'metadata' in taskData &&
28
- 'postponed' in taskData.metadata &&
29
- taskData.metadata.postponed != null
28
+ 'plannedStart' in taskData.metadata &&
29
+ taskData.metadata.plannedStart != null
30
+ );
31
+ const plannedFinishDateExists = (
32
+ 'metadata' in taskData &&
33
+ 'plannedFinish' in taskData.metadata &&
34
+ taskData.metadata.plannedFinish != null
30
35
  );
31
36
  const assignedExists = (
32
37
  'metadata' in taskData &&
@@ -108,10 +113,10 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
108
113
  },
109
114
  {
110
115
  type: 'expand',
111
- name: 'editPostponed',
112
- message: 'Edit or remove postponed date?',
116
+ name: 'editPlannedStart',
117
+ message: 'Edit or remove planned start date?',
113
118
  default: 'none',
114
- when: answers => postponedDateExists,
119
+ when: answers => plannedStartDateExists,
115
120
  choices: [
116
121
  {
117
122
  key: 'e',
@@ -133,18 +138,58 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
133
138
  },
134
139
  {
135
140
  type: 'confirm',
136
- name: 'setPostponed',
137
- message: 'Set a postponed date?',
141
+ name: 'setPlannedStart',
142
+ message: 'Set a planned start date?',
138
143
  default: false,
139
- when: answers => !postponedDateExists
144
+ when: answers => !plannedStartDateExists
140
145
  },
141
146
  {
142
147
  type: 'datepicker',
143
- name: 'postponed',
144
- message: 'Postponed date:',
145
- default: postponedDateExists ? taskData.metadata.postponed : new Date(),
148
+ name: 'plannedStart',
149
+ message: 'Planned start date:',
150
+ default: plannedStartDateExists ? taskData.metadata.plannedStart : new Date(),
146
151
  format: ['Y', '/', 'MM', '/', 'DD'],
147
- when: answers => answers.setPostponed || answers.editPostponed === 'edit'
152
+ when: answers => answers.setPlannedStart || answers.editPlannedStart === 'edit'
153
+ },
154
+ {
155
+ type: 'expand',
156
+ name: 'editPlannedFinish',
157
+ message: 'Edit or remove planned finish date?',
158
+ default: 'none',
159
+ when: answers => plannedFinishDateExists,
160
+ choices: [
161
+ {
162
+ key: 'e',
163
+ name: 'Edit',
164
+ value: 'edit'
165
+ },
166
+ {
167
+ key: 'r',
168
+ name: 'Remove',
169
+ value: 'remove'
170
+ },
171
+ new inquirer.Separator(),
172
+ {
173
+ key: 'n',
174
+ name: 'Do nothing',
175
+ value: 'none'
176
+ }
177
+ ]
178
+ },
179
+ {
180
+ type: 'confirm',
181
+ name: 'setPlannedFinish',
182
+ message: 'Set a planned finish date?',
183
+ default: false,
184
+ when: answers => !plannedFinishDateExists
185
+ },
186
+ {
187
+ type: 'datepicker',
188
+ name: 'plannedFinish',
189
+ message: 'Planned finish date:',
190
+ default: plannedFinishDateExists ? taskData.metadata.plannedFinish : new Date(),
191
+ format: ['Y', '/', 'MM', '/', 'DD'],
192
+ when: answers => answers.setPlannedFinish || answers.editPlannedFinish === 'edit'
148
193
  },
149
194
  {
150
195
  type: 'expand',
@@ -475,14 +520,26 @@ module.exports = async args => {
475
520
  }
476
521
  }
477
522
 
478
- // Postponed date
479
- if (args.postponed) {
523
+ // Planned start date
524
+ if (args.plannedStart) {
480
525
  if (!('metadata' in taskData)) {
481
526
  taskData.metadata = {};
482
527
  }
483
- taskData.metadata.postponed = chrono.parseDate(utility.strArg(args.postponed));
484
- if (taskData.metadata.postponed === null) {
485
- utility.error('Unable to parse postponed date');
528
+ taskData.metadata.plannedStart = chrono.parseDate(utility.strArg(args.plannedStart));
529
+ if (taskData.metadata.plannedStart === null) {
530
+ utility.error('Unable to parse plannedStart date');
531
+ return;
532
+ }
533
+ }
534
+
535
+ // Planned finish date
536
+ if (args.plannedFinish) {
537
+ if (!('metadata' in taskData)) {
538
+ taskData.metadata = {};
539
+ }
540
+ taskData.metadata.plannedFinish = chrono.parseDate(utility.strArg(args.plannedFinish));
541
+ if (taskData.metadata.plannedFinish === null) {
542
+ utility.error('Unable to parse plannedFinish date');
486
543
  return;
487
544
  }
488
545
  }
@@ -720,14 +777,24 @@ module.exports = async args => {
720
777
  taskData.metadata.due = answers.due;
721
778
  }
722
779
 
723
- // Remove postponed date
724
- if ('editPostponed' in answers && answers.editPostponed === 'remove') {
725
- delete taskData.metadata.postponed;
780
+ // Remove planned start date
781
+ if ('editPlannedStart' in answers && answers.editPlannedStart === 'remove') {
782
+ delete taskData.metadata.plannedStart;
783
+ }
784
+
785
+ // Planned start date
786
+ if ('plannedStart' in answers) {
787
+ taskData.metadata.plannedStart = answers.plannedStart;
788
+ }
789
+
790
+ // Remove planned finish date
791
+ if ('editPlannedFinish' in answers && answers.editPlannedFinish === 'remove') {
792
+ delete taskData.metadata.plannedFinish;
726
793
  }
727
794
 
728
- // Postponed date
729
- if ('postponed' in answers) {
730
- taskData.metadata.postponed = answers.postponed;
795
+ // Planned finish date
796
+ if ('plannedFinish' in answers) {
797
+ taskData.metadata.plannedFinish = answers.plannedFinish;
731
798
  }
732
799
 
733
800
  // Remove assigned
@@ -53,8 +53,13 @@ const searchFields = [
53
53
  type: 'date'
54
54
  },
55
55
  {
56
- name: 'Postponed',
57
- field: 'postponed',
56
+ name: 'Planned start',
57
+ field: 'plannedStart',
58
+ type: 'date'
59
+ },
60
+ {
61
+ name: 'Planned finish',
62
+ field: 'plannedFinish',
58
63
  type: 'date'
59
64
  },
60
65
  {
@@ -369,7 +374,7 @@ module.exports = async args => {
369
374
  }
370
375
  }
371
376
  if ('workload' in filters){
372
- if (!convertNumericFilters(filters, 'workoad')) {
377
+ if (!convertNumericFilters(filters, 'workload')) {
373
378
  utility.error('Workload filter value must be numeric');
374
379
  return;
375
380
  }
@@ -412,6 +417,18 @@ module.exports = async args => {
412
417
  return;
413
418
  }
414
419
  }
420
+ if ('plannedStart' in filters) {
421
+ if (!convertDateFilters(filters, 'plannedStart')) {
422
+ utility.error('Unable to parse plannedStart date');
423
+ return;
424
+ }
425
+ }
426
+ if ('plannedFinish' in filters) {
427
+ if (!convertDateFilters(filters, 'plannedFinish')) {
428
+ utility.error('Unable to parse plannedFinish date');
429
+ return;
430
+ }
431
+ }
415
432
 
416
433
  // Check and add custom field filters
417
434
  if ('customFields' in index.options) {
@@ -363,10 +363,9 @@ module.exports = async args => {
363
363
  });
364
364
 
365
365
  // Render x-axis
366
- console.log('');
367
366
  const [ticks, labels] = renderXAxisLabels(data.from, data.to, barWidth, dateFormat);
368
- console.log(` ${''.padEnd(maxNameWidth)} │${renderTickRow(ticks, nowPosition)}│`);
369
- console.log(` ${''.padEnd(maxNameWidth)} │${labels}│`);
367
+ console.log(` ${''.padEnd(maxNameWidth)} ${renderTickRow(ticks, nowPosition)}`);
368
+ console.log(` ${''.padEnd(maxNameWidth)} ${labels}`);
370
369
  }
371
370
  })
372
371
  .catch(error => {
@@ -0,0 +1,105 @@
1
+ const kanbn = require('../main');
2
+ const utility = require('../utility');
3
+ const chrono = require('chrono-node');
4
+ const formatDate = require('dateformat');
5
+
6
+ function formatHistoryEvent(event) {
7
+ const details = [];
8
+ if ('column' in event.event) {
9
+ details.push(`column: ${event.event.column}`);
10
+ }
11
+ if ('fromColumn' in event.event || 'toColumn' in event.event) {
12
+ details.push(`column: ${event.event.fromColumn || '?'} -> ${event.event.toColumn || '?'}`);
13
+ }
14
+ if ('fromProgress' in event.event || 'toProgress' in event.event) {
15
+ details.push(`progress: ${event.event.fromProgress || 0} -> ${event.event.toProgress || 0}`);
16
+ }
17
+ if ('author' in event.event && event.event.author) {
18
+ details.push(`author: ${event.event.author}`);
19
+ }
20
+ return details.join(', ');
21
+ }
22
+
23
+ function formatMetadataEvent(event, dateFormat) {
24
+ if (!('field' in event.event) || !('value' in event.event) || !(event.event.value instanceof Date)) {
25
+ return '';
26
+ }
27
+ return `${event.event.field}: ${formatDate(event.event.value, dateFormat)}`;
28
+ }
29
+
30
+ module.exports = async args => {
31
+
32
+ // Make sure kanbn has been initialised
33
+ if (!await kanbn.initialised()) {
34
+ utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
35
+ return;
36
+ }
37
+ const index = await kanbn.getIndex();
38
+
39
+ // Get sprint numbers or names
40
+ let sprints = null;
41
+ if (args.sprint) {
42
+ sprints = utility.arrayArg(args.sprint).map(s => {
43
+ const sprintNumber = parseInt(s);
44
+ return isNaN(sprintNumber) ? s : sprintNumber;
45
+ });
46
+ }
47
+
48
+ // Get dates
49
+ let dates = null;
50
+ if (args.date) {
51
+ dates = utility.arrayArg(args.date);
52
+ if (dates.length) {
53
+ for (let i = 0; i < dates.length; i++) {
54
+ const dateValue = chrono.parseDate(dates[i]);
55
+ if (dateValue === null) {
56
+ utility.error('Unable to parse date');
57
+ return;
58
+ }
59
+ dates[i] = dateValue;
60
+ }
61
+ }
62
+ }
63
+
64
+ // Get assigned
65
+ let assigned = null;
66
+ if (args.assigned) {
67
+ assigned = utility.strArg(args.assigned);
68
+ }
69
+
70
+ // Get task ids
71
+ let taskIds = null;
72
+ if (args.task) {
73
+ taskIds = utility.arrayArg(args.task).map((taskId) => utility.trimLeftEscapeCharacters(taskId));
74
+ }
75
+
76
+ kanbn
77
+ .history(sprints, dates, assigned, taskIds)
78
+ .then(data => {
79
+ if (args.json) {
80
+ console.log(JSON.stringify(data, null, 2));
81
+ return;
82
+ }
83
+
84
+ if (data.events.length === 0) {
85
+ console.log('No history events found');
86
+ return;
87
+ }
88
+
89
+ const dateFormat = kanbn.getDateFormat(index);
90
+ const lines = data.events.map((event) => {
91
+ const eventDetails = event.source === 'history'
92
+ ? formatHistoryEvent(event)
93
+ : formatMetadataEvent(event, dateFormat);
94
+ const source = event.source === 'history' ? event.eventType : `metadata.${event.eventType}`;
95
+ const line = `${formatDate(event.date, dateFormat)} | ${event.task.id} | ${source}`;
96
+ return eventDetails
97
+ ? `${line} | ${eventDetails}`
98
+ : line;
99
+ });
100
+ console.log(lines.join('\n'));
101
+ })
102
+ .catch(error => {
103
+ utility.error(error);
104
+ });
105
+ };
@@ -123,10 +123,18 @@ const sorterFields = [
123
123
  filterable: false
124
124
  },
125
125
  {
126
- name: 'Postponed date',
127
- field: 'postponed',
126
+ name: 'Planned start date',
127
+ field: 'plannedStart',
128
128
  options: [
129
- '--postponed'
129
+ '--plannedStart'
130
+ ],
131
+ filterable: false
132
+ },
133
+ {
134
+ name: 'Planned finish date',
135
+ field: 'plannedFinish',
136
+ options: [
137
+ '--plannedFinish'
130
138
  ],
131
139
  filterable: false
132
140
  },
package/src/main.js CHANGED
@@ -266,7 +266,8 @@ function sortColumnInIndex(index, tasks, columnName, sorters) {
266
266
  started: "started" in task.metadata ? task.metadata.started : "",
267
267
  completed: "completed" in task.metadata ? task.metadata.completed : "",
268
268
  due: "due" in task.metadata ? task.metadata.due : "",
269
- postponed: "postponed" in task.metadata ? task.metadata.postponed : "",
269
+ plannedStart: "plannedStart" in task.metadata ? task.metadata.plannedStart : "",
270
+ plannedFinish: "plannedFinish" in task.metadata ? task.metadata.plannedFinish : "",
270
271
  assigned: "assigned" in task.metadata ? task.metadata.assigned : "",
271
272
  countSubTasks: task.subTasks.length,
272
273
  subTasks: task.subTasks.map((subTask) => `[${subTask.completed ? "x" : ""}] ${subTask.text}`).join("\n"),
@@ -840,9 +841,10 @@ function endOfDay(date) {
840
841
  */
841
842
  function getTaskGanttAnchor(task) {
842
843
  return minDate([
843
- task.metadata.postponed,
844
+ task.metadata.plannedStart,
844
845
  task.created,
845
846
  task.started,
847
+ task.metadata.plannedFinish,
846
848
  task.completed
847
849
  ]) || new Date(0);
848
850
  }
@@ -986,20 +988,47 @@ function buildGanttSchedule(index, tasks, now) {
986
988
  .filter((scheduledDependency) => scheduledDependency)
987
989
  .map((scheduledDependency) => scheduledDependency.end)
988
990
  );
989
- const taskBaseDate = maxDate([
990
- task.created,
991
- task.started,
992
- task.metadata.postponed,
993
- ]);
991
+ const hasPlannedStart = task.metadata.plannedStart instanceof Date;
992
+ const hasPlannedFinish = task.metadata.plannedFinish instanceof Date;
993
+ const hasStarted = task.started instanceof Date;
994
+ const hasCompleted = task.completed instanceof Date;
995
+
996
+ const duration = hasPlannedStart && hasPlannedFinish
997
+ ? Math.max(DAY, task.metadata.plannedFinish.getTime() - task.metadata.plannedStart.getTime())
998
+ : hasStarted && hasCompleted
999
+ ? Math.max(DAY, task.completed.getTime() - task.started.getTime())
1000
+ : Math.max(DAY, Math.ceil(Math.max(1, task.workload)) * DAY);
1001
+
1002
+ const preferredStart = hasPlannedStart
1003
+ ? task.metadata.plannedStart
1004
+ : hasStarted
1005
+ ? task.started
1006
+ : task.created;
1007
+ const finishHint = hasPlannedFinish
1008
+ ? task.metadata.plannedFinish
1009
+ : hasCompleted
1010
+ ? task.completed
1011
+ : null;
1012
+
1013
+ const derivedStartFromFinish = finishHint instanceof Date
1014
+ ? new Date(finishHint.getTime() - duration)
1015
+ : null;
1016
+ const taskBaseDate = hasPlannedStart || hasStarted
1017
+ ? preferredStart
1018
+ : (derivedStartFromFinish || preferredStart);
1019
+
994
1020
  const scheduleAnchor = maxDate([
995
1021
  taskBaseDate,
996
1022
  dependencyEnd
997
1023
  ]) || new Date(0);
998
- const duration = task.completed instanceof Date && task.started instanceof Date
999
- ? Math.max(DAY, task.completed.getTime() - task.started.getTime())
1000
- : Math.max(DAY, Math.ceil(Math.max(1, task.workload)) * DAY);
1024
+
1001
1025
  let start = new Date(scheduleAnchor.getTime());
1002
- let end = task.completed instanceof Date ? new Date(task.completed.getTime()) : new Date(start.getTime() + duration);
1026
+ let end = hasPlannedFinish
1027
+ ? new Date(task.metadata.plannedFinish.getTime())
1028
+ : hasCompleted
1029
+ ? new Date(task.completed.getTime())
1030
+ : new Date(start.getTime() + duration);
1031
+
1003
1032
  if (end.getTime() < start.getTime()) {
1004
1033
  end = new Date(start.getTime() + duration);
1005
1034
  }
@@ -1009,7 +1038,7 @@ function buildGanttSchedule(index, tasks, now) {
1009
1038
  dependencies,
1010
1039
  start,
1011
1040
  end,
1012
- blocked: dependencyEnd !== null && taskBaseDate !== null && dependencyEnd.getTime() > taskBaseDate.getTime()
1041
+ blocked: dependencyEnd !== null && preferredStart instanceof Date && dependencyEnd.getTime() > preferredStart.getTime()
1013
1042
  };
1014
1043
  scheduleById.set(task.id, scheduledTask);
1015
1044
  scheduledTasks.push(scheduledTask);
@@ -2520,7 +2549,7 @@ class Kanbn {
2520
2549
  if (indexSprints === null) {
2521
2550
  throw new Error(`No sprints defined`);
2522
2551
  } else {
2523
- for (sprint of sprints) {
2552
+ for (let sprint of sprints) {
2524
2553
  let sprintIndex = null;
2525
2554
 
2526
2555
  // Select sprint by number (1-based index)
@@ -2632,6 +2661,138 @@ class Kanbn {
2632
2661
  return { series };
2633
2662
  }
2634
2663
 
2664
+ /**
2665
+ * Get a chronological list of task events from metadata dates and structured task history
2666
+ * @param {?string[]} [sprints=null] The sprint names or numbers to include, or null for no sprint filter
2667
+ * @param {?Date[]} [dates=null] One or more dates to define a range filter, or null for no date filter
2668
+ * @param {?string} [assigned=null] The assigned user to filter for, or null for no assigned filter
2669
+ * @param {?string[]} [taskIds=null] One or more task ids to filter for, or null for no task id filter
2670
+ * @return {Promise<object>} A list of chronological task history events
2671
+ */
2672
+ async history(sprints = null, dates = null, assigned = null, taskIds = null) {
2673
+ // Check if this folder has been initialised
2674
+ if (!(await this.initialised())) {
2675
+ throw new Error("Not initialised in this folder");
2676
+ }
2677
+
2678
+ const index = await this.loadIndex();
2679
+ const selectedTaskIds = taskIds === null
2680
+ ? null
2681
+ : new Set(taskIds.map((taskId) => removeFileExtension(taskId)));
2682
+
2683
+ // Build date filter periods from sprints and/or dates
2684
+ const periods = [];
2685
+ const indexSprints = "sprints" in index.options && index.options.sprints.length ? index.options.sprints : null;
2686
+ if (sprints !== null) {
2687
+ if (indexSprints === null) {
2688
+ throw new Error("No sprints defined");
2689
+ }
2690
+ for (let sprint of sprints) {
2691
+ let sprintIndex = null;
2692
+
2693
+ // Select sprint by number (1-based index)
2694
+ if (typeof sprint === "number") {
2695
+ if (sprint < 1 || sprint > indexSprints.length) {
2696
+ throw new Error(`Sprint ${sprint} does not exist`);
2697
+ }
2698
+ sprintIndex = sprint - 1;
2699
+
2700
+ // Or select sprint by name
2701
+ } else if (typeof sprint === "string") {
2702
+ sprintIndex = indexSprints.findIndex((s) => s.name === sprint);
2703
+ if (sprintIndex === -1) {
2704
+ throw new Error(`No sprint found with name "${sprint}"`);
2705
+ }
2706
+ }
2707
+
2708
+ if (sprintIndex === null) {
2709
+ throw new Error(`Invalid sprint "${sprint}"`);
2710
+ }
2711
+
2712
+ periods.push({
2713
+ type: "sprint",
2714
+ label: indexSprints[sprintIndex].name,
2715
+ from: new Date(indexSprints[sprintIndex].start),
2716
+ to: sprintIndex < indexSprints.length - 1
2717
+ ? new Date(indexSprints[sprintIndex + 1].start)
2718
+ : new Date(),
2719
+ });
2720
+ }
2721
+ }
2722
+
2723
+ if (dates !== null) {
2724
+ periods.push({
2725
+ type: "date",
2726
+ from: new Date(Math.min(...dates)),
2727
+ to: dates.length === 1 ? new Date() : new Date(Math.max(...dates)),
2728
+ });
2729
+ }
2730
+
2731
+ const includeEventDate = (date) => {
2732
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
2733
+ return false;
2734
+ }
2735
+ if (periods.length === 0) {
2736
+ return true;
2737
+ }
2738
+ return periods.some((period) => date >= period.from && date <= period.to);
2739
+ };
2740
+
2741
+ const buildBaseTask = (task) => ({
2742
+ id: task.id,
2743
+ name: task.name,
2744
+ assigned: getTaskMetadata(task, "assigned") || null,
2745
+ column: findTaskColumn(index, task.id),
2746
+ });
2747
+
2748
+ const tasks = await this.loadAllTrackedTasks(index);
2749
+ let events = tasks
2750
+ .filter((task) => {
2751
+ const assignedUser = getTaskMetadata(task, "assigned") || null;
2752
+ return (
2753
+ (assigned === null || assignedUser === assigned) &&
2754
+ (selectedTaskIds === null || selectedTaskIds.has(task.id))
2755
+ );
2756
+ })
2757
+ .map((task) => {
2758
+ const taskBase = buildBaseTask(task);
2759
+
2760
+ const metadataEvents = Object.entries(task.metadata)
2761
+ .filter(([, value]) => value instanceof Date)
2762
+ .map(([field, value]) => ({
2763
+ date: value,
2764
+ source: "metadata",
2765
+ eventType: field,
2766
+ task: taskBase,
2767
+ event: {
2768
+ field,
2769
+ value,
2770
+ },
2771
+ }));
2772
+
2773
+ const historyEvents = ("history" in task && Array.isArray(task.history) ? task.history : [])
2774
+ .filter((historyEvent) => historyEvent.date instanceof Date)
2775
+ .map((historyEvent) => {
2776
+ const event = { ...historyEvent };
2777
+ delete event.date;
2778
+ return {
2779
+ date: historyEvent.date,
2780
+ source: "history",
2781
+ eventType: historyEvent.type,
2782
+ task: taskBase,
2783
+ event,
2784
+ };
2785
+ });
2786
+
2787
+ return [...metadataEvents, ...historyEvents];
2788
+ })
2789
+ .flat()
2790
+ .filter((event) => includeEventDate(event.date));
2791
+
2792
+ events.sort((a, b) => compareValues(a.date, b.date) || compareValues(a.task.id, b.task.id));
2793
+ return { periods, events };
2794
+ }
2795
+
2635
2796
  /**
2636
2797
  * Output gantt chart data
2637
2798
  * @param {?string} [assigned=null] The assigned user to filter for, or null for no assigned filter
@@ -2659,7 +2820,8 @@ class Kanbn {
2659
2820
  created,
2660
2821
  started: "started" in task.metadata ? task.metadata.started : false,
2661
2822
  completed: "completed" in task.metadata ? task.metadata.completed : false,
2662
- postponed: "postponed" in task.metadata ? task.metadata.postponed : false,
2823
+ plannedStart: "plannedStart" in task.metadata ? task.metadata.plannedStart : false,
2824
+ plannedFinish: "plannedFinish" in task.metadata ? task.metadata.plannedFinish : false,
2663
2825
  workload: taskWorkload(index, task),
2664
2826
  progress: taskProgress(index, task),
2665
2827
  column: taskColumn,