gantt-lib 0.53.1 → 0.60.0

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/index.mjs CHANGED
@@ -22,32 +22,49 @@ var __copyProps = (to, from, except, desc) => {
22
22
  };
23
23
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
24
 
25
- // src/utils/dateUtils.ts
26
- var dateUtils_exports = {};
27
- __export(dateUtils_exports, {
28
- addBusinessDays: () => addBusinessDays,
29
- createCustomDayPredicate: () => createCustomDayPredicate,
30
- createDateKey: () => createDateKey,
31
- formatDateLabel: () => formatDateLabel,
32
- formatDateRangeLabel: () => formatDateRangeLabel,
33
- getBusinessDaysCount: () => getBusinessDaysCount,
34
- getDayOffset: () => getDayOffset,
35
- getMonthBlocks: () => getMonthBlocks,
36
- getMonthDays: () => getMonthDays,
37
- getMonthSpans: () => getMonthSpans,
38
- getMultiMonthDays: () => getMultiMonthDays,
39
- getWeekBlocks: () => getWeekBlocks,
40
- getWeekSpans: () => getWeekSpans,
41
- getYearSpans: () => getYearSpans,
42
- isToday: () => isToday,
43
- isWeekend: () => isWeekend,
44
- normalizeTaskDates: () => normalizeTaskDates,
45
- parseUTCDate: () => parseUTCDate,
46
- subtractBusinessDays: () => subtractBusinessDays
47
- });
25
+ // src/core/scheduling/dateMath.ts
26
+ function normalizeUTCDate(date) {
27
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
28
+ }
29
+ function parseDateOnly(date) {
30
+ const parsed = typeof date === "string" ? /* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(date);
31
+ return normalizeUTCDate(parsed);
32
+ }
33
+ function getBusinessDayOffset(fromDate, toDate, weekendPredicate) {
34
+ const from = normalizeUTCDate(fromDate);
35
+ const to = normalizeUTCDate(toDate);
36
+ if (from.getTime() === to.getTime()) {
37
+ return 0;
38
+ }
39
+ const step = to.getTime() > from.getTime() ? 1 : -1;
40
+ const current = new Date(from);
41
+ let offset = 0;
42
+ while (current.getTime() !== to.getTime()) {
43
+ current.setUTCDate(current.getUTCDate() + step);
44
+ if (!weekendPredicate(current)) {
45
+ offset += step;
46
+ }
47
+ }
48
+ return offset;
49
+ }
50
+ function shiftBusinessDayOffset(date, offset, weekendPredicate) {
51
+ const current = normalizeUTCDate(date);
52
+ if (offset === 0) {
53
+ return current;
54
+ }
55
+ const step = offset > 0 ? 1 : -1;
56
+ let remaining = Math.abs(offset);
57
+ while (remaining > 0) {
58
+ current.setUTCDate(current.getUTCDate() + step);
59
+ if (!weekendPredicate(current)) {
60
+ remaining--;
61
+ }
62
+ }
63
+ return current;
64
+ }
48
65
  function getBusinessDaysCount(startDate, endDate, weekendPredicate) {
49
- const start = parseUTCDate(startDate);
50
- const end = parseUTCDate(endDate);
66
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
67
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
51
68
  let count = 0;
52
69
  const current = new Date(start);
53
70
  while (current.getTime() <= end.getTime()) {
@@ -59,7 +76,7 @@ function getBusinessDaysCount(startDate, endDate, weekendPredicate) {
59
76
  return Math.max(1, count);
60
77
  }
61
78
  function addBusinessDays(startDate, businessDays, weekendPredicate) {
62
- const start = parseUTCDate(startDate);
79
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
63
80
  const current = new Date(start);
64
81
  let targetDays = Math.max(1, businessDays);
65
82
  let businessDaysCounted = 0;
@@ -71,10 +88,10 @@ function addBusinessDays(startDate, businessDays, weekendPredicate) {
71
88
  current.setUTCDate(current.getUTCDate() + 1);
72
89
  }
73
90
  }
74
- return current.toISOString().split("T")[0];
91
+ return current;
75
92
  }
76
93
  function subtractBusinessDays(endDate, businessDays, weekendPredicate) {
77
- const end = parseUTCDate(endDate);
94
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
78
95
  const current = new Date(end);
79
96
  let targetDays = Math.max(1, businessDays);
80
97
  let businessDaysCounted = 0;
@@ -86,12 +103,70 @@ function subtractBusinessDays(endDate, businessDays, weekendPredicate) {
86
103
  current.setUTCDate(current.getUTCDate() - 1);
87
104
  }
88
105
  }
89
- return current.toISOString().split("T")[0];
106
+ return current;
107
+ }
108
+ function alignToWorkingDay(date, direction, weekendPredicate) {
109
+ const current = normalizeUTCDate(date);
110
+ while (weekendPredicate(current)) {
111
+ current.setUTCDate(current.getUTCDate() + direction);
112
+ }
113
+ return current;
114
+ }
115
+ function getTaskDuration(startDate, endDate, businessDays = false, weekendPredicate) {
116
+ const start = parseDateOnly(startDate);
117
+ const end = parseDateOnly(endDate);
118
+ if (businessDays && weekendPredicate) {
119
+ return getBusinessDaysCount(start, end, weekendPredicate);
120
+ }
121
+ return Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1);
122
+ }
123
+ var DAY_MS;
124
+ var init_dateMath = __esm({
125
+ "src/core/scheduling/dateMath.ts"() {
126
+ "use strict";
127
+ DAY_MS = 24 * 60 * 60 * 1e3;
128
+ }
129
+ });
130
+
131
+ // src/utils/dateUtils.ts
132
+ var dateUtils_exports = {};
133
+ __export(dateUtils_exports, {
134
+ addBusinessDays: () => addBusinessDays2,
135
+ createCustomDayPredicate: () => createCustomDayPredicate,
136
+ createDateKey: () => createDateKey,
137
+ formatDateLabel: () => formatDateLabel,
138
+ formatDateRangeLabel: () => formatDateRangeLabel,
139
+ getBusinessDaysCount: () => getBusinessDaysCount2,
140
+ getDayOffset: () => getDayOffset,
141
+ getMonthBlocks: () => getMonthBlocks,
142
+ getMonthDays: () => getMonthDays,
143
+ getMonthSpans: () => getMonthSpans,
144
+ getMultiMonthDays: () => getMultiMonthDays,
145
+ getWeekBlocks: () => getWeekBlocks,
146
+ getWeekSpans: () => getWeekSpans,
147
+ getYearSpans: () => getYearSpans,
148
+ isToday: () => isToday,
149
+ isWeekend: () => isWeekend,
150
+ normalizeTaskDates: () => normalizeTaskDates,
151
+ parseUTCDate: () => parseUTCDate,
152
+ subtractBusinessDays: () => subtractBusinessDays2
153
+ });
154
+ function getBusinessDaysCount2(startDate, endDate, weekendPredicate) {
155
+ return getBusinessDaysCount(startDate, endDate, weekendPredicate);
156
+ }
157
+ function addBusinessDays2(startDate, businessDays, weekendPredicate) {
158
+ const result = addBusinessDays(startDate, businessDays, weekendPredicate);
159
+ return result.toISOString().split("T")[0];
160
+ }
161
+ function subtractBusinessDays2(endDate, businessDays, weekendPredicate) {
162
+ const result = subtractBusinessDays(endDate, businessDays, weekendPredicate);
163
+ return result.toISOString().split("T")[0];
90
164
  }
91
165
  var parseUTCDate, getMonthDays, getDayOffset, isToday, isWeekend, createDateKey, createCustomDayPredicate, getMultiMonthDays, getMonthSpans, formatDateLabel, MONTH_ABBR, formatDateRangeLabel, getWeekBlocks, getWeekSpans, getMonthBlocks, getYearSpans, normalizeTaskDates;
92
166
  var init_dateUtils = __esm({
93
167
  "src/utils/dateUtils.ts"() {
94
168
  "use strict";
169
+ init_dateMath();
95
170
  parseUTCDate = (date) => {
96
171
  if (typeof date === "string") {
97
172
  const dateStr = date.includes("T") ? date : `${date}T00:00:00Z`;
@@ -419,43 +494,11 @@ var init_dateUtils = __esm({
419
494
  init_dateUtils();
420
495
  import { useMemo as useMemo9, useCallback as useCallback6, useRef as useRef7, useState as useState7, useEffect as useEffect7, useImperativeHandle, forwardRef } from "react";
421
496
 
422
- // src/utils/dependencyUtils.ts
423
- init_dateUtils();
424
- function normalizeUTCDate(date) {
425
- return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
426
- }
427
- function getBusinessDayOffset(fromDate, toDate, weekendPredicate) {
428
- const from = normalizeUTCDate(fromDate);
429
- const to = normalizeUTCDate(toDate);
430
- if (from.getTime() === to.getTime()) {
431
- return 0;
432
- }
433
- const step = to.getTime() > from.getTime() ? 1 : -1;
434
- const current = new Date(from);
435
- let offset = 0;
436
- while (current.getTime() !== to.getTime()) {
437
- current.setUTCDate(current.getUTCDate() + step);
438
- if (!weekendPredicate(current)) {
439
- offset += step;
440
- }
441
- }
442
- return offset;
443
- }
444
- function shiftBusinessDayOffset(date, offset, weekendPredicate) {
445
- const current = normalizeUTCDate(date);
446
- if (offset === 0) {
447
- return current;
448
- }
449
- const step = offset > 0 ? 1 : -1;
450
- let remaining = Math.abs(offset);
451
- while (remaining > 0) {
452
- current.setUTCDate(current.getUTCDate() + step);
453
- if (!weekendPredicate(current)) {
454
- remaining--;
455
- }
456
- }
457
- return current;
458
- }
497
+ // src/core/scheduling/index.ts
498
+ init_dateMath();
499
+
500
+ // src/core/scheduling/dependencies.ts
501
+ init_dateMath();
459
502
  function getDependencyLag(dep) {
460
503
  return Number.isFinite(dep.lag) ? dep.lag : 0;
461
504
  }
@@ -471,219 +514,70 @@ function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd,
471
514
  );
472
515
  return Math.max(-predecessorDuration, lag);
473
516
  }
474
- function alignToWorkingDay(date, direction, weekendPredicate) {
475
- const current = normalizeUTCDate(date);
476
- while (weekendPredicate(current)) {
477
- current.setUTCDate(current.getUTCDate() + direction);
478
- }
479
- return current;
480
- }
481
- function getTaskDuration(startDate, endDate, businessDays = false, weekendPredicate) {
482
- const start = parseDateOnly(startDate);
483
- const end = parseDateOnly(endDate);
484
- if (businessDays && weekendPredicate) {
485
- return getBusinessDaysCount(start, end, weekendPredicate);
486
- }
487
- return Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1);
488
- }
489
- function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
490
- const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
491
- if (businessDays && weekendPredicate) {
492
- return {
493
- start: normalizedStart,
494
- end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
495
- };
517
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate) {
518
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
519
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
520
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
521
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
522
+ if (!businessDays || !weekendPredicate) {
523
+ switch (linkType) {
524
+ case "FS":
525
+ return normalizeDependencyLag(
526
+ linkType,
527
+ Math.round((sS - pE) / DAY_MS) - 1,
528
+ predStart,
529
+ predEnd,
530
+ businessDays,
531
+ weekendPredicate
532
+ );
533
+ case "SS":
534
+ return Math.round((sS - pS) / DAY_MS);
535
+ case "FF":
536
+ return Math.round((sE - pE) / DAY_MS);
537
+ case "SF":
538
+ return Math.round((sE - pS) / DAY_MS) + 1;
539
+ }
496
540
  }
497
- return {
498
- start: normalizedStart,
499
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS)
500
- };
501
- }
502
- function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
503
- const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
504
- if (businessDays && weekendPredicate) {
505
- return {
506
- start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
507
- end: normalizedEnd
508
- };
541
+ const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
542
+ const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
543
+ const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
544
+ switch (linkType) {
545
+ case "FS":
546
+ return normalizeDependencyLag(
547
+ linkType,
548
+ businessOffset - 1,
549
+ predStart,
550
+ predEnd,
551
+ businessDays,
552
+ weekendPredicate
553
+ );
554
+ case "SS":
555
+ return businessOffset;
556
+ case "FF":
557
+ return businessOffset;
558
+ case "SF":
559
+ return businessOffset + 1;
509
560
  }
510
- return {
511
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS),
512
- end: normalizedEnd
513
- };
514
561
  }
515
- function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
516
- return buildTaskRangeFromStart(
517
- proposedStart,
518
- getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
562
+ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate) {
563
+ const normalizedLag = normalizeDependencyLag(
564
+ linkType,
565
+ lag,
566
+ predecessorStart,
567
+ predecessorEnd,
519
568
  businessDays,
520
- weekendPredicate,
521
- snapDirection
569
+ weekendPredicate
522
570
  );
523
- }
524
- function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate) {
525
- if (!task.dependencies?.length) {
526
- return { start: proposedStart, end: proposedEnd };
527
- }
528
- let minAllowedStart = null;
529
- for (const dep of task.dependencies) {
530
- if (dep.type !== "FS") {
531
- continue;
532
- }
533
- const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
534
- if (!predecessor) {
535
- continue;
536
- }
537
- const predecessorStart = parseDateOnly(predecessor.startDate);
538
- const predecessorEnd = parseDateOnly(predecessor.endDate);
539
- const predecessorDuration = getTaskDuration(
540
- predecessorStart,
541
- predecessorEnd,
542
- businessDays,
543
- weekendPredicate
544
- );
545
- const candidateMinStart = calculateSuccessorDate(
546
- predecessorStart,
547
- predecessorEnd,
548
- "FS",
549
- -predecessorDuration,
550
- businessDays,
551
- weekendPredicate
552
- );
553
- if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
554
- minAllowedStart = candidateMinStart;
555
- }
556
- }
557
- if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
558
- return { start: proposedStart, end: proposedEnd };
559
- }
560
- return buildTaskRangeFromStart(
561
- minAllowedStart,
562
- getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
563
- businessDays,
564
- weekendPredicate
565
- );
566
- }
567
- function parseDateOnly(date) {
568
- const parsed = typeof date === "string" ? /* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(date);
569
- return normalizeUTCDate(parsed);
570
- }
571
- function buildAdjacencyList(tasks) {
572
- const taskMap = new Map(tasks.map((t) => [t.id, t]));
573
- const graph = /* @__PURE__ */ new Map();
574
- for (const task of tasks) {
575
- const successors = [];
576
- for (const otherTask of tasks) {
577
- if (otherTask.dependencies) {
578
- for (const dep of otherTask.dependencies) {
579
- if (dep.taskId === task.id) {
580
- successors.push(otherTask.id);
581
- break;
582
- }
583
- }
584
- }
585
- }
586
- graph.set(task.id, successors);
587
- }
588
- return graph;
589
- }
590
- function detectCycles(tasks) {
591
- const graph = buildAdjacencyList(tasks);
592
- const visiting = /* @__PURE__ */ new Set();
593
- const visited = /* @__PURE__ */ new Set();
594
- const path = [];
595
- function dfs(taskId) {
596
- if (visiting.has(taskId)) {
597
- return true;
598
- }
599
- if (visited.has(taskId)) {
600
- return false;
601
- }
602
- visiting.add(taskId);
603
- path.push(taskId);
604
- const successors = graph.get(taskId) || [];
605
- for (const successor of successors) {
606
- if (dfs(successor)) {
607
- return true;
608
- }
609
- }
610
- visiting.delete(taskId);
611
- path.pop();
612
- visited.add(taskId);
613
- return false;
614
- }
615
- for (const task of tasks) {
616
- if (dfs(task.id)) {
617
- return { hasCycle: true, cyclePath: [...path] };
618
- }
619
- }
620
- return { hasCycle: false };
621
- }
622
- function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate) {
623
- const DAY_MS3 = 24 * 60 * 60 * 1e3;
624
- const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
625
- const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
626
- const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
627
- const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
628
- if (!businessDays || !weekendPredicate) {
629
- switch (linkType) {
630
- case "FS":
631
- return normalizeDependencyLag(
632
- linkType,
633
- Math.round((sS - pE) / DAY_MS3) - 1,
634
- predStart,
635
- predEnd,
636
- businessDays,
637
- weekendPredicate
638
- );
639
- case "SS":
640
- return Math.round((sS - pS) / DAY_MS3);
641
- case "FF":
642
- return Math.round((sE - pE) / DAY_MS3);
643
- case "SF":
644
- return Math.round((sE - pS) / DAY_MS3) + 1;
645
- }
646
- }
647
- const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
648
- const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
649
- const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
650
- switch (linkType) {
651
- case "FS":
652
- return normalizeDependencyLag(
653
- linkType,
654
- businessOffset - 1,
655
- predStart,
656
- predEnd,
657
- businessDays,
658
- weekendPredicate
659
- );
660
- case "SS":
661
- return businessOffset;
662
- case "FF":
663
- return businessOffset;
664
- case "SF":
665
- return businessOffset + 1;
666
- }
667
- }
668
- function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate) {
669
- const normalizedLag = normalizeDependencyLag(
670
- linkType,
671
- lag,
672
- predecessorStart,
673
- predecessorEnd,
674
- businessDays,
675
- weekendPredicate
676
- );
677
- if (!businessDays || !weekendPredicate) {
678
- switch (linkType) {
679
- case "FS":
680
- return new Date(predecessorEnd.getTime() + (normalizedLag + 1) * DAY_MS);
681
- case "SS":
682
- return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
683
- case "FF":
684
- return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
685
- case "SF":
686
- return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
571
+ if (!businessDays || !weekendPredicate) {
572
+ switch (linkType) {
573
+ case "FS":
574
+ return new Date(predecessorEnd.getTime() + (normalizedLag + 1) * DAY_MS);
575
+ case "SS":
576
+ return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
577
+ case "FF":
578
+ return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
579
+ case "SF":
580
+ return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
687
581
  }
688
582
  }
689
583
  const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
@@ -700,236 +594,15 @@ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag
700
594
  break;
701
595
  case "SF":
702
596
  offset = normalizedLag - 1;
703
- break;
704
- }
705
- return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
706
- }
707
- function validateDependencies(tasks) {
708
- const errors = [];
709
- const taskIds = new Set(tasks.map((t) => t.id));
710
- for (const task of tasks) {
711
- if (task.dependencies) {
712
- for (const dep of task.dependencies) {
713
- if (!taskIds.has(dep.taskId)) {
714
- errors.push({
715
- type: "missing-task",
716
- taskId: task.id,
717
- message: `Dependency references non-existent task: ${dep.taskId}`,
718
- relatedTaskIds: [dep.taskId]
719
- });
720
- }
721
- }
722
- }
723
- }
724
- for (const task of tasks) {
725
- if (!task.dependencies) continue;
726
- for (const dep of task.dependencies) {
727
- if (!taskIds.has(dep.taskId)) {
728
- continue;
729
- }
730
- if (areTasksHierarchicallyRelated(task.id, dep.taskId, tasks)) {
731
- errors.push({
732
- type: "constraint",
733
- taskId: task.id,
734
- message: `Dependencies between parent and child tasks are not allowed: ${dep.taskId} -> ${task.id}`,
735
- relatedTaskIds: [dep.taskId, task.id]
736
- });
737
- }
738
- }
739
- }
740
- const cycleResult = detectCycles(tasks);
741
- if (cycleResult.hasCycle && cycleResult.cyclePath) {
742
- errors.push({
743
- type: "cycle",
744
- taskId: cycleResult.cyclePath[0],
745
- message: "Circular dependency detected",
746
- relatedTaskIds: cycleResult.cyclePath
747
- });
748
- }
749
- return {
750
- isValid: errors.length === 0,
751
- errors
752
- };
753
- }
754
- function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
755
- const successorMap = /* @__PURE__ */ new Map();
756
- for (const task of allTasks) {
757
- successorMap.set(task.id, []);
758
- }
759
- for (const task of allTasks) {
760
- if (!task.dependencies) continue;
761
- for (const dep of task.dependencies) {
762
- if (linkTypes.includes(dep.type)) {
763
- const list = successorMap.get(dep.taskId) ?? [];
764
- list.push(task.id);
765
- successorMap.set(dep.taskId, list);
766
- }
767
- }
768
- }
769
- const taskById = new Map(allTasks.map((t) => [t.id, t]));
770
- const visited = /* @__PURE__ */ new Set();
771
- const queue = [draggedTaskId];
772
- const chain = [];
773
- visited.add(draggedTaskId);
774
- while (queue.length > 0) {
775
- const current = queue.shift();
776
- const successors = successorMap.get(current) ?? [];
777
- for (const sid of successors) {
778
- if (!visited.has(sid)) {
779
- visited.add(sid);
780
- const t = taskById.get(sid);
781
- if (t) {
782
- chain.push(t);
783
- queue.push(sid);
784
- }
785
- }
786
- }
787
- }
788
- return chain;
789
- }
790
- function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
791
- const taskById = new Map(allTasks.map((t) => [t.id, t]));
792
- const updatedDates = /* @__PURE__ */ new Map();
793
- updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
794
- const result = [];
795
- const queue = [movedTaskId];
796
- const visited = /* @__PURE__ */ new Set([movedTaskId]);
797
- while (queue.length > 0) {
798
- const currentId = queue.shift();
799
- const { start: predStart, end: predEnd } = updatedDates.get(currentId);
800
- if (!skipChildCascade) {
801
- const children = getChildren(currentId, allTasks);
802
- for (const child of children) {
803
- if (visited.has(child.id) || child.locked) continue;
804
- const origStart = new Date(child.startDate);
805
- const origEnd = new Date(child.endDate);
806
- const durationMs = origEnd.getTime() - origStart.getTime();
807
- const parentOrig = taskById.get(currentId);
808
- const parentOrigStart = new Date(parentOrig.startDate);
809
- const parentOrigEnd = new Date(parentOrig.endDate);
810
- const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
811
- const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
812
- const newChildStart = new Date(origStart.getTime() + parentStartDelta);
813
- const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
814
- visited.add(child.id);
815
- updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
816
- result.push({
817
- ...child,
818
- startDate: newChildStart.toISOString().split("T")[0],
819
- endDate: newChildEnd.toISOString().split("T")[0]
820
- });
821
- queue.push(child.id);
822
- }
823
- }
824
- for (const task of allTasks) {
825
- if (visited.has(task.id) || !task.dependencies || task.locked) continue;
826
- for (const dep of task.dependencies) {
827
- if (dep.taskId !== currentId) continue;
828
- const orig = taskById.get(task.id);
829
- const origStart = new Date(orig.startDate);
830
- const origEnd = new Date(orig.endDate);
831
- const duration = getTaskDuration(origStart, origEnd);
832
- const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
833
- let newSuccStart;
834
- let newSuccEnd;
835
- if (dep.type === "FS" || dep.type === "SS") {
836
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
837
- } else {
838
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
839
- }
840
- visited.add(task.id);
841
- updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
842
- result.push({
843
- ...task,
844
- startDate: newSuccStart.toISOString().split("T")[0],
845
- endDate: newSuccEnd.toISOString().split("T")[0]
846
- });
847
- queue.push(task.id);
848
- break;
849
- }
850
- }
851
- }
852
- return result;
853
- }
854
- function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
855
- const allTypesSuccessorMap = /* @__PURE__ */ new Map();
856
- for (const task of allTasks) {
857
- allTypesSuccessorMap.set(task.id, []);
858
- }
859
- for (const task of allTasks) {
860
- if (!task.dependencies) continue;
861
- for (const dep of task.dependencies) {
862
- const list = allTypesSuccessorMap.get(dep.taskId) ?? [];
863
- list.push(task);
864
- allTypesSuccessorMap.set(dep.taskId, list);
865
- }
866
- }
867
- const directChildren = getChildren(changedTaskId, allTasks);
868
- const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
869
- const initialChain = [...directChildren, ...directSuccessors].filter(
870
- (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
871
- );
872
- const chain = [...initialChain];
873
- const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
874
- const queue = [...initialChain];
875
- while (queue.length > 0) {
876
- const current = queue.shift();
877
- const children = getChildren(current.id, allTasks);
878
- for (const child of children) {
879
- if (!visited.has(child.id)) {
880
- visited.add(child.id);
881
- chain.push(child);
882
- queue.push(child);
883
- }
884
- }
885
- const successors = allTypesSuccessorMap.get(current.id) ?? [];
886
- for (const successor of successors) {
887
- if (!visited.has(successor.id)) {
888
- visited.add(successor.id);
889
- chain.push(successor);
890
- queue.push(successor);
891
- }
892
- }
893
- }
894
- return chain;
895
- }
896
- function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate) {
897
- if (!task.dependencies) return [];
898
- return task.dependencies.map((dep) => {
899
- const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
900
- if (!predecessor) {
901
- return { ...dep, lag: getDependencyLag(dep) };
902
- }
903
- const predecessorStart = new Date(predecessor.startDate);
904
- const predecessorEnd = new Date(predecessor.endDate);
905
- const nextLag = computeLagFromDates(
906
- dep.type,
907
- predecessorStart,
908
- predecessorEnd,
909
- newStartDate,
910
- newEndDate,
911
- businessDays,
912
- weekendPredicate
913
- );
914
- return { ...dep, lag: nextLag };
915
- });
916
- }
917
- function getAllDependencyEdges(tasks) {
918
- const edges = [];
919
- for (const task of tasks) {
920
- if (task.dependencies) {
921
- for (const dep of task.dependencies) {
922
- edges.push({
923
- predecessorId: dep.taskId,
924
- successorId: task.id,
925
- type: dep.type,
926
- lag: getDependencyLag(dep)
927
- });
928
- }
929
- }
597
+ break;
930
598
  }
931
- return edges;
599
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
932
600
  }
601
+
602
+ // src/core/scheduling/cascade.ts
603
+ init_dateMath();
604
+
605
+ // src/core/scheduling/hierarchy.ts
933
606
  function getChildren(parentId, tasks) {
934
607
  return tasks.filter((t) => t.parentId === parentId);
935
608
  }
@@ -974,6 +647,37 @@ function computeParentProgress(parentId, tasks) {
974
647
  }
975
648
  return Math.round(weightedSum / totalWeight * 10) / 10;
976
649
  }
650
+ function getAllDescendants(parentId, tasks) {
651
+ const descendants = [];
652
+ const visited = /* @__PURE__ */ new Set();
653
+ function collectChildren(taskId) {
654
+ if (visited.has(taskId)) return;
655
+ visited.add(taskId);
656
+ const children = getChildren(taskId, tasks);
657
+ for (const child of children) {
658
+ descendants.push(child);
659
+ collectChildren(child.id);
660
+ }
661
+ }
662
+ collectChildren(parentId);
663
+ return descendants;
664
+ }
665
+ function getAllDependencyEdges(tasks) {
666
+ const edges = [];
667
+ for (const task of tasks) {
668
+ if (task.dependencies) {
669
+ for (const dep of task.dependencies) {
670
+ edges.push({
671
+ predecessorId: dep.taskId,
672
+ successorId: task.id,
673
+ type: dep.type,
674
+ lag: dep.lag ?? 0
675
+ });
676
+ }
677
+ }
678
+ }
679
+ return edges;
680
+ }
977
681
  function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
978
682
  return tasks.map((task) => {
979
683
  if (task.id === taskId1 || task.id === taskId2) {
@@ -1003,36 +707,324 @@ function isAncestorTask(ancestorId, taskId, tasks) {
1003
707
  if (current.parentId === ancestorId) {
1004
708
  return true;
1005
709
  }
1006
- if (visited.has(current.parentId)) {
1007
- return false;
710
+ if (visited.has(current.parentId)) {
711
+ return false;
712
+ }
713
+ visited.add(current.parentId);
714
+ current = taskById.get(current.parentId);
715
+ }
716
+ return false;
717
+ }
718
+ function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
719
+ if (taskId1 === taskId2) {
720
+ return true;
721
+ }
722
+ return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
723
+ }
724
+
725
+ // src/core/scheduling/commands.ts
726
+ init_dateMath();
727
+ function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
728
+ const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
729
+ if (businessDays && weekendPredicate) {
730
+ return {
731
+ start: normalizedStart,
732
+ end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
733
+ };
734
+ }
735
+ const DAY_MS3 = 24 * 60 * 60 * 1e3;
736
+ return {
737
+ start: normalizedStart,
738
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS3)
739
+ };
740
+ }
741
+ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
742
+ const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
743
+ if (businessDays && weekendPredicate) {
744
+ return {
745
+ start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
746
+ end: normalizedEnd
747
+ };
748
+ }
749
+ const DAY_MS3 = 24 * 60 * 60 * 1e3;
750
+ return {
751
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS3),
752
+ end: normalizedEnd
753
+ };
754
+ }
755
+ function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
756
+ return buildTaskRangeFromStart(
757
+ proposedStart,
758
+ getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
759
+ businessDays,
760
+ weekendPredicate,
761
+ snapDirection
762
+ );
763
+ }
764
+ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate) {
765
+ if (!task.dependencies?.length) {
766
+ return { start: proposedStart, end: proposedEnd };
767
+ }
768
+ let minAllowedStart = null;
769
+ for (const dep of task.dependencies) {
770
+ if (dep.type !== "FS") {
771
+ continue;
772
+ }
773
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
774
+ if (!predecessor) {
775
+ continue;
776
+ }
777
+ const predecessorStart = parseDateOnly(predecessor.startDate);
778
+ const predecessorEnd = parseDateOnly(predecessor.endDate);
779
+ const predecessorDuration = getTaskDuration(
780
+ predecessorStart,
781
+ predecessorEnd,
782
+ businessDays,
783
+ weekendPredicate
784
+ );
785
+ const candidateMinStart = calculateSuccessorDate(
786
+ predecessorStart,
787
+ predecessorEnd,
788
+ "FS",
789
+ -predecessorDuration,
790
+ businessDays,
791
+ weekendPredicate
792
+ );
793
+ if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
794
+ minAllowedStart = candidateMinStart;
795
+ }
796
+ }
797
+ if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
798
+ return { start: proposedStart, end: proposedEnd };
799
+ }
800
+ return buildTaskRangeFromStart(
801
+ minAllowedStart,
802
+ getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
803
+ businessDays,
804
+ weekendPredicate
805
+ );
806
+ }
807
+ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate) {
808
+ if (!task.dependencies) return [];
809
+ return task.dependencies.map((dep) => {
810
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
811
+ if (!predecessor) {
812
+ return { ...dep, lag: getDependencyLag(dep) };
813
+ }
814
+ const predecessorStart = new Date(predecessor.startDate);
815
+ const predecessorEnd = new Date(predecessor.endDate);
816
+ const nextLag = computeLagFromDates(
817
+ dep.type,
818
+ predecessorStart,
819
+ predecessorEnd,
820
+ newStartDate,
821
+ newEndDate,
822
+ businessDays,
823
+ weekendPredicate
824
+ );
825
+ return { ...dep, lag: nextLag };
826
+ });
827
+ }
828
+ function resolveDateRangeFromPixels(mode, left, width, monthStart, dayWidth, task, businessDays, weekendPredicate) {
829
+ const dayOffset = Math.round(left / dayWidth);
830
+ const rawStartDate = new Date(Date.UTC(
831
+ monthStart.getUTCFullYear(),
832
+ monthStart.getUTCMonth(),
833
+ monthStart.getUTCDate() + dayOffset
834
+ ));
835
+ const rawEndOffset = dayOffset + Math.round(width / dayWidth) - 1;
836
+ const rawEndDate = new Date(Date.UTC(
837
+ monthStart.getUTCFullYear(),
838
+ monthStart.getUTCMonth(),
839
+ monthStart.getUTCDate() + rawEndOffset
840
+ ));
841
+ if (!(businessDays && weekendPredicate)) {
842
+ return { start: rawStartDate, end: rawEndDate };
843
+ }
844
+ if (mode === "move") {
845
+ const originalStart2 = new Date(task.startDate);
846
+ const snapDirection2 = rawStartDate.getTime() >= originalStart2.getTime() ? 1 : -1;
847
+ return moveTaskRange(
848
+ task.startDate,
849
+ task.endDate,
850
+ rawStartDate,
851
+ true,
852
+ weekendPredicate,
853
+ snapDirection2
854
+ );
855
+ }
856
+ if (mode === "resize-right") {
857
+ const fixedStart = new Date(task.startDate);
858
+ const originalEnd = new Date(task.endDate);
859
+ const snapDirection2 = rawEndDate.getTime() >= originalEnd.getTime() ? 1 : -1;
860
+ const alignedEnd = alignToWorkingDay(rawEndDate, snapDirection2, weekendPredicate);
861
+ const duration2 = Math.max(1, getBusinessDaysCount(fixedStart, alignedEnd, weekendPredicate));
862
+ return buildTaskRangeFromStart(fixedStart, duration2, true, weekendPredicate);
863
+ }
864
+ const fixedEnd = new Date(task.endDate);
865
+ const originalStart = new Date(task.startDate);
866
+ const snapDirection = rawStartDate.getTime() >= originalStart.getTime() ? 1 : -1;
867
+ const alignedStart = alignToWorkingDay(rawStartDate, snapDirection, weekendPredicate);
868
+ const duration = Math.max(1, getBusinessDaysCount(alignedStart, fixedEnd, weekendPredicate));
869
+ return buildTaskRangeFromEnd(fixedEnd, duration, true, weekendPredicate);
870
+ }
871
+ function clampDateRangeForIncomingFS(task, range, allTasks, mode, businessDays, weekendPredicate) {
872
+ if (mode === "resize-right") {
873
+ return range;
874
+ }
875
+ return clampTaskRangeForIncomingFS(
876
+ task,
877
+ range.start,
878
+ range.end,
879
+ allTasks,
880
+ businessDays,
881
+ weekendPredicate
882
+ );
883
+ }
884
+
885
+ // src/core/scheduling/cascade.ts
886
+ function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
887
+ const successorMap = /* @__PURE__ */ new Map();
888
+ for (const task of allTasks) {
889
+ successorMap.set(task.id, []);
890
+ }
891
+ for (const task of allTasks) {
892
+ if (!task.dependencies) continue;
893
+ for (const dep of task.dependencies) {
894
+ if (linkTypes.includes(dep.type)) {
895
+ const list = successorMap.get(dep.taskId) ?? [];
896
+ list.push(task.id);
897
+ successorMap.set(dep.taskId, list);
898
+ }
899
+ }
900
+ }
901
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
902
+ const visited = /* @__PURE__ */ new Set();
903
+ const queue = [draggedTaskId];
904
+ const chain = [];
905
+ visited.add(draggedTaskId);
906
+ while (queue.length > 0) {
907
+ const current = queue.shift();
908
+ const successors = successorMap.get(current) ?? [];
909
+ for (const sid of successors) {
910
+ if (!visited.has(sid)) {
911
+ visited.add(sid);
912
+ const t = taskById.get(sid);
913
+ if (t) {
914
+ chain.push(t);
915
+ queue.push(sid);
916
+ }
917
+ }
918
+ }
919
+ }
920
+ return chain;
921
+ }
922
+ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
923
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
924
+ const updatedDates = /* @__PURE__ */ new Map();
925
+ updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
926
+ const result = [];
927
+ const queue = [movedTaskId];
928
+ const visited = /* @__PURE__ */ new Set([movedTaskId]);
929
+ while (queue.length > 0) {
930
+ const currentId = queue.shift();
931
+ const { start: predStart, end: predEnd } = updatedDates.get(currentId);
932
+ if (!skipChildCascade) {
933
+ const children = getChildren(currentId, allTasks);
934
+ for (const child of children) {
935
+ if (visited.has(child.id) || child.locked) continue;
936
+ const origStart = new Date(child.startDate);
937
+ const origEnd = new Date(child.endDate);
938
+ const durationMs = origEnd.getTime() - origStart.getTime();
939
+ const parentOrig = taskById.get(currentId);
940
+ const parentOrigStart = new Date(parentOrig.startDate);
941
+ const parentOrigEnd = new Date(parentOrig.endDate);
942
+ const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
943
+ const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
944
+ const newChildStart = new Date(origStart.getTime() + parentStartDelta);
945
+ const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
946
+ visited.add(child.id);
947
+ updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
948
+ result.push({
949
+ ...child,
950
+ startDate: newChildStart.toISOString().split("T")[0],
951
+ endDate: newChildEnd.toISOString().split("T")[0]
952
+ });
953
+ queue.push(child.id);
954
+ }
955
+ }
956
+ for (const task of allTasks) {
957
+ if (visited.has(task.id) || !task.dependencies || task.locked) continue;
958
+ for (const dep of task.dependencies) {
959
+ if (dep.taskId !== currentId) continue;
960
+ const orig = taskById.get(task.id);
961
+ const origStart = new Date(orig.startDate);
962
+ const origEnd = new Date(orig.endDate);
963
+ const duration = getTaskDuration(origStart, origEnd);
964
+ const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
965
+ let newSuccStart;
966
+ let newSuccEnd;
967
+ if (dep.type === "FS" || dep.type === "SS") {
968
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
969
+ } else {
970
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
971
+ }
972
+ visited.add(task.id);
973
+ updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
974
+ result.push({
975
+ ...task,
976
+ startDate: newSuccStart.toISOString().split("T")[0],
977
+ endDate: newSuccEnd.toISOString().split("T")[0]
978
+ });
979
+ queue.push(task.id);
980
+ break;
981
+ }
1008
982
  }
1009
- visited.add(current.parentId);
1010
- current = taskById.get(current.parentId);
1011
983
  }
1012
- return false;
984
+ return result;
1013
985
  }
1014
- function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1015
- if (taskId1 === taskId2) {
1016
- return true;
986
+ function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
987
+ const allTypesSuccessorMap = /* @__PURE__ */ new Map();
988
+ for (const task of allTasks) {
989
+ allTypesSuccessorMap.set(task.id, []);
1017
990
  }
1018
- return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1019
- }
1020
- function getAllDescendants(parentId, tasks) {
1021
- const descendants = [];
1022
- const visited = /* @__PURE__ */ new Set();
1023
- function collectChildren(taskId) {
1024
- if (visited.has(taskId)) return;
1025
- visited.add(taskId);
1026
- const children = getChildren(taskId, tasks);
991
+ for (const task of allTasks) {
992
+ if (!task.dependencies) continue;
993
+ for (const dep of task.dependencies) {
994
+ const list = allTypesSuccessorMap.get(dep.taskId) ?? [];
995
+ list.push(task);
996
+ allTypesSuccessorMap.set(dep.taskId, list);
997
+ }
998
+ }
999
+ const directChildren = getChildren(changedTaskId, allTasks);
1000
+ const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
1001
+ const initialChain = [...directChildren, ...directSuccessors].filter(
1002
+ (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
1003
+ );
1004
+ const chain = [...initialChain];
1005
+ const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
1006
+ const queue = [...initialChain];
1007
+ while (queue.length > 0) {
1008
+ const current = queue.shift();
1009
+ const children = getChildren(current.id, allTasks);
1027
1010
  for (const child of children) {
1028
- descendants.push(child);
1029
- collectChildren(child.id);
1011
+ if (!visited.has(child.id)) {
1012
+ visited.add(child.id);
1013
+ chain.push(child);
1014
+ queue.push(child);
1015
+ }
1016
+ }
1017
+ const successors = allTypesSuccessorMap.get(current.id) ?? [];
1018
+ for (const successor of successors) {
1019
+ if (!visited.has(successor.id)) {
1020
+ visited.add(successor.id);
1021
+ chain.push(successor);
1022
+ queue.push(successor);
1023
+ }
1030
1024
  }
1031
1025
  }
1032
- collectChildren(parentId);
1033
- return descendants;
1026
+ return chain;
1034
1027
  }
1035
- var DAY_MS = 24 * 60 * 60 * 1e3;
1036
1028
  function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate) {
1037
1029
  const taskById = new Map(allTasks.map((t) => [t.id, t]));
1038
1030
  const updatedDates = /* @__PURE__ */ new Map();
@@ -1205,6 +1197,105 @@ function reflowTasksOnModeSwitch(sourceTasks, toBusinessDays, weekendPredicate)
1205
1197
  return tasks;
1206
1198
  }
1207
1199
 
1200
+ // src/core/scheduling/validation.ts
1201
+ function buildAdjacencyList(tasks) {
1202
+ const graph = /* @__PURE__ */ new Map();
1203
+ for (const task of tasks) {
1204
+ const successors = [];
1205
+ for (const otherTask of tasks) {
1206
+ if (otherTask.dependencies) {
1207
+ for (const dep of otherTask.dependencies) {
1208
+ if (dep.taskId === task.id) {
1209
+ successors.push(otherTask.id);
1210
+ break;
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+ graph.set(task.id, successors);
1216
+ }
1217
+ return graph;
1218
+ }
1219
+ function detectCycles(tasks) {
1220
+ const graph = buildAdjacencyList(tasks);
1221
+ const visiting = /* @__PURE__ */ new Set();
1222
+ const visited = /* @__PURE__ */ new Set();
1223
+ const path = [];
1224
+ function dfs(taskId) {
1225
+ if (visiting.has(taskId)) {
1226
+ return true;
1227
+ }
1228
+ if (visited.has(taskId)) {
1229
+ return false;
1230
+ }
1231
+ visiting.add(taskId);
1232
+ path.push(taskId);
1233
+ const successors = graph.get(taskId) || [];
1234
+ for (const successor of successors) {
1235
+ if (dfs(successor)) {
1236
+ return true;
1237
+ }
1238
+ }
1239
+ visiting.delete(taskId);
1240
+ path.pop();
1241
+ visited.add(taskId);
1242
+ return false;
1243
+ }
1244
+ for (const task of tasks) {
1245
+ if (dfs(task.id)) {
1246
+ return { hasCycle: true, cyclePath: [...path] };
1247
+ }
1248
+ }
1249
+ return { hasCycle: false };
1250
+ }
1251
+ function validateDependencies(tasks) {
1252
+ const errors = [];
1253
+ const taskIds = new Set(tasks.map((t) => t.id));
1254
+ for (const task of tasks) {
1255
+ if (task.dependencies) {
1256
+ for (const dep of task.dependencies) {
1257
+ if (!taskIds.has(dep.taskId)) {
1258
+ errors.push({
1259
+ type: "missing-task",
1260
+ taskId: task.id,
1261
+ message: `Dependency references non-existent task: ${dep.taskId}`,
1262
+ relatedTaskIds: [dep.taskId]
1263
+ });
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ for (const task of tasks) {
1269
+ if (!task.dependencies) continue;
1270
+ for (const dep of task.dependencies) {
1271
+ if (!taskIds.has(dep.taskId)) {
1272
+ continue;
1273
+ }
1274
+ if (areTasksHierarchicallyRelated(task.id, dep.taskId, tasks)) {
1275
+ errors.push({
1276
+ type: "constraint",
1277
+ taskId: task.id,
1278
+ message: `Dependencies between parent and child tasks are not allowed: ${dep.taskId} -> ${task.id}`,
1279
+ relatedTaskIds: [dep.taskId, task.id]
1280
+ });
1281
+ }
1282
+ }
1283
+ }
1284
+ const cycleResult = detectCycles(tasks);
1285
+ if (cycleResult.hasCycle && cycleResult.cyclePath) {
1286
+ errors.push({
1287
+ type: "cycle",
1288
+ taskId: cycleResult.cyclePath[0],
1289
+ message: "Circular dependency detected",
1290
+ relatedTaskIds: cycleResult.cyclePath
1291
+ });
1292
+ }
1293
+ return {
1294
+ isValid: errors.length === 0,
1295
+ errors
1296
+ };
1297
+ }
1298
+
1208
1299
  // src/utils/hierarchyOrder.ts
1209
1300
  init_dateUtils();
1210
1301
  function flattenHierarchy(tasks) {
@@ -1686,7 +1777,6 @@ var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
1686
1777
 
1687
1778
  // src/hooks/useTaskDrag.ts
1688
1779
  import { useEffect, useRef, useState, useCallback } from "react";
1689
- init_dateUtils();
1690
1780
  var globalActiveDrag = null;
1691
1781
  var globalRafId = null;
1692
1782
  function getDayOffsetFromMonthStart(date, monthStart) {
@@ -1694,62 +1784,6 @@ function getDayOffsetFromMonthStart(date, monthStart) {
1694
1784
  (Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) - Date.UTC(monthStart.getUTCFullYear(), monthStart.getUTCMonth(), monthStart.getUTCDate())) / (24 * 60 * 60 * 1e3)
1695
1785
  );
1696
1786
  }
1697
- function resolveDraggedRange(mode, left, width, monthStart, dayWidth, task, businessDays, weekendPredicate) {
1698
- const dayOffset = Math.round(left / dayWidth);
1699
- const rawStartDate = new Date(Date.UTC(
1700
- monthStart.getUTCFullYear(),
1701
- monthStart.getUTCMonth(),
1702
- monthStart.getUTCDate() + dayOffset
1703
- ));
1704
- const rawEndOffset = dayOffset + Math.round(width / dayWidth) - 1;
1705
- const rawEndDate = new Date(Date.UTC(
1706
- monthStart.getUTCFullYear(),
1707
- monthStart.getUTCMonth(),
1708
- monthStart.getUTCDate() + rawEndOffset
1709
- ));
1710
- if (!(businessDays && weekendPredicate)) {
1711
- return { start: rawStartDate, end: rawEndDate };
1712
- }
1713
- if (mode === "move") {
1714
- const originalStart2 = new Date(task.startDate);
1715
- const snapDirection2 = rawStartDate.getTime() >= originalStart2.getTime() ? 1 : -1;
1716
- return moveTaskRange(
1717
- task.startDate,
1718
- task.endDate,
1719
- rawStartDate,
1720
- true,
1721
- weekendPredicate,
1722
- snapDirection2
1723
- );
1724
- }
1725
- if (mode === "resize-right") {
1726
- const fixedStart = new Date(task.startDate);
1727
- const originalEnd = new Date(task.endDate);
1728
- const snapDirection2 = rawEndDate.getTime() >= originalEnd.getTime() ? 1 : -1;
1729
- const alignedEnd = alignToWorkingDay(rawEndDate, snapDirection2, weekendPredicate);
1730
- const duration2 = Math.max(1, getBusinessDaysCount(fixedStart, alignedEnd, weekendPredicate));
1731
- return buildTaskRangeFromStart(fixedStart, duration2, true, weekendPredicate);
1732
- }
1733
- const fixedEnd = new Date(task.endDate);
1734
- const originalStart = new Date(task.startDate);
1735
- const snapDirection = rawStartDate.getTime() >= originalStart.getTime() ? 1 : -1;
1736
- const alignedStart = alignToWorkingDay(rawStartDate, snapDirection, weekendPredicate);
1737
- const duration = Math.max(1, getBusinessDaysCount(alignedStart, fixedEnd, weekendPredicate));
1738
- return buildTaskRangeFromEnd(fixedEnd, duration, true, weekendPredicate);
1739
- }
1740
- function clampDraggedRangeForIncomingFS(task, range, allTasks, mode, businessDays, weekendPredicate) {
1741
- if (mode === "resize-right") {
1742
- return range;
1743
- }
1744
- return clampTaskRangeForIncomingFS(
1745
- task,
1746
- range.start,
1747
- range.end,
1748
- allTasks,
1749
- businessDays,
1750
- weekendPredicate
1751
- );
1752
- }
1753
1787
  function completeDrag() {
1754
1788
  if (globalRafId !== null) {
1755
1789
  cancelAnimationFrame(globalRafId);
@@ -1807,9 +1841,9 @@ function handleGlobalMouseMove(e) {
1807
1841
  }
1808
1842
  const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
1809
1843
  if (activeDrag.businessDays && activeDrag.weekendPredicate && draggedTask) {
1810
- const previewRange = clampDraggedRangeForIncomingFS(
1844
+ const previewRange = clampDateRangeForIncomingFS(
1811
1845
  draggedTask,
1812
- resolveDraggedRange(
1846
+ resolveDateRangeFromPixels(
1813
1847
  mode,
1814
1848
  newLeft,
1815
1849
  newWidth,
@@ -1829,9 +1863,9 @@ function handleGlobalMouseMove(e) {
1829
1863
  newLeft = Math.round(alignedStartDay * dayWidth);
1830
1864
  newWidth = Math.round((alignedEndDay - alignedStartDay + 1) * dayWidth);
1831
1865
  } else if (draggedTask) {
1832
- const previewRange = clampDraggedRangeForIncomingFS(
1866
+ const previewRange = clampDateRangeForIncomingFS(
1833
1867
  draggedTask,
1834
- resolveDraggedRange(
1868
+ resolveDateRangeFromPixels(
1835
1869
  mode,
1836
1870
  newLeft,
1837
1871
  newWidth,
@@ -1850,9 +1884,9 @@ function handleGlobalMouseMove(e) {
1850
1884
  if (!activeDrag.disableConstraints && activeDrag.onCascadeProgress) {
1851
1885
  const { dayWidth: dayWidth2, monthStart: mStart, taskId: dragId } = activeDrag;
1852
1886
  const originalDraggedTask = draggedTask ?? allTasks.find((t) => t.id === dragId);
1853
- const previewRange = originalDraggedTask ? clampDraggedRangeForIncomingFS(
1887
+ const previewRange = originalDraggedTask ? clampDateRangeForIncomingFS(
1854
1888
  originalDraggedTask,
1855
- resolveDraggedRange(
1889
+ resolveDateRangeFromPixels(
1856
1890
  mode,
1857
1891
  newLeft,
1858
1892
  newWidth,
@@ -2034,9 +2068,9 @@ var useTaskDrag = (options) => {
2034
2068
  const wasOwner = isOwnerRef.current;
2035
2069
  isOwnerRef.current = false;
2036
2070
  const currentTask = allTasks.find((t) => t.id === taskId);
2037
- const finalRange = currentTask ? clampDraggedRangeForIncomingFS(
2071
+ const finalRange = currentTask ? clampDateRangeForIncomingFS(
2038
2072
  currentTask,
2039
- resolveDraggedRange(
2073
+ resolveDateRangeFromPixels(
2040
2074
  finalMode,
2041
2075
  finalLeft,
2042
2076
  finalWidth,
@@ -3803,7 +3837,7 @@ var DepChip = ({
3803
3837
  newStart = constraintDate;
3804
3838
  if (businessDays) {
3805
3839
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3806
- newEnd = /* @__PURE__ */ new Date(`${addBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3840
+ newEnd = addBusinessDays(constraintDate, businessDuration, weekendPredicate);
3807
3841
  } else {
3808
3842
  newEnd = new Date(constraintDate.getTime() + durationMs);
3809
3843
  }
@@ -3811,7 +3845,7 @@ var DepChip = ({
3811
3845
  newEnd = constraintDate;
3812
3846
  if (businessDays) {
3813
3847
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3814
- newStart = /* @__PURE__ */ new Date(`${subtractBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3848
+ newStart = subtractBusinessDays(constraintDate, businessDuration, weekendPredicate);
3815
3849
  } else {
3816
3850
  newStart = new Date(constraintDate.getTime() - durationMs);
3817
3851
  }
@@ -4079,7 +4113,7 @@ var TaskListRow = React9.memo(
4079
4113
  );
4080
4114
  const getEndDate = useCallback4(
4081
4115
  (start, duration) => {
4082
- return businessDays ? addBusinessDays(start, duration, weekendPredicate) : getEndDateFromDuration(start, duration);
4116
+ return businessDays ? addBusinessDays(start, duration, weekendPredicate).toISOString().split("T")[0] : getEndDateFromDuration(start, duration);
4083
4117
  },
4084
4118
  [businessDays, weekendPredicate]
4085
4119
  );
@@ -7045,6 +7079,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7045
7079
  export {
7046
7080
  Button,
7047
7081
  Calendar,
7082
+ DAY_MS,
7048
7083
  DatePicker,
7049
7084
  DragGuideLines_default as DragGuideLines,
7050
7085
  GanttChart,
@@ -7057,7 +7092,7 @@ export {
7057
7092
  TaskRow_default as TaskRow,
7058
7093
  TimeScaleHeader_default as TimeScaleHeader,
7059
7094
  TodayIndicator_default as TodayIndicator,
7060
- addBusinessDays,
7095
+ addBusinessDays2 as addBusinessDays,
7061
7096
  alignToWorkingDay,
7062
7097
  and,
7063
7098
  areTasksHierarchicallyRelated,
@@ -7090,7 +7125,8 @@ export {
7090
7125
  formatDateRangeLabel,
7091
7126
  getAllDependencyEdges,
7092
7127
  getAllDescendants,
7093
- getBusinessDaysCount,
7128
+ getBusinessDayOffset,
7129
+ getBusinessDaysCount2 as getBusinessDaysCount,
7094
7130
  getChildren,
7095
7131
  getCursorForPosition,
7096
7132
  getDayOffset,
@@ -7117,15 +7153,18 @@ export {
7117
7153
  normalizeDependencyLag,
7118
7154
  normalizeHierarchyTasks,
7119
7155
  normalizeTaskDates,
7156
+ normalizeUTCDate,
7120
7157
  not,
7121
7158
  or,
7159
+ parseDateOnly,
7122
7160
  parseUTCDate,
7123
7161
  pixelsToDate,
7124
7162
  progressInRange,
7125
7163
  recalculateIncomingLags,
7126
7164
  reflowTasksOnModeSwitch,
7127
7165
  removeDependenciesBetweenTasks,
7128
- subtractBusinessDays,
7166
+ shiftBusinessDayOffset,
7167
+ subtractBusinessDays2 as subtractBusinessDays,
7129
7168
  universalCascade,
7130
7169
  useTaskDrag,
7131
7170
  validateDependencies,