@workiom/frappe-gantt 1.0.5 → 1.0.7

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.
package/src/index.js CHANGED
@@ -879,6 +879,12 @@ export default class Gantt {
879
879
 
880
880
  make_arrows() {
881
881
  this.arrows = [];
882
+
883
+ // Calculate critical path if enabled
884
+ if (this.options.critical_path) {
885
+ this.calculate_critical_path();
886
+ }
887
+
882
888
  for (let task of this.tasks) {
883
889
  let arrows = [];
884
890
  arrows = task.dependencies
@@ -898,6 +904,95 @@ export default class Gantt {
898
904
  }
899
905
  }
900
906
 
907
+ calculate_critical_path() {
908
+ // Reset critical path flags
909
+ this.tasks.forEach(task => task._is_critical = false);
910
+
911
+ // Calculate Early Start (ES) and Early Finish (EF) - Forward pass
912
+ const task_es_ef = {};
913
+ this.tasks.forEach(task => {
914
+ task_es_ef[task.id] = { es: 0, ef: 0, ls: 0, lf: 0 };
915
+ });
916
+
917
+ // Forward pass: Calculate ES and EF
918
+ const calculateES = (task) => {
919
+ if (task_es_ef[task.id].ef > 0) return task_es_ef[task.id];
920
+
921
+ let maxEF = 0;
922
+ if (task.dependencies && task.dependencies.length > 0) {
923
+ task.dependencies.forEach(dep_id => {
924
+ const dep_task = this.get_task(dep_id);
925
+ if (dep_task) {
926
+ const dep_values = calculateES(dep_task);
927
+ maxEF = Math.max(maxEF, dep_values.ef);
928
+ }
929
+ });
930
+ }
931
+
932
+ task_es_ef[task.id].es = maxEF;
933
+ const duration = date_utils.diff(task._end, task._start, 'hour') / 24; // in days
934
+ task_es_ef[task.id].ef = maxEF + duration;
935
+
936
+ return task_es_ef[task.id];
937
+ };
938
+
939
+ // Calculate ES/EF for all tasks
940
+ this.tasks.forEach(task => calculateES(task));
941
+
942
+ // Find project completion time
943
+ const projectDuration = Math.max(...Object.values(task_es_ef).map(v => v.ef));
944
+
945
+ // Backward pass: Calculate LS and LF
946
+ const calculateLS = (task) => {
947
+ if (task_es_ef[task.id].ls > 0 || task_es_ef[task.id].lf > 0) {
948
+ return task_es_ef[task.id];
949
+ }
950
+
951
+ // Find tasks that depend on this task
952
+ const dependents = this.tasks.filter(t =>
953
+ t.dependencies && t.dependencies.includes(task.id)
954
+ );
955
+
956
+ let minLS = projectDuration;
957
+ if (dependents.length > 0) {
958
+ dependents.forEach(dep_task => {
959
+ const dep_values = calculateLS(dep_task);
960
+ minLS = Math.min(minLS, dep_values.ls);
961
+ });
962
+ }
963
+
964
+ const duration = date_utils.diff(task._end, task._start, 'hour') / 24; // in days
965
+ task_es_ef[task.id].lf = minLS;
966
+ task_es_ef[task.id].ls = minLS - duration;
967
+
968
+ return task_es_ef[task.id];
969
+ };
970
+
971
+ // Calculate LS/LF for all tasks
972
+ this.tasks.forEach(task => calculateLS(task));
973
+
974
+ // Identify critical path: tasks where ES = LS (or slack = 0)
975
+ this.tasks.forEach(task => {
976
+ const values = task_es_ef[task.id];
977
+ const slack = values.ls - values.es;
978
+ task._is_critical = Math.abs(slack) < 0.01; // Use small epsilon for float comparison
979
+ });
980
+ }
981
+
982
+ update_arrow_critical_path() {
983
+ // Update arrow styling based on new critical path calculation
984
+ this.arrows.forEach(arrow => {
985
+ const is_critical = arrow.from_task.task._is_critical === true &&
986
+ arrow.to_task.task._is_critical === true;
987
+
988
+ if (is_critical) {
989
+ arrow.element.classList.add('arrow-critical');
990
+ } else {
991
+ arrow.element.classList.remove('arrow-critical');
992
+ }
993
+ });
994
+ }
995
+
901
996
  map_arrows_on_bars() {
902
997
  for (let bar of this.bars) {
903
998
  bar.arrows = this.arrows.filter((arrow) => {
@@ -1341,19 +1436,42 @@ export default class Gantt {
1341
1436
 
1342
1437
  $.on(this.$svg, 'mouseup', (e) => {
1343
1438
  this.bar_being_dragged = null;
1439
+ const tasks_changed = [];
1440
+
1344
1441
  bars.forEach((bar) => {
1345
1442
  const $bar = bar.$bar;
1346
1443
  if (!$bar.finaldx) return;
1347
1444
  bar.date_changed();
1348
1445
  bar.compute_progress();
1349
1446
  bar.set_action_completed();
1447
+ // Track tasks that changed
1448
+ tasks_changed.push({
1449
+ task: bar.task,
1450
+ start: bar.task._start,
1451
+ end: date_utils.add(bar.task._end, -1, 'second')
1452
+ });
1350
1453
  });
1351
1454
 
1352
1455
  // Update dependent tasks based on dependencies_type
1353
1456
  // Only update for the parent bar that was actually moved
1354
1457
  const parent_bar = this.get_bar(parent_bar_id);
1355
1458
  if (parent_bar && parent_bar.$bar.finaldx) {
1356
- this.update_dependent_tasks_by_type(parent_bar);
1459
+ const dependent_changes = this.update_dependent_tasks_by_type(parent_bar);
1460
+ // Add dependent task changes to the list
1461
+ tasks_changed.push(...dependent_changes);
1462
+ }
1463
+
1464
+ // Recalculate critical path if enabled and any bar was moved
1465
+ if (this.options.critical_path && bars.some(bar => bar.$bar.finaldx)) {
1466
+ this.calculate_critical_path();
1467
+ this.update_arrow_critical_path();
1468
+ }
1469
+
1470
+ // Trigger on_after_date_change for all tasks that changed
1471
+ if (tasks_changed.length > 0) {
1472
+ tasks_changed.forEach(({task, start, end}) => {
1473
+ this.trigger_event('after_date_change', [task, start, end]);
1474
+ });
1357
1475
  }
1358
1476
  });
1359
1477
 
@@ -1462,9 +1580,10 @@ export default class Gantt {
1462
1580
 
1463
1581
  update_dependent_tasks_by_type(parent_bar) {
1464
1582
  const dependencies_type = parent_bar.task.dependencies_type || this.options.dependencies_type;
1583
+ const changed_tasks = [];
1465
1584
 
1466
1585
  // Skip if using fixed dependency type (current behavior)
1467
- if (dependencies_type === 'fixed') return;
1586
+ if (dependencies_type === 'fixed') return changed_tasks;
1468
1587
 
1469
1588
  // Get all tasks that depend on this task
1470
1589
  const dependent_task_ids = this.dependency_map[parent_bar.task.id] || [];
@@ -1548,9 +1667,19 @@ export default class Gantt {
1548
1667
  date_utils.add(new_end, -1, 'second'),
1549
1668
  ]);
1550
1669
 
1551
- // Recursively update dependents of this task
1552
- this.update_dependent_tasks_by_type(dependent_bar);
1670
+ // Track this changed task
1671
+ changed_tasks.push({
1672
+ task: dependent_task,
1673
+ start: new_start,
1674
+ end: date_utils.add(new_end, -1, 'second')
1675
+ });
1676
+
1677
+ // Recursively update dependents of this task and collect their changes
1678
+ const recursive_changes = this.update_dependent_tasks_by_type(dependent_bar);
1679
+ changed_tasks.push(...recursive_changes);
1553
1680
  });
1681
+
1682
+ return changed_tasks;
1554
1683
  }
1555
1684
 
1556
1685
  get_snap_position(dx, ox) {
@@ -8,6 +8,7 @@
8
8
  --g-text-light-dark: #ececec;
9
9
  --g-text-color-dark: #f7f7f7;
10
10
  --g-progress-color: #8a8aff;
11
+ --g-arrow-critical-color-dark: #ff9999;
11
12
  }
12
13
 
13
14
  .dark > .gantt-container .gantt {
@@ -27,6 +28,10 @@
27
28
  stroke: var(--g-text-muted-dark);
28
29
  }
29
30
 
31
+ & .arrow-critical {
32
+ stroke: var(--g-arrow-critical-color-dark);
33
+ }
34
+
30
35
  & .bar {
31
36
  fill: var(--g-bar-color-dark);
32
37
  stroke: none;
@@ -267,6 +267,10 @@
267
267
  stroke-width: 1.5;
268
268
  }
269
269
 
270
+ & .arrow-critical {
271
+ stroke: var(--g-arrow-critical-color);
272
+ }
273
+
270
274
  & .bar-wrapper .bar {
271
275
  fill: var(--g-bar-color);
272
276
  stroke: var(--g-bar-border);
@@ -340,4 +344,35 @@
340
344
  }
341
345
  }
342
346
  }
347
+
348
+ & .add-task-icon {
349
+ cursor: pointer;
350
+ transition: opacity 0.2s ease;
351
+
352
+ & .add-task-icon-bg {
353
+ fill: var(--g-bar-color);
354
+ stroke: var(--g-bar-border);
355
+ stroke-width: 1;
356
+ transition: all 0.2s ease;
357
+ }
358
+
359
+ & .add-task-icon-plus {
360
+ stroke: var(--g-text-dark);
361
+ stroke-width: 2;
362
+ stroke-linecap: round;
363
+ transition: stroke 0.2s ease;
364
+ }
365
+
366
+ &.active,
367
+ &:hover {
368
+ & .add-task-icon-bg {
369
+ fill: var(--g-progress-color);
370
+ stroke: var(--g-progress-color);
371
+ }
372
+
373
+ & .add-task-icon-plus {
374
+ stroke: var(--g-text-light);
375
+ }
376
+ }
377
+ }
343
378
  }
@@ -1,5 +1,6 @@
1
1
  :root {
2
2
  --g-arrow-color: #1f2937;
3
+ --g-arrow-critical-color: #ff7676;
3
4
  --g-bar-color: #fff;
4
5
  --g-bar-border: #fff;
5
6
  --g-tick-color-thick: #ededed;