gantt-lib 0.60.0 → 0.60.2

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.
@@ -0,0 +1,935 @@
1
+ // src/core/scheduling/dateMath.ts
2
+ var DAY_MS = 24 * 60 * 60 * 1e3;
3
+ function normalizeUTCDate(date) {
4
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
5
+ }
6
+ function parseDateOnly(date) {
7
+ const parsed = typeof date === "string" ? /* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(date);
8
+ return normalizeUTCDate(parsed);
9
+ }
10
+ function getBusinessDayOffset(fromDate, toDate, weekendPredicate) {
11
+ const from = normalizeUTCDate(fromDate);
12
+ const to = normalizeUTCDate(toDate);
13
+ if (from.getTime() === to.getTime()) {
14
+ return 0;
15
+ }
16
+ const step = to.getTime() > from.getTime() ? 1 : -1;
17
+ const current = new Date(from);
18
+ let offset = 0;
19
+ while (current.getTime() !== to.getTime()) {
20
+ current.setUTCDate(current.getUTCDate() + step);
21
+ if (!weekendPredicate(current)) {
22
+ offset += step;
23
+ }
24
+ }
25
+ return offset;
26
+ }
27
+ function shiftBusinessDayOffset(date, offset, weekendPredicate) {
28
+ const current = normalizeUTCDate(date);
29
+ if (offset === 0) {
30
+ return current;
31
+ }
32
+ const step = offset > 0 ? 1 : -1;
33
+ let remaining = Math.abs(offset);
34
+ while (remaining > 0) {
35
+ current.setUTCDate(current.getUTCDate() + step);
36
+ if (!weekendPredicate(current)) {
37
+ remaining--;
38
+ }
39
+ }
40
+ return current;
41
+ }
42
+ function getBusinessDaysCount(startDate, endDate, weekendPredicate) {
43
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
44
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
45
+ let count = 0;
46
+ const current = new Date(start);
47
+ while (current.getTime() <= end.getTime()) {
48
+ if (!weekendPredicate(current)) {
49
+ count++;
50
+ }
51
+ current.setUTCDate(current.getUTCDate() + 1);
52
+ }
53
+ return Math.max(1, count);
54
+ }
55
+ function addBusinessDays(startDate, businessDays, weekendPredicate) {
56
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
57
+ const current = new Date(start);
58
+ let targetDays = Math.max(1, businessDays);
59
+ let businessDaysCounted = 0;
60
+ while (businessDaysCounted < targetDays) {
61
+ if (!weekendPredicate(current)) {
62
+ businessDaysCounted++;
63
+ }
64
+ if (businessDaysCounted < targetDays) {
65
+ current.setUTCDate(current.getUTCDate() + 1);
66
+ }
67
+ }
68
+ return current;
69
+ }
70
+ function subtractBusinessDays(endDate, businessDays, weekendPredicate) {
71
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
72
+ const current = new Date(end);
73
+ let targetDays = Math.max(1, businessDays);
74
+ let businessDaysCounted = 0;
75
+ while (businessDaysCounted < targetDays) {
76
+ if (!weekendPredicate(current)) {
77
+ businessDaysCounted++;
78
+ }
79
+ if (businessDaysCounted < targetDays) {
80
+ current.setUTCDate(current.getUTCDate() - 1);
81
+ }
82
+ }
83
+ return current;
84
+ }
85
+ function alignToWorkingDay(date, direction, weekendPredicate) {
86
+ const current = normalizeUTCDate(date);
87
+ while (weekendPredicate(current)) {
88
+ current.setUTCDate(current.getUTCDate() + direction);
89
+ }
90
+ return current;
91
+ }
92
+ function getTaskDuration(startDate, endDate, businessDays = false, weekendPredicate) {
93
+ const start = parseDateOnly(startDate);
94
+ const end = parseDateOnly(endDate);
95
+ if (businessDays && weekendPredicate) {
96
+ return getBusinessDaysCount(start, end, weekendPredicate);
97
+ }
98
+ return Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1);
99
+ }
100
+
101
+ // src/core/scheduling/dependencies.ts
102
+ function getDependencyLag(dep) {
103
+ return Number.isFinite(dep.lag) ? dep.lag : 0;
104
+ }
105
+ function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd, businessDays = false, weekendPredicate) {
106
+ if (linkType !== "FS") {
107
+ return lag;
108
+ }
109
+ const predecessorDuration = getTaskDuration(
110
+ predecessorStart,
111
+ predecessorEnd,
112
+ businessDays,
113
+ weekendPredicate
114
+ );
115
+ return Math.max(-predecessorDuration, lag);
116
+ }
117
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate) {
118
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
119
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
120
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
121
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
122
+ if (!businessDays || !weekendPredicate) {
123
+ switch (linkType) {
124
+ case "FS":
125
+ return normalizeDependencyLag(
126
+ linkType,
127
+ Math.round((sS - pE) / DAY_MS) - 1,
128
+ predStart,
129
+ predEnd,
130
+ businessDays,
131
+ weekendPredicate
132
+ );
133
+ case "SS":
134
+ return Math.round((sS - pS) / DAY_MS);
135
+ case "FF":
136
+ return Math.round((sE - pE) / DAY_MS);
137
+ case "SF":
138
+ return Math.round((sE - pS) / DAY_MS) + 1;
139
+ }
140
+ }
141
+ const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
142
+ const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
143
+ const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
144
+ switch (linkType) {
145
+ case "FS":
146
+ return normalizeDependencyLag(
147
+ linkType,
148
+ businessOffset - 1,
149
+ predStart,
150
+ predEnd,
151
+ businessDays,
152
+ weekendPredicate
153
+ );
154
+ case "SS":
155
+ return businessOffset;
156
+ case "FF":
157
+ return businessOffset;
158
+ case "SF":
159
+ return businessOffset + 1;
160
+ }
161
+ }
162
+ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate) {
163
+ const normalizedLag = normalizeDependencyLag(
164
+ linkType,
165
+ lag,
166
+ predecessorStart,
167
+ predecessorEnd,
168
+ businessDays,
169
+ weekendPredicate
170
+ );
171
+ if (!businessDays || !weekendPredicate) {
172
+ switch (linkType) {
173
+ case "FS":
174
+ return new Date(predecessorEnd.getTime() + (normalizedLag + 1) * DAY_MS);
175
+ case "SS":
176
+ return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
177
+ case "FF":
178
+ return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
179
+ case "SF":
180
+ return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
181
+ }
182
+ }
183
+ const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
184
+ let offset;
185
+ switch (linkType) {
186
+ case "FS":
187
+ offset = normalizedLag + 1;
188
+ break;
189
+ case "SS":
190
+ offset = normalizedLag;
191
+ break;
192
+ case "FF":
193
+ offset = normalizedLag;
194
+ break;
195
+ case "SF":
196
+ offset = normalizedLag - 1;
197
+ break;
198
+ }
199
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
200
+ }
201
+
202
+ // src/core/scheduling/hierarchy.ts
203
+ function getChildren(parentId, tasks) {
204
+ return tasks.filter((t) => t.parentId === parentId);
205
+ }
206
+ function isTaskParent(taskId, tasks) {
207
+ return tasks.some((t) => t.parentId === taskId);
208
+ }
209
+ function computeParentDates(parentId, tasks) {
210
+ const children = getChildren(parentId, tasks);
211
+ if (children.length === 0) {
212
+ const parent = tasks.find((t) => t.id === parentId);
213
+ const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
214
+ const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
215
+ return { startDate: start, endDate: end };
216
+ }
217
+ const startDates = children.map((c) => new Date(c.startDate));
218
+ const endDates = children.map((c) => new Date(c.endDate));
219
+ const minTime = Math.min(...startDates.map((d) => d.getTime()));
220
+ const maxTime = Math.max(...endDates.map((d) => d.getTime()));
221
+ return {
222
+ startDate: new Date(minTime),
223
+ endDate: new Date(maxTime)
224
+ };
225
+ }
226
+ function computeParentProgress(parentId, tasks) {
227
+ const children = getChildren(parentId, tasks);
228
+ if (children.length === 0) {
229
+ return 0;
230
+ }
231
+ const DAY_MS2 = 24 * 60 * 60 * 1e3;
232
+ let totalWeight = 0;
233
+ let weightedSum = 0;
234
+ for (const child of children) {
235
+ const start = new Date(child.startDate).getTime();
236
+ const end = new Date(child.endDate).getTime();
237
+ const duration = (end - start + DAY_MS2) / DAY_MS2;
238
+ const progress = child.progress ?? 0;
239
+ totalWeight += duration;
240
+ weightedSum += duration * progress;
241
+ }
242
+ if (totalWeight === 0) {
243
+ return 0;
244
+ }
245
+ return Math.round(weightedSum / totalWeight * 10) / 10;
246
+ }
247
+ function getAllDescendants(parentId, tasks) {
248
+ const descendants = [];
249
+ const visited = /* @__PURE__ */ new Set();
250
+ function collectChildren(taskId) {
251
+ if (visited.has(taskId)) return;
252
+ visited.add(taskId);
253
+ const children = getChildren(taskId, tasks);
254
+ for (const child of children) {
255
+ descendants.push(child);
256
+ collectChildren(child.id);
257
+ }
258
+ }
259
+ collectChildren(parentId);
260
+ return descendants;
261
+ }
262
+ function getAllDependencyEdges(tasks) {
263
+ const edges = [];
264
+ for (const task of tasks) {
265
+ if (task.dependencies) {
266
+ for (const dep of task.dependencies) {
267
+ edges.push({
268
+ predecessorId: dep.taskId,
269
+ successorId: task.id,
270
+ type: dep.type,
271
+ lag: dep.lag ?? 0
272
+ });
273
+ }
274
+ }
275
+ }
276
+ return edges;
277
+ }
278
+ function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
279
+ return tasks.map((task) => {
280
+ if (task.id === taskId1 || task.id === taskId2) {
281
+ if (!task.dependencies) return task;
282
+ const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
283
+ const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
284
+ if (filteredDependencies.length === task.dependencies.length) {
285
+ return task;
286
+ }
287
+ return {
288
+ ...task,
289
+ dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
290
+ };
291
+ }
292
+ return task;
293
+ });
294
+ }
295
+ function findParentId(taskId, tasks) {
296
+ const task = tasks.find((t) => t.id === taskId);
297
+ return task?.parentId;
298
+ }
299
+ function isAncestorTask(ancestorId, taskId, tasks) {
300
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
301
+ const visited = /* @__PURE__ */ new Set();
302
+ let current = taskById.get(taskId);
303
+ while (current?.parentId) {
304
+ if (current.parentId === ancestorId) {
305
+ return true;
306
+ }
307
+ if (visited.has(current.parentId)) {
308
+ return false;
309
+ }
310
+ visited.add(current.parentId);
311
+ current = taskById.get(current.parentId);
312
+ }
313
+ return false;
314
+ }
315
+ function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
316
+ if (taskId1 === taskId2) {
317
+ return true;
318
+ }
319
+ return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
320
+ }
321
+
322
+ // src/core/scheduling/commands.ts
323
+ function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
324
+ const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
325
+ if (businessDays && weekendPredicate) {
326
+ return {
327
+ start: normalizedStart,
328
+ end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
329
+ };
330
+ }
331
+ const DAY_MS2 = 24 * 60 * 60 * 1e3;
332
+ return {
333
+ start: normalizedStart,
334
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS2)
335
+ };
336
+ }
337
+ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
338
+ const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
339
+ if (businessDays && weekendPredicate) {
340
+ return {
341
+ start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
342
+ end: normalizedEnd
343
+ };
344
+ }
345
+ const DAY_MS2 = 24 * 60 * 60 * 1e3;
346
+ return {
347
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS2),
348
+ end: normalizedEnd
349
+ };
350
+ }
351
+ function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
352
+ return buildTaskRangeFromStart(
353
+ proposedStart,
354
+ getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
355
+ businessDays,
356
+ weekendPredicate,
357
+ snapDirection
358
+ );
359
+ }
360
+ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate) {
361
+ if (!task.dependencies?.length) {
362
+ return { start: proposedStart, end: proposedEnd };
363
+ }
364
+ let minAllowedStart = null;
365
+ for (const dep of task.dependencies) {
366
+ if (dep.type !== "FS") {
367
+ continue;
368
+ }
369
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
370
+ if (!predecessor) {
371
+ continue;
372
+ }
373
+ const predecessorStart = parseDateOnly(predecessor.startDate);
374
+ const predecessorEnd = parseDateOnly(predecessor.endDate);
375
+ const predecessorDuration = getTaskDuration(
376
+ predecessorStart,
377
+ predecessorEnd,
378
+ businessDays,
379
+ weekendPredicate
380
+ );
381
+ const candidateMinStart = calculateSuccessorDate(
382
+ predecessorStart,
383
+ predecessorEnd,
384
+ "FS",
385
+ -predecessorDuration,
386
+ businessDays,
387
+ weekendPredicate
388
+ );
389
+ if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
390
+ minAllowedStart = candidateMinStart;
391
+ }
392
+ }
393
+ if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
394
+ return { start: proposedStart, end: proposedEnd };
395
+ }
396
+ return buildTaskRangeFromStart(
397
+ minAllowedStart,
398
+ getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
399
+ businessDays,
400
+ weekendPredicate
401
+ );
402
+ }
403
+ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate) {
404
+ if (!task.dependencies) return [];
405
+ return task.dependencies.map((dep) => {
406
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
407
+ if (!predecessor) {
408
+ return { ...dep, lag: getDependencyLag(dep) };
409
+ }
410
+ const predecessorStart = new Date(predecessor.startDate);
411
+ const predecessorEnd = new Date(predecessor.endDate);
412
+ const nextLag = computeLagFromDates(
413
+ dep.type,
414
+ predecessorStart,
415
+ predecessorEnd,
416
+ newStartDate,
417
+ newEndDate,
418
+ businessDays,
419
+ weekendPredicate
420
+ );
421
+ return { ...dep, lag: nextLag };
422
+ });
423
+ }
424
+ function resolveDateRangeFromPixels(mode, left, width, monthStart, dayWidth, task, businessDays, weekendPredicate) {
425
+ const dayOffset = Math.round(left / dayWidth);
426
+ const rawStartDate = new Date(Date.UTC(
427
+ monthStart.getUTCFullYear(),
428
+ monthStart.getUTCMonth(),
429
+ monthStart.getUTCDate() + dayOffset
430
+ ));
431
+ const rawEndOffset = dayOffset + Math.round(width / dayWidth) - 1;
432
+ const rawEndDate = new Date(Date.UTC(
433
+ monthStart.getUTCFullYear(),
434
+ monthStart.getUTCMonth(),
435
+ monthStart.getUTCDate() + rawEndOffset
436
+ ));
437
+ if (!(businessDays && weekendPredicate)) {
438
+ return { start: rawStartDate, end: rawEndDate };
439
+ }
440
+ if (mode === "move") {
441
+ const originalStart2 = new Date(task.startDate);
442
+ const snapDirection2 = rawStartDate.getTime() >= originalStart2.getTime() ? 1 : -1;
443
+ return moveTaskRange(
444
+ task.startDate,
445
+ task.endDate,
446
+ rawStartDate,
447
+ true,
448
+ weekendPredicate,
449
+ snapDirection2
450
+ );
451
+ }
452
+ if (mode === "resize-right") {
453
+ const fixedStart = new Date(task.startDate);
454
+ const originalEnd = new Date(task.endDate);
455
+ const snapDirection2 = rawEndDate.getTime() >= originalEnd.getTime() ? 1 : -1;
456
+ const alignedEnd = alignToWorkingDay(rawEndDate, snapDirection2, weekendPredicate);
457
+ const duration2 = Math.max(1, getBusinessDaysCount(fixedStart, alignedEnd, weekendPredicate));
458
+ return buildTaskRangeFromStart(fixedStart, duration2, true, weekendPredicate);
459
+ }
460
+ const fixedEnd = new Date(task.endDate);
461
+ const originalStart = new Date(task.startDate);
462
+ const snapDirection = rawStartDate.getTime() >= originalStart.getTime() ? 1 : -1;
463
+ const alignedStart = alignToWorkingDay(rawStartDate, snapDirection, weekendPredicate);
464
+ const duration = Math.max(1, getBusinessDaysCount(alignedStart, fixedEnd, weekendPredicate));
465
+ return buildTaskRangeFromEnd(fixedEnd, duration, true, weekendPredicate);
466
+ }
467
+ function clampDateRangeForIncomingFS(task, range, allTasks, mode, businessDays, weekendPredicate) {
468
+ if (mode === "resize-right") {
469
+ return range;
470
+ }
471
+ return clampTaskRangeForIncomingFS(
472
+ task,
473
+ range.start,
474
+ range.end,
475
+ allTasks,
476
+ businessDays,
477
+ weekendPredicate
478
+ );
479
+ }
480
+
481
+ // src/core/scheduling/cascade.ts
482
+ function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
483
+ const successorMap = /* @__PURE__ */ new Map();
484
+ for (const task of allTasks) {
485
+ successorMap.set(task.id, []);
486
+ }
487
+ for (const task of allTasks) {
488
+ if (!task.dependencies) continue;
489
+ for (const dep of task.dependencies) {
490
+ if (linkTypes.includes(dep.type)) {
491
+ const list = successorMap.get(dep.taskId) ?? [];
492
+ list.push(task.id);
493
+ successorMap.set(dep.taskId, list);
494
+ }
495
+ }
496
+ }
497
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
498
+ const visited = /* @__PURE__ */ new Set();
499
+ const queue = [draggedTaskId];
500
+ const chain = [];
501
+ visited.add(draggedTaskId);
502
+ while (queue.length > 0) {
503
+ const current = queue.shift();
504
+ const successors = successorMap.get(current) ?? [];
505
+ for (const sid of successors) {
506
+ if (!visited.has(sid)) {
507
+ visited.add(sid);
508
+ const t = taskById.get(sid);
509
+ if (t) {
510
+ chain.push(t);
511
+ queue.push(sid);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return chain;
517
+ }
518
+ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
519
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
520
+ const updatedDates = /* @__PURE__ */ new Map();
521
+ updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
522
+ const result = [];
523
+ const queue = [movedTaskId];
524
+ const visited = /* @__PURE__ */ new Set([movedTaskId]);
525
+ while (queue.length > 0) {
526
+ const currentId = queue.shift();
527
+ const { start: predStart, end: predEnd } = updatedDates.get(currentId);
528
+ if (!skipChildCascade) {
529
+ const children = getChildren(currentId, allTasks);
530
+ for (const child of children) {
531
+ if (visited.has(child.id) || child.locked) continue;
532
+ const origStart = new Date(child.startDate);
533
+ const origEnd = new Date(child.endDate);
534
+ const durationMs = origEnd.getTime() - origStart.getTime();
535
+ const parentOrig = taskById.get(currentId);
536
+ const parentOrigStart = new Date(parentOrig.startDate);
537
+ const parentOrigEnd = new Date(parentOrig.endDate);
538
+ const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
539
+ const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
540
+ const newChildStart = new Date(origStart.getTime() + parentStartDelta);
541
+ const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
542
+ visited.add(child.id);
543
+ updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
544
+ result.push({
545
+ ...child,
546
+ startDate: newChildStart.toISOString().split("T")[0],
547
+ endDate: newChildEnd.toISOString().split("T")[0]
548
+ });
549
+ queue.push(child.id);
550
+ }
551
+ }
552
+ for (const task of allTasks) {
553
+ if (visited.has(task.id) || !task.dependencies || task.locked) continue;
554
+ for (const dep of task.dependencies) {
555
+ if (dep.taskId !== currentId) continue;
556
+ const orig = taskById.get(task.id);
557
+ const origStart = new Date(orig.startDate);
558
+ const origEnd = new Date(orig.endDate);
559
+ const duration = getTaskDuration(origStart, origEnd);
560
+ const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
561
+ let newSuccStart;
562
+ let newSuccEnd;
563
+ if (dep.type === "FS" || dep.type === "SS") {
564
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
565
+ } else {
566
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
567
+ }
568
+ visited.add(task.id);
569
+ updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
570
+ result.push({
571
+ ...task,
572
+ startDate: newSuccStart.toISOString().split("T")[0],
573
+ endDate: newSuccEnd.toISOString().split("T")[0]
574
+ });
575
+ queue.push(task.id);
576
+ break;
577
+ }
578
+ }
579
+ }
580
+ return result;
581
+ }
582
+ function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
583
+ const allTypesSuccessorMap = /* @__PURE__ */ new Map();
584
+ for (const task of allTasks) {
585
+ allTypesSuccessorMap.set(task.id, []);
586
+ }
587
+ for (const task of allTasks) {
588
+ if (!task.dependencies) continue;
589
+ for (const dep of task.dependencies) {
590
+ const list = allTypesSuccessorMap.get(dep.taskId) ?? [];
591
+ list.push(task);
592
+ allTypesSuccessorMap.set(dep.taskId, list);
593
+ }
594
+ }
595
+ const directChildren = getChildren(changedTaskId, allTasks);
596
+ const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
597
+ const initialChain = [...directChildren, ...directSuccessors].filter(
598
+ (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
599
+ );
600
+ const chain = [...initialChain];
601
+ const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
602
+ const queue = [...initialChain];
603
+ while (queue.length > 0) {
604
+ const current = queue.shift();
605
+ const children = getChildren(current.id, allTasks);
606
+ for (const child of children) {
607
+ if (!visited.has(child.id)) {
608
+ visited.add(child.id);
609
+ chain.push(child);
610
+ queue.push(child);
611
+ }
612
+ }
613
+ const successors = allTypesSuccessorMap.get(current.id) ?? [];
614
+ for (const successor of successors) {
615
+ if (!visited.has(successor.id)) {
616
+ visited.add(successor.id);
617
+ chain.push(successor);
618
+ queue.push(successor);
619
+ }
620
+ }
621
+ }
622
+ return chain;
623
+ }
624
+ function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate) {
625
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
626
+ const updatedDates = /* @__PURE__ */ new Map();
627
+ updatedDates.set(movedTask.id, { start: newStart, end: newEnd });
628
+ const resultMap = /* @__PURE__ */ new Map();
629
+ resultMap.set(movedTask.id, {
630
+ ...movedTask,
631
+ startDate: newStart.toISOString().split("T")[0],
632
+ endDate: newEnd.toISOString().split("T")[0]
633
+ });
634
+ const queue = [[movedTask.id, "direct"]];
635
+ const childShifted = /* @__PURE__ */ new Set();
636
+ let iterations = 0;
637
+ const MAX_ITERATIONS = allTasks.length * 3;
638
+ while (queue.length > 0 && iterations < MAX_ITERATIONS) {
639
+ iterations++;
640
+ const [currentId, arrivalMode] = queue.shift();
641
+ const { start: currStart, end: currEnd } = updatedDates.get(currentId);
642
+ const currentOriginal = taskById.get(currentId);
643
+ if (arrivalMode !== "parent-recalc") {
644
+ const children = getChildren(currentId, allTasks);
645
+ for (const child of children) {
646
+ if (childShifted.has(child.id) || child.locked) continue;
647
+ const parentOrigStart = new Date(currentOriginal.startDate);
648
+ const parentOrigEnd = new Date(currentOriginal.endDate);
649
+ const childOrigStart = new Date(child.startDate);
650
+ const childOrigEnd = new Date(child.endDate);
651
+ const startDeltaMs = currStart.getTime() - parentOrigStart.getTime();
652
+ const endDeltaMs = currEnd.getTime() - parentOrigEnd.getTime();
653
+ let childNewStart;
654
+ let childNewEnd;
655
+ if (businessDays && weekendPredicate) {
656
+ const proposedStart = new Date(childOrigStart.getTime() + startDeltaMs);
657
+ const snapDirection = currStart.getTime() >= parentOrigStart.getTime() ? 1 : -1;
658
+ const movedRange = moveTaskRange(
659
+ child.startDate,
660
+ child.endDate,
661
+ proposedStart,
662
+ true,
663
+ weekendPredicate,
664
+ snapDirection
665
+ );
666
+ childNewStart = movedRange.start;
667
+ childNewEnd = movedRange.end;
668
+ } else {
669
+ childNewStart = new Date(childOrigStart.getTime() + startDeltaMs);
670
+ childNewEnd = new Date(childOrigEnd.getTime() + endDeltaMs);
671
+ }
672
+ const prev = updatedDates.get(child.id);
673
+ if (prev && prev.start.getTime() === childNewStart.getTime() && prev.end.getTime() === childNewEnd.getTime()) {
674
+ continue;
675
+ }
676
+ updatedDates.set(child.id, { start: childNewStart, end: childNewEnd });
677
+ childShifted.add(child.id);
678
+ queue.push([child.id, "child-delta"]);
679
+ resultMap.set(child.id, {
680
+ ...child,
681
+ startDate: childNewStart.toISOString().split("T")[0],
682
+ endDate: childNewEnd.toISOString().split("T")[0]
683
+ });
684
+ }
685
+ }
686
+ const parentId = currentOriginal.parentId;
687
+ if (parentId) {
688
+ const parent = taskById.get(parentId);
689
+ if (parent && !parent.locked) {
690
+ const siblings = getChildren(parentId, allTasks);
691
+ const siblingPositions = siblings.map((sib) => {
692
+ if (updatedDates.has(sib.id)) return updatedDates.get(sib.id);
693
+ return { start: new Date(sib.startDate), end: new Date(sib.endDate) };
694
+ });
695
+ const minStart = new Date(Math.min(...siblingPositions.map((p) => p.start.getTime())));
696
+ const maxEnd = new Date(Math.max(...siblingPositions.map((p) => p.end.getTime())));
697
+ const prev = updatedDates.get(parentId);
698
+ if (!prev || prev.start.getTime() !== minStart.getTime() || prev.end.getTime() !== maxEnd.getTime()) {
699
+ updatedDates.set(parentId, { start: minStart, end: maxEnd });
700
+ queue.push([parentId, "parent-recalc"]);
701
+ resultMap.set(parentId, {
702
+ ...parent,
703
+ startDate: minStart.toISOString().split("T")[0],
704
+ endDate: maxEnd.toISOString().split("T")[0]
705
+ });
706
+ }
707
+ }
708
+ }
709
+ for (const task of allTasks) {
710
+ if (task.locked || !task.dependencies) continue;
711
+ const dep = task.dependencies.find((d) => d.taskId === currentId);
712
+ if (!dep) continue;
713
+ const origStart = new Date(task.startDate);
714
+ const origEnd = new Date(task.endDate);
715
+ const constraintDate = calculateSuccessorDate(
716
+ currStart,
717
+ currEnd,
718
+ dep.type,
719
+ getDependencyLag(dep),
720
+ businessDays,
721
+ weekendPredicate
722
+ );
723
+ let succNewStart;
724
+ let succNewEnd;
725
+ const duration = getTaskDuration(origStart, origEnd, businessDays, weekendPredicate);
726
+ if (dep.type === "FS" || dep.type === "SS") {
727
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromStart(
728
+ constraintDate,
729
+ duration,
730
+ businessDays,
731
+ weekendPredicate
732
+ ));
733
+ } else {
734
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromEnd(
735
+ constraintDate,
736
+ duration,
737
+ businessDays,
738
+ weekendPredicate
739
+ ));
740
+ }
741
+ const prev = updatedDates.get(task.id);
742
+ if (prev && prev.start.getTime() === succNewStart.getTime() && prev.end.getTime() === succNewEnd.getTime()) {
743
+ continue;
744
+ }
745
+ updatedDates.set(task.id, { start: succNewStart, end: succNewEnd });
746
+ queue.push([task.id, "dependency"]);
747
+ resultMap.set(task.id, {
748
+ ...task,
749
+ startDate: succNewStart.toISOString().split("T")[0],
750
+ endDate: succNewEnd.toISOString().split("T")[0]
751
+ });
752
+ }
753
+ }
754
+ return Array.from(resultMap.values());
755
+ }
756
+ function reflowTasksOnModeSwitch(sourceTasks, toBusinessDays, weekendPredicate) {
757
+ const fromBusinessDays = !toBusinessDays;
758
+ let tasks = sourceTasks.map((t) => ({ ...t }));
759
+ const toISO = (d) => d.toISOString().split("T")[0];
760
+ for (const task of tasks) {
761
+ if (isTaskParent(task.id, tasks)) continue;
762
+ const start = normalizeUTCDate(/* @__PURE__ */ new Date(`${task.startDate}T00:00:00.000Z`));
763
+ const duration = getTaskDuration(task.startDate, task.endDate, fromBusinessDays, weekendPredicate);
764
+ let range;
765
+ if (toBusinessDays) {
766
+ const alignedStart = alignToWorkingDay(start, 1, weekendPredicate);
767
+ range = buildTaskRangeFromStart(alignedStart, duration, true, weekendPredicate);
768
+ } else {
769
+ range = buildTaskRangeFromStart(start, duration, false);
770
+ }
771
+ task.startDate = toISO(range.start);
772
+ task.endDate = toISO(range.end);
773
+ }
774
+ for (const task of tasks) {
775
+ if (!isTaskParent(task.id, tasks)) continue;
776
+ const { startDate, endDate } = computeParentDates(task.id, tasks);
777
+ task.startDate = toISO(startDate);
778
+ task.endDate = toISO(endDate);
779
+ }
780
+ if (toBusinessDays) {
781
+ const rootSeeds = tasks.filter(
782
+ (t) => !t.parentId && (!t.dependencies || t.dependencies.length === 0)
783
+ );
784
+ for (const seed of rootSeeds) {
785
+ const current = tasks.find((t) => t.id === seed.id);
786
+ const start = /* @__PURE__ */ new Date(`${current.startDate}T00:00:00.000Z`);
787
+ const end = /* @__PURE__ */ new Date(`${current.endDate}T00:00:00.000Z`);
788
+ const cascaded = universalCascade(current, start, end, tasks, toBusinessDays, weekendPredicate);
789
+ const updates = new Map(cascaded.map((t) => [t.id, t]));
790
+ tasks = tasks.map((t) => updates.get(t.id) ?? t);
791
+ }
792
+ }
793
+ return tasks;
794
+ }
795
+
796
+ // src/core/scheduling/validation.ts
797
+ function buildAdjacencyList(tasks) {
798
+ const graph = /* @__PURE__ */ new Map();
799
+ for (const task of tasks) {
800
+ const successors = [];
801
+ for (const otherTask of tasks) {
802
+ if (otherTask.dependencies) {
803
+ for (const dep of otherTask.dependencies) {
804
+ if (dep.taskId === task.id) {
805
+ successors.push(otherTask.id);
806
+ break;
807
+ }
808
+ }
809
+ }
810
+ }
811
+ graph.set(task.id, successors);
812
+ }
813
+ return graph;
814
+ }
815
+ function detectCycles(tasks) {
816
+ const graph = buildAdjacencyList(tasks);
817
+ const visiting = /* @__PURE__ */ new Set();
818
+ const visited = /* @__PURE__ */ new Set();
819
+ const path = [];
820
+ function dfs(taskId) {
821
+ if (visiting.has(taskId)) {
822
+ return true;
823
+ }
824
+ if (visited.has(taskId)) {
825
+ return false;
826
+ }
827
+ visiting.add(taskId);
828
+ path.push(taskId);
829
+ const successors = graph.get(taskId) || [];
830
+ for (const successor of successors) {
831
+ if (dfs(successor)) {
832
+ return true;
833
+ }
834
+ }
835
+ visiting.delete(taskId);
836
+ path.pop();
837
+ visited.add(taskId);
838
+ return false;
839
+ }
840
+ for (const task of tasks) {
841
+ if (dfs(task.id)) {
842
+ return { hasCycle: true, cyclePath: [...path] };
843
+ }
844
+ }
845
+ return { hasCycle: false };
846
+ }
847
+ function validateDependencies(tasks) {
848
+ const errors = [];
849
+ const taskIds = new Set(tasks.map((t) => t.id));
850
+ for (const task of tasks) {
851
+ if (task.dependencies) {
852
+ for (const dep of task.dependencies) {
853
+ if (!taskIds.has(dep.taskId)) {
854
+ errors.push({
855
+ type: "missing-task",
856
+ taskId: task.id,
857
+ message: `Dependency references non-existent task: ${dep.taskId}`,
858
+ relatedTaskIds: [dep.taskId]
859
+ });
860
+ }
861
+ }
862
+ }
863
+ }
864
+ for (const task of tasks) {
865
+ if (!task.dependencies) continue;
866
+ for (const dep of task.dependencies) {
867
+ if (!taskIds.has(dep.taskId)) {
868
+ continue;
869
+ }
870
+ if (areTasksHierarchicallyRelated(task.id, dep.taskId, tasks)) {
871
+ errors.push({
872
+ type: "constraint",
873
+ taskId: task.id,
874
+ message: `Dependencies between parent and child tasks are not allowed: ${dep.taskId} -> ${task.id}`,
875
+ relatedTaskIds: [dep.taskId, task.id]
876
+ });
877
+ }
878
+ }
879
+ }
880
+ const cycleResult = detectCycles(tasks);
881
+ if (cycleResult.hasCycle && cycleResult.cyclePath) {
882
+ errors.push({
883
+ type: "cycle",
884
+ taskId: cycleResult.cyclePath[0],
885
+ message: "Circular dependency detected",
886
+ relatedTaskIds: cycleResult.cyclePath
887
+ });
888
+ }
889
+ return {
890
+ isValid: errors.length === 0,
891
+ errors
892
+ };
893
+ }
894
+ export {
895
+ DAY_MS,
896
+ addBusinessDays,
897
+ alignToWorkingDay,
898
+ areTasksHierarchicallyRelated,
899
+ buildAdjacencyList,
900
+ buildTaskRangeFromEnd,
901
+ buildTaskRangeFromStart,
902
+ calculateSuccessorDate,
903
+ cascadeByLinks,
904
+ clampDateRangeForIncomingFS,
905
+ clampTaskRangeForIncomingFS,
906
+ computeLagFromDates,
907
+ computeParentDates,
908
+ computeParentProgress,
909
+ detectCycles,
910
+ findParentId,
911
+ getAllDependencyEdges,
912
+ getAllDescendants,
913
+ getBusinessDayOffset,
914
+ getBusinessDaysCount,
915
+ getChildren,
916
+ getDependencyLag,
917
+ getSuccessorChain,
918
+ getTaskDuration,
919
+ getTransitiveCascadeChain,
920
+ isAncestorTask,
921
+ isTaskParent,
922
+ moveTaskRange,
923
+ normalizeDependencyLag,
924
+ normalizeUTCDate,
925
+ parseDateOnly,
926
+ recalculateIncomingLags,
927
+ reflowTasksOnModeSwitch,
928
+ removeDependenciesBetweenTasks,
929
+ resolveDateRangeFromPixels,
930
+ shiftBusinessDayOffset,
931
+ subtractBusinessDays,
932
+ universalCascade,
933
+ validateDependencies
934
+ };
935
+ //# sourceMappingURL=index.mjs.map