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