@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/README.md +39 -1
- package/dist/frappe-gantt.css +1 -1
- package/dist/frappe-gantt.es.js +463 -329
- package/dist/frappe-gantt.umd.js +12 -12
- package/package.json +1 -1
- package/src/arrow.js +10 -0
- package/src/bar.js +188 -2
- package/src/defaults.js +2 -0
- package/src/index.js +133 -4
- package/src/styles/dark.css +5 -0
- package/src/styles/gantt.css +35 -0
- package/src/styles/light.css +1 -0
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
|
-
//
|
|
1552
|
-
|
|
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) {
|
package/src/styles/dark.css
CHANGED
|
@@ -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;
|
package/src/styles/gantt.css
CHANGED
|
@@ -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
|
}
|