@workiom/frappe-gantt 1.0.26 → 1.0.27
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/dist/frappe-gantt.es.js +407 -383
- package/dist/frappe-gantt.umd.js +36 -36
- package/package.json +1 -1
- package/src/index.js +99 -55
package/src/index.js
CHANGED
|
@@ -1123,76 +1123,120 @@ export default class Gantt {
|
|
|
1123
1123
|
|
|
1124
1124
|
calculate_critical_path() {
|
|
1125
1125
|
// Reset critical path flags
|
|
1126
|
-
this.tasks.forEach(task => task._is_critical = false);
|
|
1126
|
+
this.tasks.forEach(task => (task._is_critical = false));
|
|
1127
|
+
if (this.tasks.length === 0) return;
|
|
1127
1128
|
|
|
1128
|
-
//
|
|
1129
|
-
const
|
|
1129
|
+
// Task duration in days (uses the actual placed dates)
|
|
1130
|
+
const duration_of = (task) =>
|
|
1131
|
+
date_utils.diff(task._end, task._start, 'hour') / 24;
|
|
1132
|
+
|
|
1133
|
+
// Project epoch = earliest actual start across all tasks. All times
|
|
1134
|
+
// below are measured in days from this epoch so that real calendar
|
|
1135
|
+
// gaps between bars show up as slack instead of being collapsed away.
|
|
1136
|
+
let epoch = this.tasks[0]._start;
|
|
1130
1137
|
this.tasks.forEach(task => {
|
|
1131
|
-
|
|
1138
|
+
if (task._start < epoch) epoch = task._start;
|
|
1132
1139
|
});
|
|
1140
|
+
const start_offset = (task) =>
|
|
1141
|
+
date_utils.diff(task._start, epoch, 'hour') / 24;
|
|
1133
1142
|
|
|
1134
|
-
//
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
let maxEF = 0;
|
|
1139
|
-
if (task.dependencies && task.dependencies.length > 0) {
|
|
1140
|
-
task.dependencies.forEach(dep => {
|
|
1141
|
-
const dep_task = this.get_task(dep.id);
|
|
1142
|
-
if (dep_task) {
|
|
1143
|
-
const dep_values = calculateES(dep_task);
|
|
1144
|
-
maxEF = Math.max(maxEF, dep_values.ef);
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
task_es_ef[task.id].es = maxEF;
|
|
1150
|
-
const duration = date_utils.diff(task._end, task._start, 'hour') / 24; // in days
|
|
1151
|
-
task_es_ef[task.id].ef = maxEF + duration;
|
|
1143
|
+
// Resolve the relationship type of a dependency edge.
|
|
1144
|
+
const resolve_type = (dep) =>
|
|
1145
|
+
dep.type || this.options.dependencies_type || 'finish-to-start';
|
|
1152
1146
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1147
|
+
// Per-task scheduling state with explicit "computed" flags so that a
|
|
1148
|
+
// legitimately-zero value is never mistaken for "not yet computed".
|
|
1149
|
+
const node = {};
|
|
1150
|
+
this.tasks.forEach(task => {
|
|
1151
|
+
node[task.id] = {
|
|
1152
|
+
d: duration_of(task),
|
|
1153
|
+
s: start_offset(task),
|
|
1154
|
+
es: 0, ef: 0, ls: 0, lf: 0,
|
|
1155
|
+
f_done: false, b_done: false,
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1155
1158
|
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1159
|
+
// Build successor lists (predecessors live on task.dependencies).
|
|
1160
|
+
const successors = {};
|
|
1161
|
+
this.tasks.forEach(task => (successors[task.id] = []));
|
|
1162
|
+
this.tasks.forEach(task => {
|
|
1163
|
+
(task.dependencies || []).forEach(dep => {
|
|
1164
|
+
if (node[dep.id]) {
|
|
1165
|
+
successors[dep.id].push({ id: task.id, type: resolve_type(dep) });
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1158
1169
|
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1170
|
+
// Forward pass: earliest start (ES) / earliest finish (EF).
|
|
1171
|
+
// `stack` breaks dependency cycles — a back-edge is treated as no
|
|
1172
|
+
// constraint rather than recursing forever.
|
|
1173
|
+
const forward = (task, stack) => {
|
|
1174
|
+
const n = node[task.id];
|
|
1175
|
+
if (n.f_done) return n;
|
|
1176
|
+
if (stack.has(task.id)) return n; // cycle guard
|
|
1177
|
+
stack.add(task.id);
|
|
1178
|
+
|
|
1179
|
+
let es = n.s; // anchor unconstrained tasks at their real start
|
|
1180
|
+
let constrained = false;
|
|
1181
|
+
(task.dependencies || []).forEach(dep => {
|
|
1182
|
+
const pred = this.get_task(dep.id);
|
|
1183
|
+
if (!pred || !node[dep.id]) return;
|
|
1184
|
+
const p = forward(pred, stack);
|
|
1185
|
+
const type = resolve_type(dep);
|
|
1186
|
+
let cand;
|
|
1187
|
+
if (type === 'start-to-start') cand = p.es;
|
|
1188
|
+
else if (type === 'finish-to-finish') cand = p.ef - n.d;
|
|
1189
|
+
else if (type === 'start-to-finish') cand = p.es - n.d;
|
|
1190
|
+
else cand = p.ef; // finish-to-start
|
|
1191
|
+
es = constrained ? Math.max(es, cand) : cand;
|
|
1192
|
+
constrained = true;
|
|
1193
|
+
});
|
|
1194
|
+
n.es = es;
|
|
1195
|
+
n.ef = es + n.d;
|
|
1196
|
+
n.f_done = true;
|
|
1197
|
+
stack.delete(task.id);
|
|
1198
|
+
return n;
|
|
1199
|
+
};
|
|
1200
|
+
this.tasks.forEach(task => forward(task, new Set()));
|
|
1161
1201
|
|
|
1162
|
-
|
|
1163
|
-
const calculateLS = (task) => {
|
|
1164
|
-
if (task_es_ef[task.id].ls > 0 || task_es_ef[task.id].lf > 0) {
|
|
1165
|
-
return task_es_ef[task.id];
|
|
1166
|
-
}
|
|
1202
|
+
const projectEnd = Math.max(...this.tasks.map(t => node[t.id].ef));
|
|
1167
1203
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
);
|
|
1204
|
+
// Backward pass: latest finish (LF) / latest start (LS).
|
|
1205
|
+
const backward = (task, stack) => {
|
|
1206
|
+
const n = node[task.id];
|
|
1207
|
+
if (n.b_done) return n;
|
|
1208
|
+
if (stack.has(task.id)) return n; // cycle guard
|
|
1209
|
+
stack.add(task.id);
|
|
1172
1210
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1211
|
+
const succs = successors[task.id];
|
|
1212
|
+
let lf;
|
|
1213
|
+
if (succs.length === 0) {
|
|
1214
|
+
lf = projectEnd;
|
|
1215
|
+
} else {
|
|
1216
|
+
let constrained = false;
|
|
1217
|
+
succs.forEach(succ => {
|
|
1218
|
+
const s = backward(this.get_task(succ.id), stack);
|
|
1219
|
+
let cand;
|
|
1220
|
+
if (succ.type === 'start-to-start') cand = s.ls + n.d;
|
|
1221
|
+
else if (succ.type === 'finish-to-finish') cand = s.lf;
|
|
1222
|
+
else if (succ.type === 'start-to-finish') cand = s.lf + n.d;
|
|
1223
|
+
else cand = s.ls; // finish-to-start
|
|
1224
|
+
lf = constrained ? Math.min(lf, cand) : cand;
|
|
1225
|
+
constrained = true;
|
|
1178
1226
|
});
|
|
1179
1227
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
return task_es_ef[task.id];
|
|
1228
|
+
n.lf = lf;
|
|
1229
|
+
n.ls = lf - n.d;
|
|
1230
|
+
n.b_done = true;
|
|
1231
|
+
stack.delete(task.id);
|
|
1232
|
+
return n;
|
|
1186
1233
|
};
|
|
1234
|
+
this.tasks.forEach(task => backward(task, new Set()));
|
|
1187
1235
|
|
|
1188
|
-
//
|
|
1189
|
-
this.tasks.forEach(task => calculateLS(task));
|
|
1190
|
-
|
|
1191
|
-
// Identify critical path: tasks where ES = LS (or slack = 0)
|
|
1236
|
+
// Critical = zero slack (LS == ES), within a small float epsilon.
|
|
1192
1237
|
this.tasks.forEach(task => {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
task._is_critical = Math.abs(slack) < 0.01; // Use small epsilon for float comparison
|
|
1238
|
+
const n = node[task.id];
|
|
1239
|
+
task._is_critical = Math.abs(n.ls - n.es) < 0.01;
|
|
1196
1240
|
});
|
|
1197
1241
|
}
|
|
1198
1242
|
|