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.js CHANGED
@@ -33,32 +33,49 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  ));
34
34
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
35
 
36
- // src/utils/dateUtils.ts
37
- var dateUtils_exports = {};
38
- __export(dateUtils_exports, {
39
- addBusinessDays: () => addBusinessDays,
40
- createCustomDayPredicate: () => createCustomDayPredicate,
41
- createDateKey: () => createDateKey,
42
- formatDateLabel: () => formatDateLabel,
43
- formatDateRangeLabel: () => formatDateRangeLabel,
44
- getBusinessDaysCount: () => getBusinessDaysCount,
45
- getDayOffset: () => getDayOffset,
46
- getMonthBlocks: () => getMonthBlocks,
47
- getMonthDays: () => getMonthDays,
48
- getMonthSpans: () => getMonthSpans,
49
- getMultiMonthDays: () => getMultiMonthDays,
50
- getWeekBlocks: () => getWeekBlocks,
51
- getWeekSpans: () => getWeekSpans,
52
- getYearSpans: () => getYearSpans,
53
- isToday: () => isToday,
54
- isWeekend: () => isWeekend,
55
- normalizeTaskDates: () => normalizeTaskDates,
56
- parseUTCDate: () => parseUTCDate,
57
- subtractBusinessDays: () => subtractBusinessDays
58
- });
36
+ // src/core/scheduling/dateMath.ts
37
+ function normalizeUTCDate(date) {
38
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
39
+ }
40
+ function parseDateOnly(date) {
41
+ const parsed = typeof date === "string" ? /* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(date);
42
+ return normalizeUTCDate(parsed);
43
+ }
44
+ function getBusinessDayOffset(fromDate, toDate, weekendPredicate) {
45
+ const from = normalizeUTCDate(fromDate);
46
+ const to = normalizeUTCDate(toDate);
47
+ if (from.getTime() === to.getTime()) {
48
+ return 0;
49
+ }
50
+ const step = to.getTime() > from.getTime() ? 1 : -1;
51
+ const current = new Date(from);
52
+ let offset = 0;
53
+ while (current.getTime() !== to.getTime()) {
54
+ current.setUTCDate(current.getUTCDate() + step);
55
+ if (!weekendPredicate(current)) {
56
+ offset += step;
57
+ }
58
+ }
59
+ return offset;
60
+ }
61
+ function shiftBusinessDayOffset(date, offset, weekendPredicate) {
62
+ const current = normalizeUTCDate(date);
63
+ if (offset === 0) {
64
+ return current;
65
+ }
66
+ const step = offset > 0 ? 1 : -1;
67
+ let remaining = Math.abs(offset);
68
+ while (remaining > 0) {
69
+ current.setUTCDate(current.getUTCDate() + step);
70
+ if (!weekendPredicate(current)) {
71
+ remaining--;
72
+ }
73
+ }
74
+ return current;
75
+ }
59
76
  function getBusinessDaysCount(startDate, endDate, weekendPredicate) {
60
- const start = parseUTCDate(startDate);
61
- const end = parseUTCDate(endDate);
77
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
78
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
62
79
  let count = 0;
63
80
  const current = new Date(start);
64
81
  while (current.getTime() <= end.getTime()) {
@@ -70,7 +87,7 @@ function getBusinessDaysCount(startDate, endDate, weekendPredicate) {
70
87
  return Math.max(1, count);
71
88
  }
72
89
  function addBusinessDays(startDate, businessDays, weekendPredicate) {
73
- const start = parseUTCDate(startDate);
90
+ const start = typeof startDate === "string" ? /* @__PURE__ */ new Date(`${startDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(startDate);
74
91
  const current = new Date(start);
75
92
  let targetDays = Math.max(1, businessDays);
76
93
  let businessDaysCounted = 0;
@@ -82,10 +99,10 @@ function addBusinessDays(startDate, businessDays, weekendPredicate) {
82
99
  current.setUTCDate(current.getUTCDate() + 1);
83
100
  }
84
101
  }
85
- return current.toISOString().split("T")[0];
102
+ return current;
86
103
  }
87
104
  function subtractBusinessDays(endDate, businessDays, weekendPredicate) {
88
- const end = parseUTCDate(endDate);
105
+ const end = typeof endDate === "string" ? /* @__PURE__ */ new Date(`${endDate.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(endDate);
89
106
  const current = new Date(end);
90
107
  let targetDays = Math.max(1, businessDays);
91
108
  let businessDaysCounted = 0;
@@ -97,12 +114,70 @@ function subtractBusinessDays(endDate, businessDays, weekendPredicate) {
97
114
  current.setUTCDate(current.getUTCDate() - 1);
98
115
  }
99
116
  }
100
- return current.toISOString().split("T")[0];
117
+ return current;
118
+ }
119
+ function alignToWorkingDay(date, direction, weekendPredicate) {
120
+ const current = normalizeUTCDate(date);
121
+ while (weekendPredicate(current)) {
122
+ current.setUTCDate(current.getUTCDate() + direction);
123
+ }
124
+ return current;
125
+ }
126
+ function getTaskDuration(startDate, endDate, businessDays = false, weekendPredicate) {
127
+ const start = parseDateOnly(startDate);
128
+ const end = parseDateOnly(endDate);
129
+ if (businessDays && weekendPredicate) {
130
+ return getBusinessDaysCount(start, end, weekendPredicate);
131
+ }
132
+ return Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1);
133
+ }
134
+ var DAY_MS;
135
+ var init_dateMath = __esm({
136
+ "src/core/scheduling/dateMath.ts"() {
137
+ "use strict";
138
+ DAY_MS = 24 * 60 * 60 * 1e3;
139
+ }
140
+ });
141
+
142
+ // src/utils/dateUtils.ts
143
+ var dateUtils_exports = {};
144
+ __export(dateUtils_exports, {
145
+ addBusinessDays: () => addBusinessDays2,
146
+ createCustomDayPredicate: () => createCustomDayPredicate,
147
+ createDateKey: () => createDateKey,
148
+ formatDateLabel: () => formatDateLabel,
149
+ formatDateRangeLabel: () => formatDateRangeLabel,
150
+ getBusinessDaysCount: () => getBusinessDaysCount2,
151
+ getDayOffset: () => getDayOffset,
152
+ getMonthBlocks: () => getMonthBlocks,
153
+ getMonthDays: () => getMonthDays,
154
+ getMonthSpans: () => getMonthSpans,
155
+ getMultiMonthDays: () => getMultiMonthDays,
156
+ getWeekBlocks: () => getWeekBlocks,
157
+ getWeekSpans: () => getWeekSpans,
158
+ getYearSpans: () => getYearSpans,
159
+ isToday: () => isToday,
160
+ isWeekend: () => isWeekend,
161
+ normalizeTaskDates: () => normalizeTaskDates,
162
+ parseUTCDate: () => parseUTCDate,
163
+ subtractBusinessDays: () => subtractBusinessDays2
164
+ });
165
+ function getBusinessDaysCount2(startDate, endDate, weekendPredicate) {
166
+ return getBusinessDaysCount(startDate, endDate, weekendPredicate);
167
+ }
168
+ function addBusinessDays2(startDate, businessDays, weekendPredicate) {
169
+ const result = addBusinessDays(startDate, businessDays, weekendPredicate);
170
+ return result.toISOString().split("T")[0];
171
+ }
172
+ function subtractBusinessDays2(endDate, businessDays, weekendPredicate) {
173
+ const result = subtractBusinessDays(endDate, businessDays, weekendPredicate);
174
+ return result.toISOString().split("T")[0];
101
175
  }
102
176
  var parseUTCDate, getMonthDays, getDayOffset, isToday, isWeekend, createDateKey, createCustomDayPredicate, getMultiMonthDays, getMonthSpans, formatDateLabel, MONTH_ABBR, formatDateRangeLabel, getWeekBlocks, getWeekSpans, getMonthBlocks, getYearSpans, normalizeTaskDates;
103
177
  var init_dateUtils = __esm({
104
178
  "src/utils/dateUtils.ts"() {
105
179
  "use strict";
180
+ init_dateMath();
106
181
  parseUTCDate = (date) => {
107
182
  if (typeof date === "string") {
108
183
  const dateStr = date.includes("T") ? date : `${date}T00:00:00Z`;
@@ -431,6 +506,7 @@ var index_exports = {};
431
506
  __export(index_exports, {
432
507
  Button: () => Button,
433
508
  Calendar: () => Calendar,
509
+ DAY_MS: () => DAY_MS,
434
510
  DatePicker: () => DatePicker,
435
511
  DragGuideLines: () => DragGuideLines_default,
436
512
  GanttChart: () => GanttChart,
@@ -443,7 +519,7 @@ __export(index_exports, {
443
519
  TaskRow: () => TaskRow_default,
444
520
  TimeScaleHeader: () => TimeScaleHeader_default,
445
521
  TodayIndicator: () => TodayIndicator_default,
446
- addBusinessDays: () => addBusinessDays,
522
+ addBusinessDays: () => addBusinessDays2,
447
523
  alignToWorkingDay: () => alignToWorkingDay,
448
524
  and: () => and,
449
525
  areTasksHierarchicallyRelated: () => areTasksHierarchicallyRelated,
@@ -476,7 +552,8 @@ __export(index_exports, {
476
552
  formatDateRangeLabel: () => formatDateRangeLabel,
477
553
  getAllDependencyEdges: () => getAllDependencyEdges,
478
554
  getAllDescendants: () => getAllDescendants,
479
- getBusinessDaysCount: () => getBusinessDaysCount,
555
+ getBusinessDayOffset: () => getBusinessDayOffset,
556
+ getBusinessDaysCount: () => getBusinessDaysCount2,
480
557
  getChildren: () => getChildren,
481
558
  getCursorForPosition: () => getCursorForPosition,
482
559
  getDayOffset: () => getDayOffset,
@@ -503,15 +580,18 @@ __export(index_exports, {
503
580
  normalizeDependencyLag: () => normalizeDependencyLag,
504
581
  normalizeHierarchyTasks: () => normalizeHierarchyTasks,
505
582
  normalizeTaskDates: () => normalizeTaskDates,
583
+ normalizeUTCDate: () => normalizeUTCDate,
506
584
  not: () => not,
507
585
  or: () => or,
586
+ parseDateOnly: () => parseDateOnly,
508
587
  parseUTCDate: () => parseUTCDate,
509
588
  pixelsToDate: () => pixelsToDate,
510
589
  progressInRange: () => progressInRange,
511
590
  recalculateIncomingLags: () => recalculateIncomingLags,
512
591
  reflowTasksOnModeSwitch: () => reflowTasksOnModeSwitch,
513
592
  removeDependenciesBetweenTasks: () => removeDependenciesBetweenTasks,
514
- subtractBusinessDays: () => subtractBusinessDays,
593
+ shiftBusinessDayOffset: () => shiftBusinessDayOffset,
594
+ subtractBusinessDays: () => subtractBusinessDays2,
515
595
  universalCascade: () => universalCascade,
516
596
  useTaskDrag: () => useTaskDrag,
517
597
  validateDependencies: () => validateDependencies,
@@ -523,43 +603,11 @@ module.exports = __toCommonJS(index_exports);
523
603
  var import_react13 = require("react");
524
604
  init_dateUtils();
525
605
 
526
- // src/utils/dependencyUtils.ts
527
- init_dateUtils();
528
- function normalizeUTCDate(date) {
529
- return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
530
- }
531
- function getBusinessDayOffset(fromDate, toDate, weekendPredicate) {
532
- const from = normalizeUTCDate(fromDate);
533
- const to = normalizeUTCDate(toDate);
534
- if (from.getTime() === to.getTime()) {
535
- return 0;
536
- }
537
- const step = to.getTime() > from.getTime() ? 1 : -1;
538
- const current = new Date(from);
539
- let offset = 0;
540
- while (current.getTime() !== to.getTime()) {
541
- current.setUTCDate(current.getUTCDate() + step);
542
- if (!weekendPredicate(current)) {
543
- offset += step;
544
- }
545
- }
546
- return offset;
547
- }
548
- function shiftBusinessDayOffset(date, offset, weekendPredicate) {
549
- const current = normalizeUTCDate(date);
550
- if (offset === 0) {
551
- return current;
552
- }
553
- const step = offset > 0 ? 1 : -1;
554
- let remaining = Math.abs(offset);
555
- while (remaining > 0) {
556
- current.setUTCDate(current.getUTCDate() + step);
557
- if (!weekendPredicate(current)) {
558
- remaining--;
559
- }
560
- }
561
- return current;
562
- }
606
+ // src/core/scheduling/index.ts
607
+ init_dateMath();
608
+
609
+ // src/core/scheduling/dependencies.ts
610
+ init_dateMath();
563
611
  function getDependencyLag(dep) {
564
612
  return Number.isFinite(dep.lag) ? dep.lag : 0;
565
613
  }
@@ -575,219 +623,70 @@ function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd,
575
623
  );
576
624
  return Math.max(-predecessorDuration, lag);
577
625
  }
578
- function alignToWorkingDay(date, direction, weekendPredicate) {
579
- const current = normalizeUTCDate(date);
580
- while (weekendPredicate(current)) {
581
- current.setUTCDate(current.getUTCDate() + direction);
582
- }
583
- return current;
584
- }
585
- function getTaskDuration(startDate, endDate, businessDays = false, weekendPredicate) {
586
- const start = parseDateOnly(startDate);
587
- const end = parseDateOnly(endDate);
588
- if (businessDays && weekendPredicate) {
589
- return getBusinessDaysCount(start, end, weekendPredicate);
590
- }
591
- return Math.max(1, Math.round((end.getTime() - start.getTime()) / DAY_MS) + 1);
592
- }
593
- function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
594
- const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
595
- if (businessDays && weekendPredicate) {
596
- return {
597
- start: normalizedStart,
598
- end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
599
- };
626
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate) {
627
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
628
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
629
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
630
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
631
+ if (!businessDays || !weekendPredicate) {
632
+ switch (linkType) {
633
+ case "FS":
634
+ return normalizeDependencyLag(
635
+ linkType,
636
+ Math.round((sS - pE) / DAY_MS) - 1,
637
+ predStart,
638
+ predEnd,
639
+ businessDays,
640
+ weekendPredicate
641
+ );
642
+ case "SS":
643
+ return Math.round((sS - pS) / DAY_MS);
644
+ case "FF":
645
+ return Math.round((sE - pE) / DAY_MS);
646
+ case "SF":
647
+ return Math.round((sE - pS) / DAY_MS) + 1;
648
+ }
600
649
  }
601
- return {
602
- start: normalizedStart,
603
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS)
604
- };
605
- }
606
- function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
607
- const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
608
- if (businessDays && weekendPredicate) {
609
- return {
610
- start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
611
- end: normalizedEnd
612
- };
650
+ const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
651
+ const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
652
+ const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
653
+ switch (linkType) {
654
+ case "FS":
655
+ return normalizeDependencyLag(
656
+ linkType,
657
+ businessOffset - 1,
658
+ predStart,
659
+ predEnd,
660
+ businessDays,
661
+ weekendPredicate
662
+ );
663
+ case "SS":
664
+ return businessOffset;
665
+ case "FF":
666
+ return businessOffset;
667
+ case "SF":
668
+ return businessOffset + 1;
613
669
  }
614
- return {
615
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS),
616
- end: normalizedEnd
617
- };
618
670
  }
619
- function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
620
- return buildTaskRangeFromStart(
621
- proposedStart,
622
- getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
671
+ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate) {
672
+ const normalizedLag = normalizeDependencyLag(
673
+ linkType,
674
+ lag,
675
+ predecessorStart,
676
+ predecessorEnd,
623
677
  businessDays,
624
- weekendPredicate,
625
- snapDirection
678
+ weekendPredicate
626
679
  );
627
- }
628
- function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate) {
629
- if (!task.dependencies?.length) {
630
- return { start: proposedStart, end: proposedEnd };
631
- }
632
- let minAllowedStart = null;
633
- for (const dep of task.dependencies) {
634
- if (dep.type !== "FS") {
635
- continue;
636
- }
637
- const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
638
- if (!predecessor) {
639
- continue;
640
- }
641
- const predecessorStart = parseDateOnly(predecessor.startDate);
642
- const predecessorEnd = parseDateOnly(predecessor.endDate);
643
- const predecessorDuration = getTaskDuration(
644
- predecessorStart,
645
- predecessorEnd,
646
- businessDays,
647
- weekendPredicate
648
- );
649
- const candidateMinStart = calculateSuccessorDate(
650
- predecessorStart,
651
- predecessorEnd,
652
- "FS",
653
- -predecessorDuration,
654
- businessDays,
655
- weekendPredicate
656
- );
657
- if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
658
- minAllowedStart = candidateMinStart;
659
- }
660
- }
661
- if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
662
- return { start: proposedStart, end: proposedEnd };
663
- }
664
- return buildTaskRangeFromStart(
665
- minAllowedStart,
666
- getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
667
- businessDays,
668
- weekendPredicate
669
- );
670
- }
671
- function parseDateOnly(date) {
672
- const parsed = typeof date === "string" ? /* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`) : normalizeUTCDate(date);
673
- return normalizeUTCDate(parsed);
674
- }
675
- function buildAdjacencyList(tasks) {
676
- const taskMap = new Map(tasks.map((t) => [t.id, t]));
677
- const graph = /* @__PURE__ */ new Map();
678
- for (const task of tasks) {
679
- const successors = [];
680
- for (const otherTask of tasks) {
681
- if (otherTask.dependencies) {
682
- for (const dep of otherTask.dependencies) {
683
- if (dep.taskId === task.id) {
684
- successors.push(otherTask.id);
685
- break;
686
- }
687
- }
688
- }
689
- }
690
- graph.set(task.id, successors);
691
- }
692
- return graph;
693
- }
694
- function detectCycles(tasks) {
695
- const graph = buildAdjacencyList(tasks);
696
- const visiting = /* @__PURE__ */ new Set();
697
- const visited = /* @__PURE__ */ new Set();
698
- const path = [];
699
- function dfs(taskId) {
700
- if (visiting.has(taskId)) {
701
- return true;
702
- }
703
- if (visited.has(taskId)) {
704
- return false;
705
- }
706
- visiting.add(taskId);
707
- path.push(taskId);
708
- const successors = graph.get(taskId) || [];
709
- for (const successor of successors) {
710
- if (dfs(successor)) {
711
- return true;
712
- }
713
- }
714
- visiting.delete(taskId);
715
- path.pop();
716
- visited.add(taskId);
717
- return false;
718
- }
719
- for (const task of tasks) {
720
- if (dfs(task.id)) {
721
- return { hasCycle: true, cyclePath: [...path] };
722
- }
723
- }
724
- return { hasCycle: false };
725
- }
726
- function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate) {
727
- const DAY_MS3 = 24 * 60 * 60 * 1e3;
728
- const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
729
- const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
730
- const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
731
- const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
732
- if (!businessDays || !weekendPredicate) {
733
- switch (linkType) {
734
- case "FS":
735
- return normalizeDependencyLag(
736
- linkType,
737
- Math.round((sS - pE) / DAY_MS3) - 1,
738
- predStart,
739
- predEnd,
740
- businessDays,
741
- weekendPredicate
742
- );
743
- case "SS":
744
- return Math.round((sS - pS) / DAY_MS3);
745
- case "FF":
746
- return Math.round((sE - pE) / DAY_MS3);
747
- case "SF":
748
- return Math.round((sE - pS) / DAY_MS3) + 1;
749
- }
750
- }
751
- const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
752
- const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
753
- const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
754
- switch (linkType) {
755
- case "FS":
756
- return normalizeDependencyLag(
757
- linkType,
758
- businessOffset - 1,
759
- predStart,
760
- predEnd,
761
- businessDays,
762
- weekendPredicate
763
- );
764
- case "SS":
765
- return businessOffset;
766
- case "FF":
767
- return businessOffset;
768
- case "SF":
769
- return businessOffset + 1;
770
- }
771
- }
772
- function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate) {
773
- const normalizedLag = normalizeDependencyLag(
774
- linkType,
775
- lag,
776
- predecessorStart,
777
- predecessorEnd,
778
- businessDays,
779
- weekendPredicate
780
- );
781
- if (!businessDays || !weekendPredicate) {
782
- switch (linkType) {
783
- case "FS":
784
- return new Date(predecessorEnd.getTime() + (normalizedLag + 1) * DAY_MS);
785
- case "SS":
786
- return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
787
- case "FF":
788
- return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
789
- case "SF":
790
- return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
680
+ if (!businessDays || !weekendPredicate) {
681
+ switch (linkType) {
682
+ case "FS":
683
+ return new Date(predecessorEnd.getTime() + (normalizedLag + 1) * DAY_MS);
684
+ case "SS":
685
+ return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
686
+ case "FF":
687
+ return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
688
+ case "SF":
689
+ return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
791
690
  }
792
691
  }
793
692
  const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
@@ -804,236 +703,15 @@ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag
804
703
  break;
805
704
  case "SF":
806
705
  offset = normalizedLag - 1;
807
- break;
808
- }
809
- return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
810
- }
811
- function validateDependencies(tasks) {
812
- const errors = [];
813
- const taskIds = new Set(tasks.map((t) => t.id));
814
- for (const task of tasks) {
815
- if (task.dependencies) {
816
- for (const dep of task.dependencies) {
817
- if (!taskIds.has(dep.taskId)) {
818
- errors.push({
819
- type: "missing-task",
820
- taskId: task.id,
821
- message: `Dependency references non-existent task: ${dep.taskId}`,
822
- relatedTaskIds: [dep.taskId]
823
- });
824
- }
825
- }
826
- }
827
- }
828
- for (const task of tasks) {
829
- if (!task.dependencies) continue;
830
- for (const dep of task.dependencies) {
831
- if (!taskIds.has(dep.taskId)) {
832
- continue;
833
- }
834
- if (areTasksHierarchicallyRelated(task.id, dep.taskId, tasks)) {
835
- errors.push({
836
- type: "constraint",
837
- taskId: task.id,
838
- message: `Dependencies between parent and child tasks are not allowed: ${dep.taskId} -> ${task.id}`,
839
- relatedTaskIds: [dep.taskId, task.id]
840
- });
841
- }
842
- }
843
- }
844
- const cycleResult = detectCycles(tasks);
845
- if (cycleResult.hasCycle && cycleResult.cyclePath) {
846
- errors.push({
847
- type: "cycle",
848
- taskId: cycleResult.cyclePath[0],
849
- message: "Circular dependency detected",
850
- relatedTaskIds: cycleResult.cyclePath
851
- });
852
- }
853
- return {
854
- isValid: errors.length === 0,
855
- errors
856
- };
857
- }
858
- function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
859
- const successorMap = /* @__PURE__ */ new Map();
860
- for (const task of allTasks) {
861
- successorMap.set(task.id, []);
862
- }
863
- for (const task of allTasks) {
864
- if (!task.dependencies) continue;
865
- for (const dep of task.dependencies) {
866
- if (linkTypes.includes(dep.type)) {
867
- const list = successorMap.get(dep.taskId) ?? [];
868
- list.push(task.id);
869
- successorMap.set(dep.taskId, list);
870
- }
871
- }
872
- }
873
- const taskById = new Map(allTasks.map((t) => [t.id, t]));
874
- const visited = /* @__PURE__ */ new Set();
875
- const queue = [draggedTaskId];
876
- const chain = [];
877
- visited.add(draggedTaskId);
878
- while (queue.length > 0) {
879
- const current = queue.shift();
880
- const successors = successorMap.get(current) ?? [];
881
- for (const sid of successors) {
882
- if (!visited.has(sid)) {
883
- visited.add(sid);
884
- const t = taskById.get(sid);
885
- if (t) {
886
- chain.push(t);
887
- queue.push(sid);
888
- }
889
- }
890
- }
891
- }
892
- return chain;
893
- }
894
- function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
895
- const taskById = new Map(allTasks.map((t) => [t.id, t]));
896
- const updatedDates = /* @__PURE__ */ new Map();
897
- updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
898
- const result = [];
899
- const queue = [movedTaskId];
900
- const visited = /* @__PURE__ */ new Set([movedTaskId]);
901
- while (queue.length > 0) {
902
- const currentId = queue.shift();
903
- const { start: predStart, end: predEnd } = updatedDates.get(currentId);
904
- if (!skipChildCascade) {
905
- const children = getChildren(currentId, allTasks);
906
- for (const child of children) {
907
- if (visited.has(child.id) || child.locked) continue;
908
- const origStart = new Date(child.startDate);
909
- const origEnd = new Date(child.endDate);
910
- const durationMs = origEnd.getTime() - origStart.getTime();
911
- const parentOrig = taskById.get(currentId);
912
- const parentOrigStart = new Date(parentOrig.startDate);
913
- const parentOrigEnd = new Date(parentOrig.endDate);
914
- const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
915
- const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
916
- const newChildStart = new Date(origStart.getTime() + parentStartDelta);
917
- const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
918
- visited.add(child.id);
919
- updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
920
- result.push({
921
- ...child,
922
- startDate: newChildStart.toISOString().split("T")[0],
923
- endDate: newChildEnd.toISOString().split("T")[0]
924
- });
925
- queue.push(child.id);
926
- }
927
- }
928
- for (const task of allTasks) {
929
- if (visited.has(task.id) || !task.dependencies || task.locked) continue;
930
- for (const dep of task.dependencies) {
931
- if (dep.taskId !== currentId) continue;
932
- const orig = taskById.get(task.id);
933
- const origStart = new Date(orig.startDate);
934
- const origEnd = new Date(orig.endDate);
935
- const duration = getTaskDuration(origStart, origEnd);
936
- const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
937
- let newSuccStart;
938
- let newSuccEnd;
939
- if (dep.type === "FS" || dep.type === "SS") {
940
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
941
- } else {
942
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
943
- }
944
- visited.add(task.id);
945
- updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
946
- result.push({
947
- ...task,
948
- startDate: newSuccStart.toISOString().split("T")[0],
949
- endDate: newSuccEnd.toISOString().split("T")[0]
950
- });
951
- queue.push(task.id);
952
- break;
953
- }
954
- }
955
- }
956
- return result;
957
- }
958
- function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
959
- const allTypesSuccessorMap = /* @__PURE__ */ new Map();
960
- for (const task of allTasks) {
961
- allTypesSuccessorMap.set(task.id, []);
962
- }
963
- for (const task of allTasks) {
964
- if (!task.dependencies) continue;
965
- for (const dep of task.dependencies) {
966
- const list = allTypesSuccessorMap.get(dep.taskId) ?? [];
967
- list.push(task);
968
- allTypesSuccessorMap.set(dep.taskId, list);
969
- }
970
- }
971
- const directChildren = getChildren(changedTaskId, allTasks);
972
- const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
973
- const initialChain = [...directChildren, ...directSuccessors].filter(
974
- (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
975
- );
976
- const chain = [...initialChain];
977
- const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
978
- const queue = [...initialChain];
979
- while (queue.length > 0) {
980
- const current = queue.shift();
981
- const children = getChildren(current.id, allTasks);
982
- for (const child of children) {
983
- if (!visited.has(child.id)) {
984
- visited.add(child.id);
985
- chain.push(child);
986
- queue.push(child);
987
- }
988
- }
989
- const successors = allTypesSuccessorMap.get(current.id) ?? [];
990
- for (const successor of successors) {
991
- if (!visited.has(successor.id)) {
992
- visited.add(successor.id);
993
- chain.push(successor);
994
- queue.push(successor);
995
- }
996
- }
997
- }
998
- return chain;
999
- }
1000
- function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate) {
1001
- if (!task.dependencies) return [];
1002
- return task.dependencies.map((dep) => {
1003
- const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
1004
- if (!predecessor) {
1005
- return { ...dep, lag: getDependencyLag(dep) };
1006
- }
1007
- const predecessorStart = new Date(predecessor.startDate);
1008
- const predecessorEnd = new Date(predecessor.endDate);
1009
- const nextLag = computeLagFromDates(
1010
- dep.type,
1011
- predecessorStart,
1012
- predecessorEnd,
1013
- newStartDate,
1014
- newEndDate,
1015
- businessDays,
1016
- weekendPredicate
1017
- );
1018
- return { ...dep, lag: nextLag };
1019
- });
1020
- }
1021
- function getAllDependencyEdges(tasks) {
1022
- const edges = [];
1023
- for (const task of tasks) {
1024
- if (task.dependencies) {
1025
- for (const dep of task.dependencies) {
1026
- edges.push({
1027
- predecessorId: dep.taskId,
1028
- successorId: task.id,
1029
- type: dep.type,
1030
- lag: getDependencyLag(dep)
1031
- });
1032
- }
1033
- }
706
+ break;
1034
707
  }
1035
- return edges;
708
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
1036
709
  }
710
+
711
+ // src/core/scheduling/cascade.ts
712
+ init_dateMath();
713
+
714
+ // src/core/scheduling/hierarchy.ts
1037
715
  function getChildren(parentId, tasks) {
1038
716
  return tasks.filter((t) => t.parentId === parentId);
1039
717
  }
@@ -1078,6 +756,37 @@ function computeParentProgress(parentId, tasks) {
1078
756
  }
1079
757
  return Math.round(weightedSum / totalWeight * 10) / 10;
1080
758
  }
759
+ function getAllDescendants(parentId, tasks) {
760
+ const descendants = [];
761
+ const visited = /* @__PURE__ */ new Set();
762
+ function collectChildren(taskId) {
763
+ if (visited.has(taskId)) return;
764
+ visited.add(taskId);
765
+ const children = getChildren(taskId, tasks);
766
+ for (const child of children) {
767
+ descendants.push(child);
768
+ collectChildren(child.id);
769
+ }
770
+ }
771
+ collectChildren(parentId);
772
+ return descendants;
773
+ }
774
+ function getAllDependencyEdges(tasks) {
775
+ const edges = [];
776
+ for (const task of tasks) {
777
+ if (task.dependencies) {
778
+ for (const dep of task.dependencies) {
779
+ edges.push({
780
+ predecessorId: dep.taskId,
781
+ successorId: task.id,
782
+ type: dep.type,
783
+ lag: dep.lag ?? 0
784
+ });
785
+ }
786
+ }
787
+ }
788
+ return edges;
789
+ }
1081
790
  function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1082
791
  return tasks.map((task) => {
1083
792
  if (task.id === taskId1 || task.id === taskId2) {
@@ -1107,36 +816,324 @@ function isAncestorTask(ancestorId, taskId, tasks) {
1107
816
  if (current.parentId === ancestorId) {
1108
817
  return true;
1109
818
  }
1110
- if (visited.has(current.parentId)) {
1111
- return false;
819
+ if (visited.has(current.parentId)) {
820
+ return false;
821
+ }
822
+ visited.add(current.parentId);
823
+ current = taskById.get(current.parentId);
824
+ }
825
+ return false;
826
+ }
827
+ function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
828
+ if (taskId1 === taskId2) {
829
+ return true;
830
+ }
831
+ return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
832
+ }
833
+
834
+ // src/core/scheduling/commands.ts
835
+ init_dateMath();
836
+ function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
837
+ const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
838
+ if (businessDays && weekendPredicate) {
839
+ return {
840
+ start: normalizedStart,
841
+ end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
842
+ };
843
+ }
844
+ const DAY_MS3 = 24 * 60 * 60 * 1e3;
845
+ return {
846
+ start: normalizedStart,
847
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS3)
848
+ };
849
+ }
850
+ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
851
+ const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
852
+ if (businessDays && weekendPredicate) {
853
+ return {
854
+ start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
855
+ end: normalizedEnd
856
+ };
857
+ }
858
+ const DAY_MS3 = 24 * 60 * 60 * 1e3;
859
+ return {
860
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS3),
861
+ end: normalizedEnd
862
+ };
863
+ }
864
+ function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
865
+ return buildTaskRangeFromStart(
866
+ proposedStart,
867
+ getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
868
+ businessDays,
869
+ weekendPredicate,
870
+ snapDirection
871
+ );
872
+ }
873
+ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate) {
874
+ if (!task.dependencies?.length) {
875
+ return { start: proposedStart, end: proposedEnd };
876
+ }
877
+ let minAllowedStart = null;
878
+ for (const dep of task.dependencies) {
879
+ if (dep.type !== "FS") {
880
+ continue;
881
+ }
882
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
883
+ if (!predecessor) {
884
+ continue;
885
+ }
886
+ const predecessorStart = parseDateOnly(predecessor.startDate);
887
+ const predecessorEnd = parseDateOnly(predecessor.endDate);
888
+ const predecessorDuration = getTaskDuration(
889
+ predecessorStart,
890
+ predecessorEnd,
891
+ businessDays,
892
+ weekendPredicate
893
+ );
894
+ const candidateMinStart = calculateSuccessorDate(
895
+ predecessorStart,
896
+ predecessorEnd,
897
+ "FS",
898
+ -predecessorDuration,
899
+ businessDays,
900
+ weekendPredicate
901
+ );
902
+ if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
903
+ minAllowedStart = candidateMinStart;
904
+ }
905
+ }
906
+ if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
907
+ return { start: proposedStart, end: proposedEnd };
908
+ }
909
+ return buildTaskRangeFromStart(
910
+ minAllowedStart,
911
+ getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
912
+ businessDays,
913
+ weekendPredicate
914
+ );
915
+ }
916
+ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate) {
917
+ if (!task.dependencies) return [];
918
+ return task.dependencies.map((dep) => {
919
+ const predecessor = allTasks.find((candidate) => candidate.id === dep.taskId);
920
+ if (!predecessor) {
921
+ return { ...dep, lag: getDependencyLag(dep) };
922
+ }
923
+ const predecessorStart = new Date(predecessor.startDate);
924
+ const predecessorEnd = new Date(predecessor.endDate);
925
+ const nextLag = computeLagFromDates(
926
+ dep.type,
927
+ predecessorStart,
928
+ predecessorEnd,
929
+ newStartDate,
930
+ newEndDate,
931
+ businessDays,
932
+ weekendPredicate
933
+ );
934
+ return { ...dep, lag: nextLag };
935
+ });
936
+ }
937
+ function resolveDateRangeFromPixels(mode, left, width, monthStart, dayWidth, task, businessDays, weekendPredicate) {
938
+ const dayOffset = Math.round(left / dayWidth);
939
+ const rawStartDate = new Date(Date.UTC(
940
+ monthStart.getUTCFullYear(),
941
+ monthStart.getUTCMonth(),
942
+ monthStart.getUTCDate() + dayOffset
943
+ ));
944
+ const rawEndOffset = dayOffset + Math.round(width / dayWidth) - 1;
945
+ const rawEndDate = new Date(Date.UTC(
946
+ monthStart.getUTCFullYear(),
947
+ monthStart.getUTCMonth(),
948
+ monthStart.getUTCDate() + rawEndOffset
949
+ ));
950
+ if (!(businessDays && weekendPredicate)) {
951
+ return { start: rawStartDate, end: rawEndDate };
952
+ }
953
+ if (mode === "move") {
954
+ const originalStart2 = new Date(task.startDate);
955
+ const snapDirection2 = rawStartDate.getTime() >= originalStart2.getTime() ? 1 : -1;
956
+ return moveTaskRange(
957
+ task.startDate,
958
+ task.endDate,
959
+ rawStartDate,
960
+ true,
961
+ weekendPredicate,
962
+ snapDirection2
963
+ );
964
+ }
965
+ if (mode === "resize-right") {
966
+ const fixedStart = new Date(task.startDate);
967
+ const originalEnd = new Date(task.endDate);
968
+ const snapDirection2 = rawEndDate.getTime() >= originalEnd.getTime() ? 1 : -1;
969
+ const alignedEnd = alignToWorkingDay(rawEndDate, snapDirection2, weekendPredicate);
970
+ const duration2 = Math.max(1, getBusinessDaysCount(fixedStart, alignedEnd, weekendPredicate));
971
+ return buildTaskRangeFromStart(fixedStart, duration2, true, weekendPredicate);
972
+ }
973
+ const fixedEnd = new Date(task.endDate);
974
+ const originalStart = new Date(task.startDate);
975
+ const snapDirection = rawStartDate.getTime() >= originalStart.getTime() ? 1 : -1;
976
+ const alignedStart = alignToWorkingDay(rawStartDate, snapDirection, weekendPredicate);
977
+ const duration = Math.max(1, getBusinessDaysCount(alignedStart, fixedEnd, weekendPredicate));
978
+ return buildTaskRangeFromEnd(fixedEnd, duration, true, weekendPredicate);
979
+ }
980
+ function clampDateRangeForIncomingFS(task, range, allTasks, mode, businessDays, weekendPredicate) {
981
+ if (mode === "resize-right") {
982
+ return range;
983
+ }
984
+ return clampTaskRangeForIncomingFS(
985
+ task,
986
+ range.start,
987
+ range.end,
988
+ allTasks,
989
+ businessDays,
990
+ weekendPredicate
991
+ );
992
+ }
993
+
994
+ // src/core/scheduling/cascade.ts
995
+ function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
996
+ const successorMap = /* @__PURE__ */ new Map();
997
+ for (const task of allTasks) {
998
+ successorMap.set(task.id, []);
999
+ }
1000
+ for (const task of allTasks) {
1001
+ if (!task.dependencies) continue;
1002
+ for (const dep of task.dependencies) {
1003
+ if (linkTypes.includes(dep.type)) {
1004
+ const list = successorMap.get(dep.taskId) ?? [];
1005
+ list.push(task.id);
1006
+ successorMap.set(dep.taskId, list);
1007
+ }
1008
+ }
1009
+ }
1010
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
1011
+ const visited = /* @__PURE__ */ new Set();
1012
+ const queue = [draggedTaskId];
1013
+ const chain = [];
1014
+ visited.add(draggedTaskId);
1015
+ while (queue.length > 0) {
1016
+ const current = queue.shift();
1017
+ const successors = successorMap.get(current) ?? [];
1018
+ for (const sid of successors) {
1019
+ if (!visited.has(sid)) {
1020
+ visited.add(sid);
1021
+ const t = taskById.get(sid);
1022
+ if (t) {
1023
+ chain.push(t);
1024
+ queue.push(sid);
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ return chain;
1030
+ }
1031
+ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
1032
+ const taskById = new Map(allTasks.map((t) => [t.id, t]));
1033
+ const updatedDates = /* @__PURE__ */ new Map();
1034
+ updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
1035
+ const result = [];
1036
+ const queue = [movedTaskId];
1037
+ const visited = /* @__PURE__ */ new Set([movedTaskId]);
1038
+ while (queue.length > 0) {
1039
+ const currentId = queue.shift();
1040
+ const { start: predStart, end: predEnd } = updatedDates.get(currentId);
1041
+ if (!skipChildCascade) {
1042
+ const children = getChildren(currentId, allTasks);
1043
+ for (const child of children) {
1044
+ if (visited.has(child.id) || child.locked) continue;
1045
+ const origStart = new Date(child.startDate);
1046
+ const origEnd = new Date(child.endDate);
1047
+ const durationMs = origEnd.getTime() - origStart.getTime();
1048
+ const parentOrig = taskById.get(currentId);
1049
+ const parentOrigStart = new Date(parentOrig.startDate);
1050
+ const parentOrigEnd = new Date(parentOrig.endDate);
1051
+ const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
1052
+ const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
1053
+ const newChildStart = new Date(origStart.getTime() + parentStartDelta);
1054
+ const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
1055
+ visited.add(child.id);
1056
+ updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
1057
+ result.push({
1058
+ ...child,
1059
+ startDate: newChildStart.toISOString().split("T")[0],
1060
+ endDate: newChildEnd.toISOString().split("T")[0]
1061
+ });
1062
+ queue.push(child.id);
1063
+ }
1064
+ }
1065
+ for (const task of allTasks) {
1066
+ if (visited.has(task.id) || !task.dependencies || task.locked) continue;
1067
+ for (const dep of task.dependencies) {
1068
+ if (dep.taskId !== currentId) continue;
1069
+ const orig = taskById.get(task.id);
1070
+ const origStart = new Date(orig.startDate);
1071
+ const origEnd = new Date(orig.endDate);
1072
+ const duration = getTaskDuration(origStart, origEnd);
1073
+ const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
1074
+ let newSuccStart;
1075
+ let newSuccEnd;
1076
+ if (dep.type === "FS" || dep.type === "SS") {
1077
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
1078
+ } else {
1079
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
1080
+ }
1081
+ visited.add(task.id);
1082
+ updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
1083
+ result.push({
1084
+ ...task,
1085
+ startDate: newSuccStart.toISOString().split("T")[0],
1086
+ endDate: newSuccEnd.toISOString().split("T")[0]
1087
+ });
1088
+ queue.push(task.id);
1089
+ break;
1090
+ }
1112
1091
  }
1113
- visited.add(current.parentId);
1114
- current = taskById.get(current.parentId);
1115
1092
  }
1116
- return false;
1093
+ return result;
1117
1094
  }
1118
- function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1119
- if (taskId1 === taskId2) {
1120
- return true;
1095
+ function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
1096
+ const allTypesSuccessorMap = /* @__PURE__ */ new Map();
1097
+ for (const task of allTasks) {
1098
+ allTypesSuccessorMap.set(task.id, []);
1121
1099
  }
1122
- return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1123
- }
1124
- function getAllDescendants(parentId, tasks) {
1125
- const descendants = [];
1126
- const visited = /* @__PURE__ */ new Set();
1127
- function collectChildren(taskId) {
1128
- if (visited.has(taskId)) return;
1129
- visited.add(taskId);
1130
- const children = getChildren(taskId, tasks);
1100
+ for (const task of allTasks) {
1101
+ if (!task.dependencies) continue;
1102
+ for (const dep of task.dependencies) {
1103
+ const list = allTypesSuccessorMap.get(dep.taskId) ?? [];
1104
+ list.push(task);
1105
+ allTypesSuccessorMap.set(dep.taskId, list);
1106
+ }
1107
+ }
1108
+ const directChildren = getChildren(changedTaskId, allTasks);
1109
+ const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
1110
+ const initialChain = [...directChildren, ...directSuccessors].filter(
1111
+ (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
1112
+ );
1113
+ const chain = [...initialChain];
1114
+ const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
1115
+ const queue = [...initialChain];
1116
+ while (queue.length > 0) {
1117
+ const current = queue.shift();
1118
+ const children = getChildren(current.id, allTasks);
1131
1119
  for (const child of children) {
1132
- descendants.push(child);
1133
- collectChildren(child.id);
1120
+ if (!visited.has(child.id)) {
1121
+ visited.add(child.id);
1122
+ chain.push(child);
1123
+ queue.push(child);
1124
+ }
1125
+ }
1126
+ const successors = allTypesSuccessorMap.get(current.id) ?? [];
1127
+ for (const successor of successors) {
1128
+ if (!visited.has(successor.id)) {
1129
+ visited.add(successor.id);
1130
+ chain.push(successor);
1131
+ queue.push(successor);
1132
+ }
1134
1133
  }
1135
1134
  }
1136
- collectChildren(parentId);
1137
- return descendants;
1135
+ return chain;
1138
1136
  }
1139
- var DAY_MS = 24 * 60 * 60 * 1e3;
1140
1137
  function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate) {
1141
1138
  const taskById = new Map(allTasks.map((t) => [t.id, t]));
1142
1139
  const updatedDates = /* @__PURE__ */ new Map();
@@ -1309,6 +1306,105 @@ function reflowTasksOnModeSwitch(sourceTasks, toBusinessDays, weekendPredicate)
1309
1306
  return tasks;
1310
1307
  }
1311
1308
 
1309
+ // src/core/scheduling/validation.ts
1310
+ function buildAdjacencyList(tasks) {
1311
+ const graph = /* @__PURE__ */ new Map();
1312
+ for (const task of tasks) {
1313
+ const successors = [];
1314
+ for (const otherTask of tasks) {
1315
+ if (otherTask.dependencies) {
1316
+ for (const dep of otherTask.dependencies) {
1317
+ if (dep.taskId === task.id) {
1318
+ successors.push(otherTask.id);
1319
+ break;
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+ graph.set(task.id, successors);
1325
+ }
1326
+ return graph;
1327
+ }
1328
+ function detectCycles(tasks) {
1329
+ const graph = buildAdjacencyList(tasks);
1330
+ const visiting = /* @__PURE__ */ new Set();
1331
+ const visited = /* @__PURE__ */ new Set();
1332
+ const path = [];
1333
+ function dfs(taskId) {
1334
+ if (visiting.has(taskId)) {
1335
+ return true;
1336
+ }
1337
+ if (visited.has(taskId)) {
1338
+ return false;
1339
+ }
1340
+ visiting.add(taskId);
1341
+ path.push(taskId);
1342
+ const successors = graph.get(taskId) || [];
1343
+ for (const successor of successors) {
1344
+ if (dfs(successor)) {
1345
+ return true;
1346
+ }
1347
+ }
1348
+ visiting.delete(taskId);
1349
+ path.pop();
1350
+ visited.add(taskId);
1351
+ return false;
1352
+ }
1353
+ for (const task of tasks) {
1354
+ if (dfs(task.id)) {
1355
+ return { hasCycle: true, cyclePath: [...path] };
1356
+ }
1357
+ }
1358
+ return { hasCycle: false };
1359
+ }
1360
+ function validateDependencies(tasks) {
1361
+ const errors = [];
1362
+ const taskIds = new Set(tasks.map((t) => t.id));
1363
+ for (const task of tasks) {
1364
+ if (task.dependencies) {
1365
+ for (const dep of task.dependencies) {
1366
+ if (!taskIds.has(dep.taskId)) {
1367
+ errors.push({
1368
+ type: "missing-task",
1369
+ taskId: task.id,
1370
+ message: `Dependency references non-existent task: ${dep.taskId}`,
1371
+ relatedTaskIds: [dep.taskId]
1372
+ });
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ for (const task of tasks) {
1378
+ if (!task.dependencies) continue;
1379
+ for (const dep of task.dependencies) {
1380
+ if (!taskIds.has(dep.taskId)) {
1381
+ continue;
1382
+ }
1383
+ if (areTasksHierarchicallyRelated(task.id, dep.taskId, tasks)) {
1384
+ errors.push({
1385
+ type: "constraint",
1386
+ taskId: task.id,
1387
+ message: `Dependencies between parent and child tasks are not allowed: ${dep.taskId} -> ${task.id}`,
1388
+ relatedTaskIds: [dep.taskId, task.id]
1389
+ });
1390
+ }
1391
+ }
1392
+ }
1393
+ const cycleResult = detectCycles(tasks);
1394
+ if (cycleResult.hasCycle && cycleResult.cyclePath) {
1395
+ errors.push({
1396
+ type: "cycle",
1397
+ taskId: cycleResult.cyclePath[0],
1398
+ message: "Circular dependency detected",
1399
+ relatedTaskIds: cycleResult.cyclePath
1400
+ });
1401
+ }
1402
+ return {
1403
+ isValid: errors.length === 0,
1404
+ errors
1405
+ };
1406
+ }
1407
+
1312
1408
  // src/utils/hierarchyOrder.ts
1313
1409
  init_dateUtils();
1314
1410
  function flattenHierarchy(tasks) {
@@ -1790,7 +1886,6 @@ var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
1790
1886
 
1791
1887
  // src/hooks/useTaskDrag.ts
1792
1888
  var import_react2 = require("react");
1793
- init_dateUtils();
1794
1889
  var globalActiveDrag = null;
1795
1890
  var globalRafId = null;
1796
1891
  function getDayOffsetFromMonthStart(date, monthStart) {
@@ -1798,62 +1893,6 @@ function getDayOffsetFromMonthStart(date, monthStart) {
1798
1893
  (Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()) - Date.UTC(monthStart.getUTCFullYear(), monthStart.getUTCMonth(), monthStart.getUTCDate())) / (24 * 60 * 60 * 1e3)
1799
1894
  );
1800
1895
  }
1801
- function resolveDraggedRange(mode, left, width, monthStart, dayWidth, task, businessDays, weekendPredicate) {
1802
- const dayOffset = Math.round(left / dayWidth);
1803
- const rawStartDate = new Date(Date.UTC(
1804
- monthStart.getUTCFullYear(),
1805
- monthStart.getUTCMonth(),
1806
- monthStart.getUTCDate() + dayOffset
1807
- ));
1808
- const rawEndOffset = dayOffset + Math.round(width / dayWidth) - 1;
1809
- const rawEndDate = new Date(Date.UTC(
1810
- monthStart.getUTCFullYear(),
1811
- monthStart.getUTCMonth(),
1812
- monthStart.getUTCDate() + rawEndOffset
1813
- ));
1814
- if (!(businessDays && weekendPredicate)) {
1815
- return { start: rawStartDate, end: rawEndDate };
1816
- }
1817
- if (mode === "move") {
1818
- const originalStart2 = new Date(task.startDate);
1819
- const snapDirection2 = rawStartDate.getTime() >= originalStart2.getTime() ? 1 : -1;
1820
- return moveTaskRange(
1821
- task.startDate,
1822
- task.endDate,
1823
- rawStartDate,
1824
- true,
1825
- weekendPredicate,
1826
- snapDirection2
1827
- );
1828
- }
1829
- if (mode === "resize-right") {
1830
- const fixedStart = new Date(task.startDate);
1831
- const originalEnd = new Date(task.endDate);
1832
- const snapDirection2 = rawEndDate.getTime() >= originalEnd.getTime() ? 1 : -1;
1833
- const alignedEnd = alignToWorkingDay(rawEndDate, snapDirection2, weekendPredicate);
1834
- const duration2 = Math.max(1, getBusinessDaysCount(fixedStart, alignedEnd, weekendPredicate));
1835
- return buildTaskRangeFromStart(fixedStart, duration2, true, weekendPredicate);
1836
- }
1837
- const fixedEnd = new Date(task.endDate);
1838
- const originalStart = new Date(task.startDate);
1839
- const snapDirection = rawStartDate.getTime() >= originalStart.getTime() ? 1 : -1;
1840
- const alignedStart = alignToWorkingDay(rawStartDate, snapDirection, weekendPredicate);
1841
- const duration = Math.max(1, getBusinessDaysCount(alignedStart, fixedEnd, weekendPredicate));
1842
- return buildTaskRangeFromEnd(fixedEnd, duration, true, weekendPredicate);
1843
- }
1844
- function clampDraggedRangeForIncomingFS(task, range, allTasks, mode, businessDays, weekendPredicate) {
1845
- if (mode === "resize-right") {
1846
- return range;
1847
- }
1848
- return clampTaskRangeForIncomingFS(
1849
- task,
1850
- range.start,
1851
- range.end,
1852
- allTasks,
1853
- businessDays,
1854
- weekendPredicate
1855
- );
1856
- }
1857
1896
  function completeDrag() {
1858
1897
  if (globalRafId !== null) {
1859
1898
  cancelAnimationFrame(globalRafId);
@@ -1911,9 +1950,9 @@ function handleGlobalMouseMove(e) {
1911
1950
  }
1912
1951
  const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
1913
1952
  if (activeDrag.businessDays && activeDrag.weekendPredicate && draggedTask) {
1914
- const previewRange = clampDraggedRangeForIncomingFS(
1953
+ const previewRange = clampDateRangeForIncomingFS(
1915
1954
  draggedTask,
1916
- resolveDraggedRange(
1955
+ resolveDateRangeFromPixels(
1917
1956
  mode,
1918
1957
  newLeft,
1919
1958
  newWidth,
@@ -1933,9 +1972,9 @@ function handleGlobalMouseMove(e) {
1933
1972
  newLeft = Math.round(alignedStartDay * dayWidth);
1934
1973
  newWidth = Math.round((alignedEndDay - alignedStartDay + 1) * dayWidth);
1935
1974
  } else if (draggedTask) {
1936
- const previewRange = clampDraggedRangeForIncomingFS(
1975
+ const previewRange = clampDateRangeForIncomingFS(
1937
1976
  draggedTask,
1938
- resolveDraggedRange(
1977
+ resolveDateRangeFromPixels(
1939
1978
  mode,
1940
1979
  newLeft,
1941
1980
  newWidth,
@@ -1954,9 +1993,9 @@ function handleGlobalMouseMove(e) {
1954
1993
  if (!activeDrag.disableConstraints && activeDrag.onCascadeProgress) {
1955
1994
  const { dayWidth: dayWidth2, monthStart: mStart, taskId: dragId } = activeDrag;
1956
1995
  const originalDraggedTask = draggedTask ?? allTasks.find((t) => t.id === dragId);
1957
- const previewRange = originalDraggedTask ? clampDraggedRangeForIncomingFS(
1996
+ const previewRange = originalDraggedTask ? clampDateRangeForIncomingFS(
1958
1997
  originalDraggedTask,
1959
- resolveDraggedRange(
1998
+ resolveDateRangeFromPixels(
1960
1999
  mode,
1961
2000
  newLeft,
1962
2001
  newWidth,
@@ -2138,9 +2177,9 @@ var useTaskDrag = (options) => {
2138
2177
  const wasOwner = isOwnerRef.current;
2139
2178
  isOwnerRef.current = false;
2140
2179
  const currentTask = allTasks.find((t) => t.id === taskId);
2141
- const finalRange = currentTask ? clampDraggedRangeForIncomingFS(
2180
+ const finalRange = currentTask ? clampDateRangeForIncomingFS(
2142
2181
  currentTask,
2143
- resolveDraggedRange(
2182
+ resolveDateRangeFromPixels(
2144
2183
  finalMode,
2145
2184
  finalLeft,
2146
2185
  finalWidth,
@@ -3883,7 +3922,7 @@ var DepChip = ({
3883
3922
  newStart = constraintDate;
3884
3923
  if (businessDays) {
3885
3924
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3886
- newEnd = /* @__PURE__ */ new Date(`${addBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3925
+ newEnd = addBusinessDays(constraintDate, businessDuration, weekendPredicate);
3887
3926
  } else {
3888
3927
  newEnd = new Date(constraintDate.getTime() + durationMs);
3889
3928
  }
@@ -3891,7 +3930,7 @@ var DepChip = ({
3891
3930
  newEnd = constraintDate;
3892
3931
  if (businessDays) {
3893
3932
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3894
- newStart = /* @__PURE__ */ new Date(`${subtractBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3933
+ newStart = subtractBusinessDays(constraintDate, businessDuration, weekendPredicate);
3895
3934
  } else {
3896
3935
  newStart = new Date(constraintDate.getTime() - durationMs);
3897
3936
  }
@@ -4159,7 +4198,7 @@ var TaskListRow = import_react10.default.memo(
4159
4198
  );
4160
4199
  const getEndDate = (0, import_react10.useCallback)(
4161
4200
  (start, duration) => {
4162
- return businessDays ? addBusinessDays(start, duration, weekendPredicate) : getEndDateFromDuration(start, duration);
4201
+ return businessDays ? addBusinessDays(start, duration, weekendPredicate).toISOString().split("T")[0] : getEndDateFromDuration(start, duration);
4163
4202
  },
4164
4203
  [businessDays, weekendPredicate]
4165
4204
  );
@@ -7126,6 +7165,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7126
7165
  0 && (module.exports = {
7127
7166
  Button,
7128
7167
  Calendar,
7168
+ DAY_MS,
7129
7169
  DatePicker,
7130
7170
  DragGuideLines,
7131
7171
  GanttChart,
@@ -7171,6 +7211,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7171
7211
  formatDateRangeLabel,
7172
7212
  getAllDependencyEdges,
7173
7213
  getAllDescendants,
7214
+ getBusinessDayOffset,
7174
7215
  getBusinessDaysCount,
7175
7216
  getChildren,
7176
7217
  getCursorForPosition,
@@ -7198,14 +7239,17 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7198
7239
  normalizeDependencyLag,
7199
7240
  normalizeHierarchyTasks,
7200
7241
  normalizeTaskDates,
7242
+ normalizeUTCDate,
7201
7243
  not,
7202
7244
  or,
7245
+ parseDateOnly,
7203
7246
  parseUTCDate,
7204
7247
  pixelsToDate,
7205
7248
  progressInRange,
7206
7249
  recalculateIncomingLags,
7207
7250
  reflowTasksOnModeSwitch,
7208
7251
  removeDependenciesBetweenTasks,
7252
+ shiftBusinessDayOffset,
7209
7253
  subtractBusinessDays,
7210
7254
  universalCascade,
7211
7255
  useTaskDrag,