@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/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
- // Calculate Early Start (ES) and Early Finish (EF) - Forward pass
1129
- const task_es_ef = {};
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
- task_es_ef[task.id] = { es: 0, ef: 0, ls: 0, lf: 0 };
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
- // Forward pass: Calculate ES and EF
1135
- const calculateES = (task) => {
1136
- if (task_es_ef[task.id].ef > 0) return task_es_ef[task.id];
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
- return task_es_ef[task.id];
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
- // Calculate ES/EF for all tasks
1157
- this.tasks.forEach(task => calculateES(task));
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
- // Find project completion time
1160
- const projectDuration = Math.max(...Object.values(task_es_ef).map(v => v.ef));
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
- // Backward pass: Calculate LS and LF
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
- // Find tasks that depend on this task
1169
- const dependents = this.tasks.filter(t =>
1170
- t.dependencies && t.dependencies.some(d => d.id === task.id)
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
- let minLS = projectDuration;
1174
- if (dependents.length > 0) {
1175
- dependents.forEach(dep_task => {
1176
- const dep_values = calculateLS(dep_task);
1177
- minLS = Math.min(minLS, dep_values.ls);
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
- const duration = date_utils.diff(task._end, task._start, 'hour') / 24; // in days
1182
- task_es_ef[task.id].lf = minLS;
1183
- task_es_ef[task.id].ls = minLS - duration;
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
- // Calculate LS/LF for all tasks
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 values = task_es_ef[task.id];
1194
- const slack = values.ls - values.es;
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