gantt-lib 0.136.1 → 0.138.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,6 +33,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/index.ts
34
34
  var index_exports = {};
35
35
  __export(index_exports, {
36
+ ACTIVITY_LANES_PADDING: () => ACTIVITY_LANES_PADDING,
37
+ ACTIVITY_LANE_BAR_HEIGHT: () => ACTIVITY_LANE_BAR_HEIGHT,
38
+ ACTIVITY_LANE_STEP: () => ACTIVITY_LANE_STEP,
36
39
  Button: () => Button,
37
40
  Calendar: () => Calendar,
38
41
  DAY_MS: () => DAY_MS,
@@ -54,6 +57,11 @@ __export(index_exports, {
54
57
  TaskRow: () => TaskRow_default,
55
58
  TimeScaleHeader: () => TimeScaleHeader_default,
56
59
  TodayIndicator: () => TodayIndicator_default,
60
+ activityBounds: () => activityBounds,
61
+ activityChainMinStartDelta: () => activityChainMinStartDelta,
62
+ activityLanesExtraHeight: () => activityLanesExtraHeight,
63
+ activitySpanMs: () => activitySpanMs,
64
+ activityStartConstraint: () => activityStartConstraint,
57
65
  addBusinessDays: () => addBusinessDays2,
58
66
  alignToWorkingDay: () => alignToWorkingDay,
59
67
  and: () => and,
@@ -76,6 +84,7 @@ __export(index_exports, {
76
84
  cascadeByLinks: () => cascadeByLinks,
77
85
  clampDateRangeForIncomingFS: () => clampDateRangeForIncomingFS,
78
86
  clampTaskRangeForIncomingFS: () => clampTaskRangeForIncomingFS,
87
+ computeActivityLanes: () => computeActivityLanes,
79
88
  computeCriticalPath: () => computeCriticalPath,
80
89
  computeLagFromDates: () => computeLagFromDates,
81
90
  computeParentDates: () => computeParentDates,
@@ -130,10 +139,12 @@ __export(index_exports, {
130
139
  normalizeUTCDate: () => normalizeUTCDate,
131
140
  not: () => not,
132
141
  or: () => or,
142
+ packIntervals: () => packIntervals,
133
143
  parseDateOnly: () => parseDateOnly,
134
144
  parseUTCDate: () => parseUTCDate,
135
145
  pixelsToDate: () => pixelsToDate,
136
146
  progressInRange: () => progressInRange,
147
+ pushActivityChain: () => pushActivityChain,
137
148
  recalculateIncomingLags: () => recalculateIncomingLags,
138
149
  recalculateProjectSchedule: () => recalculateProjectSchedule,
139
150
  recalculateTaskFromDependencies: () => recalculateTaskFromDependencies,
@@ -143,6 +154,8 @@ __export(index_exports, {
143
154
  resolveDateRangeFromPixels: () => resolveDateRangeFromPixels,
144
155
  resolveTaskHorizontalGeometry: () => resolveTaskHorizontalGeometry,
145
156
  scaleTaskSubtreeDuration: () => scaleTaskSubtreeDuration,
157
+ shiftActivityChain: () => shiftActivityChain,
158
+ shiftActivityDate: () => shiftActivityDate,
146
159
  shiftBusinessDayOffset: () => shiftBusinessDayOffset,
147
160
  subtractBusinessDays: () => subtractBusinessDays2,
148
161
  universalCascade: () => universalCascade,
@@ -611,949 +624,1487 @@ function subtractBusinessDays2(endDate, businessDays, weekendPredicate) {
611
624
  return result.toISOString().split("T")[0];
612
625
  }
613
626
 
614
- // src/core/scheduling/dependencies.ts
615
- function normalizePredecessorDates(predecessor, parseDateFn) {
616
- const predStart = parseDateFn(predecessor.startDate);
617
- const predEnd = predecessor.type === "milestone" ? predStart : parseDateFn(predecessor.endDate);
618
- return { predStart, predEnd };
619
- }
620
- function getDependencyLag(dep) {
621
- return Number.isFinite(dep.lag) ? dep.lag : 0;
622
- }
623
- function normalizeTaskDependencyLags(task) {
624
- return task;
625
- }
626
- function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd, businessDays = false, weekendPredicate, successorType) {
627
- if (linkType !== "FS") {
628
- return lag;
629
- }
630
- const predecessorDuration = getTaskDuration(
631
- predecessorStart,
632
- predecessorEnd,
633
- businessDays,
634
- weekendPredicate
627
+ // src/utils/geometry.ts
628
+ var getUTCDayDifference = (date1, date2) => {
629
+ const ms1 = Date.UTC(
630
+ date1.getUTCFullYear(),
631
+ date1.getUTCMonth(),
632
+ date1.getUTCDate()
635
633
  );
636
- const minimumLag = successorType === "milestone" ? -Math.max(0, predecessorDuration - 1) : -predecessorDuration;
637
- return Math.max(minimumLag, lag);
638
- }
639
- function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate, successorType) {
640
- const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
641
- const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
642
- const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
643
- const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
644
- if (!businessDays || !weekendPredicate) {
645
- switch (linkType) {
646
- case "FS":
647
- return normalizeDependencyLag(
648
- linkType,
649
- Math.round((sS - pE) / DAY_MS) - (successorType === "milestone" ? 0 : 1),
650
- predStart,
651
- predEnd,
652
- businessDays,
653
- weekendPredicate,
654
- successorType
655
- );
656
- case "SS":
657
- return Math.round((sS - pS) / DAY_MS);
658
- case "FF":
659
- return Math.round((sE - pE) / DAY_MS);
660
- case "SF":
661
- return Math.round((sE - pS) / DAY_MS) + 1;
634
+ const ms2 = Date.UTC(
635
+ date2.getUTCFullYear(),
636
+ date2.getUTCMonth(),
637
+ date2.getUTCDate()
638
+ );
639
+ return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
640
+ };
641
+ var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
642
+ const startOffset = getUTCDayDifference(taskStartDate, monthStart);
643
+ const duration = getUTCDayDifference(taskEndDate, taskStartDate);
644
+ const left = Math.round(startOffset * dayWidth);
645
+ const width = Math.round((duration + 1) * dayWidth);
646
+ return { left, width };
647
+ };
648
+ var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
649
+ const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
650
+ const centerX = Math.round(left + width / 2);
651
+ const halfSize = Math.round(size / 2);
652
+ return {
653
+ centerX,
654
+ left: centerX - halfSize,
655
+ right: centerX + halfSize,
656
+ size
657
+ };
658
+ };
659
+ var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
660
+ const halfDiagonal = Math.round(size / Math.SQRT2);
661
+ const centerX = Math.round(dayLeft + dayWidth / 2);
662
+ return {
663
+ left: centerX - halfDiagonal,
664
+ right: centerX + halfDiagonal
665
+ };
666
+ };
667
+ var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
668
+ const startDate = new Date(task.startDate);
669
+ const endDate = new Date(task.endDate);
670
+ if (task.type === "milestone") {
671
+ const size = 14;
672
+ if (override) {
673
+ const bounds2 = calculateMilestoneConnectionBounds(override.left, dayWidth, size);
674
+ return {
675
+ ...bounds2,
676
+ centerX: Math.round(override.left + dayWidth / 2)
677
+ };
662
678
  }
679
+ const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
680
+ const bounds = calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
681
+ return {
682
+ ...bounds,
683
+ centerX: Math.round(bar2.left + dayWidth / 2)
684
+ };
663
685
  }
664
- const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
665
- const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
666
- const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
667
- switch (linkType) {
668
- case "FS":
669
- return normalizeDependencyLag(
670
- linkType,
671
- businessOffset - (successorType === "milestone" ? 0 : 1),
672
- predStart,
673
- predEnd,
674
- businessDays,
675
- weekendPredicate,
676
- successorType
677
- );
678
- case "SS":
679
- return businessOffset;
680
- case "FF":
681
- return businessOffset;
682
- case "SF":
683
- return businessOffset + 1;
686
+ if (override) {
687
+ return {
688
+ left: override.left,
689
+ right: override.left + override.width,
690
+ centerX: Math.round(override.left + override.width / 2)
691
+ };
684
692
  }
685
- }
686
- function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate, successorType) {
687
- const normalizedLag = normalizeDependencyLag(
688
- linkType,
689
- lag,
690
- predecessorStart,
691
- predecessorEnd,
692
- businessDays,
693
- weekendPredicate,
694
- successorType
693
+ const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
694
+ return {
695
+ left: bar.left,
696
+ right: bar.left + bar.width,
697
+ centerX: Math.round(bar.left + bar.width / 2)
698
+ };
699
+ };
700
+ var pixelsToDate = (pixels, monthStart, dayWidth) => {
701
+ const days = Math.round(pixels / dayWidth);
702
+ return new Date(Date.UTC(
703
+ monthStart.getUTCFullYear(),
704
+ monthStart.getUTCMonth(),
705
+ monthStart.getUTCDate() + days
706
+ ));
707
+ };
708
+ var calculateGridWidth = (daysInMonth, dayWidth) => {
709
+ return Math.round(daysInMonth * dayWidth);
710
+ };
711
+ var MIN_EDGE_ZONE_PX = 8;
712
+ var MIN_MOVE_ZONE_PX = 16;
713
+ var detectEdgeZone = (clientX, taskBarElement, edgeZoneWidth = 12) => {
714
+ const rect = taskBarElement.getBoundingClientRect();
715
+ const relativeX = Math.round(clientX - rect.left);
716
+ const width = Math.round(rect.width);
717
+ const effectiveZone = Math.max(
718
+ MIN_EDGE_ZONE_PX,
719
+ Math.min(edgeZoneWidth, (width - MIN_MOVE_ZONE_PX) / 2)
695
720
  );
696
- if (!businessDays || !weekendPredicate) {
697
- switch (linkType) {
698
- case "FS":
699
- return new Date(predecessorEnd.getTime() + (normalizedLag + (successorType === "milestone" ? 0 : 1)) * DAY_MS);
700
- case "SS":
701
- return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
702
- case "FF":
703
- return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
704
- case "SF":
705
- return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
706
- }
707
- }
708
- const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
709
- let offset;
710
- switch (linkType) {
711
- case "FS":
712
- offset = normalizedLag + (successorType === "milestone" ? 0 : 1);
713
- break;
714
- case "SS":
715
- offset = normalizedLag;
716
- break;
717
- case "FF":
718
- offset = normalizedLag;
719
- break;
720
- case "SF":
721
- offset = normalizedLag - 1;
722
- break;
721
+ if (relativeX >= 0 && relativeX <= effectiveZone) {
722
+ return "left";
723
723
  }
724
- return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
725
- }
726
-
727
- // src/core/scheduling/criticalPath.ts
728
- function computeCriticalPath(tasks, options = {}) {
729
- const { businessDays = false, weekendPredicate } = options;
730
- const taskById = new Map(tasks.map((task) => [task.id, task]));
731
- const childIds = /* @__PURE__ */ new Set();
732
- for (const task of tasks) {
733
- if (task.parentId) childIds.add(task.parentId);
724
+ if (relativeX >= width - effectiveZone && relativeX <= width) {
725
+ return "right";
734
726
  }
735
- const leafTasks = tasks.filter((task) => !childIds.has(task.id));
736
- const leafIds = new Set(leafTasks.map((task) => task.id));
737
- const leafIdsByAncestor = /* @__PURE__ */ new Map();
738
- for (const leaf of leafTasks) {
739
- let ancestor = leaf.parentId;
740
- while (ancestor) {
741
- const set = leafIdsByAncestor.get(ancestor) ?? /* @__PURE__ */ new Set();
742
- set.add(leaf.id);
743
- leafIdsByAncestor.set(ancestor, set);
744
- ancestor = taskById.get(ancestor)?.parentId;
745
- }
727
+ return "move";
728
+ };
729
+ var getCursorForPosition = (position) => {
730
+ switch (position) {
731
+ case "left":
732
+ case "right":
733
+ return "ew-resize";
734
+ case "move":
735
+ return "grab";
736
+ default:
737
+ return "default";
746
738
  }
747
- const durationOf = (task) => getTaskDuration(task.startDate, task.endDate, businessDays, weekendPredicate);
748
- const projectStartDate = leafTasks.reduce((earliest, task) => {
749
- const taskStart = parseDateOnly(task.startDate);
750
- return taskStart.getTime() < earliest.getTime() ? taskStart : earliest;
751
- }, parseDateOnly(leafTasks[0]?.startDate ?? /* @__PURE__ */ new Date()));
752
- const offsetFromProjectStart = (date) => {
753
- const targetDate = parseDateOnly(date);
754
- if (businessDays && weekendPredicate) {
755
- return getBusinessDayOffset(projectStartDate, targetDate, weekendPredicate);
756
- }
757
- return Math.round((targetDate.getTime() - projectStartDate.getTime()) / DAY_MS);
758
- };
759
- const scheduledStart = /* @__PURE__ */ new Map();
760
- for (const leaf of leafTasks) {
761
- scheduledStart.set(leaf.id, offsetFromProjectStart(leaf.startDate));
739
+ };
740
+ var calculateGridLines = (dateRange, dayWidth) => {
741
+ const lines = [];
742
+ for (let i = 0; i < dateRange.length; i++) {
743
+ const date = dateRange[i];
744
+ const x = Math.round(i * dayWidth);
745
+ const isMonthStart = i === 0 ? false : date.getUTCDate() === 1;
746
+ const isWeekStart = date.getUTCDay() === 1;
747
+ lines.push({ x, isMonthStart, isWeekStart });
762
748
  }
763
- const succByPred = /* @__PURE__ */ new Map();
764
- const predBySucc = /* @__PURE__ */ new Map();
765
- const seenEdges = /* @__PURE__ */ new Set();
766
- const addEdge = (predecessorId, succId, lag, type) => {
767
- const edgeKey = `${predecessorId}|${succId}|${type}`;
768
- if (seenEdges.has(edgeKey)) return;
769
- seenEdges.add(edgeKey);
770
- const outEdges = succByPred.get(predecessorId) ?? [];
771
- outEdges.push({ succId, lag, type });
772
- succByPred.set(predecessorId, outEdges);
773
- const inEdges = predBySucc.get(succId) ?? [];
774
- inEdges.push({ predId: predecessorId, lag, type });
775
- predBySucc.set(succId, inEdges);
776
- };
777
- for (const succ of leafTasks) {
778
- for (const dep of succ.dependencies ?? []) {
779
- const lag = getDependencyLag(dep);
780
- if (leafIds.has(dep.taskId)) {
781
- addEdge(dep.taskId, succ.id, lag, dep.type);
782
- continue;
783
- }
784
- const descendants = leafIdsByAncestor.get(dep.taskId);
785
- if (!descendants || descendants.size === 0) continue;
786
- for (const leafId of descendants) {
787
- if (leafId === succ.id) continue;
788
- addEdge(leafId, succ.id, lag, dep.type);
789
- }
790
- }
749
+ if (dateRange.length > 0) {
750
+ lines.push({
751
+ x: Math.round(dateRange.length * dayWidth),
752
+ isMonthStart: false,
753
+ isWeekStart: false
754
+ });
791
755
  }
792
- const indegree = /* @__PURE__ */ new Map();
793
- for (const leaf of leafTasks) indegree.set(leaf.id, 0);
794
- for (const edges of succByPred.values()) {
795
- for (const { succId } of edges) {
796
- indegree.set(succId, (indegree.get(succId) ?? 0) + 1);
756
+ return lines;
757
+ };
758
+ var calculateWeekendBlocks = (dateRange, dayWidth, isCustomWeekend) => {
759
+ const blocks = [];
760
+ let inWeekend = false;
761
+ let weekendStartIndex = -1;
762
+ for (let i = 0; i < dateRange.length; i++) {
763
+ const date = dateRange[i];
764
+ const isWeekend3 = isCustomWeekend ? isCustomWeekend(date) : date.getUTCDay() === 0 || date.getUTCDay() === 6;
765
+ if (isWeekend3 && !inWeekend) {
766
+ inWeekend = true;
767
+ weekendStartIndex = i;
768
+ } else if (!isWeekend3 && inWeekend) {
769
+ inWeekend = false;
770
+ const left = Math.round(weekendStartIndex * dayWidth);
771
+ const width = Math.round((i - weekendStartIndex) * dayWidth);
772
+ blocks.push({ left, width });
797
773
  }
798
774
  }
799
- const order = [];
800
- const enqueued = /* @__PURE__ */ new Set();
801
- const queue = leafTasks.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).map((leaf) => leaf.id);
802
- for (const id of queue) enqueued.add(id);
803
- while (queue.length > 0) {
804
- const current = queue.shift();
805
- order.push(current);
806
- for (const { succId } of succByPred.get(current) ?? []) {
807
- const next = (indegree.get(succId) ?? 0) - 1;
808
- indegree.set(succId, next);
809
- if (next === 0 && !enqueued.has(succId)) {
810
- enqueued.add(succId);
811
- queue.push(succId);
812
- }
813
- }
775
+ if (inWeekend && weekendStartIndex >= 0) {
776
+ const left = Math.round(weekendStartIndex * dayWidth);
777
+ const width = Math.round((dateRange.length - weekendStartIndex) * dayWidth);
778
+ blocks.push({ left, width });
814
779
  }
815
- const projectFinish = Math.max(
816
- 0,
817
- ...leafTasks.map((leaf) => (scheduledStart.get(leaf.id) ?? 0) + durationOf(leaf))
818
- );
819
- const LF = /* @__PURE__ */ new Map();
820
- const LS = /* @__PURE__ */ new Map();
821
- for (const leaf of leafTasks) LF.set(leaf.id, projectFinish);
822
- for (let i = order.length - 1; i >= 0; i--) {
823
- const id = order[i];
824
- const task = taskById.get(id);
825
- const durPred = durationOf(task);
826
- const outEdges = succByPred.get(id) ?? [];
827
- let lf = projectFinish;
828
- if (outEdges.length > 0) {
829
- const candidates = outEdges.map(({ succId, lag, type }) => {
830
- const succLs = LS.get(succId) ?? projectFinish;
831
- const durSucc = durationOf(taskById.get(succId));
832
- switch (type) {
833
- case "FS":
834
- return succLs - lag;
835
- case "SS":
836
- return succLs - lag + durPred;
837
- case "FF":
838
- return succLs + durSucc - lag;
839
- case "SF":
840
- return succLs + durSucc - lag + durPred;
841
- default:
842
- return projectFinish;
843
- }
844
- });
845
- lf = Math.min(projectFinish, ...candidates);
780
+ return blocks;
781
+ };
782
+ var calculateBezierPath = (from, to) => {
783
+ const verticalDistance = Math.abs(to.y - from.y);
784
+ const cpOffset = Math.max(verticalDistance * 0.5, 20);
785
+ if (from.y === to.y) {
786
+ const arcHeight = 20;
787
+ const midX = (from.x + to.x) / 2;
788
+ return `M ${from.x} ${from.y} Q ${midX} ${from.y - arcHeight} ${to.x} ${to.y}`;
789
+ }
790
+ const cp1x = from.x;
791
+ const cp1y = from.y + (to.y > from.y ? cpOffset : -cpOffset);
792
+ const cp2x = to.x;
793
+ const cp2y = to.y - (to.y > from.y ? cpOffset : -cpOffset);
794
+ return `M ${Math.round(from.x)} ${Math.round(from.y)} C ${Math.round(cp1x)} ${Math.round(cp1y)}, ${Math.round(cp2x)} ${Math.round(cp2y)}, ${Math.round(to.x)} ${Math.round(to.y)}`;
795
+ };
796
+ var calculateDependencyPath = (from, to, arrivesFromRight) => {
797
+ const fx = Math.round(from.x);
798
+ const fy = Math.round(from.y);
799
+ const tx = Math.round(to.x);
800
+ const ty = Math.round(to.y);
801
+ if (fy === ty) {
802
+ return `M ${fx} ${fy} H ${tx}`;
803
+ }
804
+ if (fx === tx) {
805
+ return `M ${fx} ${fy} V ${ty}`;
806
+ }
807
+ const C = 2;
808
+ const goingDown = ty > fy;
809
+ const dirY = goingDown ? 1 : -1;
810
+ if (arrivesFromRight) {
811
+ const goingRight = tx >= fx;
812
+ const dirX = goingRight ? 1 : -1;
813
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
814
+ return [
815
+ `M ${fx} ${fy}`,
816
+ `H ${tx - dirX * C}`,
817
+ `L ${tx} ${fy + dirY * C}`,
818
+ `V ${ty}`
819
+ ].join(" ");
846
820
  }
847
- LF.set(id, lf);
848
- LS.set(id, lf - durPred);
821
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
822
+ } else {
823
+ const goingRight = tx >= fx;
824
+ const dirX = goingRight ? 1 : -1;
825
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
826
+ return [
827
+ `M ${fx} ${fy}`,
828
+ `H ${tx - dirX * C}`,
829
+ `L ${tx} ${fy + dirY * C}`,
830
+ `V ${ty}`
831
+ ].join(" ");
832
+ }
833
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
849
834
  }
850
- const critical = /* @__PURE__ */ new Set();
851
- const ordered = new Set(order);
852
- for (const leaf of leafTasks) {
853
- if (!ordered.has(leaf.id)) continue;
854
- const hasGraphEdge = (succByPred.get(leaf.id)?.length ?? 0) > 0 || (predBySucc.get(leaf.id)?.length ?? 0) > 0;
855
- if (!hasGraphEdge && (leaf.dependencies?.length ?? 0) > 0) continue;
856
- const es = scheduledStart.get(leaf.id) ?? 0;
857
- const ls = LS.get(leaf.id) ?? 0;
858
- if (ls - es <= 0) critical.add(leaf.id);
835
+ };
836
+ var calculateWeekGridLines = (dateRange, dayWidth) => {
837
+ const blocks = getWeekBlocks(dateRange);
838
+ const lines = [];
839
+ let currentDayIndex = 0;
840
+ for (let i = 0; i < blocks.length; i++) {
841
+ const block = blocks[i];
842
+ currentDayIndex += block.days;
843
+ const blockEndMonth = dateRange[currentDayIndex - 1];
844
+ const nextBlockMonth = currentDayIndex < dateRange.length ? dateRange[currentDayIndex] : null;
845
+ const isMonthBoundary = nextBlockMonth && blockEndMonth.getUTCMonth() !== nextBlockMonth.getUTCMonth();
846
+ lines.push({
847
+ x: Math.round(currentDayIndex * dayWidth),
848
+ isMonthStart: isMonthBoundary || false
849
+ });
859
850
  }
860
- return critical;
861
- }
862
- function extendCriticalIdsToParents(criticalLeafIds, tasks) {
863
- const result = new Set(criticalLeafIds);
864
- const parentByChild = /* @__PURE__ */ new Map();
865
- for (const task of tasks) {
866
- if (task.parentId) parentByChild.set(task.id, task.parentId);
851
+ if (lines.length > 0 && lines[lines.length - 1].x === Math.round(dateRange.length * dayWidth)) {
852
+ lines.pop();
867
853
  }
868
- const stack = Array.from(criticalLeafIds);
869
- while (stack.length > 0) {
870
- const id = stack.pop();
871
- const parentId = parentByChild.get(id);
872
- if (parentId && !result.has(parentId)) {
873
- result.add(parentId);
874
- stack.push(parentId);
854
+ return lines;
855
+ };
856
+ var calculateMonthGridLines = (dateRange, dayWidth) => {
857
+ const lines = [];
858
+ for (let i = 1; i < dateRange.length; i++) {
859
+ const date = dateRange[i];
860
+ if (date.getUTCDate() === 1) {
861
+ const x = Math.round(i * dayWidth);
862
+ const isYearBoundary = date.getUTCMonth() === 0;
863
+ lines.push({ x, isMonthStart: isYearBoundary });
875
864
  }
876
865
  }
877
- return result;
878
- }
866
+ return lines;
867
+ };
868
+ var calculateOrthogonalPath = (from, to) => {
869
+ const fx = Math.round(from.x);
870
+ const fy = Math.round(from.y);
871
+ const tx = Math.round(to.x);
872
+ const ty = Math.round(to.y);
873
+ if (fy === ty) {
874
+ return `M ${fx} ${fy} H ${tx}`;
875
+ }
876
+ const C = 2;
877
+ const goingDown = ty > fy;
878
+ const goingRight = tx >= fx;
879
+ const dirY = goingDown ? 1 : -1;
880
+ const dirX = goingRight ? 1 : -1;
881
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
882
+ return [
883
+ `M ${fx} ${fy}`,
884
+ `H ${tx - dirX * C}`,
885
+ `L ${tx} ${fy + dirY * C}`,
886
+ `V ${ty}`
887
+ ].join(" ");
888
+ }
889
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
890
+ };
879
891
 
880
- // src/core/scheduling/commands.ts
881
- function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
882
- const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
883
- if (businessDays && weekendPredicate) {
892
+ // src/utils/activities.ts
893
+ var ACTIVITY_LANE_STEP = 28;
894
+ var ACTIVITY_LANE_BAR_HEIGHT = 24;
895
+ var ACTIVITY_LANES_PADDING = 2;
896
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
897
+ function packIntervals(intervals) {
898
+ const sorted = [...intervals].sort((a, b) => a.start - b.start);
899
+ const laneEnds = [];
900
+ return sorted.map((item) => {
901
+ let lane = 0;
902
+ while (lane < laneEnds.length && item.start < laneEnds[lane]) {
903
+ lane += 1;
904
+ }
905
+ laneEnds[lane] = item.end;
906
+ return { ...item, lane };
907
+ });
908
+ }
909
+ function computeActivityLanes(activities, monthStart, dayWidth) {
910
+ if (activities.length === 0) {
911
+ return { segments: [], laneCount: 0 };
912
+ }
913
+ const packed = packIntervals(activities.map((activity) => {
914
+ const bar = calculateTaskBar(parseUTCDate(activity.startDate), parseUTCDate(activity.endDate), monthStart, dayWidth);
884
915
  return {
885
- start: normalizedStart,
886
- end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
916
+ id: activity.id,
917
+ name: activity.name,
918
+ color: activity.color,
919
+ lag: activity.lag,
920
+ progress: activity.progress,
921
+ start: bar.left,
922
+ end: bar.left + bar.width,
923
+ left: bar.left,
924
+ width: bar.width
887
925
  };
888
- }
889
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
926
+ }));
890
927
  return {
891
- start: normalizedStart,
892
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS6)
928
+ segments: packed.map(({ id, name, color, lag, progress, lane, left, width }) => ({ id, name, color, lag, progress, lane, left, width })),
929
+ laneCount: Math.max(...packed.map((item) => item.lane + 1))
893
930
  };
894
931
  }
895
- function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
896
- const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
897
- if (businessDays && weekendPredicate) {
898
- return {
899
- start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
900
- end: normalizedEnd
901
- };
902
- }
903
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
932
+ function activityBounds(activities) {
933
+ if (!activities || activities.length === 0) return null;
934
+ let min = Number.POSITIVE_INFINITY;
935
+ let max = Number.NEGATIVE_INFINITY;
936
+ for (const activity of activities) {
937
+ const start = parseUTCDate(activity.startDate).getTime();
938
+ const end = parseUTCDate(activity.endDate).getTime();
939
+ if (start < min) min = start;
940
+ if (end > max) max = end;
941
+ }
942
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
943
+ const toIso = (ms) => new Date(ms).toISOString().slice(0, 10);
944
+ return { startDate: toIso(min), endDate: toIso(max) };
945
+ }
946
+ function activityLanesExtraHeight(laneCount) {
947
+ if (laneCount <= 1) return 0;
948
+ return laneCount * ACTIVITY_LANE_STEP + ACTIVITY_LANES_PADDING * 2;
949
+ }
950
+ function activitySpanMs(activity) {
904
951
  return {
905
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS6),
906
- end: normalizedEnd
952
+ start: parseUTCDate(activity.startDate).getTime(),
953
+ end: parseUTCDate(activity.endDate).getTime() + DAY_MS2
907
954
  };
908
955
  }
909
- function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
910
- return buildTaskRangeFromStart(
911
- proposedStart,
912
- getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
913
- businessDays,
914
- weekendPredicate,
915
- snapDirection
916
- );
956
+ function shiftActivityDate(value, days) {
957
+ const shifted = new Date(parseUTCDate(value).getTime() + days * DAY_MS2);
958
+ return typeof value === "string" ? shifted.toISOString().slice(0, 10) : shifted;
917
959
  }
918
- function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate, taskById) {
919
- if (!task.dependencies?.length) {
920
- return { start: proposedStart, end: proposedEnd };
921
- }
922
- let minAllowedStart = null;
923
- for (const dep of task.dependencies) {
924
- if (dep.type !== "FS") {
925
- continue;
960
+ var CHAIN_KEY_SEPARATOR = "\0";
961
+ var chainKey = (taskId, activityId) => `${taskId}${CHAIN_KEY_SEPARATOR}${activityId}`;
962
+ var toDayNumber = (value) => Math.round(parseUTCDate(value).getTime() / DAY_MS2);
963
+ function buildChainGraph(orderedTasks) {
964
+ const successors = /* @__PURE__ */ new Map();
965
+ const predecessors = /* @__PURE__ */ new Map();
966
+ const info = /* @__PURE__ */ new Map();
967
+ const link = (from, to, lag) => {
968
+ const list = successors.get(from);
969
+ if (list) {
970
+ if (!list.some((edge) => edge.key === to)) list.push({ key: to, lag });
971
+ } else {
972
+ successors.set(from, [{ key: to, lag }]);
926
973
  }
927
- const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
928
- if (!predecessor) {
929
- continue;
974
+ const back = predecessors.get(to);
975
+ if (back) {
976
+ if (!back.some((edge) => edge.key === from)) back.push({ key: from, lag });
977
+ } else {
978
+ predecessors.set(to, [{ key: from, lag }]);
930
979
  }
931
- const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
932
- const predecessorDuration = getTaskDuration(
933
- predecessorStart,
934
- predecessorEnd,
935
- businessDays,
936
- weekendPredicate
937
- );
938
- const candidateMinStart = calculateSuccessorDate(
939
- predecessorStart,
940
- predecessorEnd,
941
- "FS",
942
- -predecessorDuration,
943
- businessDays,
944
- weekendPredicate,
945
- task.type
946
- );
947
- if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
948
- minAllowedStart = candidateMinStart;
980
+ };
981
+ const flagged = orderedTasks.filter((task) => Boolean(task.activityChain) && Array.isArray(task.activities) && task.activities.length > 0);
982
+ flagged.forEach((task, floorIdx) => {
983
+ task.activities.forEach((activity, workIdx) => {
984
+ info.set(chainKey(task.id, activity.id), {
985
+ taskId: task.id,
986
+ activityId: activity.id,
987
+ startDay: toDayNumber(activity.startDate),
988
+ durationDays: Math.max(1, toDayNumber(activity.endDate) - toDayNumber(activity.startDate) + 1),
989
+ floorIdx,
990
+ workIdx
991
+ });
992
+ });
993
+ for (let i = 0; i < task.activities.length - 1; i += 1) {
994
+ link(chainKey(task.id, task.activities[i].id), chainKey(task.id, task.activities[i + 1].id), task.activities[i + 1].lag ?? 0);
995
+ }
996
+ });
997
+ for (let f = 0; f < flagged.length - 1; f += 1) {
998
+ const current = flagged[f];
999
+ const next = flagged[f + 1];
1000
+ if (current.parentId !== next.parentId) continue;
1001
+ for (const activity of current.activities) {
1002
+ const twin = next.activities.find((candidate) => candidate.id === activity.id || candidate.name === activity.name);
1003
+ if (twin) link(chainKey(current.id, activity.id), chainKey(next.id, twin.id), 0);
949
1004
  }
950
1005
  }
951
- if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
952
- return { start: proposedStart, end: proposedEnd };
1006
+ return { successors, predecessors, info };
1007
+ }
1008
+ function collectDownstream(graph, startKey) {
1009
+ const visited = /* @__PURE__ */ new Set();
1010
+ const queue = [...(graph.successors.get(startKey) ?? []).map((edge) => edge.key)];
1011
+ while (queue.length > 0) {
1012
+ const key = queue.shift();
1013
+ if (visited.has(key)) continue;
1014
+ visited.add(key);
1015
+ queue.push(...(graph.successors.get(key) ?? []).map((edge) => edge.key));
953
1016
  }
954
- return buildTaskRangeFromStart(
955
- minAllowedStart,
956
- getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
957
- businessDays,
958
- weekendPredicate
959
- );
1017
+ return visited;
960
1018
  }
961
- function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate, taskById) {
962
- if (!task.dependencies) return [];
963
- return task.dependencies.map((dep) => {
964
- const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
965
- if (!predecessor) {
966
- return { ...dep, lag: getDependencyLag(dep) };
1019
+ function shiftsFromDeltas(orderedTasks, deltasByTask) {
1020
+ const shifts = [];
1021
+ for (const task of orderedTasks) {
1022
+ const deltas = deltasByTask.get(task.id);
1023
+ if (!deltas || deltas.size === 0) continue;
1024
+ shifts.push({
1025
+ taskId: task.id,
1026
+ shiftedIds: new Set(deltas.keys()),
1027
+ deltas,
1028
+ activities: (task.activities ?? []).map((activity) => deltas.has(activity.id) ? {
1029
+ ...activity,
1030
+ startDate: shiftActivityDate(activity.startDate, deltas.get(activity.id)),
1031
+ endDate: shiftActivityDate(activity.endDate, deltas.get(activity.id))
1032
+ } : activity)
1033
+ });
1034
+ }
1035
+ return shifts;
1036
+ }
1037
+ function shiftActivityChain(orderedTasks, draggedTaskId, draggedActivityId, deltaDays) {
1038
+ if (deltaDays === 0) return [];
1039
+ const graph = buildChainGraph(orderedTasks);
1040
+ const shifted = collectDownstream(graph, chainKey(draggedTaskId, draggedActivityId));
1041
+ if (shifted.size === 0) return [];
1042
+ const deltasByTask = /* @__PURE__ */ new Map();
1043
+ for (const key of shifted) {
1044
+ const node = graph.info.get(key);
1045
+ const set = deltasByTask.get(node.taskId);
1046
+ if (set) set.set(node.activityId, deltaDays);
1047
+ else deltasByTask.set(node.taskId, /* @__PURE__ */ new Map([[node.activityId, deltaDays]]));
1048
+ }
1049
+ return shiftsFromDeltas(orderedTasks, deltasByTask);
1050
+ }
1051
+ function activityStartConstraint(orderedTasks, taskId, activityId) {
1052
+ const graph = buildChainGraph(orderedTasks);
1053
+ const key = chainKey(taskId, activityId);
1054
+ const node = graph.info.get(key);
1055
+ if (!node) return null;
1056
+ const candidates = [];
1057
+ let minStart = Number.NEGATIVE_INFINITY;
1058
+ for (const pred of graph.predecessors.get(key) ?? []) {
1059
+ const predNode = graph.info.get(pred.key);
1060
+ if (!predNode) continue;
1061
+ const min = predNode.startDay + predNode.durationDays + pred.lag;
1062
+ candidates.push({ taskId: predNode.taskId, activityId: predNode.activityId, min, lag: pred.lag });
1063
+ if (min > minStart) minStart = min;
1064
+ }
1065
+ if (!Number.isFinite(minStart)) return null;
1066
+ const blockers = candidates.filter((candidate) => candidate.min === minStart);
1067
+ return {
1068
+ minStartDay: minStart,
1069
+ minStartDelta: minStart - node.startDay,
1070
+ blockers: blockers.map(({ taskId: blockerTaskId, activityId: blockerActivityId }) => ({
1071
+ taskId: blockerTaskId,
1072
+ activityId: blockerActivityId
1073
+ })),
1074
+ lag: Math.max(0, ...blockers.map((blocker) => blocker.lag))
1075
+ };
1076
+ }
1077
+ function activityChainMinStartDelta(orderedTasks, taskId, activityId) {
1078
+ return activityStartConstraint(orderedTasks, taskId, activityId)?.minStartDelta ?? null;
1079
+ }
1080
+ function pushActivityChain(orderedTasks, draggedTaskId, draggedActivityId, requestedDeltaDays) {
1081
+ const graph = buildChainGraph(orderedTasks);
1082
+ const draggedKey = chainKey(draggedTaskId, draggedActivityId);
1083
+ if (!graph.info.has(draggedKey)) {
1084
+ return { shifts: [], draggedDeltaDays: requestedDeltaDays };
1085
+ }
1086
+ const downstream = collectDownstream(graph, draggedKey);
1087
+ if (downstream.size === 0) {
1088
+ return { shifts: [], draggedDeltaDays: requestedDeltaDays };
1089
+ }
1090
+ const newStart = /* @__PURE__ */ new Map();
1091
+ const dragged = graph.info.get(draggedKey);
1092
+ newStart.set(draggedKey, dragged.startDay + requestedDeltaDays);
1093
+ const endOfDay = (key) => {
1094
+ const node = graph.info.get(key);
1095
+ return (newStart.get(key) ?? node.startDay) + node.durationDays;
1096
+ };
1097
+ const order = [...downstream].map((key) => ({ key, node: graph.info.get(key) })).sort((a, b) => a.node.workIdx - b.node.workIdx || a.node.floorIdx - b.node.floorIdx);
1098
+ for (const { key, node } of order) {
1099
+ let earliest = Number.NEGATIVE_INFINITY;
1100
+ let oldEarliest = Number.NEGATIVE_INFINITY;
1101
+ for (const pred of graph.predecessors.get(key) ?? []) {
1102
+ const predNode = graph.info.get(pred.key);
1103
+ earliest = Math.max(earliest, endOfDay(pred.key) + pred.lag);
1104
+ oldEarliest = Math.max(oldEarliest, predNode.startDay + predNode.durationDays + pred.lag);
1105
+ }
1106
+ let nextStart;
1107
+ if (earliest > node.startDay) {
1108
+ nextStart = earliest;
1109
+ } else if (earliest < oldEarliest) {
1110
+ nextStart = earliest;
1111
+ } else {
1112
+ nextStart = node.startDay;
967
1113
  }
968
- const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
969
- predecessor,
970
- (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
971
- );
972
- const nextLag = computeLagFromDates(
973
- dep.type,
974
- predecessorStart,
975
- predecessorEnd,
976
- newStartDate,
977
- newEndDate,
978
- businessDays,
979
- weekendPredicate,
980
- task.type
981
- );
982
- return { ...dep, lag: nextLag };
983
- });
1114
+ newStart.set(key, nextStart);
1115
+ }
1116
+ const deltasByTask = /* @__PURE__ */ new Map();
1117
+ for (const key of downstream) {
1118
+ const node = graph.info.get(key);
1119
+ const delta = (newStart.get(key) ?? node.startDay) - node.startDay;
1120
+ if (delta === 0) continue;
1121
+ const set = deltasByTask.get(node.taskId);
1122
+ if (set) set.set(node.activityId, delta);
1123
+ else deltasByTask.set(node.taskId, /* @__PURE__ */ new Map([[node.activityId, delta]]));
1124
+ }
1125
+ return { shifts: shiftsFromDeltas(orderedTasks, deltasByTask), draggedDeltaDays: requestedDeltaDays };
984
1126
  }
985
1127
 
986
- // src/core/scheduling/cascade.ts
987
- function parseCascadeDateInput(date) {
988
- if (date instanceof Date) {
989
- return normalizeUTCDate(date);
1128
+ // src/core/scheduling/dependencies.ts
1129
+ function normalizePredecessorDates(predecessor, parseDateFn) {
1130
+ const predStart = parseDateFn(predecessor.startDate);
1131
+ const predEnd = predecessor.type === "milestone" ? predStart : parseDateFn(predecessor.endDate);
1132
+ return { predStart, predEnd };
1133
+ }
1134
+ function getDependencyLag(dep) {
1135
+ return Number.isFinite(dep.lag) ? dep.lag : 0;
1136
+ }
1137
+ function normalizeTaskDependencyLags(task) {
1138
+ return task;
1139
+ }
1140
+ function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd, businessDays = false, weekendPredicate, successorType) {
1141
+ if (linkType !== "FS") {
1142
+ return lag;
990
1143
  }
991
- return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
1144
+ const predecessorDuration = getTaskDuration(
1145
+ predecessorStart,
1146
+ predecessorEnd,
1147
+ businessDays,
1148
+ weekendPredicate
1149
+ );
1150
+ const minimumLag = successorType === "milestone" ? -Math.max(0, predecessorDuration - 1) : -predecessorDuration;
1151
+ return Math.max(minimumLag, lag);
992
1152
  }
993
- function createCascadeContext(allTasks) {
994
- const childrenByParentId = /* @__PURE__ */ new Map();
995
- const dependentsByPredecessorId = /* @__PURE__ */ new Map();
996
- const taskById = /* @__PURE__ */ new Map();
997
- for (const task of allTasks) {
998
- taskById.set(task.id, task);
999
- if (task.parentId) {
1000
- const children = childrenByParentId.get(task.parentId) ?? [];
1001
- children.push(task);
1002
- childrenByParentId.set(task.parentId, children);
1153
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate, successorType) {
1154
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
1155
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
1156
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
1157
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
1158
+ if (!businessDays || !weekendPredicate) {
1159
+ switch (linkType) {
1160
+ case "FS":
1161
+ return normalizeDependencyLag(
1162
+ linkType,
1163
+ Math.round((sS - pE) / DAY_MS) - (successorType === "milestone" ? 0 : 1),
1164
+ predStart,
1165
+ predEnd,
1166
+ businessDays,
1167
+ weekendPredicate,
1168
+ successorType
1169
+ );
1170
+ case "SS":
1171
+ return Math.round((sS - pS) / DAY_MS);
1172
+ case "FF":
1173
+ return Math.round((sE - pE) / DAY_MS);
1174
+ case "SF":
1175
+ return Math.round((sE - pS) / DAY_MS) + 1;
1003
1176
  }
1004
- if (!task.dependencies) continue;
1005
- task.dependencies.forEach((dependency, dependencyIndex) => {
1006
- const dependents = dependentsByPredecessorId.get(dependency.taskId) ?? [];
1007
- dependents.push({ task, dependencyIndex });
1008
- dependentsByPredecessorId.set(dependency.taskId, dependents);
1009
- });
1010
1177
  }
1011
- return { childrenByParentId, dependentsByPredecessorId, taskById };
1178
+ const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
1179
+ const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
1180
+ const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
1181
+ switch (linkType) {
1182
+ case "FS":
1183
+ return normalizeDependencyLag(
1184
+ linkType,
1185
+ businessOffset - (successorType === "milestone" ? 0 : 1),
1186
+ predStart,
1187
+ predEnd,
1188
+ businessDays,
1189
+ weekendPredicate,
1190
+ successorType
1191
+ );
1192
+ case "SS":
1193
+ return businessOffset;
1194
+ case "FF":
1195
+ return businessOffset;
1196
+ case "SF":
1197
+ return businessOffset + 1;
1198
+ }
1012
1199
  }
1013
- function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
1014
- const { dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1015
- const visited = /* @__PURE__ */ new Set();
1016
- const queue = [draggedTaskId];
1017
- const chain = [];
1018
- visited.add(draggedTaskId);
1019
- while (queue.length > 0) {
1020
- const current = queue.shift();
1021
- const successors = dependentsByPredecessorId.get(current) ?? [];
1022
- for (const { task } of successors) {
1023
- if (!linkTypes.includes(task.dependencies?.find((dependency) => dependency.taskId === current)?.type ?? "FS")) {
1024
- continue;
1025
- }
1026
- const sid = task.id;
1027
- if (!visited.has(sid)) {
1028
- visited.add(sid);
1029
- const successorTask = taskById.get(sid);
1030
- if (successorTask) {
1031
- chain.push(successorTask);
1032
- queue.push(sid);
1033
- }
1034
- }
1200
+ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate, successorType) {
1201
+ const normalizedLag = normalizeDependencyLag(
1202
+ linkType,
1203
+ lag,
1204
+ predecessorStart,
1205
+ predecessorEnd,
1206
+ businessDays,
1207
+ weekendPredicate,
1208
+ successorType
1209
+ );
1210
+ if (!businessDays || !weekendPredicate) {
1211
+ switch (linkType) {
1212
+ case "FS":
1213
+ return new Date(predecessorEnd.getTime() + (normalizedLag + (successorType === "milestone" ? 0 : 1)) * DAY_MS);
1214
+ case "SS":
1215
+ return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
1216
+ case "FF":
1217
+ return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
1218
+ case "SF":
1219
+ return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
1035
1220
  }
1036
1221
  }
1037
- return chain;
1222
+ const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
1223
+ let offset;
1224
+ switch (linkType) {
1225
+ case "FS":
1226
+ offset = normalizedLag + (successorType === "milestone" ? 0 : 1);
1227
+ break;
1228
+ case "SS":
1229
+ offset = normalizedLag;
1230
+ break;
1231
+ case "FF":
1232
+ offset = normalizedLag;
1233
+ break;
1234
+ case "SF":
1235
+ offset = normalizedLag - 1;
1236
+ break;
1237
+ }
1238
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
1038
1239
  }
1039
- function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
1040
- const { childrenByParentId, dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1041
- const updatedDates = /* @__PURE__ */ new Map();
1042
- updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
1043
- const result = [];
1044
- const queue = [movedTaskId];
1045
- const visited = /* @__PURE__ */ new Set([movedTaskId]);
1046
- while (queue.length > 0) {
1047
- const currentId = queue.shift();
1048
- const { start: predStart, end: predEnd } = updatedDates.get(currentId);
1049
- if (!skipChildCascade) {
1050
- const children = childrenByParentId.get(currentId) ?? [];
1051
- for (const child of children) {
1052
- if (visited.has(child.id) || child.locked) continue;
1053
- const origStart = new Date(child.startDate);
1054
- const origEnd = new Date(child.endDate);
1055
- const durationMs = origEnd.getTime() - origStart.getTime();
1056
- const parentOrig = taskById.get(currentId);
1057
- const parentOrigStart = new Date(parentOrig.startDate);
1058
- const parentOrigEnd = new Date(parentOrig.endDate);
1059
- const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
1060
- const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
1061
- const newChildStart = new Date(origStart.getTime() + parentStartDelta);
1062
- const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
1063
- visited.add(child.id);
1064
- updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
1065
- result.push(normalizeTaskDependencyLags({
1066
- ...child,
1067
- startDate: newChildStart.toISOString().split("T")[0],
1068
- endDate: newChildEnd.toISOString().split("T")[0]
1069
- }));
1070
- queue.push(child.id);
1071
- }
1240
+
1241
+ // src/core/scheduling/criticalPath.ts
1242
+ function computeCriticalPath(tasks, options = {}) {
1243
+ const { businessDays = false, weekendPredicate } = options;
1244
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
1245
+ const childIds = /* @__PURE__ */ new Set();
1246
+ for (const task of tasks) {
1247
+ if (task.parentId) childIds.add(task.parentId);
1248
+ }
1249
+ const leafTasks = tasks.filter((task) => !childIds.has(task.id));
1250
+ const leafIds = new Set(leafTasks.map((task) => task.id));
1251
+ const leafIdsByAncestor = /* @__PURE__ */ new Map();
1252
+ for (const leaf of leafTasks) {
1253
+ let ancestor = leaf.parentId;
1254
+ while (ancestor) {
1255
+ const set = leafIdsByAncestor.get(ancestor) ?? /* @__PURE__ */ new Set();
1256
+ set.add(leaf.id);
1257
+ leafIdsByAncestor.set(ancestor, set);
1258
+ ancestor = taskById.get(ancestor)?.parentId;
1072
1259
  }
1073
- const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1074
- for (const { task, dependencyIndex } of dependents) {
1075
- if (visited.has(task.id) || !task.dependencies || task.locked) continue;
1076
- const dep = task.dependencies[dependencyIndex];
1077
- if (!dep) continue;
1078
- const orig = taskById.get(task.id);
1079
- const origStart = new Date(orig.startDate);
1080
- const origEnd = new Date(orig.endDate);
1081
- const duration = getTaskDuration(origStart, origEnd);
1082
- const currentTask = taskById.get(currentId);
1083
- const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1084
- {
1085
- startDate: predStart,
1086
- endDate: predEnd,
1087
- type: currentTask.type
1088
- },
1089
- parseCascadeDateInput
1090
- );
1091
- const constraintDate = calculateSuccessorDate(
1092
- normalizedPredStart,
1093
- normalizedPredEnd,
1094
- dep.type,
1095
- getDependencyLag(dep),
1096
- false,
1097
- void 0,
1098
- task.type
1099
- );
1100
- let newSuccStart;
1101
- let newSuccEnd;
1102
- if (dep.type === "FS" || dep.type === "SS") {
1103
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
1104
- } else {
1105
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
1260
+ }
1261
+ const durationOf = (task) => getTaskDuration(task.startDate, task.endDate, businessDays, weekendPredicate);
1262
+ const projectStartDate = leafTasks.reduce((earliest, task) => {
1263
+ const taskStart = parseDateOnly(task.startDate);
1264
+ return taskStart.getTime() < earliest.getTime() ? taskStart : earliest;
1265
+ }, parseDateOnly(leafTasks[0]?.startDate ?? /* @__PURE__ */ new Date()));
1266
+ const offsetFromProjectStart = (date) => {
1267
+ const targetDate = parseDateOnly(date);
1268
+ if (businessDays && weekendPredicate) {
1269
+ return getBusinessDayOffset(projectStartDate, targetDate, weekendPredicate);
1270
+ }
1271
+ return Math.round((targetDate.getTime() - projectStartDate.getTime()) / DAY_MS);
1272
+ };
1273
+ const scheduledStart = /* @__PURE__ */ new Map();
1274
+ for (const leaf of leafTasks) {
1275
+ scheduledStart.set(leaf.id, offsetFromProjectStart(leaf.startDate));
1276
+ }
1277
+ const succByPred = /* @__PURE__ */ new Map();
1278
+ const predBySucc = /* @__PURE__ */ new Map();
1279
+ const seenEdges = /* @__PURE__ */ new Set();
1280
+ const addEdge = (predecessorId, succId, lag, type) => {
1281
+ const edgeKey = `${predecessorId}|${succId}|${type}`;
1282
+ if (seenEdges.has(edgeKey)) return;
1283
+ seenEdges.add(edgeKey);
1284
+ const outEdges = succByPred.get(predecessorId) ?? [];
1285
+ outEdges.push({ succId, lag, type });
1286
+ succByPred.set(predecessorId, outEdges);
1287
+ const inEdges = predBySucc.get(succId) ?? [];
1288
+ inEdges.push({ predId: predecessorId, lag, type });
1289
+ predBySucc.set(succId, inEdges);
1290
+ };
1291
+ for (const succ of leafTasks) {
1292
+ for (const dep of succ.dependencies ?? []) {
1293
+ const lag = getDependencyLag(dep);
1294
+ if (leafIds.has(dep.taskId)) {
1295
+ addEdge(dep.taskId, succ.id, lag, dep.type);
1296
+ continue;
1297
+ }
1298
+ const descendants = leafIdsByAncestor.get(dep.taskId);
1299
+ if (!descendants || descendants.size === 0) continue;
1300
+ for (const leafId of descendants) {
1301
+ if (leafId === succ.id) continue;
1302
+ addEdge(leafId, succ.id, lag, dep.type);
1106
1303
  }
1107
- visited.add(task.id);
1108
- updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
1109
- result.push(normalizeTaskDependencyLags({
1110
- ...task,
1111
- startDate: newSuccStart.toISOString().split("T")[0],
1112
- endDate: newSuccEnd.toISOString().split("T")[0]
1113
- }));
1114
- queue.push(task.id);
1115
1304
  }
1116
1305
  }
1117
- return result;
1118
- }
1119
- function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
1120
- const { childrenByParentId, dependentsByPredecessorId } = createCascadeContext(allTasks);
1121
- const directChildren = childrenByParentId.get(changedTaskId) ?? [];
1122
- const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
1123
- const initialChain = [...directChildren, ...directSuccessors].filter(
1124
- (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
1125
- );
1126
- const chain = [...initialChain];
1127
- const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
1128
- const queue = [...initialChain];
1306
+ const indegree = /* @__PURE__ */ new Map();
1307
+ for (const leaf of leafTasks) indegree.set(leaf.id, 0);
1308
+ for (const edges of succByPred.values()) {
1309
+ for (const { succId } of edges) {
1310
+ indegree.set(succId, (indegree.get(succId) ?? 0) + 1);
1311
+ }
1312
+ }
1313
+ const order = [];
1314
+ const enqueued = /* @__PURE__ */ new Set();
1315
+ const queue = leafTasks.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).map((leaf) => leaf.id);
1316
+ for (const id of queue) enqueued.add(id);
1129
1317
  while (queue.length > 0) {
1130
1318
  const current = queue.shift();
1131
- const children = childrenByParentId.get(current.id) ?? [];
1132
- for (const child of children) {
1133
- if (!visited.has(child.id)) {
1134
- visited.add(child.id);
1135
- chain.push(child);
1136
- queue.push(child);
1319
+ order.push(current);
1320
+ for (const { succId } of succByPred.get(current) ?? []) {
1321
+ const next = (indegree.get(succId) ?? 0) - 1;
1322
+ indegree.set(succId, next);
1323
+ if (next === 0 && !enqueued.has(succId)) {
1324
+ enqueued.add(succId);
1325
+ queue.push(succId);
1137
1326
  }
1138
1327
  }
1139
- const successors = dependentsByPredecessorId.get(current.id) ?? [];
1140
- for (const { task: successor } of successors) {
1141
- if (!visited.has(successor.id)) {
1142
- visited.add(successor.id);
1143
- chain.push(successor);
1144
- queue.push(successor);
1145
- }
1328
+ }
1329
+ const projectFinish = Math.max(
1330
+ 0,
1331
+ ...leafTasks.map((leaf) => (scheduledStart.get(leaf.id) ?? 0) + durationOf(leaf))
1332
+ );
1333
+ const LF = /* @__PURE__ */ new Map();
1334
+ const LS = /* @__PURE__ */ new Map();
1335
+ for (const leaf of leafTasks) LF.set(leaf.id, projectFinish);
1336
+ for (let i = order.length - 1; i >= 0; i--) {
1337
+ const id = order[i];
1338
+ const task = taskById.get(id);
1339
+ const durPred = durationOf(task);
1340
+ const outEdges = succByPred.get(id) ?? [];
1341
+ let lf = projectFinish;
1342
+ if (outEdges.length > 0) {
1343
+ const candidates = outEdges.map(({ succId, lag, type }) => {
1344
+ const succLs = LS.get(succId) ?? projectFinish;
1345
+ const durSucc = durationOf(taskById.get(succId));
1346
+ switch (type) {
1347
+ case "FS":
1348
+ return succLs - lag;
1349
+ case "SS":
1350
+ return succLs - lag + durPred;
1351
+ case "FF":
1352
+ return succLs + durSucc - lag;
1353
+ case "SF":
1354
+ return succLs + durSucc - lag + durPred;
1355
+ default:
1356
+ return projectFinish;
1357
+ }
1358
+ });
1359
+ lf = Math.min(projectFinish, ...candidates);
1146
1360
  }
1361
+ LF.set(id, lf);
1362
+ LS.set(id, lf - durPred);
1147
1363
  }
1148
- return chain;
1364
+ const critical = /* @__PURE__ */ new Set();
1365
+ const ordered = new Set(order);
1366
+ for (const leaf of leafTasks) {
1367
+ if (!ordered.has(leaf.id)) continue;
1368
+ const hasGraphEdge = (succByPred.get(leaf.id)?.length ?? 0) > 0 || (predBySucc.get(leaf.id)?.length ?? 0) > 0;
1369
+ if (!hasGraphEdge && (leaf.dependencies?.length ?? 0) > 0) continue;
1370
+ const es = scheduledStart.get(leaf.id) ?? 0;
1371
+ const ls = LS.get(leaf.id) ?? 0;
1372
+ if (ls - es <= 0) critical.add(leaf.id);
1373
+ }
1374
+ return critical;
1149
1375
  }
1150
- function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate, context = createCascadeContext(allTasks)) {
1151
- const { childrenByParentId, dependentsByPredecessorId, taskById } = context;
1152
- const normalizedMovedTask = normalizeTaskDependencyLags({
1153
- ...movedTask,
1154
- startDate: newStart.toISOString().split("T")[0],
1155
- endDate: newEnd.toISOString().split("T")[0]
1156
- });
1157
- if (!movedTask.parentId && !childrenByParentId.has(movedTask.id) && !dependentsByPredecessorId.has(movedTask.id)) {
1158
- return [normalizedMovedTask];
1376
+ function extendCriticalIdsToParents(criticalLeafIds, tasks) {
1377
+ const result = new Set(criticalLeafIds);
1378
+ const parentByChild = /* @__PURE__ */ new Map();
1379
+ for (const task of tasks) {
1380
+ if (task.parentId) parentByChild.set(task.id, task.parentId);
1159
1381
  }
1160
- const updatedDates = /* @__PURE__ */ new Map();
1161
- updatedDates.set(movedTask.id, { start: newStart, end: newEnd });
1162
- const resultMap = /* @__PURE__ */ new Map();
1163
- resultMap.set(movedTask.id, normalizedMovedTask);
1164
- const queue = [[movedTask.id, "direct"]];
1165
- const childShifted = /* @__PURE__ */ new Set();
1166
- let iterations = 0;
1167
- const MAX_ITERATIONS = allTasks.length * 3;
1168
- while (queue.length > 0 && iterations < MAX_ITERATIONS) {
1169
- iterations++;
1170
- const [currentId, arrivalMode] = queue.shift();
1171
- const { start: currStart, end: currEnd } = updatedDates.get(currentId);
1172
- const currentOriginal = taskById.get(currentId);
1173
- if (arrivalMode !== "parent-recalc") {
1174
- const children = childrenByParentId.get(currentId) ?? [];
1175
- for (const child of children) {
1176
- if (childShifted.has(child.id) || child.locked) continue;
1177
- const parentOrigStart = new Date(currentOriginal.startDate);
1178
- const parentOrigEnd = new Date(currentOriginal.endDate);
1179
- const childOrigStart = new Date(child.startDate);
1180
- const childOrigEnd = new Date(child.endDate);
1181
- const startDeltaMs = currStart.getTime() - parentOrigStart.getTime();
1182
- const endDeltaMs = currEnd.getTime() - parentOrigEnd.getTime();
1183
- let childNewStart;
1184
- let childNewEnd;
1185
- if (businessDays && weekendPredicate) {
1186
- const proposedStart = new Date(childOrigStart.getTime() + startDeltaMs);
1187
- const snapDirection = currStart.getTime() >= parentOrigStart.getTime() ? 1 : -1;
1188
- const movedRange = moveTaskRange(
1189
- child.startDate,
1190
- child.endDate,
1191
- proposedStart,
1192
- true,
1193
- weekendPredicate,
1194
- snapDirection
1195
- );
1196
- childNewStart = movedRange.start;
1197
- childNewEnd = movedRange.end;
1198
- } else {
1199
- childNewStart = new Date(childOrigStart.getTime() + startDeltaMs);
1200
- childNewEnd = new Date(childOrigEnd.getTime() + endDeltaMs);
1201
- }
1202
- const prev = updatedDates.get(child.id);
1203
- if (prev && prev.start.getTime() === childNewStart.getTime() && prev.end.getTime() === childNewEnd.getTime()) {
1204
- continue;
1205
- }
1206
- updatedDates.set(child.id, { start: childNewStart, end: childNewEnd });
1207
- childShifted.add(child.id);
1208
- queue.push([child.id, "child-delta"]);
1209
- resultMap.set(child.id, normalizeTaskDependencyLags({
1210
- ...child,
1211
- startDate: childNewStart.toISOString().split("T")[0],
1212
- endDate: childNewEnd.toISOString().split("T")[0]
1213
- }));
1214
- }
1215
- }
1216
- const parentId = currentOriginal.parentId;
1217
- if (parentId && parentId !== movedTask.id) {
1218
- const parent = taskById.get(parentId);
1219
- if (parent && !parent.locked) {
1220
- const siblings = childrenByParentId.get(parentId) ?? [];
1221
- const siblingPositions = siblings.map((sib) => {
1222
- if (updatedDates.has(sib.id)) return updatedDates.get(sib.id);
1223
- return { start: new Date(sib.startDate), end: new Date(sib.endDate) };
1224
- });
1225
- const minStart = new Date(Math.min(...siblingPositions.map((p) => p.start.getTime())));
1226
- const maxEnd = new Date(Math.max(...siblingPositions.map((p) => p.end.getTime())));
1227
- const prev = updatedDates.get(parentId);
1228
- if (!prev || prev.start.getTime() !== minStart.getTime() || prev.end.getTime() !== maxEnd.getTime()) {
1229
- updatedDates.set(parentId, { start: minStart, end: maxEnd });
1230
- queue.push([parentId, "parent-recalc"]);
1231
- resultMap.set(parentId, normalizeTaskDependencyLags({
1232
- ...parent,
1233
- startDate: minStart.toISOString().split("T")[0],
1234
- endDate: maxEnd.toISOString().split("T")[0]
1235
- }));
1236
- }
1237
- }
1238
- }
1239
- const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1240
- for (const { task, dependencyIndex } of dependents) {
1241
- if (task.locked || !task.dependencies) continue;
1242
- if (childShifted.has(task.id)) continue;
1243
- const dep = task.dependencies[dependencyIndex];
1244
- if (!dep) continue;
1245
- const origStart = new Date(task.startDate);
1246
- const origEnd = new Date(task.endDate);
1247
- const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1248
- {
1249
- startDate: currStart,
1250
- endDate: currEnd,
1251
- type: currentOriginal.type
1252
- },
1253
- parseCascadeDateInput
1254
- );
1255
- const constraintDate = calculateSuccessorDate(
1256
- normalizedPredStart,
1257
- normalizedPredEnd,
1258
- dep.type,
1259
- getDependencyLag(dep),
1260
- businessDays,
1261
- weekendPredicate,
1262
- task.type
1263
- );
1264
- let succNewStart;
1265
- let succNewEnd;
1266
- const duration = getTaskDuration(origStart, origEnd, businessDays, weekendPredicate);
1267
- if (dep.type === "FS" || dep.type === "SS") {
1268
- ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromStart(
1269
- constraintDate,
1270
- duration,
1271
- businessDays,
1272
- weekendPredicate
1273
- ));
1274
- } else {
1275
- ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromEnd(
1276
- constraintDate,
1277
- duration,
1278
- businessDays,
1279
- weekendPredicate
1280
- ));
1281
- }
1282
- const prev = updatedDates.get(task.id);
1283
- if (prev && prev.start.getTime() === succNewStart.getTime() && prev.end.getTime() === succNewEnd.getTime()) {
1284
- continue;
1285
- }
1286
- updatedDates.set(task.id, { start: succNewStart, end: succNewEnd });
1287
- queue.push([task.id, "dependency"]);
1288
- resultMap.set(task.id, normalizeTaskDependencyLags({
1289
- ...task,
1290
- startDate: succNewStart.toISOString().split("T")[0],
1291
- endDate: succNewEnd.toISOString().split("T")[0]
1292
- }));
1382
+ const stack = Array.from(criticalLeafIds);
1383
+ while (stack.length > 0) {
1384
+ const id = stack.pop();
1385
+ const parentId = parentByChild.get(id);
1386
+ if (parentId && !result.has(parentId)) {
1387
+ result.add(parentId);
1388
+ stack.push(parentId);
1293
1389
  }
1294
1390
  }
1295
- return Array.from(resultMap.values());
1391
+ return result;
1296
1392
  }
1297
1393
 
1298
- // src/core/scheduling/hierarchy.ts
1299
- function getChildren(parentId, tasks) {
1300
- return tasks.filter((t) => t.parentId === parentId);
1301
- }
1302
- function isTaskParent(taskId, tasks) {
1303
- return tasks.some((t) => t.parentId === taskId);
1394
+ // src/core/scheduling/commands.ts
1395
+ function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
1396
+ const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
1397
+ if (businessDays && weekendPredicate) {
1398
+ return {
1399
+ start: normalizedStart,
1400
+ end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
1401
+ };
1402
+ }
1403
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1404
+ return {
1405
+ start: normalizedStart,
1406
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS7)
1407
+ };
1304
1408
  }
1305
- function computeParentDates(parentId, tasks) {
1306
- const children = getChildren(parentId, tasks);
1307
- if (children.length === 0) {
1308
- const parent = tasks.find((t) => t.id === parentId);
1309
- const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
1310
- const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
1311
- return { startDate: start, endDate: end };
1409
+ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
1410
+ const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
1411
+ if (businessDays && weekendPredicate) {
1412
+ return {
1413
+ start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
1414
+ end: normalizedEnd
1415
+ };
1312
1416
  }
1313
- const startDates = children.map((c) => new Date(c.startDate));
1314
- const endDates = children.map((c) => new Date(c.endDate));
1315
- const minTime = Math.min(...startDates.map((d) => d.getTime()));
1316
- const maxTime = Math.max(...endDates.map((d) => d.getTime()));
1417
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1317
1418
  return {
1318
- startDate: new Date(minTime),
1319
- endDate: new Date(maxTime)
1419
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS7),
1420
+ end: normalizedEnd
1320
1421
  };
1321
1422
  }
1322
- function computeParentProgress(parentId, tasks) {
1323
- const children = getChildren(parentId, tasks);
1324
- if (children.length === 0) {
1325
- return 0;
1423
+ function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
1424
+ return buildTaskRangeFromStart(
1425
+ proposedStart,
1426
+ getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
1427
+ businessDays,
1428
+ weekendPredicate,
1429
+ snapDirection
1430
+ );
1431
+ }
1432
+ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate, taskById) {
1433
+ if (!task.dependencies?.length) {
1434
+ return { start: proposedStart, end: proposedEnd };
1326
1435
  }
1327
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
1328
- let totalWeight = 0;
1329
- let weightedSum = 0;
1330
- for (const child of children) {
1331
- const start = new Date(child.startDate).getTime();
1332
- const end = new Date(child.endDate).getTime();
1333
- const duration = (end - start + DAY_MS6) / DAY_MS6;
1334
- const progress = child.progress ?? 0;
1335
- totalWeight += duration;
1336
- weightedSum += duration * progress;
1436
+ let minAllowedStart = null;
1437
+ for (const dep of task.dependencies) {
1438
+ if (dep.type !== "FS") {
1439
+ continue;
1440
+ }
1441
+ const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
1442
+ if (!predecessor) {
1443
+ continue;
1444
+ }
1445
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1446
+ const predecessorDuration = getTaskDuration(
1447
+ predecessorStart,
1448
+ predecessorEnd,
1449
+ businessDays,
1450
+ weekendPredicate
1451
+ );
1452
+ const candidateMinStart = calculateSuccessorDate(
1453
+ predecessorStart,
1454
+ predecessorEnd,
1455
+ "FS",
1456
+ -predecessorDuration,
1457
+ businessDays,
1458
+ weekendPredicate,
1459
+ task.type
1460
+ );
1461
+ if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
1462
+ minAllowedStart = candidateMinStart;
1463
+ }
1337
1464
  }
1338
- if (totalWeight === 0) {
1339
- return 0;
1465
+ if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
1466
+ return { start: proposedStart, end: proposedEnd };
1340
1467
  }
1341
- return Math.round(weightedSum / totalWeight * 10) / 10;
1468
+ return buildTaskRangeFromStart(
1469
+ minAllowedStart,
1470
+ getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
1471
+ businessDays,
1472
+ weekendPredicate
1473
+ );
1342
1474
  }
1343
- function getAllDescendants(parentId, tasks) {
1344
- const descendants = [];
1345
- const visited = /* @__PURE__ */ new Set();
1346
- function collectChildren(taskId) {
1347
- if (visited.has(taskId)) return;
1348
- visited.add(taskId);
1349
- const children = getChildren(taskId, tasks);
1350
- for (const child of children) {
1351
- descendants.push(child);
1352
- collectChildren(child.id);
1475
+ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate, taskById) {
1476
+ if (!task.dependencies) return [];
1477
+ return task.dependencies.map((dep) => {
1478
+ const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
1479
+ if (!predecessor) {
1480
+ return { ...dep, lag: getDependencyLag(dep) };
1353
1481
  }
1482
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
1483
+ predecessor,
1484
+ (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
1485
+ );
1486
+ const nextLag = computeLagFromDates(
1487
+ dep.type,
1488
+ predecessorStart,
1489
+ predecessorEnd,
1490
+ newStartDate,
1491
+ newEndDate,
1492
+ businessDays,
1493
+ weekendPredicate,
1494
+ task.type
1495
+ );
1496
+ return { ...dep, lag: nextLag };
1497
+ });
1498
+ }
1499
+
1500
+ // src/core/scheduling/cascade.ts
1501
+ function parseCascadeDateInput(date) {
1502
+ if (date instanceof Date) {
1503
+ return normalizeUTCDate(date);
1354
1504
  }
1355
- collectChildren(parentId);
1356
- return descendants;
1505
+ return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
1357
1506
  }
1358
- function cascadeTaskProgress(task, tasks) {
1359
- const progress = task.progress;
1360
- if (progress !== 0 && progress !== 100) {
1361
- return [task];
1507
+ function projectDayDelta(from, to, businessDays, weekendPredicate) {
1508
+ if (businessDays && weekendPredicate) {
1509
+ return getBusinessDayOffset(from, to, weekendPredicate);
1362
1510
  }
1363
- const descendants = getAllDescendants(task.id, tasks);
1364
- if (descendants.length === 0) {
1365
- return [task];
1511
+ return Math.round((to.getTime() - from.getTime()) / DAY_MS);
1512
+ }
1513
+ function shiftTaskActivities(activities, dayDelta, businessDays, weekendPredicate) {
1514
+ if (!activities || activities.length === 0 || dayDelta === 0) return void 0;
1515
+ const shiftDate = (value) => {
1516
+ const base = parseDateOnly(value);
1517
+ const shifted = businessDays && weekendPredicate ? shiftBusinessDayOffset(base, dayDelta, weekendPredicate) : new Date(base.getTime() + dayDelta * DAY_MS);
1518
+ return typeof value === "string" ? shifted.toISOString().slice(0, 10) : shifted;
1519
+ };
1520
+ return activities.map((activity) => ({
1521
+ ...activity,
1522
+ startDate: shiftDate(activity.startDate),
1523
+ endDate: shiftDate(activity.endDate)
1524
+ }));
1525
+ }
1526
+ function shiftedActivitiesPatch(activities, dayDelta, businessDays, weekendPredicate) {
1527
+ const shifted = shiftTaskActivities(activities, dayDelta, businessDays, weekendPredicate);
1528
+ return shifted ? { activities: shifted } : {};
1529
+ }
1530
+ function createCascadeContext(allTasks) {
1531
+ const childrenByParentId = /* @__PURE__ */ new Map();
1532
+ const dependentsByPredecessorId = /* @__PURE__ */ new Map();
1533
+ const taskById = /* @__PURE__ */ new Map();
1534
+ for (const task of allTasks) {
1535
+ taskById.set(task.id, task);
1536
+ if (task.parentId) {
1537
+ const children = childrenByParentId.get(task.parentId) ?? [];
1538
+ children.push(task);
1539
+ childrenByParentId.set(task.parentId, children);
1540
+ }
1541
+ if (!task.dependencies) continue;
1542
+ task.dependencies.forEach((dependency, dependencyIndex) => {
1543
+ const dependents = dependentsByPredecessorId.get(dependency.taskId) ?? [];
1544
+ dependents.push({ task, dependencyIndex });
1545
+ dependentsByPredecessorId.set(dependency.taskId, dependents);
1546
+ });
1366
1547
  }
1367
- return [
1368
- { ...task, progress },
1369
- ...descendants.map((descendant) => ({ ...descendant, progress }))
1370
- ];
1548
+ return { childrenByParentId, dependentsByPredecessorId, taskById };
1371
1549
  }
1372
- function getAllDependencyEdges(tasks) {
1373
- const edges = [];
1374
- for (const task of tasks) {
1375
- if (task.dependencies) {
1376
- for (const dep of task.dependencies) {
1377
- edges.push({
1378
- predecessorId: dep.taskId,
1379
- successorId: task.id,
1380
- type: dep.type,
1381
- lag: dep.lag ?? 0
1382
- });
1550
+ function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
1551
+ const { dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1552
+ const visited = /* @__PURE__ */ new Set();
1553
+ const queue = [draggedTaskId];
1554
+ const chain = [];
1555
+ visited.add(draggedTaskId);
1556
+ while (queue.length > 0) {
1557
+ const current = queue.shift();
1558
+ const successors = dependentsByPredecessorId.get(current) ?? [];
1559
+ for (const { task } of successors) {
1560
+ if (!linkTypes.includes(task.dependencies?.find((dependency) => dependency.taskId === current)?.type ?? "FS")) {
1561
+ continue;
1562
+ }
1563
+ const sid = task.id;
1564
+ if (!visited.has(sid)) {
1565
+ visited.add(sid);
1566
+ const successorTask = taskById.get(sid);
1567
+ if (successorTask) {
1568
+ chain.push(successorTask);
1569
+ queue.push(sid);
1570
+ }
1383
1571
  }
1384
1572
  }
1385
1573
  }
1386
- return edges;
1574
+ return chain;
1387
1575
  }
1388
- function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1389
- return tasks.map((task) => {
1390
- if (task.id === taskId1 || task.id === taskId2) {
1391
- if (!task.dependencies) return task;
1392
- const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
1393
- const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
1394
- if (filteredDependencies.length === task.dependencies.length) {
1395
- return task;
1576
+ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
1577
+ const { childrenByParentId, dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1578
+ const updatedDates = /* @__PURE__ */ new Map();
1579
+ updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
1580
+ const result = [];
1581
+ const queue = [movedTaskId];
1582
+ const visited = /* @__PURE__ */ new Set([movedTaskId]);
1583
+ while (queue.length > 0) {
1584
+ const currentId = queue.shift();
1585
+ const { start: predStart, end: predEnd } = updatedDates.get(currentId);
1586
+ if (!skipChildCascade) {
1587
+ const children = childrenByParentId.get(currentId) ?? [];
1588
+ for (const child of children) {
1589
+ if (visited.has(child.id) || child.locked) continue;
1590
+ const origStart = new Date(child.startDate);
1591
+ const origEnd = new Date(child.endDate);
1592
+ const durationMs = origEnd.getTime() - origStart.getTime();
1593
+ const parentOrig = taskById.get(currentId);
1594
+ const parentOrigStart = new Date(parentOrig.startDate);
1595
+ const parentOrigEnd = new Date(parentOrig.endDate);
1596
+ const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
1597
+ const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
1598
+ const newChildStart = new Date(origStart.getTime() + parentStartDelta);
1599
+ const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
1600
+ visited.add(child.id);
1601
+ updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
1602
+ result.push(normalizeTaskDependencyLags({
1603
+ ...child,
1604
+ startDate: newChildStart.toISOString().split("T")[0],
1605
+ endDate: newChildEnd.toISOString().split("T")[0],
1606
+ ...shiftedActivitiesPatch(child.activities, Math.round(parentStartDelta / DAY_MS), false)
1607
+ }));
1608
+ queue.push(child.id);
1396
1609
  }
1397
- return {
1610
+ }
1611
+ const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1612
+ for (const { task, dependencyIndex } of dependents) {
1613
+ if (visited.has(task.id) || !task.dependencies || task.locked) continue;
1614
+ const dep = task.dependencies[dependencyIndex];
1615
+ if (!dep) continue;
1616
+ const orig = taskById.get(task.id);
1617
+ const origStart = new Date(orig.startDate);
1618
+ const origEnd = new Date(orig.endDate);
1619
+ const duration = getTaskDuration(origStart, origEnd);
1620
+ const currentTask = taskById.get(currentId);
1621
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1622
+ {
1623
+ startDate: predStart,
1624
+ endDate: predEnd,
1625
+ type: currentTask.type
1626
+ },
1627
+ parseCascadeDateInput
1628
+ );
1629
+ const constraintDate = calculateSuccessorDate(
1630
+ normalizedPredStart,
1631
+ normalizedPredEnd,
1632
+ dep.type,
1633
+ getDependencyLag(dep),
1634
+ false,
1635
+ void 0,
1636
+ task.type
1637
+ );
1638
+ let newSuccStart;
1639
+ let newSuccEnd;
1640
+ if (dep.type === "FS" || dep.type === "SS") {
1641
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
1642
+ } else {
1643
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
1644
+ }
1645
+ visited.add(task.id);
1646
+ updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
1647
+ result.push(normalizeTaskDependencyLags({
1398
1648
  ...task,
1399
- dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
1400
- };
1649
+ startDate: newSuccStart.toISOString().split("T")[0],
1650
+ endDate: newSuccEnd.toISOString().split("T")[0]
1651
+ }));
1652
+ queue.push(task.id);
1401
1653
  }
1402
- return task;
1403
- });
1404
- }
1405
- function findParentId(taskId, tasks) {
1406
- const task = tasks.find((t) => t.id === taskId);
1407
- return task?.parentId;
1654
+ }
1655
+ return result;
1408
1656
  }
1409
- function isAncestorTask(ancestorId, taskId, tasks) {
1410
- const taskById = new Map(tasks.map((task) => [task.id, task]));
1411
- const visited = /* @__PURE__ */ new Set();
1412
- let current = taskById.get(taskId);
1413
- while (current?.parentId) {
1414
- if (current.parentId === ancestorId) {
1415
- return true;
1657
+ function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
1658
+ const { childrenByParentId, dependentsByPredecessorId } = createCascadeContext(allTasks);
1659
+ const directChildren = childrenByParentId.get(changedTaskId) ?? [];
1660
+ const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
1661
+ const initialChain = [...directChildren, ...directSuccessors].filter(
1662
+ (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
1663
+ );
1664
+ const chain = [...initialChain];
1665
+ const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
1666
+ const queue = [...initialChain];
1667
+ while (queue.length > 0) {
1668
+ const current = queue.shift();
1669
+ const children = childrenByParentId.get(current.id) ?? [];
1670
+ for (const child of children) {
1671
+ if (!visited.has(child.id)) {
1672
+ visited.add(child.id);
1673
+ chain.push(child);
1674
+ queue.push(child);
1675
+ }
1416
1676
  }
1417
- if (visited.has(current.parentId)) {
1418
- return false;
1677
+ const successors = dependentsByPredecessorId.get(current.id) ?? [];
1678
+ for (const { task: successor } of successors) {
1679
+ if (!visited.has(successor.id)) {
1680
+ visited.add(successor.id);
1681
+ chain.push(successor);
1682
+ queue.push(successor);
1683
+ }
1419
1684
  }
1420
- visited.add(current.parentId);
1421
- current = taskById.get(current.parentId);
1422
- }
1423
- return false;
1424
- }
1425
- function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1426
- if (taskId1 === taskId2) {
1427
- return true;
1428
1685
  }
1429
- return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1430
- }
1431
-
1432
- // src/core/scheduling/execute.ts
1433
- function toIsoDate(date) {
1434
- return date.toISOString().split("T")[0];
1435
- }
1436
- function createChangedResult(snapshot, nextTasks) {
1437
- const originalById = new Map(snapshot.map((task) => [task.id, task]));
1438
- const changedTasks = nextTasks.filter((task) => JSON.stringify(originalById.get(task.id)) !== JSON.stringify(task));
1439
- return {
1440
- changedTasks,
1441
- changedIds: changedTasks.map((task) => task.id)
1442
- };
1686
+ return chain;
1443
1687
  }
1444
- function moveTaskWithCascade(taskId, newStart, snapshot, options) {
1445
- const task = snapshot.find((t) => t.id === taskId);
1446
- if (!task) {
1447
- return { changedTasks: [], changedIds: [] };
1448
- }
1449
- const businessDays = options?.businessDays ?? false;
1450
- const weekendPredicate = options?.weekendPredicate;
1451
- const newRange = moveTaskRange(
1452
- task.startDate,
1453
- task.endDate,
1454
- newStart,
1455
- businessDays,
1456
- weekendPredicate
1457
- );
1458
- const updatedDependencies = recalculateIncomingLags(
1459
- task,
1460
- newRange.start,
1461
- newRange.end,
1462
- snapshot,
1463
- businessDays,
1464
- weekendPredicate
1465
- );
1466
- const movedTask = {
1467
- ...task,
1468
- startDate: newRange.start.toISOString().split("T")[0],
1469
- endDate: newRange.end.toISOString().split("T")[0],
1470
- dependencies: updatedDependencies
1471
- };
1472
- if (options?.skipCascade) {
1473
- return {
1474
- changedTasks: [movedTask],
1475
- changedIds: [movedTask.id]
1476
- };
1688
+ function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate, context = createCascadeContext(allTasks)) {
1689
+ const { childrenByParentId, dependentsByPredecessorId, taskById } = context;
1690
+ const normalizedMovedTask = normalizeTaskDependencyLags({
1691
+ ...movedTask,
1692
+ startDate: newStart.toISOString().split("T")[0],
1693
+ endDate: newEnd.toISOString().split("T")[0]
1694
+ });
1695
+ if (!movedTask.parentId && !childrenByParentId.has(movedTask.id) && !dependentsByPredecessorId.has(movedTask.id)) {
1696
+ return [normalizedMovedTask];
1477
1697
  }
1478
- const cascadeResult = universalCascade(
1479
- movedTask,
1480
- newRange.start,
1481
- newRange.end,
1482
- snapshot,
1483
- businessDays,
1484
- weekendPredicate
1485
- );
1698
+ const updatedDates = /* @__PURE__ */ new Map();
1699
+ updatedDates.set(movedTask.id, { start: newStart, end: newEnd });
1486
1700
  const resultMap = /* @__PURE__ */ new Map();
1487
- resultMap.set(movedTask.id, movedTask);
1488
- for (const t of cascadeResult) {
1489
- resultMap.set(t.id, t);
1701
+ resultMap.set(movedTask.id, normalizedMovedTask);
1702
+ const queue = [[movedTask.id, "direct"]];
1703
+ const childShifted = /* @__PURE__ */ new Set();
1704
+ let iterations = 0;
1705
+ const MAX_ITERATIONS = allTasks.length * 3;
1706
+ while (queue.length > 0 && iterations < MAX_ITERATIONS) {
1707
+ iterations++;
1708
+ const [currentId, arrivalMode] = queue.shift();
1709
+ const { start: currStart, end: currEnd } = updatedDates.get(currentId);
1710
+ const currentOriginal = taskById.get(currentId);
1711
+ if (arrivalMode !== "parent-recalc") {
1712
+ const children = childrenByParentId.get(currentId) ?? [];
1713
+ for (const child of children) {
1714
+ if (childShifted.has(child.id) || child.locked) continue;
1715
+ const parentOrigStart = new Date(currentOriginal.startDate);
1716
+ const parentOrigEnd = new Date(currentOriginal.endDate);
1717
+ const childOrigStart = new Date(child.startDate);
1718
+ const childOrigEnd = new Date(child.endDate);
1719
+ const startDeltaMs = currStart.getTime() - parentOrigStart.getTime();
1720
+ const endDeltaMs = currEnd.getTime() - parentOrigEnd.getTime();
1721
+ let childNewStart;
1722
+ let childNewEnd;
1723
+ if (businessDays && weekendPredicate) {
1724
+ const proposedStart = new Date(childOrigStart.getTime() + startDeltaMs);
1725
+ const snapDirection = currStart.getTime() >= parentOrigStart.getTime() ? 1 : -1;
1726
+ const movedRange = moveTaskRange(
1727
+ child.startDate,
1728
+ child.endDate,
1729
+ proposedStart,
1730
+ true,
1731
+ weekendPredicate,
1732
+ snapDirection
1733
+ );
1734
+ childNewStart = movedRange.start;
1735
+ childNewEnd = movedRange.end;
1736
+ } else {
1737
+ childNewStart = new Date(childOrigStart.getTime() + startDeltaMs);
1738
+ childNewEnd = new Date(childOrigEnd.getTime() + endDeltaMs);
1739
+ }
1740
+ const prev = updatedDates.get(child.id);
1741
+ if (prev && prev.start.getTime() === childNewStart.getTime() && prev.end.getTime() === childNewEnd.getTime()) {
1742
+ continue;
1743
+ }
1744
+ updatedDates.set(child.id, { start: childNewStart, end: childNewEnd });
1745
+ childShifted.add(child.id);
1746
+ queue.push([child.id, "child-delta"]);
1747
+ resultMap.set(child.id, normalizeTaskDependencyLags({
1748
+ ...child,
1749
+ startDate: childNewStart.toISOString().split("T")[0],
1750
+ endDate: childNewEnd.toISOString().split("T")[0],
1751
+ // Дети едут параллельно: вместе с датами строки сдвигаются и её работы.
1752
+ ...shiftedActivitiesPatch(
1753
+ child.activities,
1754
+ projectDayDelta(childOrigStart, childNewStart, businessDays, weekendPredicate),
1755
+ businessDays,
1756
+ weekendPredicate
1757
+ )
1758
+ }));
1759
+ }
1760
+ }
1761
+ const parentId = currentOriginal.parentId;
1762
+ if (parentId && parentId !== movedTask.id) {
1763
+ const parent = taskById.get(parentId);
1764
+ if (parent && !parent.locked) {
1765
+ const siblings = childrenByParentId.get(parentId) ?? [];
1766
+ const siblingPositions = siblings.map((sib) => {
1767
+ if (updatedDates.has(sib.id)) return updatedDates.get(sib.id);
1768
+ return { start: new Date(sib.startDate), end: new Date(sib.endDate) };
1769
+ });
1770
+ const minStart = new Date(Math.min(...siblingPositions.map((p) => p.start.getTime())));
1771
+ const maxEnd = new Date(Math.max(...siblingPositions.map((p) => p.end.getTime())));
1772
+ const prev = updatedDates.get(parentId);
1773
+ if (!prev || prev.start.getTime() !== minStart.getTime() || prev.end.getTime() !== maxEnd.getTime()) {
1774
+ updatedDates.set(parentId, { start: minStart, end: maxEnd });
1775
+ queue.push([parentId, "parent-recalc"]);
1776
+ resultMap.set(parentId, normalizeTaskDependencyLags({
1777
+ ...parent,
1778
+ startDate: minStart.toISOString().split("T")[0],
1779
+ endDate: maxEnd.toISOString().split("T")[0]
1780
+ }));
1781
+ }
1782
+ }
1783
+ }
1784
+ const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1785
+ for (const { task, dependencyIndex } of dependents) {
1786
+ if (task.locked || !task.dependencies) continue;
1787
+ if (childShifted.has(task.id)) continue;
1788
+ const dep = task.dependencies[dependencyIndex];
1789
+ if (!dep) continue;
1790
+ const origStart = new Date(task.startDate);
1791
+ const origEnd = new Date(task.endDate);
1792
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1793
+ {
1794
+ startDate: currStart,
1795
+ endDate: currEnd,
1796
+ type: currentOriginal.type
1797
+ },
1798
+ parseCascadeDateInput
1799
+ );
1800
+ const constraintDate = calculateSuccessorDate(
1801
+ normalizedPredStart,
1802
+ normalizedPredEnd,
1803
+ dep.type,
1804
+ getDependencyLag(dep),
1805
+ businessDays,
1806
+ weekendPredicate,
1807
+ task.type
1808
+ );
1809
+ let succNewStart;
1810
+ let succNewEnd;
1811
+ const duration = getTaskDuration(origStart, origEnd, businessDays, weekendPredicate);
1812
+ if (dep.type === "FS" || dep.type === "SS") {
1813
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromStart(
1814
+ constraintDate,
1815
+ duration,
1816
+ businessDays,
1817
+ weekendPredicate
1818
+ ));
1819
+ } else {
1820
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromEnd(
1821
+ constraintDate,
1822
+ duration,
1823
+ businessDays,
1824
+ weekendPredicate
1825
+ ));
1826
+ }
1827
+ const prev = updatedDates.get(task.id);
1828
+ if (prev && prev.start.getTime() === succNewStart.getTime() && prev.end.getTime() === succNewEnd.getTime()) {
1829
+ continue;
1830
+ }
1831
+ updatedDates.set(task.id, { start: succNewStart, end: succNewEnd });
1832
+ queue.push([task.id, "dependency"]);
1833
+ resultMap.set(task.id, normalizeTaskDependencyLags({
1834
+ ...task,
1835
+ startDate: succNewStart.toISOString().split("T")[0],
1836
+ endDate: succNewEnd.toISOString().split("T")[0],
1837
+ ...shiftedActivitiesPatch(
1838
+ task.activities,
1839
+ projectDayDelta(origStart, succNewStart, businessDays, weekendPredicate),
1840
+ businessDays,
1841
+ weekendPredicate
1842
+ )
1843
+ }));
1844
+ }
1490
1845
  }
1491
- const changedTasks = Array.from(resultMap.values());
1846
+ return Array.from(resultMap.values());
1847
+ }
1848
+
1849
+ // src/core/scheduling/hierarchy.ts
1850
+ function getChildren(parentId, tasks) {
1851
+ return tasks.filter((t) => t.parentId === parentId);
1852
+ }
1853
+ function isTaskParent(taskId, tasks) {
1854
+ return tasks.some((t) => t.parentId === taskId);
1855
+ }
1856
+ function computeParentDates(parentId, tasks) {
1857
+ const children = getChildren(parentId, tasks);
1858
+ if (children.length === 0) {
1859
+ const parent = tasks.find((t) => t.id === parentId);
1860
+ const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
1861
+ const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
1862
+ return { startDate: start, endDate: end };
1863
+ }
1864
+ const startDates = children.map((c) => new Date(c.startDate));
1865
+ const endDates = children.map((c) => new Date(c.endDate));
1866
+ const minTime = Math.min(...startDates.map((d) => d.getTime()));
1867
+ const maxTime = Math.max(...endDates.map((d) => d.getTime()));
1492
1868
  return {
1493
- changedTasks,
1494
- changedIds: changedTasks.map((t) => t.id)
1869
+ startDate: new Date(minTime),
1870
+ endDate: new Date(maxTime)
1495
1871
  };
1496
1872
  }
1497
- function resizeTaskWithCascade(taskId, anchor, newDate, snapshot, options) {
1498
- const task = snapshot.find((t) => t.id === taskId);
1499
- if (!task) {
1500
- return { changedTasks: [], changedIds: [] };
1501
- }
1502
- const businessDays = options?.businessDays ?? false;
1503
- const weekendPredicate = options?.weekendPredicate;
1504
- const originalStart = parseDateOnly(task.startDate);
1505
- const originalEnd = parseDateOnly(task.endDate);
1506
- let newRange;
1507
- if (anchor === "start") {
1508
- newRange = { start: originalStart, end: newDate };
1509
- } else {
1510
- newRange = { start: newDate, end: originalEnd };
1873
+ function computeParentProgress(parentId, tasks) {
1874
+ const children = getChildren(parentId, tasks);
1875
+ if (children.length === 0) {
1876
+ return 0;
1511
1877
  }
1512
- const updatedDependencies = recalculateIncomingLags(
1513
- task,
1514
- newRange.start,
1515
- newRange.end,
1516
- snapshot,
1517
- businessDays,
1518
- weekendPredicate
1519
- );
1520
- const resizedTask = {
1521
- ...task,
1522
- startDate: newRange.start.toISOString().split("T")[0],
1523
- endDate: newRange.end.toISOString().split("T")[0],
1524
- dependencies: updatedDependencies
1525
- };
1526
- if (options?.skipCascade) {
1527
- return {
1528
- changedTasks: [resizedTask],
1529
- changedIds: [resizedTask.id]
1530
- };
1878
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1879
+ let totalWeight = 0;
1880
+ let weightedSum = 0;
1881
+ for (const child of children) {
1882
+ const start = new Date(child.startDate).getTime();
1883
+ const end = new Date(child.endDate).getTime();
1884
+ const duration = (end - start + DAY_MS7) / DAY_MS7;
1885
+ const progress = child.progress ?? 0;
1886
+ totalWeight += duration;
1887
+ weightedSum += duration * progress;
1531
1888
  }
1532
- const cascadeResult = universalCascade(
1533
- resizedTask,
1534
- newRange.start,
1535
- newRange.end,
1536
- snapshot,
1537
- businessDays,
1538
- weekendPredicate
1539
- );
1540
- const resultMap = /* @__PURE__ */ new Map();
1541
- resultMap.set(resizedTask.id, resizedTask);
1542
- for (const t of cascadeResult) {
1543
- resultMap.set(t.id, t);
1889
+ if (totalWeight === 0) {
1890
+ return 0;
1544
1891
  }
1545
- const changedTasks = Array.from(resultMap.values());
1546
- return {
1547
- changedTasks,
1548
- changedIds: changedTasks.map((t) => t.id)
1549
- };
1892
+ return Math.round(weightedSum / totalWeight * 10) / 10;
1550
1893
  }
1551
- function recalculateTaskFromDependencies(taskId, snapshot, options) {
1552
- const task = snapshot.find((t) => t.id === taskId);
1553
- if (!task) {
1554
- return { changedTasks: [], changedIds: [] };
1894
+ function getAllDescendants(parentId, tasks) {
1895
+ const descendants = [];
1896
+ const visited = /* @__PURE__ */ new Set();
1897
+ function collectChildren(taskId) {
1898
+ if (visited.has(taskId)) return;
1899
+ visited.add(taskId);
1900
+ const children = getChildren(taskId, tasks);
1901
+ for (const child of children) {
1902
+ descendants.push(child);
1903
+ collectChildren(child.id);
1904
+ }
1555
1905
  }
1556
- const businessDays = options?.businessDays ?? false;
1906
+ collectChildren(parentId);
1907
+ return descendants;
1908
+ }
1909
+ function cascadeTaskProgress(task, tasks) {
1910
+ const progress = task.progress;
1911
+ if (progress !== 0 && progress !== 100) {
1912
+ return [task];
1913
+ }
1914
+ const descendants = getAllDescendants(task.id, tasks);
1915
+ if (descendants.length === 0) {
1916
+ return [task];
1917
+ }
1918
+ return [
1919
+ { ...task, progress },
1920
+ ...descendants.map((descendant) => ({ ...descendant, progress }))
1921
+ ];
1922
+ }
1923
+ function getAllDependencyEdges(tasks) {
1924
+ const edges = [];
1925
+ for (const task of tasks) {
1926
+ if (task.dependencies) {
1927
+ for (const dep of task.dependencies) {
1928
+ edges.push({
1929
+ predecessorId: dep.taskId,
1930
+ successorId: task.id,
1931
+ type: dep.type,
1932
+ lag: dep.lag ?? 0
1933
+ });
1934
+ }
1935
+ }
1936
+ }
1937
+ return edges;
1938
+ }
1939
+ function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1940
+ return tasks.map((task) => {
1941
+ if (task.id === taskId1 || task.id === taskId2) {
1942
+ if (!task.dependencies) return task;
1943
+ const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
1944
+ const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
1945
+ if (filteredDependencies.length === task.dependencies.length) {
1946
+ return task;
1947
+ }
1948
+ return {
1949
+ ...task,
1950
+ dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
1951
+ };
1952
+ }
1953
+ return task;
1954
+ });
1955
+ }
1956
+ function findParentId(taskId, tasks) {
1957
+ const task = tasks.find((t) => t.id === taskId);
1958
+ return task?.parentId;
1959
+ }
1960
+ function isAncestorTask(ancestorId, taskId, tasks) {
1961
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
1962
+ const visited = /* @__PURE__ */ new Set();
1963
+ let current = taskById.get(taskId);
1964
+ while (current?.parentId) {
1965
+ if (current.parentId === ancestorId) {
1966
+ return true;
1967
+ }
1968
+ if (visited.has(current.parentId)) {
1969
+ return false;
1970
+ }
1971
+ visited.add(current.parentId);
1972
+ current = taskById.get(current.parentId);
1973
+ }
1974
+ return false;
1975
+ }
1976
+ function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1977
+ if (taskId1 === taskId2) {
1978
+ return true;
1979
+ }
1980
+ return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1981
+ }
1982
+
1983
+ // src/core/scheduling/execute.ts
1984
+ function toIsoDate(date) {
1985
+ return date.toISOString().split("T")[0];
1986
+ }
1987
+ function createChangedResult(snapshot, nextTasks) {
1988
+ const originalById = new Map(snapshot.map((task) => [task.id, task]));
1989
+ const changedTasks = nextTasks.filter((task) => JSON.stringify(originalById.get(task.id)) !== JSON.stringify(task));
1990
+ return {
1991
+ changedTasks,
1992
+ changedIds: changedTasks.map((task) => task.id)
1993
+ };
1994
+ }
1995
+ function moveTaskWithCascade(taskId, newStart, snapshot, options) {
1996
+ const task = snapshot.find((t) => t.id === taskId);
1997
+ if (!task) {
1998
+ return { changedTasks: [], changedIds: [] };
1999
+ }
2000
+ const businessDays = options?.businessDays ?? false;
2001
+ const weekendPredicate = options?.weekendPredicate;
2002
+ const newRange = moveTaskRange(
2003
+ task.startDate,
2004
+ task.endDate,
2005
+ newStart,
2006
+ businessDays,
2007
+ weekendPredicate
2008
+ );
2009
+ const updatedDependencies = recalculateIncomingLags(
2010
+ task,
2011
+ newRange.start,
2012
+ newRange.end,
2013
+ snapshot,
2014
+ businessDays,
2015
+ weekendPredicate
2016
+ );
2017
+ const movedTask = {
2018
+ ...task,
2019
+ startDate: newRange.start.toISOString().split("T")[0],
2020
+ endDate: newRange.end.toISOString().split("T")[0],
2021
+ dependencies: updatedDependencies
2022
+ };
2023
+ if (options?.skipCascade) {
2024
+ return {
2025
+ changedTasks: [movedTask],
2026
+ changedIds: [movedTask.id]
2027
+ };
2028
+ }
2029
+ const cascadeResult = universalCascade(
2030
+ movedTask,
2031
+ newRange.start,
2032
+ newRange.end,
2033
+ snapshot,
2034
+ businessDays,
2035
+ weekendPredicate
2036
+ );
2037
+ const resultMap = /* @__PURE__ */ new Map();
2038
+ resultMap.set(movedTask.id, movedTask);
2039
+ for (const t of cascadeResult) {
2040
+ resultMap.set(t.id, t);
2041
+ }
2042
+ const changedTasks = Array.from(resultMap.values());
2043
+ return {
2044
+ changedTasks,
2045
+ changedIds: changedTasks.map((t) => t.id)
2046
+ };
2047
+ }
2048
+ function resizeTaskWithCascade(taskId, anchor, newDate, snapshot, options) {
2049
+ const task = snapshot.find((t) => t.id === taskId);
2050
+ if (!task) {
2051
+ return { changedTasks: [], changedIds: [] };
2052
+ }
2053
+ const businessDays = options?.businessDays ?? false;
2054
+ const weekendPredicate = options?.weekendPredicate;
2055
+ const originalStart = parseDateOnly(task.startDate);
2056
+ const originalEnd = parseDateOnly(task.endDate);
2057
+ let newRange;
2058
+ if (anchor === "start") {
2059
+ newRange = { start: originalStart, end: newDate };
2060
+ } else {
2061
+ newRange = { start: newDate, end: originalEnd };
2062
+ }
2063
+ const updatedDependencies = recalculateIncomingLags(
2064
+ task,
2065
+ newRange.start,
2066
+ newRange.end,
2067
+ snapshot,
2068
+ businessDays,
2069
+ weekendPredicate
2070
+ );
2071
+ const resizedTask = {
2072
+ ...task,
2073
+ startDate: newRange.start.toISOString().split("T")[0],
2074
+ endDate: newRange.end.toISOString().split("T")[0],
2075
+ dependencies: updatedDependencies
2076
+ };
2077
+ if (options?.skipCascade) {
2078
+ return {
2079
+ changedTasks: [resizedTask],
2080
+ changedIds: [resizedTask.id]
2081
+ };
2082
+ }
2083
+ const cascadeResult = universalCascade(
2084
+ resizedTask,
2085
+ newRange.start,
2086
+ newRange.end,
2087
+ snapshot,
2088
+ businessDays,
2089
+ weekendPredicate
2090
+ );
2091
+ const resultMap = /* @__PURE__ */ new Map();
2092
+ resultMap.set(resizedTask.id, resizedTask);
2093
+ for (const t of cascadeResult) {
2094
+ resultMap.set(t.id, t);
2095
+ }
2096
+ const changedTasks = Array.from(resultMap.values());
2097
+ return {
2098
+ changedTasks,
2099
+ changedIds: changedTasks.map((t) => t.id)
2100
+ };
2101
+ }
2102
+ function recalculateTaskFromDependencies(taskId, snapshot, options) {
2103
+ const task = snapshot.find((t) => t.id === taskId);
2104
+ if (!task) {
2105
+ return { changedTasks: [], changedIds: [] };
2106
+ }
2107
+ const businessDays = options?.businessDays ?? false;
1557
2108
  const weekendPredicate = options?.weekendPredicate;
1558
2109
  if (!task.dependencies || task.dependencies.length === 0) {
1559
2110
  return {
@@ -3036,7 +3587,11 @@ function flattenHierarchy(tasks) {
3036
3587
  }
3037
3588
  function normalizeHierarchyTasks(tasks) {
3038
3589
  const orderedTasks = flattenHierarchy(tasks).map((task) => {
3039
- const { startDate, endDate } = normalizeTaskDates(task.startDate, task.endDate);
3590
+ const bounds = activityBounds(task.activities);
3591
+ const { startDate, endDate } = normalizeTaskDates(
3592
+ bounds?.startDate ?? task.startDate,
3593
+ bounds?.endDate ?? task.endDate
3594
+ );
3040
3595
  return { ...task, startDate, endDate };
3041
3596
  });
3042
3597
  const childrenByParent = /* @__PURE__ */ new Map();
@@ -3047,7 +3602,7 @@ function normalizeHierarchyTasks(tasks) {
3047
3602
  children.push(index);
3048
3603
  childrenByParent.set(parentId, children);
3049
3604
  }
3050
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
3605
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
3051
3606
  for (let index = orderedTasks.length - 1; index >= 0; index--) {
3052
3607
  const children = childrenByParent.get(orderedTasks[index].id);
3053
3608
  if (!children?.length) continue;
@@ -3061,7 +3616,7 @@ function normalizeHierarchyTasks(tasks) {
3061
3616
  const end = new Date(child.endDate).getTime();
3062
3617
  minStart = Math.min(minStart, start);
3063
3618
  maxEnd = Math.max(maxEnd, end);
3064
- const duration = (end - start + DAY_MS6) / DAY_MS6;
3619
+ const duration = (end - start + DAY_MS7) / DAY_MS7;
3065
3620
  totalWeight += duration;
3066
3621
  weightedProgress += duration * (child.progress ?? 0);
3067
3622
  }
@@ -3217,361 +3772,96 @@ var TimeScaleHeader = ({
3217
3772
  },
3218
3773
  `month-${index}`
3219
3774
  ))
3220
- )
3221
- }
3222
- ),
3223
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3224
- "div",
3225
- {
3226
- className: "gantt-tsh-dayRow",
3227
- style: {
3228
- height: `${rowHeight}px`,
3229
- gridTemplateColumns: viewMode === "week" ? weekGridTemplate : viewMode === "month" ? monthGridTemplate : dayGridTemplate
3230
- },
3231
- children: viewMode === "week" ? (
3232
- // Week-view row 2: week block start day numbers (variable width)
3233
- weekBlocks.map((block, index) => {
3234
- const prevBlock = weekBlocks[index - 1];
3235
- const isMonthBoundary = index > 0 && prevBlock && prevBlock.startDate.getUTCMonth() !== block.startDate.getUTCMonth();
3236
- const showDate = block.days === 7;
3237
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3238
- "div",
3239
- {
3240
- className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3241
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: showDate ? String(block.startDate.getUTCDate()).padStart(2, "0") : "" })
3242
- },
3243
- `week-${index}`
3244
- );
3245
- })
3246
- ) : viewMode === "month" ? (
3247
- // Month-view row 2: one column per month, shows abbreviated month name
3248
- monthBlocks.map((block, index) => {
3249
- const MIN_DAYS_TO_SHOW_LABEL = 15;
3250
- const showLabel = block.days >= MIN_DAYS_TO_SHOW_LABEL;
3251
- const isYearBoundary = index > 0 && block.startDate.getUTCMonth() === 0;
3252
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3253
- "div",
3254
- {
3255
- className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3256
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: showLabel ? (() => {
3257
- const s = block.startDate.toLocaleString("ru-RU", { month: "long", timeZone: "UTC" });
3258
- return s.charAt(0).toUpperCase() + s.slice(1);
3259
- })() : "" })
3260
- },
3261
- `mblock-${index}`
3262
- );
3263
- })
3264
- ) : (
3265
- // Day-view row 2: individual day numbers (existing code)
3266
- days.map((day, index) => {
3267
- const isWeekendDay = isCustomWeekend ? isCustomWeekend(day) : day.getUTCDay() === 0 || day.getUTCDay() === 6;
3268
- const prevDay = days[index - 1];
3269
- const isMonthBoundary = index > 0 && prevDay && prevDay.getUTCMonth() !== day.getUTCMonth();
3270
- const isTodayDate = day.getUTCFullYear() === today.getUTCFullYear() && day.getUTCMonth() === today.getUTCMonth() && day.getUTCDate() === today.getUTCDate();
3271
- const markerKey = `${day.getUTCFullYear()}-${day.getUTCMonth()}-${day.getUTCDate()}`;
3272
- const marker = markerByDayKey.get(markerKey);
3273
- const markerColor = marker?.color;
3274
- const formattedDate = `${String(day.getUTCDate()).padStart(2, "0")}.${String(day.getUTCMonth() + 1).padStart(2, "0")}.${String(day.getUTCFullYear()).slice(-2)}`;
3275
- const tooltipText = isTodayDate ? `${formattedDate} \u2014 \u0421\u0435\u0433\u043E\u0434\u043D\u044F` : marker?.name ? `${formattedDate} \u2014 ${marker.name}` : void 0;
3276
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3277
- "div",
3278
- {
3279
- className: `gantt-tsh-dayCell ${isWeekendDay ? "gantt-tsh-weekendDay" : ""} ${isTodayDate ? "gantt-tsh-today" : ""} ${marker && !isTodayDate ? "gantt-tsh-markerDay" : ""} ${tooltipText ? "gantt-tsh-dayCell-tooltipTrigger" : ""}`,
3280
- style: markerColor && !isTodayDate ? {
3281
- ["--gantt-marker-day-color"]: markerColor
3282
- } : void 0,
3283
- "aria-label": tooltipText,
3284
- onMouseEnter: () => {
3285
- if (!tooltipText) return;
3286
- onTimelineHover?.({
3287
- label: tooltipText,
3288
- left: index * dayWidth,
3289
- color: isTodayDate ? "#dc2626" : markerColor || "var(--gantt-timeline-marker-color, #dc2626)"
3290
- });
3291
- },
3292
- onMouseLeave: onTimelineHoverEnd,
3293
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: (0, import_date_fns.format)(day, "d") })
3294
- },
3295
- `day-${index}`
3296
- );
3297
- })
3298
- )
3299
- }
3300
- )
3301
- ]
3302
- }
3303
- );
3304
- };
3305
- var TimeScaleHeader_default = TimeScaleHeader;
3306
-
3307
- // src/components/TaskRow/TaskRow.tsx
3308
- var import_react3 = __toESM(require("react"));
3309
- var import_react_dom = require("react-dom");
3310
-
3311
- // src/utils/geometry.ts
3312
- var getUTCDayDifference = (date1, date2) => {
3313
- const ms1 = Date.UTC(
3314
- date1.getUTCFullYear(),
3315
- date1.getUTCMonth(),
3316
- date1.getUTCDate()
3317
- );
3318
- const ms2 = Date.UTC(
3319
- date2.getUTCFullYear(),
3320
- date2.getUTCMonth(),
3321
- date2.getUTCDate()
3322
- );
3323
- return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
3324
- };
3325
- var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
3326
- const startOffset = getUTCDayDifference(taskStartDate, monthStart);
3327
- const duration = getUTCDayDifference(taskEndDate, taskStartDate);
3328
- const left = Math.round(startOffset * dayWidth);
3329
- const width = Math.round((duration + 1) * dayWidth);
3330
- return { left, width };
3331
- };
3332
- var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
3333
- const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
3334
- const centerX = Math.round(left + width / 2);
3335
- const halfSize = Math.round(size / 2);
3336
- return {
3337
- centerX,
3338
- left: centerX - halfSize,
3339
- right: centerX + halfSize,
3340
- size
3341
- };
3342
- };
3343
- var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
3344
- const halfDiagonal = Math.round(size / Math.SQRT2);
3345
- const centerX = Math.round(dayLeft + dayWidth / 2);
3346
- return {
3347
- left: centerX - halfDiagonal,
3348
- right: centerX + halfDiagonal
3349
- };
3350
- };
3351
- var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
3352
- const startDate = new Date(task.startDate);
3353
- const endDate = new Date(task.endDate);
3354
- if (task.type === "milestone") {
3355
- const size = 14;
3356
- if (override) {
3357
- const bounds2 = calculateMilestoneConnectionBounds(override.left, dayWidth, size);
3358
- return {
3359
- ...bounds2,
3360
- centerX: Math.round(override.left + dayWidth / 2)
3361
- };
3362
- }
3363
- const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
3364
- const bounds = calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
3365
- return {
3366
- ...bounds,
3367
- centerX: Math.round(bar2.left + dayWidth / 2)
3368
- };
3369
- }
3370
- if (override) {
3371
- return {
3372
- left: override.left,
3373
- right: override.left + override.width,
3374
- centerX: Math.round(override.left + override.width / 2)
3375
- };
3376
- }
3377
- const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3378
- return {
3379
- left: bar.left,
3380
- right: bar.left + bar.width,
3381
- centerX: Math.round(bar.left + bar.width / 2)
3382
- };
3383
- };
3384
- var pixelsToDate = (pixels, monthStart, dayWidth) => {
3385
- const days = Math.round(pixels / dayWidth);
3386
- return new Date(Date.UTC(
3387
- monthStart.getUTCFullYear(),
3388
- monthStart.getUTCMonth(),
3389
- monthStart.getUTCDate() + days
3390
- ));
3391
- };
3392
- var calculateGridWidth = (daysInMonth, dayWidth) => {
3393
- return Math.round(daysInMonth * dayWidth);
3394
- };
3395
- var MIN_EDGE_ZONE_PX = 8;
3396
- var MIN_MOVE_ZONE_PX = 16;
3397
- var detectEdgeZone = (clientX, taskBarElement, edgeZoneWidth = 12) => {
3398
- const rect = taskBarElement.getBoundingClientRect();
3399
- const relativeX = Math.round(clientX - rect.left);
3400
- const width = Math.round(rect.width);
3401
- const effectiveZone = Math.max(
3402
- MIN_EDGE_ZONE_PX,
3403
- Math.min(edgeZoneWidth, (width - MIN_MOVE_ZONE_PX) / 2)
3404
- );
3405
- if (relativeX >= 0 && relativeX <= effectiveZone) {
3406
- return "left";
3407
- }
3408
- if (relativeX >= width - effectiveZone && relativeX <= width) {
3409
- return "right";
3410
- }
3411
- return "move";
3412
- };
3413
- var getCursorForPosition = (position) => {
3414
- switch (position) {
3415
- case "left":
3416
- case "right":
3417
- return "ew-resize";
3418
- case "move":
3419
- return "grab";
3420
- default:
3421
- return "default";
3422
- }
3423
- };
3424
- var calculateGridLines = (dateRange, dayWidth) => {
3425
- const lines = [];
3426
- for (let i = 0; i < dateRange.length; i++) {
3427
- const date = dateRange[i];
3428
- const x = Math.round(i * dayWidth);
3429
- const isMonthStart = i === 0 ? false : date.getUTCDate() === 1;
3430
- const isWeekStart = date.getUTCDay() === 1;
3431
- lines.push({ x, isMonthStart, isWeekStart });
3432
- }
3433
- if (dateRange.length > 0) {
3434
- lines.push({
3435
- x: Math.round(dateRange.length * dayWidth),
3436
- isMonthStart: false,
3437
- isWeekStart: false
3438
- });
3439
- }
3440
- return lines;
3441
- };
3442
- var calculateWeekendBlocks = (dateRange, dayWidth, isCustomWeekend) => {
3443
- const blocks = [];
3444
- let inWeekend = false;
3445
- let weekendStartIndex = -1;
3446
- for (let i = 0; i < dateRange.length; i++) {
3447
- const date = dateRange[i];
3448
- const isWeekend3 = isCustomWeekend ? isCustomWeekend(date) : date.getUTCDay() === 0 || date.getUTCDay() === 6;
3449
- if (isWeekend3 && !inWeekend) {
3450
- inWeekend = true;
3451
- weekendStartIndex = i;
3452
- } else if (!isWeekend3 && inWeekend) {
3453
- inWeekend = false;
3454
- const left = Math.round(weekendStartIndex * dayWidth);
3455
- const width = Math.round((i - weekendStartIndex) * dayWidth);
3456
- blocks.push({ left, width });
3457
- }
3458
- }
3459
- if (inWeekend && weekendStartIndex >= 0) {
3460
- const left = Math.round(weekendStartIndex * dayWidth);
3461
- const width = Math.round((dateRange.length - weekendStartIndex) * dayWidth);
3462
- blocks.push({ left, width });
3463
- }
3464
- return blocks;
3465
- };
3466
- var calculateBezierPath = (from, to) => {
3467
- const verticalDistance = Math.abs(to.y - from.y);
3468
- const cpOffset = Math.max(verticalDistance * 0.5, 20);
3469
- if (from.y === to.y) {
3470
- const arcHeight = 20;
3471
- const midX = (from.x + to.x) / 2;
3472
- return `M ${from.x} ${from.y} Q ${midX} ${from.y - arcHeight} ${to.x} ${to.y}`;
3473
- }
3474
- const cp1x = from.x;
3475
- const cp1y = from.y + (to.y > from.y ? cpOffset : -cpOffset);
3476
- const cp2x = to.x;
3477
- const cp2y = to.y - (to.y > from.y ? cpOffset : -cpOffset);
3478
- return `M ${Math.round(from.x)} ${Math.round(from.y)} C ${Math.round(cp1x)} ${Math.round(cp1y)}, ${Math.round(cp2x)} ${Math.round(cp2y)}, ${Math.round(to.x)} ${Math.round(to.y)}`;
3479
- };
3480
- var calculateDependencyPath = (from, to, arrivesFromRight) => {
3481
- const fx = Math.round(from.x);
3482
- const fy = Math.round(from.y);
3483
- const tx = Math.round(to.x);
3484
- const ty = Math.round(to.y);
3485
- if (fy === ty) {
3486
- return `M ${fx} ${fy} H ${tx}`;
3487
- }
3488
- if (fx === tx) {
3489
- return `M ${fx} ${fy} V ${ty}`;
3490
- }
3491
- const C = 2;
3492
- const goingDown = ty > fy;
3493
- const dirY = goingDown ? 1 : -1;
3494
- if (arrivesFromRight) {
3495
- const goingRight = tx >= fx;
3496
- const dirX = goingRight ? 1 : -1;
3497
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3498
- return [
3499
- `M ${fx} ${fy}`,
3500
- `H ${tx - dirX * C}`,
3501
- `L ${tx} ${fy + dirY * C}`,
3502
- `V ${ty}`
3503
- ].join(" ");
3504
- }
3505
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3506
- } else {
3507
- const goingRight = tx >= fx;
3508
- const dirX = goingRight ? 1 : -1;
3509
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3510
- return [
3511
- `M ${fx} ${fy}`,
3512
- `H ${tx - dirX * C}`,
3513
- `L ${tx} ${fy + dirY * C}`,
3514
- `V ${ty}`
3515
- ].join(" ");
3516
- }
3517
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3518
- }
3519
- };
3520
- var calculateWeekGridLines = (dateRange, dayWidth) => {
3521
- const blocks = getWeekBlocks(dateRange);
3522
- const lines = [];
3523
- let currentDayIndex = 0;
3524
- for (let i = 0; i < blocks.length; i++) {
3525
- const block = blocks[i];
3526
- currentDayIndex += block.days;
3527
- const blockEndMonth = dateRange[currentDayIndex - 1];
3528
- const nextBlockMonth = currentDayIndex < dateRange.length ? dateRange[currentDayIndex] : null;
3529
- const isMonthBoundary = nextBlockMonth && blockEndMonth.getUTCMonth() !== nextBlockMonth.getUTCMonth();
3530
- lines.push({
3531
- x: Math.round(currentDayIndex * dayWidth),
3532
- isMonthStart: isMonthBoundary || false
3533
- });
3534
- }
3535
- if (lines.length > 0 && lines[lines.length - 1].x === Math.round(dateRange.length * dayWidth)) {
3536
- lines.pop();
3537
- }
3538
- return lines;
3539
- };
3540
- var calculateMonthGridLines = (dateRange, dayWidth) => {
3541
- const lines = [];
3542
- for (let i = 1; i < dateRange.length; i++) {
3543
- const date = dateRange[i];
3544
- if (date.getUTCDate() === 1) {
3545
- const x = Math.round(i * dayWidth);
3546
- const isYearBoundary = date.getUTCMonth() === 0;
3547
- lines.push({ x, isMonthStart: isYearBoundary });
3775
+ )
3776
+ }
3777
+ ),
3778
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3779
+ "div",
3780
+ {
3781
+ className: "gantt-tsh-dayRow",
3782
+ style: {
3783
+ height: `${rowHeight}px`,
3784
+ gridTemplateColumns: viewMode === "week" ? weekGridTemplate : viewMode === "month" ? monthGridTemplate : dayGridTemplate
3785
+ },
3786
+ children: viewMode === "week" ? (
3787
+ // Week-view row 2: week block start day numbers (variable width)
3788
+ weekBlocks.map((block, index) => {
3789
+ const prevBlock = weekBlocks[index - 1];
3790
+ const isMonthBoundary = index > 0 && prevBlock && prevBlock.startDate.getUTCMonth() !== block.startDate.getUTCMonth();
3791
+ const showDate = block.days === 7;
3792
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3793
+ "div",
3794
+ {
3795
+ className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3796
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: showDate ? String(block.startDate.getUTCDate()).padStart(2, "0") : "" })
3797
+ },
3798
+ `week-${index}`
3799
+ );
3800
+ })
3801
+ ) : viewMode === "month" ? (
3802
+ // Month-view row 2: one column per month, shows abbreviated month name
3803
+ monthBlocks.map((block, index) => {
3804
+ const MIN_DAYS_TO_SHOW_LABEL = 15;
3805
+ const showLabel = block.days >= MIN_DAYS_TO_SHOW_LABEL;
3806
+ const isYearBoundary = index > 0 && block.startDate.getUTCMonth() === 0;
3807
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3808
+ "div",
3809
+ {
3810
+ className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3811
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: showLabel ? (() => {
3812
+ const s = block.startDate.toLocaleString("ru-RU", { month: "long", timeZone: "UTC" });
3813
+ return s.charAt(0).toUpperCase() + s.slice(1);
3814
+ })() : "" })
3815
+ },
3816
+ `mblock-${index}`
3817
+ );
3818
+ })
3819
+ ) : (
3820
+ // Day-view row 2: individual day numbers (existing code)
3821
+ days.map((day, index) => {
3822
+ const isWeekendDay = isCustomWeekend ? isCustomWeekend(day) : day.getUTCDay() === 0 || day.getUTCDay() === 6;
3823
+ const prevDay = days[index - 1];
3824
+ const isMonthBoundary = index > 0 && prevDay && prevDay.getUTCMonth() !== day.getUTCMonth();
3825
+ const isTodayDate = day.getUTCFullYear() === today.getUTCFullYear() && day.getUTCMonth() === today.getUTCMonth() && day.getUTCDate() === today.getUTCDate();
3826
+ const markerKey = `${day.getUTCFullYear()}-${day.getUTCMonth()}-${day.getUTCDate()}`;
3827
+ const marker = markerByDayKey.get(markerKey);
3828
+ const markerColor = marker?.color;
3829
+ const formattedDate = `${String(day.getUTCDate()).padStart(2, "0")}.${String(day.getUTCMonth() + 1).padStart(2, "0")}.${String(day.getUTCFullYear()).slice(-2)}`;
3830
+ const tooltipText = isTodayDate ? `${formattedDate} \u2014 \u0421\u0435\u0433\u043E\u0434\u043D\u044F` : marker?.name ? `${formattedDate} \u2014 ${marker.name}` : void 0;
3831
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3832
+ "div",
3833
+ {
3834
+ className: `gantt-tsh-dayCell ${isWeekendDay ? "gantt-tsh-weekendDay" : ""} ${isTodayDate ? "gantt-tsh-today" : ""} ${marker && !isTodayDate ? "gantt-tsh-markerDay" : ""} ${tooltipText ? "gantt-tsh-dayCell-tooltipTrigger" : ""}`,
3835
+ style: markerColor && !isTodayDate ? {
3836
+ ["--gantt-marker-day-color"]: markerColor
3837
+ } : void 0,
3838
+ "aria-label": tooltipText,
3839
+ onMouseEnter: () => {
3840
+ if (!tooltipText) return;
3841
+ onTimelineHover?.({
3842
+ label: tooltipText,
3843
+ left: index * dayWidth,
3844
+ color: isTodayDate ? "#dc2626" : markerColor || "var(--gantt-timeline-marker-color, #dc2626)"
3845
+ });
3846
+ },
3847
+ onMouseLeave: onTimelineHoverEnd,
3848
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gantt-tsh-dayLabel", children: (0, import_date_fns.format)(day, "d") })
3849
+ },
3850
+ `day-${index}`
3851
+ );
3852
+ })
3853
+ )
3854
+ }
3855
+ )
3856
+ ]
3548
3857
  }
3549
- }
3550
- return lines;
3551
- };
3552
- var calculateOrthogonalPath = (from, to) => {
3553
- const fx = Math.round(from.x);
3554
- const fy = Math.round(from.y);
3555
- const tx = Math.round(to.x);
3556
- const ty = Math.round(to.y);
3557
- if (fy === ty) {
3558
- return `M ${fx} ${fy} H ${tx}`;
3559
- }
3560
- const C = 2;
3561
- const goingDown = ty > fy;
3562
- const goingRight = tx >= fx;
3563
- const dirY = goingDown ? 1 : -1;
3564
- const dirX = goingRight ? 1 : -1;
3565
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3566
- return [
3567
- `M ${fx} ${fy}`,
3568
- `H ${tx - dirX * C}`,
3569
- `L ${tx} ${fy + dirY * C}`,
3570
- `V ${ty}`
3571
- ].join(" ");
3572
- }
3573
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3858
+ );
3574
3859
  };
3860
+ var TimeScaleHeader_default = TimeScaleHeader;
3861
+
3862
+ // src/components/TaskRow/TaskRow.tsx
3863
+ var import_react3 = __toESM(require("react"));
3864
+ var import_react_dom = require("react-dom");
3575
3865
 
3576
3866
  // src/utils/expired.ts
3577
3867
  var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
@@ -4410,21 +4700,245 @@ var useTaskDrag = (options) => {
4410
4700
  }
4411
4701
  }
4412
4702
  };
4413
- };
4703
+ };
4704
+
4705
+ // src/components/GanttChart/previewStore.ts
4706
+ function positionsEqual(left, right) {
4707
+ return left?.left === right?.left && left?.width === right?.width;
4708
+ }
4709
+ function createTaskPreviewPositionStore() {
4710
+ let positions = /* @__PURE__ */ new Map();
4711
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4712
+ function notify(taskIds) {
4713
+ for (const taskId of taskIds) {
4714
+ const listeners = listenersByTaskId.get(taskId);
4715
+ if (!listeners) continue;
4716
+ for (const listener of listeners) {
4717
+ listener();
4718
+ }
4719
+ }
4720
+ }
4721
+ return {
4722
+ subscribeTask(taskId, listener) {
4723
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4724
+ listeners.add(listener);
4725
+ listenersByTaskId.set(taskId, listeners);
4726
+ return () => {
4727
+ listeners.delete(listener);
4728
+ if (listeners.size === 0) {
4729
+ listenersByTaskId.delete(taskId);
4730
+ }
4731
+ };
4732
+ },
4733
+ getTaskPosition(taskId) {
4734
+ return positions.get(taskId);
4735
+ },
4736
+ setPositions(nextPositions) {
4737
+ const changedTaskIds = /* @__PURE__ */ new Set();
4738
+ const mergedPositions = /* @__PURE__ */ new Map();
4739
+ for (const [taskId, nextPosition] of nextPositions) {
4740
+ const currentPosition = positions.get(taskId);
4741
+ if (positionsEqual(currentPosition, nextPosition)) {
4742
+ if (currentPosition) {
4743
+ mergedPositions.set(taskId, currentPosition);
4744
+ }
4745
+ continue;
4746
+ }
4747
+ changedTaskIds.add(taskId);
4748
+ mergedPositions.set(taskId, {
4749
+ left: nextPosition.left,
4750
+ width: nextPosition.width
4751
+ });
4752
+ }
4753
+ for (const taskId of positions.keys()) {
4754
+ if (!nextPositions.has(taskId)) {
4755
+ changedTaskIds.add(taskId);
4756
+ }
4757
+ }
4758
+ if (changedTaskIds.size === 0) {
4759
+ return;
4760
+ }
4761
+ positions = mergedPositions;
4762
+ notify(changedTaskIds);
4763
+ },
4764
+ clear() {
4765
+ if (positions.size === 0) {
4766
+ return;
4767
+ }
4768
+ const changedTaskIds = new Set(positions.keys());
4769
+ positions = /* @__PURE__ */ new Map();
4770
+ notify(changedTaskIds);
4771
+ }
4772
+ };
4773
+ }
4774
+ function createActivityPreviewStore() {
4775
+ let overridesByTask = /* @__PURE__ */ new Map();
4776
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4777
+ function notify(taskIds) {
4778
+ for (const taskId of taskIds) {
4779
+ for (const listener of listenersByTaskId.get(taskId) ?? []) {
4780
+ listener();
4781
+ }
4782
+ }
4783
+ }
4784
+ return {
4785
+ subscribeTask(taskId, listener) {
4786
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4787
+ listeners.add(listener);
4788
+ listenersByTaskId.set(taskId, listeners);
4789
+ return () => {
4790
+ listeners.delete(listener);
4791
+ if (listeners.size === 0) {
4792
+ listenersByTaskId.delete(taskId);
4793
+ }
4794
+ };
4795
+ },
4796
+ getTaskOverrides(taskId) {
4797
+ return overridesByTask.get(taskId);
4798
+ },
4799
+ setOverrides(nextOverrides) {
4800
+ const changedTaskIds = new Set(nextOverrides.keys());
4801
+ for (const taskId of overridesByTask.keys()) {
4802
+ changedTaskIds.add(taskId);
4803
+ }
4804
+ if ([...changedTaskIds].every((taskId) => {
4805
+ const current = overridesByTask.get(taskId);
4806
+ const next = nextOverrides.get(taskId);
4807
+ if (!current || !next || current.size !== next.size) return false;
4808
+ return [...next].every(([activityId, delta]) => current.get(activityId) === delta);
4809
+ })) {
4810
+ return;
4811
+ }
4812
+ overridesByTask = new Map([...nextOverrides].map(([taskId, deltas]) => [taskId, new Map(deltas)]));
4813
+ notify(changedTaskIds);
4814
+ },
4815
+ clear() {
4816
+ if (overridesByTask.size === 0) {
4817
+ return;
4818
+ }
4819
+ const changedTaskIds = new Set(overridesByTask.keys());
4820
+ overridesByTask = /* @__PURE__ */ new Map();
4821
+ notify(changedTaskIds);
4822
+ }
4823
+ };
4824
+ }
4825
+ function createActivityBlockStore() {
4826
+ let blockersByTask = /* @__PURE__ */ new Map();
4827
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4828
+ function notify(taskIds) {
4829
+ for (const taskId of taskIds) {
4830
+ for (const listener of listenersByTaskId.get(taskId) ?? []) {
4831
+ listener();
4832
+ }
4833
+ }
4834
+ }
4835
+ return {
4836
+ subscribeTask(taskId, listener) {
4837
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4838
+ listeners.add(listener);
4839
+ listenersByTaskId.set(taskId, listeners);
4840
+ return () => {
4841
+ listeners.delete(listener);
4842
+ if (listeners.size === 0) {
4843
+ listenersByTaskId.delete(taskId);
4844
+ }
4845
+ };
4846
+ },
4847
+ getTaskBlockers(taskId) {
4848
+ return blockersByTask.get(taskId);
4849
+ },
4850
+ setBlockers(nextBlockers) {
4851
+ const changedTaskIds = new Set(nextBlockers.keys());
4852
+ for (const taskId of blockersByTask.keys()) {
4853
+ changedTaskIds.add(taskId);
4854
+ }
4855
+ if ([...changedTaskIds].every((taskId) => {
4856
+ const current = blockersByTask.get(taskId);
4857
+ const next = nextBlockers.get(taskId);
4858
+ if (!current || !next || current.size !== next.size) return false;
4859
+ return [...next].every((activityId) => current.has(activityId));
4860
+ })) {
4861
+ return;
4862
+ }
4863
+ blockersByTask = new Map([...nextBlockers].map(([taskId, ids]) => [taskId, new Set(ids)]));
4864
+ notify(changedTaskIds);
4865
+ },
4866
+ clear() {
4867
+ if (blockersByTask.size === 0) {
4868
+ return;
4869
+ }
4870
+ const changedTaskIds = new Set(blockersByTask.keys());
4871
+ blockersByTask = /* @__PURE__ */ new Map();
4872
+ notify(changedTaskIds);
4873
+ }
4874
+ };
4875
+ }
4876
+ var activityOwnerKey = (taskId, activityId) => `${taskId}\0${activityId}`;
4877
+ function createActivityDragOwner() {
4878
+ let ownerKey = null;
4879
+ return {
4880
+ get: () => ownerKey,
4881
+ set: (nextOwnerKey) => {
4882
+ ownerKey = nextOwnerKey;
4883
+ }
4884
+ };
4885
+ }
4886
+ function createActivityActivationStore() {
4887
+ let activeKey = null;
4888
+ const listeners = /* @__PURE__ */ new Set();
4889
+ return {
4890
+ subscribe(listener) {
4891
+ listeners.add(listener);
4892
+ return () => {
4893
+ listeners.delete(listener);
4894
+ };
4895
+ },
4896
+ getActiveKey: () => activeKey,
4897
+ setActiveKey(nextKey) {
4898
+ if (activeKey === nextKey) return;
4899
+ activeKey = nextKey;
4900
+ for (const listener of listeners) listener();
4901
+ }
4902
+ };
4903
+ }
4414
4904
 
4415
4905
  // src/components/TaskRow/TaskRow.tsx
4416
4906
  var import_jsx_runtime2 = require("react/jsx-runtime");
4417
4907
  var COMPOSITE_SEGMENT_CUT_MIN_PX = 10;
4908
+ var toIsoDay = (date) => date.toISOString().slice(0, 10);
4909
+ var shiftActivityDay = (value, days) => new Date(parseUTCDate(value).getTime() + days * DAY_MS);
4910
+ var activityDurationDays = (activity) => Math.max(1, Math.round((parseUTCDate(activity.endDate).getTime() - parseUTCDate(activity.startDate).getTime()) / DAY_MS) + 1);
4911
+ var ACTIVITY_TEXT_LUMINANCE_THRESHOLD = 0.55;
4912
+ var relativeLuminance = (hex) => {
4913
+ const channels = [1, 3, 5].map((index) => {
4914
+ const value = parseInt(hex.slice(index, index + 2), 16) / 255;
4915
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
4916
+ });
4917
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
4918
+ };
4919
+ var activityTextColor = (background) => {
4920
+ const lightText = "var(--gantt-task-bar-text-color, #ffffff)";
4921
+ if (!background) return lightText;
4922
+ const value = background.trim();
4923
+ if (value.startsWith("#")) {
4924
+ return relativeLuminance(value) > ACTIVITY_TEXT_LUMINANCE_THRESHOLD ? "#111111" : "#ffffff";
4925
+ }
4926
+ const oklch = /oklch\(\s*([0-9.]+)/i.exec(value);
4927
+ if (oklch) {
4928
+ return Number(oklch[1]) > 0.66 ? "#111111" : "#ffffff";
4929
+ }
4930
+ return lightText;
4931
+ };
4418
4932
  var formatCompositePreviewDate = (date) => {
4419
4933
  const month = new Intl.DateTimeFormat("ru-RU", { month: "short", timeZone: "UTC" }).format(date).replace(/\.$/, "");
4420
4934
  const yearSuffix = date.getUTCFullYear() === (/* @__PURE__ */ new Date()).getFullYear() ? "" : ` ${String(date.getUTCFullYear()).slice(-2)}`;
4421
4935
  return `${date.getUTCDate()} ${month}${yearSuffix}`;
4422
4936
  };
4423
4937
  var arePropsEqual = (prevProps, nextProps) => {
4424
- return prevProps.task.id === nextProps.task.id && prevProps.task.name === nextProps.task.name && prevProps.task.startDate === nextProps.task.startDate && prevProps.task.endDate === nextProps.task.endDate && prevProps.task.baselineStartDate === nextProps.task.baselineStartDate && prevProps.task.baselineEndDate === nextProps.task.baselineEndDate && prevProps.task.type === nextProps.task.type && prevProps.task.color === nextProps.task.color && prevProps.task.progress === nextProps.task.progress && prevProps.task.accepted === nextProps.task.accepted && prevProps.monthStart.getTime() === nextProps.monthStart.getTime() && prevProps.dayWidth === nextProps.dayWidth && prevProps.rowHeight === nextProps.rowHeight && prevProps.overridePosition?.left === nextProps.overridePosition?.left && prevProps.overridePosition?.width === nextProps.overridePosition?.width && prevProps.previewPositionStore === nextProps.previewPositionStore && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.deferCascadePreview === nextProps.deferCascadePreview && prevProps.task.locked === nextProps.task.locked && prevProps.task.synced === nextProps.task.synced && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isCritical === nextProps.isCritical && prevProps.showBaseline === nextProps.showBaseline && prevProps.onCompositeToggle === nextProps.onCompositeToggle && prevProps.compactDetail === nextProps.compactDetail && prevProps.compositeParentColor === nextProps.compositeParentColor && prevProps.compositeExpanded === nextProps.compositeExpanded && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag && prevProps.disableDependencyEditing === nextProps.disableDependencyEditing && prevProps.isDependencyDragActive === nextProps.isDependencyDragActive && prevProps.viewMode === nextProps.viewMode && prevProps.showTaskDateLabels === nextProps.showTaskDateLabels && prevProps.showTaskNames === nextProps.showTaskNames && prevProps.showCompositeSegments === nextProps.showCompositeSegments;
4938
+ return prevProps.task.id === nextProps.task.id && prevProps.task.name === nextProps.task.name && prevProps.task.startDate === nextProps.task.startDate && prevProps.task.endDate === nextProps.task.endDate && prevProps.task.baselineStartDate === nextProps.task.baselineStartDate && prevProps.task.baselineEndDate === nextProps.task.baselineEndDate && prevProps.task.type === nextProps.task.type && prevProps.task.color === nextProps.task.color && prevProps.task.progress === nextProps.task.progress && prevProps.task.accepted === nextProps.task.accepted && prevProps.task.activities === nextProps.task.activities && prevProps.monthStart.getTime() === nextProps.monthStart.getTime() && prevProps.dayWidth === nextProps.dayWidth && prevProps.rowHeight === nextProps.rowHeight && prevProps.overridePosition?.left === nextProps.overridePosition?.left && prevProps.overridePosition?.width === nextProps.overridePosition?.width && prevProps.previewPositionStore === nextProps.previewPositionStore && prevProps.activityPreviewStore === nextProps.activityPreviewStore && prevProps.activityBlockStore === nextProps.activityBlockStore && prevProps.activityDragOwner === nextProps.activityDragOwner && prevProps.activityActivationStore === nextProps.activityActivationStore && prevProps.activityClickToDrag === nextProps.activityClickToDrag && prevProps.activityActivationMode === nextProps.activityActivationMode && prevProps.showActivityLinks === nextProps.showActivityLinks && prevProps.showActivityLag === nextProps.showActivityLag && prevProps.horizontalWindow?.startPx === nextProps.horizontalWindow?.startPx && prevProps.horizontalWindow?.endPx === nextProps.horizontalWindow?.endPx && prevProps.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.deferCascadePreview === nextProps.deferCascadePreview && prevProps.task.locked === nextProps.task.locked && prevProps.task.synced === nextProps.task.synced && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isCritical === nextProps.isCritical && prevProps.showBaseline === nextProps.showBaseline && prevProps.onCompositeToggle === nextProps.onCompositeToggle && prevProps.compactDetail === nextProps.compactDetail && prevProps.compositeParentColor === nextProps.compositeParentColor && prevProps.compositeExpanded === nextProps.compositeExpanded && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag && prevProps.disableDependencyEditing === nextProps.disableDependencyEditing && prevProps.isDependencyDragActive === nextProps.isDependencyDragActive && prevProps.viewMode === nextProps.viewMode && prevProps.showTaskDateLabels === nextProps.showTaskDateLabels && prevProps.showTaskNames === nextProps.showTaskNames && prevProps.showCompositeSegments === nextProps.showCompositeSegments;
4425
4939
  };
4426
4940
  var TaskRow = import_react3.default.memo(
4427
- ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onScheduleIntent, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, deferCascadePreview = false, overridePosition, previewPositionStore, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isCritical = false, showBaseline = false, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false, disableDependencyEditing = false, onDependencyPortPointerDown, isDependencyDragActive = false, viewMode = "day", showTaskDateLabels = true, showTaskNames = true, showCompositeSegments: showCompositeSegmentsProp = true, onCompositeToggle, compactDetail = false, compositeParentColor, compositeExpanded = false }) => {
4941
+ ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onScheduleIntent, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, deferCascadePreview = false, overridePosition, previewPositionStore, activityPreviewStore, activityBlockStore, activityDragOwner, activityActivationStore, activityClickToDrag = false, activityActivationMode = "none", showActivityLinks = true, showActivityLag = true, activityTooltip, horizontalWindow, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isCritical = false, showBaseline = false, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false, disableDependencyEditing = false, onDependencyPortPointerDown, isDependencyDragActive = false, viewMode = "day", showTaskDateLabels = true, showTaskNames = true, showCompositeSegments: showCompositeSegmentsProp = true, onCompositeToggle, compactDetail = false, compositeParentColor, compositeExpanded = false }) => {
4428
4942
  const defaultParentBarColor = "#782FC4";
4429
4943
  const [showCompositePreview, setShowCompositePreview] = (0, import_react3.useState)(false);
4430
4944
  const [compositePreviewPosition, setCompositePreviewPosition] = (0, import_react3.useState)(null);
@@ -4634,6 +5148,347 @@ var TaskRow = import_react3.default.memo(
4634
5148
  const shouldRenderCompositeSegments = showCompositeSegmentsProp && task.composite && compositeSegments.length > 0 && visualWidth / compositeSegments.length >= 22;
4635
5149
  const shouldRenderBaseline = showBaseline && baselineGeometry !== null;
4636
5150
  const hasPreviewPosition = isDragging || effectiveOverridePosition !== void 0;
5151
+ const activityLayout = (0, import_react3.useMemo)(
5152
+ () => task.activities && task.activities.length > 0 ? computeActivityLanes(task.activities, monthStart, dayWidth) : null,
5153
+ [task.activities, monthStart, dayWidth]
5154
+ );
5155
+ const renderActivities = activityLayout !== null && !compactDetail;
5156
+ const activityTopOffset = activityLayout ? Math.max(0, (rowHeight - activityLayout.laneCount * ACTIVITY_LANE_STEP) / 2) : 0;
5157
+ const activityDragEnabled = !task.locked && !disableTaskDrag;
5158
+ const [activityDrag, setActivityDrag] = (0, import_react3.useState)(null);
5159
+ const [activityTipId, setActivityTipId] = (0, import_react3.useState)(null);
5160
+ const [activityTipPosition, setActivityTipPosition] = (0, import_react3.useState)(null);
5161
+ const activityTipAnchor = (0, import_react3.useRef)(null);
5162
+ const activityDragStart = (0, import_react3.useRef)(null);
5163
+ const activityConstraintRef = (0, import_react3.useRef)(null);
5164
+ const [activityBlockedBy, setActivityBlockedBy] = (0, import_react3.useState)(null);
5165
+ const visibleActivitySegments = (0, import_react3.useMemo)(() => {
5166
+ if (!activityLayout) return [];
5167
+ const window2 = horizontalWindow;
5168
+ if (!window2) return activityLayout.segments;
5169
+ const draggedId = activityDrag?.id;
5170
+ return activityLayout.segments.filter((segment) => segment.id === draggedId || segment.left <= window2.endPx && segment.left + segment.width >= window2.startPx);
5171
+ }, [activityLayout, horizontalWindow, activityDrag?.id]);
5172
+ const subscribeActivityOverrides = (0, import_react3.useCallback)(
5173
+ (listener) => activityPreviewStore?.subscribeTask(task.id, listener) ?? (() => {
5174
+ }),
5175
+ [activityPreviewStore, task.id]
5176
+ );
5177
+ const getActivityOverridesSnapshot = (0, import_react3.useCallback)(
5178
+ () => activityPreviewStore?.getTaskOverrides(task.id),
5179
+ [activityPreviewStore, task.id]
5180
+ );
5181
+ const activityOverrides = (0, import_react3.useSyncExternalStore)(
5182
+ subscribeActivityOverrides,
5183
+ getActivityOverridesSnapshot,
5184
+ () => void 0
5185
+ );
5186
+ const subscribeActivityBlock = (0, import_react3.useCallback)(
5187
+ (listener) => activityBlockStore?.subscribeTask(task.id, listener) ?? (() => {
5188
+ }),
5189
+ [activityBlockStore, task.id]
5190
+ );
5191
+ const getActivityBlockSnapshot = (0, import_react3.useCallback)(
5192
+ () => activityBlockStore?.getTaskBlockers(task.id),
5193
+ [activityBlockStore, task.id]
5194
+ );
5195
+ const blockedActivityIds = (0, import_react3.useSyncExternalStore)(
5196
+ subscribeActivityBlock,
5197
+ getActivityBlockSnapshot,
5198
+ () => void 0
5199
+ );
5200
+ const subscribeActivation = (0, import_react3.useCallback)(
5201
+ (listener) => activityActivationStore?.subscribe(listener) ?? (() => {
5202
+ }),
5203
+ [activityActivationStore]
5204
+ );
5205
+ const getActivationSnapshot = (0, import_react3.useCallback)(
5206
+ () => activityActivationStore?.getActiveKey() ?? null,
5207
+ [activityActivationStore]
5208
+ );
5209
+ const activeActivityKey = (0, import_react3.useSyncExternalStore)(
5210
+ subscribeActivation,
5211
+ getActivationSnapshot,
5212
+ () => null
5213
+ );
5214
+ const activeWork = (0, import_react3.useMemo)(() => {
5215
+ if (!activeActivityKey) return null;
5216
+ const [activeTaskId, activeActivityId] = activeActivityKey.split("\0");
5217
+ const source = (allTasks ?? []).find((candidate) => candidate.id === activeTaskId);
5218
+ const activity = source?.activities?.find((candidate) => candidate.id === activeActivityId);
5219
+ return { id: activeActivityId, name: activity?.name ?? null };
5220
+ }, [activeActivityKey, allTasks]);
5221
+ const activationVisualEnabled = activityClickToDrag || activityActivationMode !== "none";
5222
+ const dimOthers = activityActivationMode === "dim";
5223
+ const highlightChain = activityActivationMode === "highlight";
5224
+ const chainEndDeltaDays = (mode, dayDelta, durationDays2) => {
5225
+ if (mode === "resize-left") return 0;
5226
+ if (mode === "resize-right") return Math.max(dayDelta, -(durationDays2 - 1));
5227
+ return dayDelta;
5228
+ };
5229
+ const publishChainPreview = (0, import_react3.useCallback)((draggedId, mode, dayDelta) => {
5230
+ if (!activityPreviewStore) return;
5231
+ if (!task.activityChain) {
5232
+ activityPreviewStore.clear();
5233
+ return;
5234
+ }
5235
+ let shifts;
5236
+ if (task.activityChain === "push") {
5237
+ let delta = dayDelta;
5238
+ if (mode !== "resize-right") {
5239
+ const minDelta = activityConstraintRef.current?.minStartDelta ?? null;
5240
+ if (minDelta !== null) delta = Math.max(delta, minDelta);
5241
+ }
5242
+ shifts = delta === 0 ? [] : pushActivityChain(allTasks ?? [], task.id, draggedId, delta).shifts;
5243
+ } else {
5244
+ const endDelta = chainEndDeltaDays(mode, dayDelta, activityDragStart.current?.durationDays ?? 1);
5245
+ shifts = endDelta === 0 ? [] : shiftActivityChain(allTasks ?? [], task.id, draggedId, endDelta);
5246
+ }
5247
+ if (shifts.length === 0) {
5248
+ activityPreviewStore.clear();
5249
+ return;
5250
+ }
5251
+ const overrides = /* @__PURE__ */ new Map();
5252
+ for (const shift of shifts) {
5253
+ const deltas = /* @__PURE__ */ new Map();
5254
+ for (const [id, d] of shift.deltas) {
5255
+ if (d !== 0) deltas.set(id, d);
5256
+ }
5257
+ if (deltas.size > 0) overrides.set(shift.taskId, deltas);
5258
+ }
5259
+ activityPreviewStore.setOverrides(overrides);
5260
+ }, [activityPreviewStore, allTasks, task.activityChain, task.id]);
5261
+ (0, import_react3.useEffect)(() => {
5262
+ if (!activityDrag) return;
5263
+ const startInfo = activityDragStart.current;
5264
+ if (!startInfo) return;
5265
+ const publishBlockers = (blocked) => {
5266
+ const constraint = activityConstraintRef.current;
5267
+ if (!activityBlockStore) return;
5268
+ if (!blocked || !constraint) {
5269
+ activityBlockStore.clear();
5270
+ setActivityBlockedBy((current) => current === null ? current : null);
5271
+ return;
5272
+ }
5273
+ const blockers = /* @__PURE__ */ new Map();
5274
+ const names = [];
5275
+ for (const blocker of constraint.blockers) {
5276
+ const set = blockers.get(blocker.taskId) ?? /* @__PURE__ */ new Set();
5277
+ set.add(blocker.activityId);
5278
+ blockers.set(blocker.taskId, set);
5279
+ const source = (allTasks ?? []).find((candidate) => candidate.id === blocker.taskId);
5280
+ const work = source?.activities?.find((activity) => activity.id === blocker.activityId);
5281
+ const workName = work?.name ?? source?.name ?? blocker.activityId;
5282
+ names.push(blocker.taskId === task.id ? workName : `${source?.name ?? blocker.taskId} \xB7 ${workName}`);
5283
+ }
5284
+ activityBlockStore.setBlockers(blockers);
5285
+ setActivityBlockedBy((current) => current && current.lag === constraint.lag && current.names.length === names.length && current.names.every((name, index) => name === names[index]) ? current : { names, lag: constraint.lag });
5286
+ };
5287
+ const onMove = (event) => {
5288
+ const rawDelta = Math.round((event.clientX - startInfo.x) / dayWidth);
5289
+ let requested = rawDelta;
5290
+ if (activityDrag.mode === "resize-left") requested = Math.min(rawDelta, startInfo.durationDays - 1);
5291
+ if (activityDrag.mode === "resize-right") requested = Math.max(rawDelta, -(startInfo.durationDays - 1));
5292
+ const constraint = activityConstraintRef.current;
5293
+ let clamped = requested;
5294
+ let blocked = false;
5295
+ if (constraint && activityDrag.mode !== "resize-right" && requested < constraint.minStartDelta) {
5296
+ clamped = constraint.minStartDelta;
5297
+ blocked = true;
5298
+ }
5299
+ setActivityDrag((current) => current ? { ...current, dayDelta: clamped } : current);
5300
+ publishBlockers(blocked);
5301
+ const anchor = activityTipAnchor.current;
5302
+ if (anchor) {
5303
+ const shiftPx = activityDrag.mode === "resize-right" ? 0 : clamped * dayWidth;
5304
+ setActivityTipPosition({
5305
+ left: Math.max(8, Math.min(anchor.left + shiftPx, window.innerWidth - 160)),
5306
+ top: anchor.top,
5307
+ below: anchor.below
5308
+ });
5309
+ }
5310
+ publishChainPreview(activityDrag.id, activityDrag.mode, clamped);
5311
+ };
5312
+ const onUp = () => {
5313
+ activityPreviewStore?.clear();
5314
+ activityBlockStore?.clear();
5315
+ activityDragOwner?.set(null);
5316
+ setActivityBlockedBy(null);
5317
+ setActivityTipId(null);
5318
+ activityTipAnchor.current = null;
5319
+ setActivityTipPosition(null);
5320
+ if (activityDrag.dayDelta !== 0) {
5321
+ const chainPush = task.activityChain === "push";
5322
+ let delta = activityDrag.dayDelta;
5323
+ if (chainPush && activityDrag.mode !== "resize-right") {
5324
+ const minDelta = activityConstraintRef.current?.minStartDelta ?? null;
5325
+ if (minDelta !== null) delta = Math.max(delta, minDelta);
5326
+ }
5327
+ const clampLeft = Math.min(delta, startInfo.durationDays - 1);
5328
+ const clampRight = Math.max(delta, -(startInfo.durationDays - 1));
5329
+ const updatedActivities = (task.activities ?? []).map((activity) => {
5330
+ if (activity.id !== activityDrag.id) return activity;
5331
+ if (activityDrag.mode === "move") {
5332
+ return {
5333
+ ...activity,
5334
+ startDate: toIsoDay(shiftActivityDay(activity.startDate, delta)),
5335
+ endDate: toIsoDay(shiftActivityDay(activity.endDate, delta))
5336
+ };
5337
+ }
5338
+ if (activityDrag.mode === "resize-left") {
5339
+ return { ...activity, startDate: toIsoDay(shiftActivityDay(activity.startDate, clampLeft)) };
5340
+ }
5341
+ return { ...activity, endDate: toIsoDay(shiftActivityDay(activity.endDate, clampRight)) };
5342
+ });
5343
+ const chainShifts = chainPush ? delta !== 0 ? pushActivityChain(allTasks ?? [], task.id, activityDrag.id, delta).shifts : [] : (() => {
5344
+ const endDeltaDays = chainEndDeltaDays(activityDrag.mode, delta, startInfo.durationDays);
5345
+ return endDeltaDays !== 0 ? shiftActivityChain(allTasks ?? [], task.id, activityDrag.id, endDeltaDays) : [];
5346
+ })();
5347
+ const changedTasks = [];
5348
+ let hostHandled = false;
5349
+ for (const shift of chainShifts) {
5350
+ if (shift.taskId === task.id) {
5351
+ hostHandled = true;
5352
+ changedTasks.push({
5353
+ ...normalizedTask,
5354
+ activities: shift.activities.map((activity) => activity.id === activityDrag.id ? updatedActivities.find((item) => item.id === activityDrag.id) ?? activity : activity)
5355
+ });
5356
+ } else {
5357
+ const source = (allTasks ?? []).find((candidate) => candidate.id === shift.taskId);
5358
+ changedTasks.push({ ...source ?? { id: shift.taskId }, activities: shift.activities });
5359
+ }
5360
+ }
5361
+ if (!hostHandled) {
5362
+ changedTasks.unshift({ ...normalizedTask, activities: updatedActivities });
5363
+ }
5364
+ onTasksChange?.(changedTasks);
5365
+ }
5366
+ setActivityDrag(null);
5367
+ activityDragStart.current = null;
5368
+ activityConstraintRef.current = null;
5369
+ document.body.style.cursor = "";
5370
+ };
5371
+ document.body.style.cursor = activityDrag.mode === "move" ? "grabbing" : "ew-resize";
5372
+ window.addEventListener("mousemove", onMove);
5373
+ window.addEventListener("mouseup", onUp);
5374
+ return () => {
5375
+ window.removeEventListener("mousemove", onMove);
5376
+ window.removeEventListener("mouseup", onUp);
5377
+ document.body.style.cursor = "";
5378
+ };
5379
+ }, [activityDrag, activityPreviewStore, activityBlockStore, activityDragOwner, allTasks, dayWidth, normalizedTask, onTasksChange, publishChainPreview, task.activities]);
5380
+ const anchorActivityTip = (rect) => {
5381
+ const below = rect.top < 34;
5382
+ const anchor = {
5383
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - 160)),
5384
+ // Вплотную к полосе (1px зазора), сверху или снизу при нехватке места.
5385
+ top: below ? rect.bottom + 1 : rect.top - 1,
5386
+ below
5387
+ };
5388
+ activityTipAnchor.current = anchor;
5389
+ return anchor;
5390
+ };
5391
+ const handleActivityPointerDown = (segment, mode) => (event) => {
5392
+ if (!activityDragEnabled) return;
5393
+ event.preventDefault();
5394
+ event.stopPropagation();
5395
+ activityDragStart.current = {
5396
+ x: event.clientX,
5397
+ durationDays: Math.max(1, Math.round(segment.width / dayWidth) - 1)
5398
+ };
5399
+ activityConstraintRef.current = task.activityChain === "push" && mode !== "resize-right" ? activityStartConstraint(allTasks ?? [], task.id, segment.id) : null;
5400
+ activityDragOwner?.set(activityOwnerKey(task.id, segment.id));
5401
+ setActivityBlockedBy(null);
5402
+ setActivityTipPosition(anchorActivityTip(event.currentTarget.getBoundingClientRect()));
5403
+ setActivityTipId(segment.id);
5404
+ setActivityDrag({ id: segment.id, mode, dayDelta: 0 });
5405
+ };
5406
+ const liveActivityGeometry = (segment) => {
5407
+ if (!activityDrag || activityDrag.id !== segment.id || activityDrag.dayDelta === 0) return segment;
5408
+ if (activityDrag.mode === "move") {
5409
+ return { left: segment.left + activityDrag.dayDelta * dayWidth, width: segment.width };
5410
+ }
5411
+ if (activityDrag.mode === "resize-left") {
5412
+ return { left: segment.left + activityDrag.dayDelta * dayWidth, width: segment.width - activityDrag.dayDelta * dayWidth };
5413
+ }
5414
+ return { left: segment.left, width: segment.width + activityDrag.dayDelta * dayWidth };
5415
+ };
5416
+ const activityLinks = (0, import_react3.useMemo)(() => {
5417
+ const gaps = [];
5418
+ const elbows = [];
5419
+ if (!showActivityLinks || !renderActivities || !activityLayout) return { gaps, elbows };
5420
+ const activities = task.activities ?? [];
5421
+ const segmentById = new Map(activityLayout.segments.map((segment) => [segment.id, segment]));
5422
+ const visibleIds = new Set(visibleActivitySegments.map((segment) => segment.id));
5423
+ const laneTopY = (lane) => activityTopOffset + lane * ACTIVITY_LANE_STEP + (ACTIVITY_LANE_STEP - ACTIVITY_LANE_BAR_HEIGHT) / 2;
5424
+ const liveRect = (segment) => {
5425
+ const geometry = liveActivityGeometry(segment);
5426
+ const chainDelta = activityOverrides?.get(segment.id) ?? 0;
5427
+ const left2 = geometry.left + (chainDelta !== 0 ? chainDelta * dayWidth : 0);
5428
+ return { left: left2, right: left2 + geometry.width };
5429
+ };
5430
+ for (let index = 0; index < activities.length - 1; index += 1) {
5431
+ const predecessor = segmentById.get(activities[index].id);
5432
+ const successor = segmentById.get(activities[index + 1].id);
5433
+ if (!predecessor || !successor) continue;
5434
+ if (!visibleIds.has(predecessor.id) || !visibleIds.has(successor.id)) continue;
5435
+ const lag = activities[index + 1].lag ?? 0;
5436
+ const predRect = liveRect(predecessor);
5437
+ const succRect = liveRect(successor);
5438
+ if (predecessor.lane === successor.lane && succRect.left >= predRect.right) {
5439
+ const width2 = succRect.left - predRect.right;
5440
+ if (width2 >= 1) {
5441
+ gaps.push({ key: `${predecessor.id}->${successor.id}`, left: predRect.right, width: width2, lane: predecessor.lane, lag });
5442
+ }
5443
+ continue;
5444
+ }
5445
+ const predecessorLaneTop = laneTopY(predecessor.lane);
5446
+ const successorLaneTop = laneTopY(successor.lane);
5447
+ const sameLane = predecessor.lane === successor.lane;
5448
+ const reverseOrder = !sameLane && successorLaneTop < predecessorLaneTop;
5449
+ const fromY = reverseOrder ? predecessorLaneTop + 6 : predecessorLaneTop + ACTIVITY_LANE_BAR_HEIGHT - 6;
5450
+ const toY = reverseOrder ? successorLaneTop + ACTIVITY_LANE_BAR_HEIGHT - 6 : successorLaneTop + (sameLane ? ACTIVITY_LANE_STEP : 0) + 6;
5451
+ const fromX = predRect.right;
5452
+ const toX = succRect.left;
5453
+ elbows.push({
5454
+ key: `${predecessor.id}->${successor.id}`,
5455
+ d: calculateDependencyPath({ x: fromX, y: fromY }, { x: toX, y: toY }, false),
5456
+ lagX: toX - 14,
5457
+ lagY: reverseOrder ? fromY - 4 : fromY + 12,
5458
+ lag
5459
+ });
5460
+ }
5461
+ return { gaps, elbows };
5462
+ }, [showActivityLinks, renderActivities, activityLayout, visibleActivitySegments, activityTopOffset, activityOverrides, activityDrag, dayWidth, task.activities]);
5463
+ const activityTipData = (0, import_react3.useMemo)(() => {
5464
+ if (!activityTipId || !task.activities) return null;
5465
+ const activity = task.activities.find((item) => item.id === activityTipId);
5466
+ if (!activity) return null;
5467
+ let startDate = parseUTCDate(activity.startDate);
5468
+ let endDate = parseUTCDate(activity.endDate);
5469
+ if (activityDrag && activityDrag.id === activity.id && activityDrag.dayDelta !== 0) {
5470
+ startDate = shiftActivityDay(startDate, activityDrag.dayDelta);
5471
+ endDate = shiftActivityDay(endDate, activityDrag.dayDelta);
5472
+ }
5473
+ const progress = activity.progress === void 0 ? void 0 : Math.min(100, Math.max(0, Math.round(activity.progress)));
5474
+ return {
5475
+ floor: task.name,
5476
+ name: activity.name,
5477
+ // Готовность — первой строкой подсказки, дальше пользовательские поля.
5478
+ fields: [
5479
+ ...progress !== void 0 ? [{ label: "\u0413\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u044C", value: `${progress}%` }] : [],
5480
+ ...activity.tooltipFields ?? []
5481
+ ],
5482
+ context: {
5483
+ task,
5484
+ activity,
5485
+ startDate,
5486
+ endDate,
5487
+ durationDays: activityDurationDays({ startDate, endDate }),
5488
+ progress
5489
+ }
5490
+ };
5491
+ }, [activityDrag, activityTipId, task, task.activities, task.name]);
4637
5492
  const currentStartDate = hasPreviewPosition ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
4638
5493
  const currentEndDate = hasPreviewPosition ? milestone ? pixelsToDate(displayLeft, monthStart, dayWidth) : pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
4639
5494
  const dateRangeLabel = formatDateRangeLabel(currentStartDate, currentEndDate);
@@ -4666,7 +5521,7 @@ var TaskRow = import_react3.default.memo(
4666
5521
  children: [
4667
5522
  taskDivider === "top" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-divider gantt-tr-divider-top" }),
4668
5523
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-taskContainer", children: [
4669
- shouldRenderBaseline && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5524
+ shouldRenderBaseline && !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4670
5525
  "div",
4671
5526
  {
4672
5527
  className: `gantt-tr-baseline ${isParent ? "gantt-tr-baseline-parent" : ""} ${milestone ? "gantt-tr-baseline-milestone" : ""}`,
@@ -4676,7 +5531,122 @@ var TaskRow = import_react3.default.memo(
4676
5531
  }
4677
5532
  }
4678
5533
  ),
4679
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5534
+ renderActivities && activityLayout && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-activityLanes", children: [
5535
+ activityLinks.elbows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: "gantt-tr-activityLinksSvg", width: "100%", height: "100%", "aria-hidden": "true", children: [
5536
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("marker", { id: "arrowhead", markerWidth: "8", markerHeight: "6", markerUnits: "userSpaceOnUse", refX: "7", refY: "3", orient: "auto", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("polygon", { points: "0 0, 8 3, 0 6", fill: "var(--gantt-dependency-line-color, #666666)" }) }) }),
5537
+ activityLinks.elbows.map((link) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
5538
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { className: "gantt-dependency-path", d: link.d, markerEnd: "url(#arrowhead)" }),
5539
+ showActivityLag && link.lag !== 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5540
+ "text",
5541
+ {
5542
+ className: "gantt-dependency-lag-label",
5543
+ x: link.lagX,
5544
+ y: link.lagY,
5545
+ textAnchor: "middle",
5546
+ fontSize: "10",
5547
+ fill: "var(--gantt-dependency-line-color, #666666)",
5548
+ children: link.lag > 0 ? `+${link.lag}` : `${link.lag}`
5549
+ }
5550
+ )
5551
+ ] }, link.key))
5552
+ ] }),
5553
+ visibleActivitySegments.map((segment) => {
5554
+ const live = liveActivityGeometry(segment);
5555
+ const chainDelta = activityOverrides?.get(segment.id) ?? 0;
5556
+ const activityProgress = segment.progress === void 0 ? 0 : Math.min(100, Math.max(0, Math.round(segment.progress)));
5557
+ const liveLeft = live.left + (chainDelta !== 0 ? chainDelta * dayWidth : 0);
5558
+ const isDraggingActivity = activityDrag?.id === segment.id;
5559
+ const isBlockingActivity = blockedActivityIds?.has(segment.id) ?? false;
5560
+ const isAtLimit = isDraggingActivity && activityBlockedBy !== null;
5561
+ const isActiveActivity = activationVisualEnabled && activeActivityKey === activityOwnerKey(task.id, segment.id);
5562
+ const isSameWorkType = activeWork !== null && (segment.id === activeWork.id || activeWork.name !== null && segment.name === activeWork.name);
5563
+ const isDimmed = dimOthers && activeWork !== null && !isSameWorkType;
5564
+ const isChainHighlighted = highlightChain && activeWork !== null && isSameWorkType;
5565
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5566
+ "div",
5567
+ {
5568
+ "data-activity-id": segment.id,
5569
+ className: `gantt-tr-activityBar${isDraggingActivity ? " gantt-tr-activityBar-dragging" : ""}${isBlockingActivity ? " gantt-tr-activityBar-blocking" : ""}${isAtLimit ? " gantt-tr-activityBar-atLimit" : ""}${isActiveActivity ? " gantt-tr-activityBar-active" : ""}${isDimmed ? " gantt-tr-activityBar-dimmed" : ""}${isChainHighlighted ? " gantt-tr-activityBar-chain" : ""}${activityDragEnabled ? "" : " gantt-tr-activityBar-locked"}`,
5570
+ style: {
5571
+ left: `${liveLeft}px`,
5572
+ width: `${live.width}px`,
5573
+ top: `${activityTopOffset + segment.lane * ACTIVITY_LANE_STEP + (ACTIVITY_LANE_STEP - ACTIVITY_LANE_BAR_HEIGHT) / 2}px`,
5574
+ height: `${ACTIVITY_LANE_BAR_HEIGHT}px`,
5575
+ backgroundColor: segment.color || "var(--gantt-task-bar-default-color)"
5576
+ },
5577
+ onMouseDown: (event) => {
5578
+ if (!activityDragEnabled) return;
5579
+ const activationKey = activityOwnerKey(task.id, segment.id);
5580
+ const isActive = activityActivationStore?.getActiveKey() === activationKey;
5581
+ if (activationVisualEnabled && !isActive) {
5582
+ activityActivationStore?.setActiveKey(activationKey);
5583
+ }
5584
+ if (activityClickToDrag && !isActive) return;
5585
+ const target = event.target;
5586
+ const mode = target.closest(".gantt-tr-resizeHandleLeft") ? "resize-left" : target.closest(".gantt-tr-resizeHandleRight") ? "resize-right" : "move";
5587
+ handleActivityPointerDown(segment, mode)(event);
5588
+ },
5589
+ onMouseEnter: (event) => {
5590
+ if (activityDrag || activityDragOwner?.get()) return;
5591
+ setActivityTipPosition(anchorActivityTip(event.currentTarget.getBoundingClientRect()));
5592
+ setActivityTipId(segment.id);
5593
+ },
5594
+ onMouseLeave: () => {
5595
+ if (!activityDrag) {
5596
+ setActivityTipId((current) => current === segment.id ? null : current);
5597
+ activityTipAnchor.current = null;
5598
+ setActivityTipPosition(null);
5599
+ }
5600
+ },
5601
+ children: [
5602
+ activityProgress > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5603
+ "span",
5604
+ {
5605
+ className: "gantt-tr-activityProgress",
5606
+ style: { width: `${activityProgress}%` }
5607
+ }
5608
+ ),
5609
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityName", style: { color: activityTextColor(segment.color) }, children: segment.name }),
5610
+ activityDragEnabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
5611
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
5612
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
5613
+ ] })
5614
+ ]
5615
+ },
5616
+ segment.id
5617
+ );
5618
+ }),
5619
+ activityLinks.gaps.map((gap) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react3.default.Fragment, { children: [
5620
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5621
+ "span",
5622
+ {
5623
+ className: "gantt-tr-activityLink",
5624
+ "aria-hidden": "true",
5625
+ style: {
5626
+ left: `${gap.left}px`,
5627
+ width: `${gap.width}px`,
5628
+ top: `${activityTopOffset + gap.lane * ACTIVITY_LANE_STEP + ACTIVITY_LANE_STEP / 2}px`
5629
+ }
5630
+ }
5631
+ ),
5632
+ showActivityLag && gap.lag > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5633
+ "span",
5634
+ {
5635
+ className: "gantt-tr-activityLag",
5636
+ title: `\u0417\u0430\u0437\u043E\u0440 ${gap.lag} \u0434`,
5637
+ style: {
5638
+ left: `${gap.left + gap.width - 14}px`,
5639
+ top: `${activityTopOffset + gap.lane * ACTIVITY_LANE_STEP + ACTIVITY_LANE_STEP / 2}px`
5640
+ },
5641
+ children: [
5642
+ "+",
5643
+ gap.lag
5644
+ ]
5645
+ }
5646
+ )
5647
+ ] }, gap.key))
5648
+ ] }),
5649
+ !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4680
5650
  "div",
4681
5651
  {
4682
5652
  "data-taskbar": true,
@@ -4832,7 +5802,33 @@ var TaskRow = import_react3.default.memo(
4832
5802
  ),
4833
5803
  document.body
4834
5804
  ),
4835
- !disableDependencyEditing && onDependencyPortPointerDown && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
5805
+ activityTipData && activityTipPosition && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
5806
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5807
+ "div",
5808
+ {
5809
+ className: `gantt-tr-activityTip${activityTipPosition.below ? " gantt-tr-activityTip-below" : ""}`,
5810
+ style: { left: `${activityTipPosition.left}px`, top: `${activityTipPosition.top}px` },
5811
+ role: "tooltip",
5812
+ children: [
5813
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityTipName", children: activityTipData.floor }),
5814
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityTipDot", "aria-hidden": "true", children: "\xB7" }),
5815
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityTipWork", children: activityTipData.name }),
5816
+ activityTooltip && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-activityTipExtra", children: activityTooltip(activityTipData.context) }),
5817
+ activityTipData.fields.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-activityTipFields", children: activityTipData.fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gantt-tr-activityTipField", children: [
5818
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityTipFieldLabel", children: field.label }),
5819
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-activityTipFieldValue", children: field.value })
5820
+ ] }, field.label)) }),
5821
+ activityBlockedBy && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-activityTipReason", children: [
5822
+ "\xB7 \u0411\u043B\u043E\u043A: ",
5823
+ activityBlockedBy.names.join(", "),
5824
+ activityBlockedBy.lag > 0 ? ` \xB7 \u0437\u0430\u0437\u043E\u0440 ${activityBlockedBy.lag} \u0434` : ""
5825
+ ] })
5826
+ ]
5827
+ }
5828
+ ),
5829
+ document.body
5830
+ ),
5831
+ !disableDependencyEditing && onDependencyPortPointerDown && !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
4836
5832
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4837
5833
  "button",
4838
5834
  {
@@ -4862,7 +5858,7 @@ var TaskRow = import_react3.default.memo(
4862
5858
  }
4863
5859
  )
4864
5860
  ] }),
4865
- showTaskDateLabels && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5861
+ showTaskDateLabels && !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4866
5862
  "div",
4867
5863
  {
4868
5864
  className: `gantt-tr-leftLabels ${task.locked ? "gantt-tr-leftLabels-locked" : ""}`,
@@ -4872,7 +5868,7 @@ var TaskRow = import_react3.default.memo(
4872
5868
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-dateLabel gantt-tr-dateLabelLeft", children: dateRangeLabel })
4873
5869
  }
4874
5870
  ),
4875
- task.locked && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5871
+ task.locked && !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4876
5872
  "svg",
4877
5873
  {
4878
5874
  className: "gantt-tr-lockIcon",
@@ -4893,15 +5889,15 @@ var TaskRow = import_react3.default.memo(
4893
5889
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM12 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" })
4894
5890
  }
4895
5891
  ),
4896
- !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
5892
+ compactDetail && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4897
5893
  "span",
4898
5894
  {
4899
- className: `gantt-tr-externalDuration gantt-tr-durationBeforeBar${compactDetail ? " gantt-tr-compositeDuration" : ""}`,
5895
+ className: "gantt-tr-externalDuration gantt-tr-durationBeforeBar gantt-tr-compositeDuration",
4900
5896
  style: { left: `${visualLeft - 46}px`, color: isParent ? task.color || defaultParentBarColor : barColor },
4901
5897
  children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434`
4902
5898
  }
4903
5899
  ),
4904
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
5900
+ !renderActivities && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
4905
5901
  "div",
4906
5902
  {
4907
5903
  className: "gantt-tr-rightLabels",
@@ -4910,6 +5906,7 @@ var TaskRow = import_react3.default.memo(
4910
5906
  color: isParent ? task.color || defaultParentBarColor : barColor
4911
5907
  },
4912
5908
  children: [
5909
+ !compactDetail && !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-externalDuration", children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4913
5910
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-externalProgress", children: [
4914
5911
  progressWidth,
4915
5912
  "%"
@@ -5037,10 +6034,10 @@ var import_react6 = __toESM(require("react"));
5037
6034
  var import_jsx_runtime5 = require("react/jsx-runtime");
5038
6035
  var arePropsEqual2 = (prevProps, nextProps) => {
5039
6036
  return prevProps.dayWidth === nextProps.dayWidth && prevProps.dateRange.length === nextProps.dateRange.length && prevProps.totalHeight === nextProps.totalHeight && // skip re-render only when totalHeight unchanged
5040
- prevProps.viewMode === nextProps.viewMode && prevProps.isCustomWeekend === nextProps.isCustomWeekend;
6037
+ prevProps.viewMode === nextProps.viewMode && prevProps.isCustomWeekend === nextProps.isCustomWeekend && prevProps.showWeekendBlocks === nextProps.showWeekendBlocks && prevProps.showGridLines === nextProps.showGridLines && prevProps.horizontalWindow?.startPx === nextProps.horizontalWindow?.startPx && prevProps.horizontalWindow?.endPx === nextProps.horizontalWindow?.endPx;
5041
6038
  };
5042
6039
  var GridBackground = import_react6.default.memo(
5043
- ({ dateRange, dayWidth, totalHeight, viewMode = "day", isCustomWeekend, className, showWeekendBlocks = true, showGridLines = true }) => {
6040
+ ({ dateRange, dayWidth, totalHeight, viewMode = "day", isCustomWeekend, className, showWeekendBlocks = true, showGridLines = true, horizontalWindow }) => {
5044
6041
  const weekGridLines = (0, import_react6.useMemo)(() => {
5045
6042
  if (viewMode !== "week") return [];
5046
6043
  return calculateWeekGridLines(dateRange, dayWidth);
@@ -5057,6 +6054,23 @@ var GridBackground = import_react6.default.memo(
5057
6054
  if (viewMode === "week" || viewMode === "month") return [];
5058
6055
  return calculateWeekendBlocks(dateRange, dayWidth, isCustomWeekend);
5059
6056
  }, [dateRange, dayWidth, viewMode, isCustomWeekend]);
6057
+ const inHorizontalWindow = (left, width = 1) => !horizontalWindow || left <= horizontalWindow.endPx && left + width >= horizontalWindow.startPx;
6058
+ const visibleWeekendBlocks = (0, import_react6.useMemo)(
6059
+ () => weekendBlocks.filter((block) => inHorizontalWindow(block.left, block.width)),
6060
+ [weekendBlocks, horizontalWindow?.startPx, horizontalWindow?.endPx]
6061
+ );
6062
+ const visibleWeekGridLines = (0, import_react6.useMemo)(
6063
+ () => weekGridLines.filter((line) => inHorizontalWindow(line.x)),
6064
+ [weekGridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
6065
+ );
6066
+ const visibleMonthGridLines = (0, import_react6.useMemo)(
6067
+ () => monthGridLines.filter((line) => inHorizontalWindow(line.x)),
6068
+ [monthGridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
6069
+ );
6070
+ const visibleGridLines = (0, import_react6.useMemo)(
6071
+ () => gridLines.filter((line) => inHorizontalWindow(line.x)),
6072
+ [gridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
6073
+ );
5060
6074
  const gridWidth = (0, import_react6.useMemo)(() => {
5061
6075
  return Math.round(dateRange.length * dayWidth);
5062
6076
  }, [dateRange.length, dayWidth]);
@@ -5069,7 +6083,7 @@ var GridBackground = import_react6.default.memo(
5069
6083
  height: `${totalHeight}px`
5070
6084
  },
5071
6085
  children: [
5072
- showWeekendBlocks && weekendBlocks.map((block, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
6086
+ showWeekendBlocks && visibleWeekendBlocks.map((block, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5073
6087
  "div",
5074
6088
  {
5075
6089
  className: "gantt-gb-weekendBlock",
@@ -5082,7 +6096,7 @@ var GridBackground = import_react6.default.memo(
5082
6096
  )),
5083
6097
  showGridLines && (viewMode === "week" ? (
5084
6098
  // Week-view: one line per week column boundary
5085
- weekGridLines.map((line, index) => {
6099
+ visibleWeekGridLines.map((line, index) => {
5086
6100
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : "gantt-gb-weekSeparator";
5087
6101
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5088
6102
  "div",
@@ -5095,7 +6109,7 @@ var GridBackground = import_react6.default.memo(
5095
6109
  })
5096
6110
  ) : viewMode === "month" ? (
5097
6111
  // Month-view: thin line at each month boundary, thick at year boundary
5098
- monthGridLines.map((line, index) => {
6112
+ visibleMonthGridLines.map((line, index) => {
5099
6113
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : "gantt-gb-weekSeparator";
5100
6114
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5101
6115
  "div",
@@ -5107,8 +6121,8 @@ var GridBackground = import_react6.default.memo(
5107
6121
  );
5108
6122
  })
5109
6123
  ) : (
5110
- // Day-view: existing code (unchanged)
5111
- gridLines.map((line, index) => {
6124
+ // Day-view: render only the lines inside the horizontal window.
6125
+ visibleGridLines.map((line, index) => {
5112
6126
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : line.isWeekStart ? "gantt-gb-weekSeparator" : "gantt-gb-dayLine";
5113
6127
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5114
6128
  "div",
@@ -5220,7 +6234,8 @@ var DependencyLines = import_react7.default.memo(({
5220
6234
  criticalTaskIds,
5221
6235
  businessDays = true,
5222
6236
  weekendPredicate,
5223
- onDependencyClick
6237
+ onDependencyClick,
6238
+ horizontalWindow
5224
6239
  }) => {
5225
6240
  const [hoveredLineId, setHoveredLineId] = (0, import_react7.useState)(null);
5226
6241
  const tasksForPositions = allTasks ?? tasks;
@@ -5384,6 +6399,10 @@ var DependencyLines = import_react7.default.memo(({
5384
6399
  }
5385
6400
  return lines2;
5386
6401
  }, [tasks, allTasks, taskPositions, taskIndices, cycleInfo, collapsedParentIds, criticalTaskIds]);
6402
+ const visibleLines = (0, import_react7.useMemo)(() => {
6403
+ if (!horizontalWindow) return lines;
6404
+ return lines.filter((line) => Math.max(line.fromX, line.toX) >= horizontalWindow.startPx && Math.min(line.fromX, line.toX) <= horizontalWindow.endPx);
6405
+ }, [lines, horizontalWindow?.startPx, horizontalWindow?.endPx]);
5387
6406
  const svgHeight = totalHeight ?? tasks.length * rowHeight;
5388
6407
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
5389
6408
  "svg",
@@ -5491,7 +6510,7 @@ var DependencyLines = import_react7.default.memo(({
5491
6510
  }
5492
6511
  )
5493
6512
  ] }),
5494
- lines.map(({ id, predecessorId, successorId, linkType, path, hasCycle, lag, fromX, toX, fromY, reverseOrder, isVirtual, isCritical }) => {
6513
+ visibleLines.map(({ id, predecessorId, successorId, linkType, path, hasCycle, lag, fromX, toX, fromY, reverseOrder, isVirtual, isCritical }) => {
5495
6514
  const isHovered = hoveredLineId === id;
5496
6515
  const isSelected = selectedDep != null && id === `${selectedDep.predecessorId}-${selectedDep.successorId}-${selectedDep.linkType}`;
5497
6516
  let pathClassName = "gantt-dependency-path";
@@ -6331,7 +7350,7 @@ var LINK_TYPE_LABELS = {
6331
7350
  };
6332
7351
 
6333
7352
  // src/components/TaskList/defaultTaskDates.ts
6334
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
7353
+ var DAY_MS3 = 24 * 60 * 60 * 1e3;
6335
7354
  var DEFAULT_TASK_DURATION_DAYS = 5;
6336
7355
  function toISODate(date) {
6337
7356
  return date.toISOString().split("T")[0];
@@ -6360,25 +7379,25 @@ function buildDefaultTaskDateRange(startDate, {
6360
7379
  }
6361
7380
  return {
6362
7381
  startDate: toISODate(start),
6363
- endDate: toISODate(new Date(start.getTime() + (durationDays - 1) * DAY_MS2))
7382
+ endDate: toISODate(new Date(start.getTime() + (durationDays - 1) * DAY_MS3))
6364
7383
  };
6365
7384
  }
6366
7385
 
6367
7386
  // src/components/TaskList/TaskListRow.tsx
6368
7387
  var import_jsx_runtime14 = require("react/jsx-runtime");
6369
- var DAY_MS3 = 24 * 60 * 60 * 1e3;
7388
+ var DAY_MS4 = 24 * 60 * 60 * 1e3;
6370
7389
  var LINK_TYPE_ORDER = ["FS", "SS", "FF", "SF"];
6371
7390
  var getInclusiveDurationDays = (startDate, endDate) => {
6372
7391
  const start = parseUTCDate(startDate);
6373
7392
  const end = parseUTCDate(endDate);
6374
7393
  return Math.max(
6375
7394
  1,
6376
- Math.round((end.getTime() - start.getTime()) / DAY_MS3) + 1
7395
+ Math.round((end.getTime() - start.getTime()) / DAY_MS4) + 1
6377
7396
  );
6378
7397
  };
6379
7398
  var getEndDateFromDuration = (startDate, durationDays) => {
6380
7399
  const start = parseUTCDate(startDate);
6381
- return new Date(start.getTime() + (durationDays - 1) * DAY_MS3).toISOString().split("T")[0];
7400
+ return new Date(start.getTime() + (durationDays - 1) * DAY_MS4).toISOString().split("T")[0];
6382
7401
  };
6383
7402
  var TrashIcon = () => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
6384
7403
  "svg",
@@ -10484,7 +11503,11 @@ var TaskList = ({
10484
11503
  onActiveCustomCellChange: setActiveCustomCell,
10485
11504
  taskListMenuCommands,
10486
11505
  hideTaskListRowActions,
10487
- rowClassName: [task.parentId && compositeParentIds.has(task.parentId) ? "gantt-tl-row-composite-detail" : "", getTaskListRowClassName?.(task)].filter(Boolean).join(" "),
11506
+ rowClassName: [
11507
+ task.parentId && compositeParentIds.has(task.parentId) ? "gantt-tl-row-composite-detail" : "",
11508
+ (task.activities?.length ?? 0) > 0 && (rowHeights?.[index] ?? rowHeight) > rowHeight ? "gantt-tl-row-activities" : "",
11509
+ getTaskListRowClassName?.(task)
11510
+ ].filter(Boolean).join(" "),
10488
11511
  compositeParentColor: task.parentId ? compositeParentsById.get(task.parentId)?.color : void 0,
10489
11512
  getTaskListNamePrefixIcon,
10490
11513
  taskDateChangeMode,
@@ -12462,7 +13485,7 @@ var import_react17 = require("react");
12462
13485
  var import_jsx_runtime19 = require("react/jsx-runtime");
12463
13486
  var AUTO_COLUMN_MIN_WIDTH = 72;
12464
13487
  var AUTO_COLUMN_MAX_WIDTH = 180;
12465
- var DAY_MS4 = 24 * 60 * 60 * 1e3;
13488
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
12466
13489
  function joinClasses(...values) {
12467
13490
  return values.filter(Boolean).join(" ");
12468
13491
  }
@@ -12497,8 +13520,8 @@ function getOverlayWidthPercent(column, overlayDateMs) {
12497
13520
  if (overlayDateMs >= endMs) {
12498
13521
  return 100;
12499
13522
  }
12500
- const totalDays = Math.max(1, Math.round((endMs - startMs) / DAY_MS4) + 1);
12501
- const elapsedDays = Math.min(totalDays, Math.max(0, Math.round((overlayDateMs - startMs) / DAY_MS4) + 1));
13523
+ const totalDays = Math.max(1, Math.round((endMs - startMs) / DAY_MS5) + 1);
13524
+ const elapsedDays = Math.min(totalDays, Math.max(0, Math.round((overlayDateMs - startMs) / DAY_MS5) + 1));
12502
13525
  return elapsedDays / totalDays * 100;
12503
13526
  }
12504
13527
  function isOverlayDateInColumn(column, overlayDateMs) {
@@ -12828,7 +13851,7 @@ function TableMatrix({
12828
13851
  // src/components/PlanFactMatrix/PlanFactMatrix.tsx
12829
13852
  var import_react18 = __toESM(require("react"));
12830
13853
  var import_jsx_runtime20 = require("react/jsx-runtime");
12831
- var DAY_MS5 = 24 * 60 * 60 * 1e3;
13854
+ var DAY_MS6 = 24 * 60 * 60 * 1e3;
12832
13855
  function joinClasses2(...values) {
12833
13856
  return values.filter(Boolean).join(" ");
12834
13857
  }
@@ -12858,8 +13881,8 @@ function getPlannedIndexRange(task, rangeStartMs, rangeLength) {
12858
13881
  if (rangeLength <= 0) return null;
12859
13882
  const start = parseUTCDate(task.startDate);
12860
13883
  const end = parseUTCDate(task.endDate);
12861
- const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS5);
12862
- const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS5);
13884
+ const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS6);
13885
+ const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS6);
12863
13886
  const clampedStartIndex = Math.max(0, startIndex);
12864
13887
  const clampedEndIndex = Math.min(rangeLength - 1, endIndex);
12865
13888
  if (clampedStartIndex > clampedEndIndex) return null;
@@ -14228,80 +15251,13 @@ async function printGanttChart({
14228
15251
  }
14229
15252
  }
14230
15253
 
14231
- // src/components/GanttChart/previewStore.ts
14232
- function positionsEqual(left, right) {
14233
- return left?.left === right?.left && left?.width === right?.width;
14234
- }
14235
- function createTaskPreviewPositionStore() {
14236
- let positions = /* @__PURE__ */ new Map();
14237
- const listenersByTaskId = /* @__PURE__ */ new Map();
14238
- function notify(taskIds) {
14239
- for (const taskId of taskIds) {
14240
- const listeners = listenersByTaskId.get(taskId);
14241
- if (!listeners) continue;
14242
- for (const listener of listeners) {
14243
- listener();
14244
- }
14245
- }
14246
- }
14247
- return {
14248
- subscribeTask(taskId, listener) {
14249
- const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
14250
- listeners.add(listener);
14251
- listenersByTaskId.set(taskId, listeners);
14252
- return () => {
14253
- listeners.delete(listener);
14254
- if (listeners.size === 0) {
14255
- listenersByTaskId.delete(taskId);
14256
- }
14257
- };
14258
- },
14259
- getTaskPosition(taskId) {
14260
- return positions.get(taskId);
14261
- },
14262
- setPositions(nextPositions) {
14263
- const changedTaskIds = /* @__PURE__ */ new Set();
14264
- const mergedPositions = /* @__PURE__ */ new Map();
14265
- for (const [taskId, nextPosition] of nextPositions) {
14266
- const currentPosition = positions.get(taskId);
14267
- if (positionsEqual(currentPosition, nextPosition)) {
14268
- if (currentPosition) {
14269
- mergedPositions.set(taskId, currentPosition);
14270
- }
14271
- continue;
14272
- }
14273
- changedTaskIds.add(taskId);
14274
- mergedPositions.set(taskId, {
14275
- left: nextPosition.left,
14276
- width: nextPosition.width
14277
- });
14278
- }
14279
- for (const taskId of positions.keys()) {
14280
- if (!nextPositions.has(taskId)) {
14281
- changedTaskIds.add(taskId);
14282
- }
14283
- }
14284
- if (changedTaskIds.size === 0) {
14285
- return;
14286
- }
14287
- positions = mergedPositions;
14288
- notify(changedTaskIds);
14289
- },
14290
- clear() {
14291
- if (positions.size === 0) {
14292
- return;
14293
- }
14294
- const changedTaskIds = new Set(positions.keys());
14295
- positions = /* @__PURE__ */ new Map();
14296
- notify(changedTaskIds);
14297
- }
14298
- };
14299
- }
14300
-
14301
15254
  // src/components/GanttChart/GanttChart.tsx
14302
15255
  var import_jsx_runtime21 = require("react/jsx-runtime");
14303
15256
  var SCROLL_TO_ROW_CONTEXT_ROWS = 2;
14304
15257
  var TASK_ROW_OVERSCAN = 8;
15258
+ var SCROLL_TOP_BUCKET_PX = 64;
15259
+ var HORIZONTAL_WINDOW_BUCKET_PX = 480;
15260
+ var HORIZONTAL_WINDOW_OVERSCAN_PX = 960;
14305
15261
  var PLAN_FACT_COLUMN_OVERSCAN = 24;
14306
15262
  var PLAN_FACT_COLUMN_WINDOW_STEP = 14;
14307
15263
  var DEFAULT_INITIAL_VIEWPORT_HEIGHT = 768;
@@ -14495,6 +15451,11 @@ function TaskGanttChartInner(props, ref) {
14495
15451
  showTaskDateLabels = true,
14496
15452
  showTaskNames = true,
14497
15453
  showCompositeSegments = true,
15454
+ activityClickToDrag = false,
15455
+ activityActivationMode = "none",
15456
+ showActivityLinks = true,
15457
+ showActivityLag = true,
15458
+ activityTooltip,
14498
15459
  timelineMarkers,
14499
15460
  additionalColumns,
14500
15461
  hiddenTaskListColumns,
@@ -14528,17 +15489,41 @@ function TaskGanttChartInner(props, ref) {
14528
15489
  const clearSelectedTaskTimeoutRef = (0, import_react19.useRef)(null);
14529
15490
  const hasAutoScrolledToTodayRef = (0, import_react19.useRef)(false);
14530
15491
  const previewPositionStoreRef = (0, import_react19.useRef)(null);
15492
+ const activityPreviewStoreRef = (0, import_react19.useRef)(null);
15493
+ const activityBlockStoreRef = (0, import_react19.useRef)(null);
15494
+ const activityDragOwnerRef = (0, import_react19.useRef)(null);
15495
+ const activityActivationStoreRef = (0, import_react19.useRef)(null);
14531
15496
  const renderedTaskIdsRef = (0, import_react19.useRef)(/* @__PURE__ */ new Set());
14532
15497
  if (previewPositionStoreRef.current === null) {
14533
15498
  previewPositionStoreRef.current = createTaskPreviewPositionStore();
14534
15499
  }
15500
+ if (activityPreviewStoreRef.current === null) {
15501
+ activityPreviewStoreRef.current = createActivityPreviewStore();
15502
+ }
15503
+ if (activityBlockStoreRef.current === null) {
15504
+ activityBlockStoreRef.current = createActivityBlockStore();
15505
+ }
15506
+ if (activityDragOwnerRef.current === null) {
15507
+ activityDragOwnerRef.current = createActivityDragOwner();
15508
+ }
15509
+ if (activityActivationStoreRef.current === null) {
15510
+ activityActivationStoreRef.current = createActivityActivationStore();
15511
+ }
14535
15512
  const previewPositionStore = previewPositionStoreRef.current;
15513
+ const activityPreviewStore = activityPreviewStoreRef.current;
15514
+ const activityBlockStore = activityBlockStoreRef.current;
15515
+ const activityDragOwner = activityDragOwnerRef.current;
15516
+ const activityActivationStore = activityActivationStoreRef.current;
14536
15517
  const [selectedTaskId, setSelectedTaskId] = (0, import_react19.useState)(null);
14537
15518
  const [taskListHasRightShadow, setTaskListHasRightShadow] = (0, import_react19.useState)(false);
14538
15519
  const [internalTaskDateChangeMode, setInternalTaskDateChangeMode] = (0, import_react19.useState)("preserve-duration");
14539
15520
  const [scrollViewport, setScrollViewport] = (0, import_react19.useState)(() => ({
14540
15521
  scrollTop: 0,
14541
- viewportHeight: getInitialScrollViewportHeight(containerHeight, headerHeight + 1)
15522
+ viewportHeight: getInitialScrollViewportHeight(containerHeight, headerHeight + 1),
15523
+ // Ширина видимой области графика и «бакетированный» горизонтальный скролл —
15524
+ // основа горизонтального окна рендера (см. horizontalWindow).
15525
+ viewportWidth: 0,
15526
+ chartScrollLeft: 0
14542
15527
  }));
14543
15528
  const [forceFullRenderForPrint, setForceFullRenderForPrint] = (0, import_react19.useState)(false);
14544
15529
  const [planFactDateWindow, setPlanFactDateWindow] = (0, import_react19.useState)({ start: 0, end: 0 });
@@ -14776,14 +15761,21 @@ function TaskGanttChartInner(props, ref) {
14776
15761
  if (isPlanFactMode) {
14777
15762
  return getPlanFactRangeTasks(normalizedTasks);
14778
15763
  }
14779
- if (!showBaseline) {
14780
- return normalizedTasks;
15764
+ const rangeTasks = [];
15765
+ for (const task of normalizedTasks) {
15766
+ if (showBaseline) {
15767
+ rangeTasks.push({
15768
+ startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
15769
+ endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
15770
+ });
15771
+ } else {
15772
+ rangeTasks.push(task);
15773
+ }
15774
+ for (const activity of task.activities ?? []) {
15775
+ rangeTasks.push({ startDate: activity.startDate, endDate: activity.endDate });
15776
+ }
14781
15777
  }
14782
- return normalizedTasks.map((task) => ({
14783
- ...task,
14784
- startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
14785
- endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
14786
- }));
15778
+ return rangeTasks;
14787
15779
  }, [isPlanFactMode, normalizedTasks, showBaseline]);
14788
15780
  const dateRange = (0, import_react19.useMemo)(
14789
15781
  () => {
@@ -14828,6 +15820,25 @@ function TaskGanttChartInner(props, ref) {
14828
15820
  }
14829
15821
  return tasks2;
14830
15822
  }, [normalizedTasks, collapsedParentIds, filterMode, taskFilter, criticalPathMode, criticalTaskIds]);
15823
+ const monthStart = (0, import_react19.useMemo)(() => {
15824
+ if (dateRange.length === 0) {
15825
+ return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
15826
+ }
15827
+ const firstDay = dateRange[0];
15828
+ if (explicitDateRange && !isPlanFactMode) {
15829
+ return new Date(firstDay);
15830
+ }
15831
+ return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
15832
+ }, [dateRange, explicitDateRange, isPlanFactMode]);
15833
+ const activityLaneCounts = (0, import_react19.useMemo)(() => {
15834
+ const counts = /* @__PURE__ */ new Map();
15835
+ for (const task of visibleTasks) {
15836
+ if (task.activities && task.activities.length > 0) {
15837
+ counts.set(task.id, computeActivityLanes(task.activities, monthStart, dayWidth).laneCount);
15838
+ }
15839
+ }
15840
+ return counts;
15841
+ }, [visibleTasks, monthStart, dayWidth]);
14831
15842
  const compositeRowLayout = (0, import_react19.useMemo)(() => {
14832
15843
  const compositeIds = new Set(normalizedTasks.filter((task) => task.composite).map((task) => task.id));
14833
15844
  const tops = [];
@@ -14835,12 +15846,12 @@ function TaskGanttChartInner(props, ref) {
14835
15846
  let top = 0;
14836
15847
  for (const task of visibleTasks) {
14837
15848
  tops.push(top);
14838
- const height = task.parentId && compositeIds.has(task.parentId) ? Math.min(effectiveRowHeight, 22) : effectiveRowHeight;
15849
+ const height = task.parentId && compositeIds.has(task.parentId) ? Math.min(effectiveRowHeight, 22) : Math.max(effectiveRowHeight, activityLanesExtraHeight(activityLaneCounts.get(task.id) ?? 0));
14839
15850
  heights.push(height);
14840
15851
  top += height;
14841
15852
  }
14842
15853
  return { tops, heights, totalHeight: top };
14843
- }, [normalizedTasks, visibleTasks, effectiveRowHeight]);
15854
+ }, [normalizedTasks, visibleTasks, effectiveRowHeight, activityLaneCounts]);
14844
15855
  const compositeParentsById = (0, import_react19.useMemo)(
14845
15856
  () => new Map(normalizedTasks.filter((task) => task.composite).map((task) => [task.id, task])),
14846
15857
  [normalizedTasks]
@@ -14879,16 +15890,6 @@ function TaskGanttChartInner(props, ref) {
14879
15890
  }
14880
15891
  return `calc(${containerHeight} - ${timelineHeaderHeight}px)`;
14881
15892
  }, [containerHeight, isPlanFactMode, isTableMatrixMode, timelineHeaderHeight]);
14882
- const monthStart = (0, import_react19.useMemo)(() => {
14883
- if (dateRange.length === 0) {
14884
- return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
14885
- }
14886
- const firstDay = dateRange[0];
14887
- if (explicitDateRange && !isPlanFactMode) {
14888
- return new Date(firstDay);
14889
- }
14890
- return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
14891
- }, [dateRange, explicitDateRange, isPlanFactMode]);
14892
15893
  const todayIndex = (0, import_react19.useMemo)(() => {
14893
15894
  const today = getTodayLocalUtcDate();
14894
15895
  return dateRange.findIndex((day) => day.getTime() === today.getTime());
@@ -14931,12 +15932,23 @@ function TaskGanttChartInner(props, ref) {
14931
15932
  const nextScrollTop = container.scrollTop;
14932
15933
  const nextScrollLeft = container.scrollLeft;
14933
15934
  const nextChartScrollLeft = Math.max(0, nextScrollLeft);
15935
+ const bucketedScrollTop = Math.floor(nextScrollTop / SCROLL_TOP_BUCKET_PX) * SCROLL_TOP_BUCKET_PX;
15936
+ const bucketedChartScrollLeft = Math.floor(nextChartScrollLeft / HORIZONTAL_WINDOW_BUCKET_PX) * HORIZONTAL_WINDOW_BUCKET_PX;
14934
15937
  setTaskListHasRightShadow(
14935
15938
  (previous) => previous === nextHasRightShadow ? previous : nextHasRightShadow
14936
15939
  );
14937
- setScrollViewport(
14938
- (previous) => previous.viewportHeight > 0 && nextViewportHeight === 0 ? { scrollTop: nextScrollTop, viewportHeight: previous.viewportHeight } : previous.scrollTop === nextScrollTop && previous.viewportHeight === nextViewportHeight ? previous : { scrollTop: nextScrollTop, viewportHeight: nextViewportHeight }
14939
- );
15940
+ setScrollViewport((previous) => {
15941
+ const resolvedViewportHeight = previous.viewportHeight > 0 && nextViewportHeight === 0 ? previous.viewportHeight : nextViewportHeight;
15942
+ if (previous.scrollTop === bucketedScrollTop && previous.viewportHeight === resolvedViewportHeight && previous.viewportWidth === nextViewportWidth && previous.chartScrollLeft === bucketedChartScrollLeft) {
15943
+ return previous;
15944
+ }
15945
+ return {
15946
+ scrollTop: bucketedScrollTop,
15947
+ viewportHeight: resolvedViewportHeight,
15948
+ viewportWidth: nextViewportWidth,
15949
+ chartScrollLeft: bucketedChartScrollLeft
15950
+ };
15951
+ });
14940
15952
  setPlanFactDateWindow((previous) => {
14941
15953
  if (!isPlanFactMode || dateRange.length === 0 || nextViewportWidth <= 0) {
14942
15954
  return previous;
@@ -15052,11 +16064,15 @@ function TaskGanttChartInner(props, ref) {
15052
16064
  return;
15053
16065
  }
15054
16066
  previewPositionStore.clear();
16067
+ activityPreviewStore.clear();
16068
+ activityBlockStore.clear();
16069
+ activityDragOwner.set(null);
16070
+ activityActivationStore.setActiveKey(null);
15055
16071
  setCascadeOverrides((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
15056
16072
  setPreviewTasksById((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
15057
16073
  setDraggedTaskOverride((current) => current === null ? current : null);
15058
16074
  setDragGuideLines((current) => current === null ? current : null);
15059
- }, [controlledScheduleIdentity, previewPositionStore]);
16075
+ }, [controlledScheduleIdentity, previewPositionStore, activityPreviewStore, activityBlockStore, activityDragOwner, activityActivationStore]);
15060
16076
  (0, import_react19.useEffect)(() => {
15061
16077
  const result = validateDependencies(tasks);
15062
16078
  setValidationResult(result);
@@ -15067,7 +16083,11 @@ function TaskGanttChartInner(props, ref) {
15067
16083
  window.clearTimeout(clearSelectedTaskTimeoutRef.current);
15068
16084
  }
15069
16085
  previewPositionStore.clear();
15070
- }, [previewPositionStore]);
16086
+ activityPreviewStore.clear();
16087
+ activityBlockStore.clear();
16088
+ activityDragOwner.set(null);
16089
+ activityActivationStore.setActiveKey(null);
16090
+ }, [previewPositionStore, activityPreviewStore, activityBlockStore, activityDragOwner, activityActivationStore]);
15071
16091
  const handleTaskChange = (0, import_react19.useCallback)((updatedTasks, durationEdit) => {
15072
16092
  let updatedTask = updatedTasks[0];
15073
16093
  if (!updatedTask) return;
@@ -15113,9 +16133,9 @@ function TaskGanttChartInner(props, ref) {
15113
16133
  return;
15114
16134
  }
15115
16135
  if (datesChanged && isTaskParent(updatedTask.id, tasks)) {
15116
- const projectDayDelta = (from, to) => businessDays && isCustomWeekend ? getBusinessDayOffset(from, to, isCustomWeekend) : Math.round((to.getTime() - from.getTime()) / DAY_MS);
15117
- const deltaStart = projectDayDelta(origStart, newStart);
15118
- const deltaEnd = projectDayDelta(origEnd, newEnd);
16136
+ const projectDayDelta2 = (from, to) => businessDays && isCustomWeekend ? getBusinessDayOffset(from, to, isCustomWeekend) : Math.round((to.getTime() - from.getTime()) / DAY_MS);
16137
+ const deltaStart = projectDayDelta2(origStart, newStart);
16138
+ const deltaEnd = projectDayDelta2(origEnd, newEnd);
15119
16139
  const isUniformMove = deltaStart === deltaEnd && deltaStart !== 0;
15120
16140
  if (!isUniformMove) {
15121
16141
  const anchor = deltaStart === 0 ? "start" : deltaEnd === 0 ? "end" : "start";
@@ -15303,6 +16323,15 @@ function TaskGanttChartInner(props, ref) {
15303
16323
  }
15304
16324
  return ids;
15305
16325
  }, [draggedTaskOverride]);
16326
+ const horizontalWindow = (0, import_react19.useMemo)(() => {
16327
+ const width = scrollViewport.viewportWidth;
16328
+ if (width <= 0) return void 0;
16329
+ const left = scrollViewport.chartScrollLeft;
16330
+ return {
16331
+ startPx: Math.max(0, left - HORIZONTAL_WINDOW_OVERSCAN_PX),
16332
+ endPx: left + width + HORIZONTAL_WINDOW_OVERSCAN_PX
16333
+ };
16334
+ }, [scrollViewport.chartScrollLeft, scrollViewport.viewportWidth]);
15306
16335
  const visibleTaskWindowIndices = (0, import_react19.useMemo)(() => {
15307
16336
  const totalTasks = visibleTasks.length;
15308
16337
  if (totalTasks === 0) {
@@ -15637,6 +16666,9 @@ function TaskGanttChartInner(props, ref) {
15637
16666
  const handlePanStart = (0, import_react19.useCallback)((e) => {
15638
16667
  if (e.button !== 0) return;
15639
16668
  const target = e.target;
16669
+ if (!target.closest(".gantt-tr-activityBar")) {
16670
+ activityActivationStore.setActiveKey(null);
16671
+ }
15640
16672
  if (target.closest("[data-taskbar]")) return;
15641
16673
  if (target.closest("input, button, textarea, [contenteditable]")) return;
15642
16674
  if (target.closest(".gantt-tl-overlay")) return;
@@ -15657,7 +16689,7 @@ function TaskGanttChartInner(props, ref) {
15657
16689
  }
15658
16690
  container.style.cursor = "grabbing";
15659
16691
  e.preventDefault();
15660
- }, []);
16692
+ }, [activityActivationStore]);
15661
16693
  (0, import_react19.useEffect)(() => {
15662
16694
  const flushPanMove = () => {
15663
16695
  const pan = panStateRef.current;
@@ -15887,7 +16919,8 @@ function TaskGanttChartInner(props, ref) {
15887
16919
  dayWidth,
15888
16920
  totalHeight: totalGridHeight,
15889
16921
  viewMode,
15890
- isCustomWeekend
16922
+ isCustomWeekend,
16923
+ horizontalWindow
15891
16924
  }
15892
16925
  ),
15893
16926
  todayInRange && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
@@ -15928,6 +16961,7 @@ function TaskGanttChartInner(props, ref) {
15928
16961
  selectedDep: selectedChip,
15929
16962
  businessDays,
15930
16963
  weekendPredicate: isCustomWeekend,
16964
+ horizontalWindow,
15931
16965
  onDependencyClick: handleDependencyLineClick,
15932
16966
  criticalTaskIds: criticalPathMode ? criticalTaskIds : void 0
15933
16967
  }
@@ -16034,6 +17068,7 @@ function TaskGanttChartInner(props, ref) {
16034
17068
  setDragGuideLines((current) => current === null ? current : null);
16035
17069
  setDraggedTaskOverride((current) => current === null ? current : null);
16036
17070
  previewPositionStore.clear();
17071
+ activityPreviewStore.clear();
16037
17072
  setCascadeOverrides((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
16038
17073
  setPreviewTasksById((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
16039
17074
  }
@@ -16044,6 +17079,16 @@ function TaskGanttChartInner(props, ref) {
16044
17079
  disableConstraints: disableConstraints ?? false,
16045
17080
  deferCascadePreview: normalizedTasks.length >= 1e3,
16046
17081
  previewPositionStore,
17082
+ activityPreviewStore,
17083
+ activityBlockStore,
17084
+ activityDragOwner,
17085
+ activityActivationStore,
17086
+ activityClickToDrag,
17087
+ activityActivationMode,
17088
+ showActivityLinks,
17089
+ showActivityLag,
17090
+ activityTooltip,
17091
+ horizontalWindow,
16047
17092
  onCascadeProgress: handleCascadeProgress,
16048
17093
  onCascade: handleCascade,
16049
17094
  onScheduleIntent,
@@ -16151,6 +17196,9 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16151
17196
  };
16152
17197
  // Annotate the CommonJS export names for ESM import in node:
16153
17198
  0 && (module.exports = {
17199
+ ACTIVITY_LANES_PADDING,
17200
+ ACTIVITY_LANE_BAR_HEIGHT,
17201
+ ACTIVITY_LANE_STEP,
16154
17202
  Button,
16155
17203
  Calendar,
16156
17204
  DAY_MS,
@@ -16172,6 +17220,11 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16172
17220
  TaskRow,
16173
17221
  TimeScaleHeader,
16174
17222
  TodayIndicator,
17223
+ activityBounds,
17224
+ activityChainMinStartDelta,
17225
+ activityLanesExtraHeight,
17226
+ activitySpanMs,
17227
+ activityStartConstraint,
16175
17228
  addBusinessDays,
16176
17229
  alignToWorkingDay,
16177
17230
  and,
@@ -16194,6 +17247,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16194
17247
  cascadeByLinks,
16195
17248
  clampDateRangeForIncomingFS,
16196
17249
  clampTaskRangeForIncomingFS,
17250
+ computeActivityLanes,
16197
17251
  computeCriticalPath,
16198
17252
  computeLagFromDates,
16199
17253
  computeParentDates,
@@ -16248,10 +17302,12 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16248
17302
  normalizeUTCDate,
16249
17303
  not,
16250
17304
  or,
17305
+ packIntervals,
16251
17306
  parseDateOnly,
16252
17307
  parseUTCDate,
16253
17308
  pixelsToDate,
16254
17309
  progressInRange,
17310
+ pushActivityChain,
16255
17311
  recalculateIncomingLags,
16256
17312
  recalculateProjectSchedule,
16257
17313
  recalculateTaskFromDependencies,
@@ -16261,6 +17317,8 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16261
17317
  resolveDateRangeFromPixels,
16262
17318
  resolveTaskHorizontalGeometry,
16263
17319
  scaleTaskSubtreeDuration,
17320
+ shiftActivityChain,
17321
+ shiftActivityDate,
16264
17322
  shiftBusinessDayOffset,
16265
17323
  subtractBusinessDays,
16266
17324
  universalCascade,