gantt-lib 0.53.0 → 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,71 +623,268 @@ 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;
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);
640
690
  }
641
- const predecessorStart = parseDateOnly(predecessor.startDate);
642
- const predecessorEnd = parseDateOnly(predecessor.endDate);
691
+ }
692
+ const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
693
+ let offset;
694
+ switch (linkType) {
695
+ case "FS":
696
+ offset = normalizedLag + 1;
697
+ break;
698
+ case "SS":
699
+ offset = normalizedLag;
700
+ break;
701
+ case "FF":
702
+ offset = normalizedLag;
703
+ break;
704
+ case "SF":
705
+ offset = normalizedLag - 1;
706
+ break;
707
+ }
708
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
709
+ }
710
+
711
+ // src/core/scheduling/cascade.ts
712
+ init_dateMath();
713
+
714
+ // src/core/scheduling/hierarchy.ts
715
+ function getChildren(parentId, tasks) {
716
+ return tasks.filter((t) => t.parentId === parentId);
717
+ }
718
+ function isTaskParent(taskId, tasks) {
719
+ return tasks.some((t) => t.parentId === taskId);
720
+ }
721
+ function computeParentDates(parentId, tasks) {
722
+ const children = getChildren(parentId, tasks);
723
+ if (children.length === 0) {
724
+ const parent = tasks.find((t) => t.id === parentId);
725
+ const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
726
+ const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
727
+ return { startDate: start, endDate: end };
728
+ }
729
+ const startDates = children.map((c) => new Date(c.startDate));
730
+ const endDates = children.map((c) => new Date(c.endDate));
731
+ const minTime = Math.min(...startDates.map((d) => d.getTime()));
732
+ const maxTime = Math.max(...endDates.map((d) => d.getTime()));
733
+ return {
734
+ startDate: new Date(minTime),
735
+ endDate: new Date(maxTime)
736
+ };
737
+ }
738
+ function computeParentProgress(parentId, tasks) {
739
+ const children = getChildren(parentId, tasks);
740
+ if (children.length === 0) {
741
+ return 0;
742
+ }
743
+ const DAY_MS3 = 24 * 60 * 60 * 1e3;
744
+ let totalWeight = 0;
745
+ let weightedSum = 0;
746
+ for (const child of children) {
747
+ const start = new Date(child.startDate).getTime();
748
+ const end = new Date(child.endDate).getTime();
749
+ const duration = (end - start + DAY_MS3) / DAY_MS3;
750
+ const progress = child.progress ?? 0;
751
+ totalWeight += duration;
752
+ weightedSum += duration * progress;
753
+ }
754
+ if (totalWeight === 0) {
755
+ return 0;
756
+ }
757
+ return Math.round(weightedSum / totalWeight * 10) / 10;
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
+ }
790
+ function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
791
+ return tasks.map((task) => {
792
+ if (task.id === taskId1 || task.id === taskId2) {
793
+ if (!task.dependencies) return task;
794
+ const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
795
+ const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
796
+ if (filteredDependencies.length === task.dependencies.length) {
797
+ return task;
798
+ }
799
+ return {
800
+ ...task,
801
+ dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
802
+ };
803
+ }
804
+ return task;
805
+ });
806
+ }
807
+ function findParentId(taskId, tasks) {
808
+ const task = tasks.find((t) => t.id === taskId);
809
+ return task?.parentId;
810
+ }
811
+ function isAncestorTask(ancestorId, taskId, tasks) {
812
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
813
+ const visited = /* @__PURE__ */ new Set();
814
+ let current = taskById.get(taskId);
815
+ while (current?.parentId) {
816
+ if (current.parentId === ancestorId) {
817
+ return true;
818
+ }
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);
643
888
  const predecessorDuration = getTaskDuration(
644
889
  predecessorStart,
645
890
  predecessorEnd,
@@ -668,193 +913,85 @@ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks,
668
913
  weekendPredicate
669
914
  );
670
915
  }
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);
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) };
791
922
  }
792
- }
793
- const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
794
- let offset;
795
- switch (linkType) {
796
- case "FS":
797
- offset = normalizedLag + 1;
798
- break;
799
- case "SS":
800
- offset = normalizedLag;
801
- break;
802
- case "FF":
803
- offset = normalizedLag;
804
- break;
805
- case "SF":
806
- offset = normalizedLag - 1;
807
- break;
808
- }
809
- return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
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
+ });
810
936
  }
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
- }
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 };
827
952
  }
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
- }
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
+ );
843
964
  }
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
- });
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);
852
972
  }
853
- return {
854
- isValid: errors.length === 0,
855
- errors
856
- };
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
+ );
857
992
  }
993
+
994
+ // src/core/scheduling/cascade.ts
858
995
  function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
859
996
  const successorMap = /* @__PURE__ */ new Map();
860
997
  for (const task of allTasks) {
@@ -918,225 +1055,85 @@ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascad
918
1055
  visited.add(child.id);
919
1056
  updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
920
1057
  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
- }
1034
- }
1035
- return edges;
1036
- }
1037
- function getChildren(parentId, tasks) {
1038
- return tasks.filter((t) => t.parentId === parentId);
1039
- }
1040
- function isTaskParent(taskId, tasks) {
1041
- return tasks.some((t) => t.parentId === taskId);
1042
- }
1043
- function computeParentDates(parentId, tasks) {
1044
- const children = getChildren(parentId, tasks);
1045
- if (children.length === 0) {
1046
- const parent = tasks.find((t) => t.id === parentId);
1047
- const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
1048
- const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
1049
- return { startDate: start, endDate: end };
1050
- }
1051
- const startDates = children.map((c) => new Date(c.startDate));
1052
- const endDates = children.map((c) => new Date(c.endDate));
1053
- const minTime = Math.min(...startDates.map((d) => d.getTime()));
1054
- const maxTime = Math.max(...endDates.map((d) => d.getTime()));
1055
- return {
1056
- startDate: new Date(minTime),
1057
- endDate: new Date(maxTime)
1058
- };
1059
- }
1060
- function computeParentProgress(parentId, tasks) {
1061
- const children = getChildren(parentId, tasks);
1062
- if (children.length === 0) {
1063
- return 0;
1064
- }
1065
- const DAY_MS3 = 24 * 60 * 60 * 1e3;
1066
- let totalWeight = 0;
1067
- let weightedSum = 0;
1068
- for (const child of children) {
1069
- const start = new Date(child.startDate).getTime();
1070
- const end = new Date(child.endDate).getTime();
1071
- const duration = (end - start + DAY_MS3) / DAY_MS3;
1072
- const progress = child.progress ?? 0;
1073
- totalWeight += duration;
1074
- weightedSum += duration * progress;
1075
- }
1076
- if (totalWeight === 0) {
1077
- return 0;
1078
- }
1079
- return Math.round(weightedSum / totalWeight * 10) / 10;
1080
- }
1081
- function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1082
- return tasks.map((task) => {
1083
- if (task.id === taskId1 || task.id === taskId2) {
1084
- if (!task.dependencies) return task;
1085
- const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
1086
- const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
1087
- if (filteredDependencies.length === task.dependencies.length) {
1088
- return task;
1089
- }
1090
- return {
1091
- ...task,
1092
- dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
1093
- };
1094
- }
1095
- return task;
1096
- });
1097
- }
1098
- function findParentId(taskId, tasks) {
1099
- const task = tasks.find((t) => t.id === taskId);
1100
- return task?.parentId;
1101
- }
1102
- function isAncestorTask(ancestorId, taskId, tasks) {
1103
- const taskById = new Map(tasks.map((task) => [task.id, task]));
1104
- const visited = /* @__PURE__ */ new Set();
1105
- let current = taskById.get(taskId);
1106
- while (current?.parentId) {
1107
- if (current.parentId === ancestorId) {
1108
- return true;
1058
+ ...child,
1059
+ startDate: newChildStart.toISOString().split("T")[0],
1060
+ endDate: newChildEnd.toISOString().split("T")[0]
1061
+ });
1062
+ queue.push(child.id);
1063
+ }
1109
1064
  }
1110
- if (visited.has(current.parentId)) {
1111
- return false;
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,
@@ -3375,6 +3414,20 @@ var DatePicker = ({
3375
3414
  },
3376
3415
  [selectedDate, updateFromDate, businessDays, isWeekend3]
3377
3416
  );
3417
+ const handleTriggerKeyDown = (0, import_react9.useCallback)((e) => {
3418
+ if (disabled) return;
3419
+ if (e.key === "ArrowUp") {
3420
+ e.preventDefault();
3421
+ e.stopPropagation();
3422
+ handleDayShift(1);
3423
+ return;
3424
+ }
3425
+ if (e.key === "ArrowDown") {
3426
+ e.preventDefault();
3427
+ e.stopPropagation();
3428
+ handleDayShift(-1);
3429
+ }
3430
+ }, [disabled, handleDayShift]);
3378
3431
  const handleKeyDown = (0, import_react9.useCallback)((e) => {
3379
3432
  if (!dateInputRef.current) return;
3380
3433
  const { value: inputVal } = dateInputRef.current;
@@ -3482,6 +3535,7 @@ var DatePicker = ({
3482
3535
  type: "button",
3483
3536
  className: `gantt-datepicker-trigger${className ? ` ${className}` : ""}`,
3484
3537
  disabled,
3538
+ onKeyDown: handleTriggerKeyDown,
3485
3539
  onClick: (e) => {
3486
3540
  e.stopPropagation();
3487
3541
  },
@@ -3868,7 +3922,7 @@ var DepChip = ({
3868
3922
  newStart = constraintDate;
3869
3923
  if (businessDays) {
3870
3924
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3871
- newEnd = /* @__PURE__ */ new Date(`${addBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3925
+ newEnd = addBusinessDays(constraintDate, businessDuration, weekendPredicate);
3872
3926
  } else {
3873
3927
  newEnd = new Date(constraintDate.getTime() + durationMs);
3874
3928
  }
@@ -3876,7 +3930,7 @@ var DepChip = ({
3876
3930
  newEnd = constraintDate;
3877
3931
  if (businessDays) {
3878
3932
  const businessDuration = getBusinessDaysCount(origStart, origEnd, weekendPredicate);
3879
- newStart = /* @__PURE__ */ new Date(`${subtractBusinessDays(constraintDate, businessDuration, weekendPredicate)}T00:00:00.000Z`);
3933
+ newStart = subtractBusinessDays(constraintDate, businessDuration, weekendPredicate);
3880
3934
  } else {
3881
3935
  newStart = new Date(constraintDate.getTime() - durationMs);
3882
3936
  }
@@ -4144,7 +4198,7 @@ var TaskListRow = import_react10.default.memo(
4144
4198
  );
4145
4199
  const getEndDate = (0, import_react10.useCallback)(
4146
4200
  (start, duration) => {
4147
- return businessDays ? addBusinessDays(start, duration, weekendPredicate) : getEndDateFromDuration(start, duration);
4201
+ return businessDays ? addBusinessDays(start, duration, weekendPredicate).toISOString().split("T")[0] : getEndDateFromDuration(start, duration);
4148
4202
  },
4149
4203
  [businessDays, weekendPredicate]
4150
4204
  );
@@ -6683,20 +6737,8 @@ function GanttChartInner(props, ref) {
6683
6737
  }
6684
6738
  return;
6685
6739
  }
6686
- const isParent = isTaskParent(updatedTask.id, tasks);
6687
- if (isParent) {
6688
- const { startDate: parentStart, endDate: parentEnd } = computeParentDates(updatedTask.id, tasks);
6689
- const parentWithRecalcDates = {
6690
- ...updatedTask,
6691
- startDate: parentStart.toISOString().split("T")[0],
6692
- endDate: parentEnd.toISOString().split("T")[0]
6693
- };
6694
- const cascadedTasks = disableConstraints ? [parentWithRecalcDates] : universalCascade(parentWithRecalcDates, parentStart, parentEnd, tasks, businessDays, isCustomWeekend);
6695
- onTasksChange?.(cascadedTasks);
6696
- } else {
6697
- const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, tasks, businessDays, isCustomWeekend);
6698
- onTasksChange?.(cascadedTasks);
6699
- }
6740
+ const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, tasks, businessDays, isCustomWeekend);
6741
+ onTasksChange?.(cascadedTasks);
6700
6742
  }, [tasks, onTasksChange, disableConstraints, editingTaskId, businessDays, isCustomWeekend]);
6701
6743
  const handleDelete = (0, import_react13.useCallback)((taskId) => {
6702
6744
  const toDelete = /* @__PURE__ */ new Set([taskId]);
@@ -7123,6 +7165,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7123
7165
  0 && (module.exports = {
7124
7166
  Button,
7125
7167
  Calendar,
7168
+ DAY_MS,
7126
7169
  DatePicker,
7127
7170
  DragGuideLines,
7128
7171
  GanttChart,
@@ -7168,6 +7211,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7168
7211
  formatDateRangeLabel,
7169
7212
  getAllDependencyEdges,
7170
7213
  getAllDescendants,
7214
+ getBusinessDayOffset,
7171
7215
  getBusinessDaysCount,
7172
7216
  getChildren,
7173
7217
  getCursorForPosition,
@@ -7195,14 +7239,17 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7195
7239
  normalizeDependencyLag,
7196
7240
  normalizeHierarchyTasks,
7197
7241
  normalizeTaskDates,
7242
+ normalizeUTCDate,
7198
7243
  not,
7199
7244
  or,
7245
+ parseDateOnly,
7200
7246
  parseUTCDate,
7201
7247
  pixelsToDate,
7202
7248
  progressInRange,
7203
7249
  recalculateIncomingLags,
7204
7250
  reflowTasksOnModeSwitch,
7205
7251
  removeDependenciesBetweenTasks,
7252
+ shiftBusinessDayOffset,
7206
7253
  subtractBusinessDays,
7207
7254
  universalCascade,
7208
7255
  useTaskDrag,