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.mjs CHANGED
@@ -461,949 +461,1487 @@ function subtractBusinessDays2(endDate, businessDays, weekendPredicate) {
461
461
  return result.toISOString().split("T")[0];
462
462
  }
463
463
 
464
- // src/core/scheduling/dependencies.ts
465
- function normalizePredecessorDates(predecessor, parseDateFn) {
466
- const predStart = parseDateFn(predecessor.startDate);
467
- const predEnd = predecessor.type === "milestone" ? predStart : parseDateFn(predecessor.endDate);
468
- return { predStart, predEnd };
469
- }
470
- function getDependencyLag(dep) {
471
- return Number.isFinite(dep.lag) ? dep.lag : 0;
472
- }
473
- function normalizeTaskDependencyLags(task) {
474
- return task;
475
- }
476
- function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd, businessDays = false, weekendPredicate, successorType) {
477
- if (linkType !== "FS") {
478
- return lag;
479
- }
480
- const predecessorDuration = getTaskDuration(
481
- predecessorStart,
482
- predecessorEnd,
483
- businessDays,
484
- weekendPredicate
464
+ // src/utils/geometry.ts
465
+ var getUTCDayDifference = (date1, date2) => {
466
+ const ms1 = Date.UTC(
467
+ date1.getUTCFullYear(),
468
+ date1.getUTCMonth(),
469
+ date1.getUTCDate()
485
470
  );
486
- const minimumLag = successorType === "milestone" ? -Math.max(0, predecessorDuration - 1) : -predecessorDuration;
487
- return Math.max(minimumLag, lag);
488
- }
489
- function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate, successorType) {
490
- const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
491
- const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
492
- const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
493
- const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
494
- if (!businessDays || !weekendPredicate) {
495
- switch (linkType) {
496
- case "FS":
497
- return normalizeDependencyLag(
498
- linkType,
499
- Math.round((sS - pE) / DAY_MS) - (successorType === "milestone" ? 0 : 1),
500
- predStart,
501
- predEnd,
502
- businessDays,
503
- weekendPredicate,
504
- successorType
505
- );
506
- case "SS":
507
- return Math.round((sS - pS) / DAY_MS);
508
- case "FF":
509
- return Math.round((sE - pE) / DAY_MS);
510
- case "SF":
511
- return Math.round((sE - pS) / DAY_MS) + 1;
471
+ const ms2 = Date.UTC(
472
+ date2.getUTCFullYear(),
473
+ date2.getUTCMonth(),
474
+ date2.getUTCDate()
475
+ );
476
+ return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
477
+ };
478
+ var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
479
+ const startOffset = getUTCDayDifference(taskStartDate, monthStart);
480
+ const duration = getUTCDayDifference(taskEndDate, taskStartDate);
481
+ const left = Math.round(startOffset * dayWidth);
482
+ const width = Math.round((duration + 1) * dayWidth);
483
+ return { left, width };
484
+ };
485
+ var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
486
+ const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
487
+ const centerX = Math.round(left + width / 2);
488
+ const halfSize = Math.round(size / 2);
489
+ return {
490
+ centerX,
491
+ left: centerX - halfSize,
492
+ right: centerX + halfSize,
493
+ size
494
+ };
495
+ };
496
+ var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
497
+ const halfDiagonal = Math.round(size / Math.SQRT2);
498
+ const centerX = Math.round(dayLeft + dayWidth / 2);
499
+ return {
500
+ left: centerX - halfDiagonal,
501
+ right: centerX + halfDiagonal
502
+ };
503
+ };
504
+ var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
505
+ const startDate = new Date(task.startDate);
506
+ const endDate = new Date(task.endDate);
507
+ if (task.type === "milestone") {
508
+ const size = 14;
509
+ if (override) {
510
+ const bounds2 = calculateMilestoneConnectionBounds(override.left, dayWidth, size);
511
+ return {
512
+ ...bounds2,
513
+ centerX: Math.round(override.left + dayWidth / 2)
514
+ };
512
515
  }
516
+ const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
517
+ const bounds = calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
518
+ return {
519
+ ...bounds,
520
+ centerX: Math.round(bar2.left + dayWidth / 2)
521
+ };
513
522
  }
514
- const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
515
- const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
516
- const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
517
- switch (linkType) {
518
- case "FS":
519
- return normalizeDependencyLag(
520
- linkType,
521
- businessOffset - (successorType === "milestone" ? 0 : 1),
522
- predStart,
523
- predEnd,
524
- businessDays,
525
- weekendPredicate,
526
- successorType
527
- );
528
- case "SS":
529
- return businessOffset;
530
- case "FF":
531
- return businessOffset;
532
- case "SF":
533
- return businessOffset + 1;
523
+ if (override) {
524
+ return {
525
+ left: override.left,
526
+ right: override.left + override.width,
527
+ centerX: Math.round(override.left + override.width / 2)
528
+ };
534
529
  }
535
- }
536
- function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate, successorType) {
537
- const normalizedLag = normalizeDependencyLag(
538
- linkType,
539
- lag,
540
- predecessorStart,
541
- predecessorEnd,
542
- businessDays,
543
- weekendPredicate,
544
- successorType
530
+ const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
531
+ return {
532
+ left: bar.left,
533
+ right: bar.left + bar.width,
534
+ centerX: Math.round(bar.left + bar.width / 2)
535
+ };
536
+ };
537
+ var pixelsToDate = (pixels, monthStart, dayWidth) => {
538
+ const days = Math.round(pixels / dayWidth);
539
+ return new Date(Date.UTC(
540
+ monthStart.getUTCFullYear(),
541
+ monthStart.getUTCMonth(),
542
+ monthStart.getUTCDate() + days
543
+ ));
544
+ };
545
+ var calculateGridWidth = (daysInMonth, dayWidth) => {
546
+ return Math.round(daysInMonth * dayWidth);
547
+ };
548
+ var MIN_EDGE_ZONE_PX = 8;
549
+ var MIN_MOVE_ZONE_PX = 16;
550
+ var detectEdgeZone = (clientX, taskBarElement, edgeZoneWidth = 12) => {
551
+ const rect = taskBarElement.getBoundingClientRect();
552
+ const relativeX = Math.round(clientX - rect.left);
553
+ const width = Math.round(rect.width);
554
+ const effectiveZone = Math.max(
555
+ MIN_EDGE_ZONE_PX,
556
+ Math.min(edgeZoneWidth, (width - MIN_MOVE_ZONE_PX) / 2)
545
557
  );
546
- if (!businessDays || !weekendPredicate) {
547
- switch (linkType) {
548
- case "FS":
549
- return new Date(predecessorEnd.getTime() + (normalizedLag + (successorType === "milestone" ? 0 : 1)) * DAY_MS);
550
- case "SS":
551
- return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
552
- case "FF":
553
- return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
554
- case "SF":
555
- return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
556
- }
557
- }
558
- const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
559
- let offset;
560
- switch (linkType) {
561
- case "FS":
562
- offset = normalizedLag + (successorType === "milestone" ? 0 : 1);
563
- break;
564
- case "SS":
565
- offset = normalizedLag;
566
- break;
567
- case "FF":
568
- offset = normalizedLag;
569
- break;
570
- case "SF":
571
- offset = normalizedLag - 1;
572
- break;
558
+ if (relativeX >= 0 && relativeX <= effectiveZone) {
559
+ return "left";
573
560
  }
574
- return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
575
- }
576
-
577
- // src/core/scheduling/criticalPath.ts
578
- function computeCriticalPath(tasks, options = {}) {
579
- const { businessDays = false, weekendPredicate } = options;
580
- const taskById = new Map(tasks.map((task) => [task.id, task]));
581
- const childIds = /* @__PURE__ */ new Set();
582
- for (const task of tasks) {
583
- if (task.parentId) childIds.add(task.parentId);
561
+ if (relativeX >= width - effectiveZone && relativeX <= width) {
562
+ return "right";
584
563
  }
585
- const leafTasks = tasks.filter((task) => !childIds.has(task.id));
586
- const leafIds = new Set(leafTasks.map((task) => task.id));
587
- const leafIdsByAncestor = /* @__PURE__ */ new Map();
588
- for (const leaf of leafTasks) {
589
- let ancestor = leaf.parentId;
590
- while (ancestor) {
591
- const set = leafIdsByAncestor.get(ancestor) ?? /* @__PURE__ */ new Set();
592
- set.add(leaf.id);
593
- leafIdsByAncestor.set(ancestor, set);
594
- ancestor = taskById.get(ancestor)?.parentId;
595
- }
564
+ return "move";
565
+ };
566
+ var getCursorForPosition = (position) => {
567
+ switch (position) {
568
+ case "left":
569
+ case "right":
570
+ return "ew-resize";
571
+ case "move":
572
+ return "grab";
573
+ default:
574
+ return "default";
596
575
  }
597
- const durationOf = (task) => getTaskDuration(task.startDate, task.endDate, businessDays, weekendPredicate);
598
- const projectStartDate = leafTasks.reduce((earliest, task) => {
599
- const taskStart = parseDateOnly(task.startDate);
600
- return taskStart.getTime() < earliest.getTime() ? taskStart : earliest;
601
- }, parseDateOnly(leafTasks[0]?.startDate ?? /* @__PURE__ */ new Date()));
602
- const offsetFromProjectStart = (date) => {
603
- const targetDate = parseDateOnly(date);
604
- if (businessDays && weekendPredicate) {
605
- return getBusinessDayOffset(projectStartDate, targetDate, weekendPredicate);
606
- }
607
- return Math.round((targetDate.getTime() - projectStartDate.getTime()) / DAY_MS);
608
- };
609
- const scheduledStart = /* @__PURE__ */ new Map();
610
- for (const leaf of leafTasks) {
611
- scheduledStart.set(leaf.id, offsetFromProjectStart(leaf.startDate));
576
+ };
577
+ var calculateGridLines = (dateRange, dayWidth) => {
578
+ const lines = [];
579
+ for (let i = 0; i < dateRange.length; i++) {
580
+ const date = dateRange[i];
581
+ const x = Math.round(i * dayWidth);
582
+ const isMonthStart = i === 0 ? false : date.getUTCDate() === 1;
583
+ const isWeekStart = date.getUTCDay() === 1;
584
+ lines.push({ x, isMonthStart, isWeekStart });
612
585
  }
613
- const succByPred = /* @__PURE__ */ new Map();
614
- const predBySucc = /* @__PURE__ */ new Map();
615
- const seenEdges = /* @__PURE__ */ new Set();
616
- const addEdge = (predecessorId, succId, lag, type) => {
617
- const edgeKey = `${predecessorId}|${succId}|${type}`;
618
- if (seenEdges.has(edgeKey)) return;
619
- seenEdges.add(edgeKey);
620
- const outEdges = succByPred.get(predecessorId) ?? [];
621
- outEdges.push({ succId, lag, type });
622
- succByPred.set(predecessorId, outEdges);
623
- const inEdges = predBySucc.get(succId) ?? [];
624
- inEdges.push({ predId: predecessorId, lag, type });
625
- predBySucc.set(succId, inEdges);
626
- };
627
- for (const succ of leafTasks) {
628
- for (const dep of succ.dependencies ?? []) {
629
- const lag = getDependencyLag(dep);
630
- if (leafIds.has(dep.taskId)) {
631
- addEdge(dep.taskId, succ.id, lag, dep.type);
632
- continue;
633
- }
634
- const descendants = leafIdsByAncestor.get(dep.taskId);
635
- if (!descendants || descendants.size === 0) continue;
636
- for (const leafId of descendants) {
637
- if (leafId === succ.id) continue;
638
- addEdge(leafId, succ.id, lag, dep.type);
639
- }
640
- }
586
+ if (dateRange.length > 0) {
587
+ lines.push({
588
+ x: Math.round(dateRange.length * dayWidth),
589
+ isMonthStart: false,
590
+ isWeekStart: false
591
+ });
641
592
  }
642
- const indegree = /* @__PURE__ */ new Map();
643
- for (const leaf of leafTasks) indegree.set(leaf.id, 0);
644
- for (const edges of succByPred.values()) {
645
- for (const { succId } of edges) {
646
- indegree.set(succId, (indegree.get(succId) ?? 0) + 1);
593
+ return lines;
594
+ };
595
+ var calculateWeekendBlocks = (dateRange, dayWidth, isCustomWeekend) => {
596
+ const blocks = [];
597
+ let inWeekend = false;
598
+ let weekendStartIndex = -1;
599
+ for (let i = 0; i < dateRange.length; i++) {
600
+ const date = dateRange[i];
601
+ const isWeekend3 = isCustomWeekend ? isCustomWeekend(date) : date.getUTCDay() === 0 || date.getUTCDay() === 6;
602
+ if (isWeekend3 && !inWeekend) {
603
+ inWeekend = true;
604
+ weekendStartIndex = i;
605
+ } else if (!isWeekend3 && inWeekend) {
606
+ inWeekend = false;
607
+ const left = Math.round(weekendStartIndex * dayWidth);
608
+ const width = Math.round((i - weekendStartIndex) * dayWidth);
609
+ blocks.push({ left, width });
647
610
  }
648
611
  }
649
- const order = [];
650
- const enqueued = /* @__PURE__ */ new Set();
651
- const queue = leafTasks.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).map((leaf) => leaf.id);
652
- for (const id of queue) enqueued.add(id);
653
- while (queue.length > 0) {
654
- const current = queue.shift();
655
- order.push(current);
656
- for (const { succId } of succByPred.get(current) ?? []) {
657
- const next = (indegree.get(succId) ?? 0) - 1;
658
- indegree.set(succId, next);
659
- if (next === 0 && !enqueued.has(succId)) {
660
- enqueued.add(succId);
661
- queue.push(succId);
662
- }
663
- }
612
+ if (inWeekend && weekendStartIndex >= 0) {
613
+ const left = Math.round(weekendStartIndex * dayWidth);
614
+ const width = Math.round((dateRange.length - weekendStartIndex) * dayWidth);
615
+ blocks.push({ left, width });
664
616
  }
665
- const projectFinish = Math.max(
666
- 0,
667
- ...leafTasks.map((leaf) => (scheduledStart.get(leaf.id) ?? 0) + durationOf(leaf))
668
- );
669
- const LF = /* @__PURE__ */ new Map();
670
- const LS = /* @__PURE__ */ new Map();
671
- for (const leaf of leafTasks) LF.set(leaf.id, projectFinish);
672
- for (let i = order.length - 1; i >= 0; i--) {
673
- const id = order[i];
674
- const task = taskById.get(id);
675
- const durPred = durationOf(task);
676
- const outEdges = succByPred.get(id) ?? [];
677
- let lf = projectFinish;
678
- if (outEdges.length > 0) {
679
- const candidates = outEdges.map(({ succId, lag, type }) => {
680
- const succLs = LS.get(succId) ?? projectFinish;
681
- const durSucc = durationOf(taskById.get(succId));
682
- switch (type) {
683
- case "FS":
684
- return succLs - lag;
685
- case "SS":
686
- return succLs - lag + durPred;
687
- case "FF":
688
- return succLs + durSucc - lag;
689
- case "SF":
690
- return succLs + durSucc - lag + durPred;
691
- default:
692
- return projectFinish;
693
- }
694
- });
695
- lf = Math.min(projectFinish, ...candidates);
617
+ return blocks;
618
+ };
619
+ var calculateBezierPath = (from, to) => {
620
+ const verticalDistance = Math.abs(to.y - from.y);
621
+ const cpOffset = Math.max(verticalDistance * 0.5, 20);
622
+ if (from.y === to.y) {
623
+ const arcHeight = 20;
624
+ const midX = (from.x + to.x) / 2;
625
+ return `M ${from.x} ${from.y} Q ${midX} ${from.y - arcHeight} ${to.x} ${to.y}`;
626
+ }
627
+ const cp1x = from.x;
628
+ const cp1y = from.y + (to.y > from.y ? cpOffset : -cpOffset);
629
+ const cp2x = to.x;
630
+ const cp2y = to.y - (to.y > from.y ? cpOffset : -cpOffset);
631
+ 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)}`;
632
+ };
633
+ var calculateDependencyPath = (from, to, arrivesFromRight) => {
634
+ const fx = Math.round(from.x);
635
+ const fy = Math.round(from.y);
636
+ const tx = Math.round(to.x);
637
+ const ty = Math.round(to.y);
638
+ if (fy === ty) {
639
+ return `M ${fx} ${fy} H ${tx}`;
640
+ }
641
+ if (fx === tx) {
642
+ return `M ${fx} ${fy} V ${ty}`;
643
+ }
644
+ const C = 2;
645
+ const goingDown = ty > fy;
646
+ const dirY = goingDown ? 1 : -1;
647
+ if (arrivesFromRight) {
648
+ const goingRight = tx >= fx;
649
+ const dirX = goingRight ? 1 : -1;
650
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
651
+ return [
652
+ `M ${fx} ${fy}`,
653
+ `H ${tx - dirX * C}`,
654
+ `L ${tx} ${fy + dirY * C}`,
655
+ `V ${ty}`
656
+ ].join(" ");
696
657
  }
697
- LF.set(id, lf);
698
- LS.set(id, lf - durPred);
658
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
659
+ } else {
660
+ const goingRight = tx >= fx;
661
+ const dirX = goingRight ? 1 : -1;
662
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
663
+ return [
664
+ `M ${fx} ${fy}`,
665
+ `H ${tx - dirX * C}`,
666
+ `L ${tx} ${fy + dirY * C}`,
667
+ `V ${ty}`
668
+ ].join(" ");
669
+ }
670
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
699
671
  }
700
- const critical = /* @__PURE__ */ new Set();
701
- const ordered = new Set(order);
702
- for (const leaf of leafTasks) {
703
- if (!ordered.has(leaf.id)) continue;
704
- const hasGraphEdge = (succByPred.get(leaf.id)?.length ?? 0) > 0 || (predBySucc.get(leaf.id)?.length ?? 0) > 0;
705
- if (!hasGraphEdge && (leaf.dependencies?.length ?? 0) > 0) continue;
706
- const es = scheduledStart.get(leaf.id) ?? 0;
707
- const ls = LS.get(leaf.id) ?? 0;
708
- if (ls - es <= 0) critical.add(leaf.id);
672
+ };
673
+ var calculateWeekGridLines = (dateRange, dayWidth) => {
674
+ const blocks = getWeekBlocks(dateRange);
675
+ const lines = [];
676
+ let currentDayIndex = 0;
677
+ for (let i = 0; i < blocks.length; i++) {
678
+ const block = blocks[i];
679
+ currentDayIndex += block.days;
680
+ const blockEndMonth = dateRange[currentDayIndex - 1];
681
+ const nextBlockMonth = currentDayIndex < dateRange.length ? dateRange[currentDayIndex] : null;
682
+ const isMonthBoundary = nextBlockMonth && blockEndMonth.getUTCMonth() !== nextBlockMonth.getUTCMonth();
683
+ lines.push({
684
+ x: Math.round(currentDayIndex * dayWidth),
685
+ isMonthStart: isMonthBoundary || false
686
+ });
709
687
  }
710
- return critical;
711
- }
712
- function extendCriticalIdsToParents(criticalLeafIds, tasks) {
713
- const result = new Set(criticalLeafIds);
714
- const parentByChild = /* @__PURE__ */ new Map();
715
- for (const task of tasks) {
716
- if (task.parentId) parentByChild.set(task.id, task.parentId);
688
+ if (lines.length > 0 && lines[lines.length - 1].x === Math.round(dateRange.length * dayWidth)) {
689
+ lines.pop();
717
690
  }
718
- const stack = Array.from(criticalLeafIds);
719
- while (stack.length > 0) {
720
- const id = stack.pop();
721
- const parentId = parentByChild.get(id);
722
- if (parentId && !result.has(parentId)) {
723
- result.add(parentId);
724
- stack.push(parentId);
691
+ return lines;
692
+ };
693
+ var calculateMonthGridLines = (dateRange, dayWidth) => {
694
+ const lines = [];
695
+ for (let i = 1; i < dateRange.length; i++) {
696
+ const date = dateRange[i];
697
+ if (date.getUTCDate() === 1) {
698
+ const x = Math.round(i * dayWidth);
699
+ const isYearBoundary = date.getUTCMonth() === 0;
700
+ lines.push({ x, isMonthStart: isYearBoundary });
725
701
  }
726
702
  }
727
- return result;
728
- }
703
+ return lines;
704
+ };
705
+ var calculateOrthogonalPath = (from, to) => {
706
+ const fx = Math.round(from.x);
707
+ const fy = Math.round(from.y);
708
+ const tx = Math.round(to.x);
709
+ const ty = Math.round(to.y);
710
+ if (fy === ty) {
711
+ return `M ${fx} ${fy} H ${tx}`;
712
+ }
713
+ const C = 2;
714
+ const goingDown = ty > fy;
715
+ const goingRight = tx >= fx;
716
+ const dirY = goingDown ? 1 : -1;
717
+ const dirX = goingRight ? 1 : -1;
718
+ if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
719
+ return [
720
+ `M ${fx} ${fy}`,
721
+ `H ${tx - dirX * C}`,
722
+ `L ${tx} ${fy + dirY * C}`,
723
+ `V ${ty}`
724
+ ].join(" ");
725
+ }
726
+ return `M ${fx} ${fy} H ${tx} V ${ty}`;
727
+ };
729
728
 
730
- // src/core/scheduling/commands.ts
731
- function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
732
- const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
733
- if (businessDays && weekendPredicate) {
729
+ // src/utils/activities.ts
730
+ var ACTIVITY_LANE_STEP = 28;
731
+ var ACTIVITY_LANE_BAR_HEIGHT = 24;
732
+ var ACTIVITY_LANES_PADDING = 2;
733
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
734
+ function packIntervals(intervals) {
735
+ const sorted = [...intervals].sort((a, b) => a.start - b.start);
736
+ const laneEnds = [];
737
+ return sorted.map((item) => {
738
+ let lane = 0;
739
+ while (lane < laneEnds.length && item.start < laneEnds[lane]) {
740
+ lane += 1;
741
+ }
742
+ laneEnds[lane] = item.end;
743
+ return { ...item, lane };
744
+ });
745
+ }
746
+ function computeActivityLanes(activities, monthStart, dayWidth) {
747
+ if (activities.length === 0) {
748
+ return { segments: [], laneCount: 0 };
749
+ }
750
+ const packed = packIntervals(activities.map((activity) => {
751
+ const bar = calculateTaskBar(parseUTCDate(activity.startDate), parseUTCDate(activity.endDate), monthStart, dayWidth);
734
752
  return {
735
- start: normalizedStart,
736
- end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
753
+ id: activity.id,
754
+ name: activity.name,
755
+ color: activity.color,
756
+ lag: activity.lag,
757
+ progress: activity.progress,
758
+ start: bar.left,
759
+ end: bar.left + bar.width,
760
+ left: bar.left,
761
+ width: bar.width
737
762
  };
738
- }
739
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
763
+ }));
740
764
  return {
741
- start: normalizedStart,
742
- end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS6)
765
+ segments: packed.map(({ id, name, color, lag, progress, lane, left, width }) => ({ id, name, color, lag, progress, lane, left, width })),
766
+ laneCount: Math.max(...packed.map((item) => item.lane + 1))
743
767
  };
744
768
  }
745
- function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
746
- const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
747
- if (businessDays && weekendPredicate) {
748
- return {
749
- start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
750
- end: normalizedEnd
751
- };
752
- }
753
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
769
+ function activityBounds(activities) {
770
+ if (!activities || activities.length === 0) return null;
771
+ let min = Number.POSITIVE_INFINITY;
772
+ let max = Number.NEGATIVE_INFINITY;
773
+ for (const activity of activities) {
774
+ const start = parseUTCDate(activity.startDate).getTime();
775
+ const end = parseUTCDate(activity.endDate).getTime();
776
+ if (start < min) min = start;
777
+ if (end > max) max = end;
778
+ }
779
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
780
+ const toIso = (ms) => new Date(ms).toISOString().slice(0, 10);
781
+ return { startDate: toIso(min), endDate: toIso(max) };
782
+ }
783
+ function activityLanesExtraHeight(laneCount) {
784
+ if (laneCount <= 1) return 0;
785
+ return laneCount * ACTIVITY_LANE_STEP + ACTIVITY_LANES_PADDING * 2;
786
+ }
787
+ function activitySpanMs(activity) {
754
788
  return {
755
- start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS6),
756
- end: normalizedEnd
789
+ start: parseUTCDate(activity.startDate).getTime(),
790
+ end: parseUTCDate(activity.endDate).getTime() + DAY_MS2
757
791
  };
758
792
  }
759
- function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
760
- return buildTaskRangeFromStart(
761
- proposedStart,
762
- getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
763
- businessDays,
764
- weekendPredicate,
765
- snapDirection
766
- );
793
+ function shiftActivityDate(value, days) {
794
+ const shifted = new Date(parseUTCDate(value).getTime() + days * DAY_MS2);
795
+ return typeof value === "string" ? shifted.toISOString().slice(0, 10) : shifted;
767
796
  }
768
- function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate, taskById) {
769
- if (!task.dependencies?.length) {
770
- return { start: proposedStart, end: proposedEnd };
771
- }
772
- let minAllowedStart = null;
773
- for (const dep of task.dependencies) {
774
- if (dep.type !== "FS") {
775
- continue;
797
+ var CHAIN_KEY_SEPARATOR = "\0";
798
+ var chainKey = (taskId, activityId) => `${taskId}${CHAIN_KEY_SEPARATOR}${activityId}`;
799
+ var toDayNumber = (value) => Math.round(parseUTCDate(value).getTime() / DAY_MS2);
800
+ function buildChainGraph(orderedTasks) {
801
+ const successors = /* @__PURE__ */ new Map();
802
+ const predecessors = /* @__PURE__ */ new Map();
803
+ const info = /* @__PURE__ */ new Map();
804
+ const link = (from, to, lag) => {
805
+ const list = successors.get(from);
806
+ if (list) {
807
+ if (!list.some((edge) => edge.key === to)) list.push({ key: to, lag });
808
+ } else {
809
+ successors.set(from, [{ key: to, lag }]);
776
810
  }
777
- const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
778
- if (!predecessor) {
779
- continue;
811
+ const back = predecessors.get(to);
812
+ if (back) {
813
+ if (!back.some((edge) => edge.key === from)) back.push({ key: from, lag });
814
+ } else {
815
+ predecessors.set(to, [{ key: from, lag }]);
780
816
  }
781
- const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
782
- const predecessorDuration = getTaskDuration(
783
- predecessorStart,
784
- predecessorEnd,
785
- businessDays,
786
- weekendPredicate
787
- );
788
- const candidateMinStart = calculateSuccessorDate(
789
- predecessorStart,
790
- predecessorEnd,
791
- "FS",
792
- -predecessorDuration,
793
- businessDays,
794
- weekendPredicate,
795
- task.type
796
- );
797
- if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
798
- minAllowedStart = candidateMinStart;
817
+ };
818
+ const flagged = orderedTasks.filter((task) => Boolean(task.activityChain) && Array.isArray(task.activities) && task.activities.length > 0);
819
+ flagged.forEach((task, floorIdx) => {
820
+ task.activities.forEach((activity, workIdx) => {
821
+ info.set(chainKey(task.id, activity.id), {
822
+ taskId: task.id,
823
+ activityId: activity.id,
824
+ startDay: toDayNumber(activity.startDate),
825
+ durationDays: Math.max(1, toDayNumber(activity.endDate) - toDayNumber(activity.startDate) + 1),
826
+ floorIdx,
827
+ workIdx
828
+ });
829
+ });
830
+ for (let i = 0; i < task.activities.length - 1; i += 1) {
831
+ link(chainKey(task.id, task.activities[i].id), chainKey(task.id, task.activities[i + 1].id), task.activities[i + 1].lag ?? 0);
832
+ }
833
+ });
834
+ for (let f = 0; f < flagged.length - 1; f += 1) {
835
+ const current = flagged[f];
836
+ const next = flagged[f + 1];
837
+ if (current.parentId !== next.parentId) continue;
838
+ for (const activity of current.activities) {
839
+ const twin = next.activities.find((candidate) => candidate.id === activity.id || candidate.name === activity.name);
840
+ if (twin) link(chainKey(current.id, activity.id), chainKey(next.id, twin.id), 0);
799
841
  }
800
842
  }
801
- if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
802
- return { start: proposedStart, end: proposedEnd };
843
+ return { successors, predecessors, info };
844
+ }
845
+ function collectDownstream(graph, startKey) {
846
+ const visited = /* @__PURE__ */ new Set();
847
+ const queue = [...(graph.successors.get(startKey) ?? []).map((edge) => edge.key)];
848
+ while (queue.length > 0) {
849
+ const key = queue.shift();
850
+ if (visited.has(key)) continue;
851
+ visited.add(key);
852
+ queue.push(...(graph.successors.get(key) ?? []).map((edge) => edge.key));
803
853
  }
804
- return buildTaskRangeFromStart(
805
- minAllowedStart,
806
- getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
807
- businessDays,
808
- weekendPredicate
809
- );
854
+ return visited;
810
855
  }
811
- function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate, taskById) {
812
- if (!task.dependencies) return [];
813
- return task.dependencies.map((dep) => {
814
- const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
815
- if (!predecessor) {
816
- return { ...dep, lag: getDependencyLag(dep) };
856
+ function shiftsFromDeltas(orderedTasks, deltasByTask) {
857
+ const shifts = [];
858
+ for (const task of orderedTasks) {
859
+ const deltas = deltasByTask.get(task.id);
860
+ if (!deltas || deltas.size === 0) continue;
861
+ shifts.push({
862
+ taskId: task.id,
863
+ shiftedIds: new Set(deltas.keys()),
864
+ deltas,
865
+ activities: (task.activities ?? []).map((activity) => deltas.has(activity.id) ? {
866
+ ...activity,
867
+ startDate: shiftActivityDate(activity.startDate, deltas.get(activity.id)),
868
+ endDate: shiftActivityDate(activity.endDate, deltas.get(activity.id))
869
+ } : activity)
870
+ });
871
+ }
872
+ return shifts;
873
+ }
874
+ function shiftActivityChain(orderedTasks, draggedTaskId, draggedActivityId, deltaDays) {
875
+ if (deltaDays === 0) return [];
876
+ const graph = buildChainGraph(orderedTasks);
877
+ const shifted = collectDownstream(graph, chainKey(draggedTaskId, draggedActivityId));
878
+ if (shifted.size === 0) return [];
879
+ const deltasByTask = /* @__PURE__ */ new Map();
880
+ for (const key of shifted) {
881
+ const node = graph.info.get(key);
882
+ const set = deltasByTask.get(node.taskId);
883
+ if (set) set.set(node.activityId, deltaDays);
884
+ else deltasByTask.set(node.taskId, /* @__PURE__ */ new Map([[node.activityId, deltaDays]]));
885
+ }
886
+ return shiftsFromDeltas(orderedTasks, deltasByTask);
887
+ }
888
+ function activityStartConstraint(orderedTasks, taskId, activityId) {
889
+ const graph = buildChainGraph(orderedTasks);
890
+ const key = chainKey(taskId, activityId);
891
+ const node = graph.info.get(key);
892
+ if (!node) return null;
893
+ const candidates = [];
894
+ let minStart = Number.NEGATIVE_INFINITY;
895
+ for (const pred of graph.predecessors.get(key) ?? []) {
896
+ const predNode = graph.info.get(pred.key);
897
+ if (!predNode) continue;
898
+ const min = predNode.startDay + predNode.durationDays + pred.lag;
899
+ candidates.push({ taskId: predNode.taskId, activityId: predNode.activityId, min, lag: pred.lag });
900
+ if (min > minStart) minStart = min;
901
+ }
902
+ if (!Number.isFinite(minStart)) return null;
903
+ const blockers = candidates.filter((candidate) => candidate.min === minStart);
904
+ return {
905
+ minStartDay: minStart,
906
+ minStartDelta: minStart - node.startDay,
907
+ blockers: blockers.map(({ taskId: blockerTaskId, activityId: blockerActivityId }) => ({
908
+ taskId: blockerTaskId,
909
+ activityId: blockerActivityId
910
+ })),
911
+ lag: Math.max(0, ...blockers.map((blocker) => blocker.lag))
912
+ };
913
+ }
914
+ function activityChainMinStartDelta(orderedTasks, taskId, activityId) {
915
+ return activityStartConstraint(orderedTasks, taskId, activityId)?.minStartDelta ?? null;
916
+ }
917
+ function pushActivityChain(orderedTasks, draggedTaskId, draggedActivityId, requestedDeltaDays) {
918
+ const graph = buildChainGraph(orderedTasks);
919
+ const draggedKey = chainKey(draggedTaskId, draggedActivityId);
920
+ if (!graph.info.has(draggedKey)) {
921
+ return { shifts: [], draggedDeltaDays: requestedDeltaDays };
922
+ }
923
+ const downstream = collectDownstream(graph, draggedKey);
924
+ if (downstream.size === 0) {
925
+ return { shifts: [], draggedDeltaDays: requestedDeltaDays };
926
+ }
927
+ const newStart = /* @__PURE__ */ new Map();
928
+ const dragged = graph.info.get(draggedKey);
929
+ newStart.set(draggedKey, dragged.startDay + requestedDeltaDays);
930
+ const endOfDay = (key) => {
931
+ const node = graph.info.get(key);
932
+ return (newStart.get(key) ?? node.startDay) + node.durationDays;
933
+ };
934
+ 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);
935
+ for (const { key, node } of order) {
936
+ let earliest = Number.NEGATIVE_INFINITY;
937
+ let oldEarliest = Number.NEGATIVE_INFINITY;
938
+ for (const pred of graph.predecessors.get(key) ?? []) {
939
+ const predNode = graph.info.get(pred.key);
940
+ earliest = Math.max(earliest, endOfDay(pred.key) + pred.lag);
941
+ oldEarliest = Math.max(oldEarliest, predNode.startDay + predNode.durationDays + pred.lag);
942
+ }
943
+ let nextStart;
944
+ if (earliest > node.startDay) {
945
+ nextStart = earliest;
946
+ } else if (earliest < oldEarliest) {
947
+ nextStart = earliest;
948
+ } else {
949
+ nextStart = node.startDay;
817
950
  }
818
- const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
819
- predecessor,
820
- (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
821
- );
822
- const nextLag = computeLagFromDates(
823
- dep.type,
824
- predecessorStart,
825
- predecessorEnd,
826
- newStartDate,
827
- newEndDate,
828
- businessDays,
829
- weekendPredicate,
830
- task.type
831
- );
832
- return { ...dep, lag: nextLag };
833
- });
951
+ newStart.set(key, nextStart);
952
+ }
953
+ const deltasByTask = /* @__PURE__ */ new Map();
954
+ for (const key of downstream) {
955
+ const node = graph.info.get(key);
956
+ const delta = (newStart.get(key) ?? node.startDay) - node.startDay;
957
+ if (delta === 0) continue;
958
+ const set = deltasByTask.get(node.taskId);
959
+ if (set) set.set(node.activityId, delta);
960
+ else deltasByTask.set(node.taskId, /* @__PURE__ */ new Map([[node.activityId, delta]]));
961
+ }
962
+ return { shifts: shiftsFromDeltas(orderedTasks, deltasByTask), draggedDeltaDays: requestedDeltaDays };
834
963
  }
835
964
 
836
- // src/core/scheduling/cascade.ts
837
- function parseCascadeDateInput(date) {
838
- if (date instanceof Date) {
839
- return normalizeUTCDate(date);
965
+ // src/core/scheduling/dependencies.ts
966
+ function normalizePredecessorDates(predecessor, parseDateFn) {
967
+ const predStart = parseDateFn(predecessor.startDate);
968
+ const predEnd = predecessor.type === "milestone" ? predStart : parseDateFn(predecessor.endDate);
969
+ return { predStart, predEnd };
970
+ }
971
+ function getDependencyLag(dep) {
972
+ return Number.isFinite(dep.lag) ? dep.lag : 0;
973
+ }
974
+ function normalizeTaskDependencyLags(task) {
975
+ return task;
976
+ }
977
+ function normalizeDependencyLag(linkType, lag, predecessorStart, predecessorEnd, businessDays = false, weekendPredicate, successorType) {
978
+ if (linkType !== "FS") {
979
+ return lag;
840
980
  }
841
- return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
981
+ const predecessorDuration = getTaskDuration(
982
+ predecessorStart,
983
+ predecessorEnd,
984
+ businessDays,
985
+ weekendPredicate
986
+ );
987
+ const minimumLag = successorType === "milestone" ? -Math.max(0, predecessorDuration - 1) : -predecessorDuration;
988
+ return Math.max(minimumLag, lag);
842
989
  }
843
- function createCascadeContext(allTasks) {
844
- const childrenByParentId = /* @__PURE__ */ new Map();
845
- const dependentsByPredecessorId = /* @__PURE__ */ new Map();
846
- const taskById = /* @__PURE__ */ new Map();
847
- for (const task of allTasks) {
848
- taskById.set(task.id, task);
849
- if (task.parentId) {
850
- const children = childrenByParentId.get(task.parentId) ?? [];
851
- children.push(task);
852
- childrenByParentId.set(task.parentId, children);
990
+ function computeLagFromDates(linkType, predStart, predEnd, succStart, succEnd, businessDays = false, weekendPredicate, successorType) {
991
+ const pS = Date.UTC(predStart.getUTCFullYear(), predStart.getUTCMonth(), predStart.getUTCDate());
992
+ const pE = Date.UTC(predEnd.getUTCFullYear(), predEnd.getUTCMonth(), predEnd.getUTCDate());
993
+ const sS = Date.UTC(succStart.getUTCFullYear(), succStart.getUTCMonth(), succStart.getUTCDate());
994
+ const sE = Date.UTC(succEnd.getUTCFullYear(), succEnd.getUTCMonth(), succEnd.getUTCDate());
995
+ if (!businessDays || !weekendPredicate) {
996
+ switch (linkType) {
997
+ case "FS":
998
+ return normalizeDependencyLag(
999
+ linkType,
1000
+ Math.round((sS - pE) / DAY_MS) - (successorType === "milestone" ? 0 : 1),
1001
+ predStart,
1002
+ predEnd,
1003
+ businessDays,
1004
+ weekendPredicate,
1005
+ successorType
1006
+ );
1007
+ case "SS":
1008
+ return Math.round((sS - pS) / DAY_MS);
1009
+ case "FF":
1010
+ return Math.round((sE - pE) / DAY_MS);
1011
+ case "SF":
1012
+ return Math.round((sE - pS) / DAY_MS) + 1;
853
1013
  }
854
- if (!task.dependencies) continue;
855
- task.dependencies.forEach((dependency, dependencyIndex) => {
856
- const dependents = dependentsByPredecessorId.get(dependency.taskId) ?? [];
857
- dependents.push({ task, dependencyIndex });
858
- dependentsByPredecessorId.set(dependency.taskId, dependents);
859
- });
860
1014
  }
861
- return { childrenByParentId, dependentsByPredecessorId, taskById };
1015
+ const anchorDate = linkType === "SS" || linkType === "SF" ? predStart : predEnd;
1016
+ const targetDate = linkType === "FS" || linkType === "SS" ? succStart : succEnd;
1017
+ const businessOffset = getBusinessDayOffset(anchorDate, targetDate, weekendPredicate);
1018
+ switch (linkType) {
1019
+ case "FS":
1020
+ return normalizeDependencyLag(
1021
+ linkType,
1022
+ businessOffset - (successorType === "milestone" ? 0 : 1),
1023
+ predStart,
1024
+ predEnd,
1025
+ businessDays,
1026
+ weekendPredicate,
1027
+ successorType
1028
+ );
1029
+ case "SS":
1030
+ return businessOffset;
1031
+ case "FF":
1032
+ return businessOffset;
1033
+ case "SF":
1034
+ return businessOffset + 1;
1035
+ }
862
1036
  }
863
- function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
864
- const { dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
865
- const visited = /* @__PURE__ */ new Set();
866
- const queue = [draggedTaskId];
867
- const chain = [];
868
- visited.add(draggedTaskId);
869
- while (queue.length > 0) {
870
- const current = queue.shift();
871
- const successors = dependentsByPredecessorId.get(current) ?? [];
872
- for (const { task } of successors) {
873
- if (!linkTypes.includes(task.dependencies?.find((dependency) => dependency.taskId === current)?.type ?? "FS")) {
874
- continue;
875
- }
876
- const sid = task.id;
877
- if (!visited.has(sid)) {
878
- visited.add(sid);
879
- const successorTask = taskById.get(sid);
880
- if (successorTask) {
881
- chain.push(successorTask);
882
- queue.push(sid);
883
- }
884
- }
1037
+ function calculateSuccessorDate(predecessorStart, predecessorEnd, linkType, lag = 0, businessDays = false, weekendPredicate, successorType) {
1038
+ const normalizedLag = normalizeDependencyLag(
1039
+ linkType,
1040
+ lag,
1041
+ predecessorStart,
1042
+ predecessorEnd,
1043
+ businessDays,
1044
+ weekendPredicate,
1045
+ successorType
1046
+ );
1047
+ if (!businessDays || !weekendPredicate) {
1048
+ switch (linkType) {
1049
+ case "FS":
1050
+ return new Date(predecessorEnd.getTime() + (normalizedLag + (successorType === "milestone" ? 0 : 1)) * DAY_MS);
1051
+ case "SS":
1052
+ return new Date(predecessorStart.getTime() + normalizedLag * DAY_MS);
1053
+ case "FF":
1054
+ return new Date(predecessorEnd.getTime() + normalizedLag * DAY_MS);
1055
+ case "SF":
1056
+ return new Date(predecessorStart.getTime() + (normalizedLag - 1) * DAY_MS);
885
1057
  }
886
1058
  }
887
- return chain;
1059
+ const anchorDate = linkType === "FS" || linkType === "FF" ? predecessorEnd : predecessorStart;
1060
+ let offset;
1061
+ switch (linkType) {
1062
+ case "FS":
1063
+ offset = normalizedLag + (successorType === "milestone" ? 0 : 1);
1064
+ break;
1065
+ case "SS":
1066
+ offset = normalizedLag;
1067
+ break;
1068
+ case "FF":
1069
+ offset = normalizedLag;
1070
+ break;
1071
+ case "SF":
1072
+ offset = normalizedLag - 1;
1073
+ break;
1074
+ }
1075
+ return shiftBusinessDayOffset(anchorDate, offset, weekendPredicate);
888
1076
  }
889
- function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
890
- const { childrenByParentId, dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
891
- const updatedDates = /* @__PURE__ */ new Map();
892
- updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
893
- const result = [];
894
- const queue = [movedTaskId];
895
- const visited = /* @__PURE__ */ new Set([movedTaskId]);
896
- while (queue.length > 0) {
897
- const currentId = queue.shift();
898
- const { start: predStart, end: predEnd } = updatedDates.get(currentId);
899
- if (!skipChildCascade) {
900
- const children = childrenByParentId.get(currentId) ?? [];
901
- for (const child of children) {
902
- if (visited.has(child.id) || child.locked) continue;
903
- const origStart = new Date(child.startDate);
904
- const origEnd = new Date(child.endDate);
905
- const durationMs = origEnd.getTime() - origStart.getTime();
906
- const parentOrig = taskById.get(currentId);
907
- const parentOrigStart = new Date(parentOrig.startDate);
908
- const parentOrigEnd = new Date(parentOrig.endDate);
909
- const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
910
- const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
911
- const newChildStart = new Date(origStart.getTime() + parentStartDelta);
912
- const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
913
- visited.add(child.id);
914
- updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
915
- result.push(normalizeTaskDependencyLags({
916
- ...child,
917
- startDate: newChildStart.toISOString().split("T")[0],
918
- endDate: newChildEnd.toISOString().split("T")[0]
919
- }));
920
- queue.push(child.id);
921
- }
1077
+
1078
+ // src/core/scheduling/criticalPath.ts
1079
+ function computeCriticalPath(tasks, options = {}) {
1080
+ const { businessDays = false, weekendPredicate } = options;
1081
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
1082
+ const childIds = /* @__PURE__ */ new Set();
1083
+ for (const task of tasks) {
1084
+ if (task.parentId) childIds.add(task.parentId);
1085
+ }
1086
+ const leafTasks = tasks.filter((task) => !childIds.has(task.id));
1087
+ const leafIds = new Set(leafTasks.map((task) => task.id));
1088
+ const leafIdsByAncestor = /* @__PURE__ */ new Map();
1089
+ for (const leaf of leafTasks) {
1090
+ let ancestor = leaf.parentId;
1091
+ while (ancestor) {
1092
+ const set = leafIdsByAncestor.get(ancestor) ?? /* @__PURE__ */ new Set();
1093
+ set.add(leaf.id);
1094
+ leafIdsByAncestor.set(ancestor, set);
1095
+ ancestor = taskById.get(ancestor)?.parentId;
922
1096
  }
923
- const dependents = dependentsByPredecessorId.get(currentId) ?? [];
924
- for (const { task, dependencyIndex } of dependents) {
925
- if (visited.has(task.id) || !task.dependencies || task.locked) continue;
926
- const dep = task.dependencies[dependencyIndex];
927
- if (!dep) continue;
928
- const orig = taskById.get(task.id);
929
- const origStart = new Date(orig.startDate);
930
- const origEnd = new Date(orig.endDate);
931
- const duration = getTaskDuration(origStart, origEnd);
932
- const currentTask = taskById.get(currentId);
933
- const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
934
- {
935
- startDate: predStart,
936
- endDate: predEnd,
937
- type: currentTask.type
938
- },
939
- parseCascadeDateInput
940
- );
941
- const constraintDate = calculateSuccessorDate(
942
- normalizedPredStart,
943
- normalizedPredEnd,
944
- dep.type,
945
- getDependencyLag(dep),
946
- false,
947
- void 0,
948
- task.type
949
- );
950
- let newSuccStart;
951
- let newSuccEnd;
952
- if (dep.type === "FS" || dep.type === "SS") {
953
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
954
- } else {
955
- ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
1097
+ }
1098
+ const durationOf = (task) => getTaskDuration(task.startDate, task.endDate, businessDays, weekendPredicate);
1099
+ const projectStartDate = leafTasks.reduce((earliest, task) => {
1100
+ const taskStart = parseDateOnly(task.startDate);
1101
+ return taskStart.getTime() < earliest.getTime() ? taskStart : earliest;
1102
+ }, parseDateOnly(leafTasks[0]?.startDate ?? /* @__PURE__ */ new Date()));
1103
+ const offsetFromProjectStart = (date) => {
1104
+ const targetDate = parseDateOnly(date);
1105
+ if (businessDays && weekendPredicate) {
1106
+ return getBusinessDayOffset(projectStartDate, targetDate, weekendPredicate);
1107
+ }
1108
+ return Math.round((targetDate.getTime() - projectStartDate.getTime()) / DAY_MS);
1109
+ };
1110
+ const scheduledStart = /* @__PURE__ */ new Map();
1111
+ for (const leaf of leafTasks) {
1112
+ scheduledStart.set(leaf.id, offsetFromProjectStart(leaf.startDate));
1113
+ }
1114
+ const succByPred = /* @__PURE__ */ new Map();
1115
+ const predBySucc = /* @__PURE__ */ new Map();
1116
+ const seenEdges = /* @__PURE__ */ new Set();
1117
+ const addEdge = (predecessorId, succId, lag, type) => {
1118
+ const edgeKey = `${predecessorId}|${succId}|${type}`;
1119
+ if (seenEdges.has(edgeKey)) return;
1120
+ seenEdges.add(edgeKey);
1121
+ const outEdges = succByPred.get(predecessorId) ?? [];
1122
+ outEdges.push({ succId, lag, type });
1123
+ succByPred.set(predecessorId, outEdges);
1124
+ const inEdges = predBySucc.get(succId) ?? [];
1125
+ inEdges.push({ predId: predecessorId, lag, type });
1126
+ predBySucc.set(succId, inEdges);
1127
+ };
1128
+ for (const succ of leafTasks) {
1129
+ for (const dep of succ.dependencies ?? []) {
1130
+ const lag = getDependencyLag(dep);
1131
+ if (leafIds.has(dep.taskId)) {
1132
+ addEdge(dep.taskId, succ.id, lag, dep.type);
1133
+ continue;
1134
+ }
1135
+ const descendants = leafIdsByAncestor.get(dep.taskId);
1136
+ if (!descendants || descendants.size === 0) continue;
1137
+ for (const leafId of descendants) {
1138
+ if (leafId === succ.id) continue;
1139
+ addEdge(leafId, succ.id, lag, dep.type);
956
1140
  }
957
- visited.add(task.id);
958
- updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
959
- result.push(normalizeTaskDependencyLags({
960
- ...task,
961
- startDate: newSuccStart.toISOString().split("T")[0],
962
- endDate: newSuccEnd.toISOString().split("T")[0]
963
- }));
964
- queue.push(task.id);
965
1141
  }
966
1142
  }
967
- return result;
968
- }
969
- function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
970
- const { childrenByParentId, dependentsByPredecessorId } = createCascadeContext(allTasks);
971
- const directChildren = childrenByParentId.get(changedTaskId) ?? [];
972
- const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
973
- const initialChain = [...directChildren, ...directSuccessors].filter(
974
- (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
975
- );
976
- const chain = [...initialChain];
977
- const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
978
- const queue = [...initialChain];
1143
+ const indegree = /* @__PURE__ */ new Map();
1144
+ for (const leaf of leafTasks) indegree.set(leaf.id, 0);
1145
+ for (const edges of succByPred.values()) {
1146
+ for (const { succId } of edges) {
1147
+ indegree.set(succId, (indegree.get(succId) ?? 0) + 1);
1148
+ }
1149
+ }
1150
+ const order = [];
1151
+ const enqueued = /* @__PURE__ */ new Set();
1152
+ const queue = leafTasks.filter((leaf) => (indegree.get(leaf.id) ?? 0) === 0).map((leaf) => leaf.id);
1153
+ for (const id of queue) enqueued.add(id);
979
1154
  while (queue.length > 0) {
980
1155
  const current = queue.shift();
981
- const children = childrenByParentId.get(current.id) ?? [];
982
- for (const child of children) {
983
- if (!visited.has(child.id)) {
984
- visited.add(child.id);
985
- chain.push(child);
986
- queue.push(child);
1156
+ order.push(current);
1157
+ for (const { succId } of succByPred.get(current) ?? []) {
1158
+ const next = (indegree.get(succId) ?? 0) - 1;
1159
+ indegree.set(succId, next);
1160
+ if (next === 0 && !enqueued.has(succId)) {
1161
+ enqueued.add(succId);
1162
+ queue.push(succId);
987
1163
  }
988
1164
  }
989
- const successors = dependentsByPredecessorId.get(current.id) ?? [];
990
- for (const { task: successor } of successors) {
991
- if (!visited.has(successor.id)) {
992
- visited.add(successor.id);
993
- chain.push(successor);
994
- queue.push(successor);
995
- }
1165
+ }
1166
+ const projectFinish = Math.max(
1167
+ 0,
1168
+ ...leafTasks.map((leaf) => (scheduledStart.get(leaf.id) ?? 0) + durationOf(leaf))
1169
+ );
1170
+ const LF = /* @__PURE__ */ new Map();
1171
+ const LS = /* @__PURE__ */ new Map();
1172
+ for (const leaf of leafTasks) LF.set(leaf.id, projectFinish);
1173
+ for (let i = order.length - 1; i >= 0; i--) {
1174
+ const id = order[i];
1175
+ const task = taskById.get(id);
1176
+ const durPred = durationOf(task);
1177
+ const outEdges = succByPred.get(id) ?? [];
1178
+ let lf = projectFinish;
1179
+ if (outEdges.length > 0) {
1180
+ const candidates = outEdges.map(({ succId, lag, type }) => {
1181
+ const succLs = LS.get(succId) ?? projectFinish;
1182
+ const durSucc = durationOf(taskById.get(succId));
1183
+ switch (type) {
1184
+ case "FS":
1185
+ return succLs - lag;
1186
+ case "SS":
1187
+ return succLs - lag + durPred;
1188
+ case "FF":
1189
+ return succLs + durSucc - lag;
1190
+ case "SF":
1191
+ return succLs + durSucc - lag + durPred;
1192
+ default:
1193
+ return projectFinish;
1194
+ }
1195
+ });
1196
+ lf = Math.min(projectFinish, ...candidates);
996
1197
  }
1198
+ LF.set(id, lf);
1199
+ LS.set(id, lf - durPred);
997
1200
  }
998
- return chain;
1201
+ const critical = /* @__PURE__ */ new Set();
1202
+ const ordered = new Set(order);
1203
+ for (const leaf of leafTasks) {
1204
+ if (!ordered.has(leaf.id)) continue;
1205
+ const hasGraphEdge = (succByPred.get(leaf.id)?.length ?? 0) > 0 || (predBySucc.get(leaf.id)?.length ?? 0) > 0;
1206
+ if (!hasGraphEdge && (leaf.dependencies?.length ?? 0) > 0) continue;
1207
+ const es = scheduledStart.get(leaf.id) ?? 0;
1208
+ const ls = LS.get(leaf.id) ?? 0;
1209
+ if (ls - es <= 0) critical.add(leaf.id);
1210
+ }
1211
+ return critical;
999
1212
  }
1000
- function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate, context = createCascadeContext(allTasks)) {
1001
- const { childrenByParentId, dependentsByPredecessorId, taskById } = context;
1002
- const normalizedMovedTask = normalizeTaskDependencyLags({
1003
- ...movedTask,
1004
- startDate: newStart.toISOString().split("T")[0],
1005
- endDate: newEnd.toISOString().split("T")[0]
1006
- });
1007
- if (!movedTask.parentId && !childrenByParentId.has(movedTask.id) && !dependentsByPredecessorId.has(movedTask.id)) {
1008
- return [normalizedMovedTask];
1213
+ function extendCriticalIdsToParents(criticalLeafIds, tasks) {
1214
+ const result = new Set(criticalLeafIds);
1215
+ const parentByChild = /* @__PURE__ */ new Map();
1216
+ for (const task of tasks) {
1217
+ if (task.parentId) parentByChild.set(task.id, task.parentId);
1009
1218
  }
1010
- const updatedDates = /* @__PURE__ */ new Map();
1011
- updatedDates.set(movedTask.id, { start: newStart, end: newEnd });
1012
- const resultMap = /* @__PURE__ */ new Map();
1013
- resultMap.set(movedTask.id, normalizedMovedTask);
1014
- const queue = [[movedTask.id, "direct"]];
1015
- const childShifted = /* @__PURE__ */ new Set();
1016
- let iterations = 0;
1017
- const MAX_ITERATIONS = allTasks.length * 3;
1018
- while (queue.length > 0 && iterations < MAX_ITERATIONS) {
1019
- iterations++;
1020
- const [currentId, arrivalMode] = queue.shift();
1021
- const { start: currStart, end: currEnd } = updatedDates.get(currentId);
1022
- const currentOriginal = taskById.get(currentId);
1023
- if (arrivalMode !== "parent-recalc") {
1024
- const children = childrenByParentId.get(currentId) ?? [];
1025
- for (const child of children) {
1026
- if (childShifted.has(child.id) || child.locked) continue;
1027
- const parentOrigStart = new Date(currentOriginal.startDate);
1028
- const parentOrigEnd = new Date(currentOriginal.endDate);
1029
- const childOrigStart = new Date(child.startDate);
1030
- const childOrigEnd = new Date(child.endDate);
1031
- const startDeltaMs = currStart.getTime() - parentOrigStart.getTime();
1032
- const endDeltaMs = currEnd.getTime() - parentOrigEnd.getTime();
1033
- let childNewStart;
1034
- let childNewEnd;
1035
- if (businessDays && weekendPredicate) {
1036
- const proposedStart = new Date(childOrigStart.getTime() + startDeltaMs);
1037
- const snapDirection = currStart.getTime() >= parentOrigStart.getTime() ? 1 : -1;
1038
- const movedRange = moveTaskRange(
1039
- child.startDate,
1040
- child.endDate,
1041
- proposedStart,
1042
- true,
1043
- weekendPredicate,
1044
- snapDirection
1045
- );
1046
- childNewStart = movedRange.start;
1047
- childNewEnd = movedRange.end;
1048
- } else {
1049
- childNewStart = new Date(childOrigStart.getTime() + startDeltaMs);
1050
- childNewEnd = new Date(childOrigEnd.getTime() + endDeltaMs);
1051
- }
1052
- const prev = updatedDates.get(child.id);
1053
- if (prev && prev.start.getTime() === childNewStart.getTime() && prev.end.getTime() === childNewEnd.getTime()) {
1054
- continue;
1055
- }
1056
- updatedDates.set(child.id, { start: childNewStart, end: childNewEnd });
1057
- childShifted.add(child.id);
1058
- queue.push([child.id, "child-delta"]);
1059
- resultMap.set(child.id, normalizeTaskDependencyLags({
1060
- ...child,
1061
- startDate: childNewStart.toISOString().split("T")[0],
1062
- endDate: childNewEnd.toISOString().split("T")[0]
1063
- }));
1064
- }
1065
- }
1066
- const parentId = currentOriginal.parentId;
1067
- if (parentId && parentId !== movedTask.id) {
1068
- const parent = taskById.get(parentId);
1069
- if (parent && !parent.locked) {
1070
- const siblings = childrenByParentId.get(parentId) ?? [];
1071
- const siblingPositions = siblings.map((sib) => {
1072
- if (updatedDates.has(sib.id)) return updatedDates.get(sib.id);
1073
- return { start: new Date(sib.startDate), end: new Date(sib.endDate) };
1074
- });
1075
- const minStart = new Date(Math.min(...siblingPositions.map((p) => p.start.getTime())));
1076
- const maxEnd = new Date(Math.max(...siblingPositions.map((p) => p.end.getTime())));
1077
- const prev = updatedDates.get(parentId);
1078
- if (!prev || prev.start.getTime() !== minStart.getTime() || prev.end.getTime() !== maxEnd.getTime()) {
1079
- updatedDates.set(parentId, { start: minStart, end: maxEnd });
1080
- queue.push([parentId, "parent-recalc"]);
1081
- resultMap.set(parentId, normalizeTaskDependencyLags({
1082
- ...parent,
1083
- startDate: minStart.toISOString().split("T")[0],
1084
- endDate: maxEnd.toISOString().split("T")[0]
1085
- }));
1086
- }
1087
- }
1088
- }
1089
- const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1090
- for (const { task, dependencyIndex } of dependents) {
1091
- if (task.locked || !task.dependencies) continue;
1092
- if (childShifted.has(task.id)) continue;
1093
- const dep = task.dependencies[dependencyIndex];
1094
- if (!dep) continue;
1095
- const origStart = new Date(task.startDate);
1096
- const origEnd = new Date(task.endDate);
1097
- const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1098
- {
1099
- startDate: currStart,
1100
- endDate: currEnd,
1101
- type: currentOriginal.type
1102
- },
1103
- parseCascadeDateInput
1104
- );
1105
- const constraintDate = calculateSuccessorDate(
1106
- normalizedPredStart,
1107
- normalizedPredEnd,
1108
- dep.type,
1109
- getDependencyLag(dep),
1110
- businessDays,
1111
- weekendPredicate,
1112
- task.type
1113
- );
1114
- let succNewStart;
1115
- let succNewEnd;
1116
- const duration = getTaskDuration(origStart, origEnd, businessDays, weekendPredicate);
1117
- if (dep.type === "FS" || dep.type === "SS") {
1118
- ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromStart(
1119
- constraintDate,
1120
- duration,
1121
- businessDays,
1122
- weekendPredicate
1123
- ));
1124
- } else {
1125
- ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromEnd(
1126
- constraintDate,
1127
- duration,
1128
- businessDays,
1129
- weekendPredicate
1130
- ));
1131
- }
1132
- const prev = updatedDates.get(task.id);
1133
- if (prev && prev.start.getTime() === succNewStart.getTime() && prev.end.getTime() === succNewEnd.getTime()) {
1134
- continue;
1135
- }
1136
- updatedDates.set(task.id, { start: succNewStart, end: succNewEnd });
1137
- queue.push([task.id, "dependency"]);
1138
- resultMap.set(task.id, normalizeTaskDependencyLags({
1139
- ...task,
1140
- startDate: succNewStart.toISOString().split("T")[0],
1141
- endDate: succNewEnd.toISOString().split("T")[0]
1142
- }));
1219
+ const stack = Array.from(criticalLeafIds);
1220
+ while (stack.length > 0) {
1221
+ const id = stack.pop();
1222
+ const parentId = parentByChild.get(id);
1223
+ if (parentId && !result.has(parentId)) {
1224
+ result.add(parentId);
1225
+ stack.push(parentId);
1143
1226
  }
1144
1227
  }
1145
- return Array.from(resultMap.values());
1228
+ return result;
1146
1229
  }
1147
1230
 
1148
- // src/core/scheduling/hierarchy.ts
1149
- function getChildren(parentId, tasks) {
1150
- return tasks.filter((t) => t.parentId === parentId);
1151
- }
1152
- function isTaskParent(taskId, tasks) {
1153
- return tasks.some((t) => t.parentId === taskId);
1231
+ // src/core/scheduling/commands.ts
1232
+ function buildTaskRangeFromStart(startDate, duration, businessDays = false, weekendPredicate, snapDirection = 1) {
1233
+ const normalizedStart = businessDays && weekendPredicate ? alignToWorkingDay(startDate, snapDirection, weekendPredicate) : normalizeUTCDate(startDate);
1234
+ if (businessDays && weekendPredicate) {
1235
+ return {
1236
+ start: normalizedStart,
1237
+ end: parseDateOnly(addBusinessDays(normalizedStart, duration, weekendPredicate))
1238
+ };
1239
+ }
1240
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1241
+ return {
1242
+ start: normalizedStart,
1243
+ end: new Date(normalizedStart.getTime() + (Math.max(1, duration) - 1) * DAY_MS7)
1244
+ };
1154
1245
  }
1155
- function computeParentDates(parentId, tasks) {
1156
- const children = getChildren(parentId, tasks);
1157
- if (children.length === 0) {
1158
- const parent = tasks.find((t) => t.id === parentId);
1159
- const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
1160
- const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
1161
- return { startDate: start, endDate: end };
1246
+ function buildTaskRangeFromEnd(endDate, duration, businessDays = false, weekendPredicate, snapDirection = -1) {
1247
+ const normalizedEnd = businessDays && weekendPredicate ? alignToWorkingDay(endDate, snapDirection, weekendPredicate) : normalizeUTCDate(endDate);
1248
+ if (businessDays && weekendPredicate) {
1249
+ return {
1250
+ start: parseDateOnly(subtractBusinessDays(normalizedEnd, duration, weekendPredicate)),
1251
+ end: normalizedEnd
1252
+ };
1162
1253
  }
1163
- const startDates = children.map((c) => new Date(c.startDate));
1164
- const endDates = children.map((c) => new Date(c.endDate));
1165
- const minTime = Math.min(...startDates.map((d) => d.getTime()));
1166
- const maxTime = Math.max(...endDates.map((d) => d.getTime()));
1254
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1167
1255
  return {
1168
- startDate: new Date(minTime),
1169
- endDate: new Date(maxTime)
1256
+ start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS7),
1257
+ end: normalizedEnd
1170
1258
  };
1171
1259
  }
1172
- function computeParentProgress(parentId, tasks) {
1173
- const children = getChildren(parentId, tasks);
1174
- if (children.length === 0) {
1175
- return 0;
1260
+ function moveTaskRange(originalStart, originalEnd, proposedStart, businessDays = false, weekendPredicate, snapDirection = 1) {
1261
+ return buildTaskRangeFromStart(
1262
+ proposedStart,
1263
+ getTaskDuration(originalStart, originalEnd, businessDays, weekendPredicate),
1264
+ businessDays,
1265
+ weekendPredicate,
1266
+ snapDirection
1267
+ );
1268
+ }
1269
+ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks, businessDays = false, weekendPredicate, taskById) {
1270
+ if (!task.dependencies?.length) {
1271
+ return { start: proposedStart, end: proposedEnd };
1176
1272
  }
1177
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
1178
- let totalWeight = 0;
1179
- let weightedSum = 0;
1180
- for (const child of children) {
1181
- const start = new Date(child.startDate).getTime();
1182
- const end = new Date(child.endDate).getTime();
1183
- const duration = (end - start + DAY_MS6) / DAY_MS6;
1184
- const progress = child.progress ?? 0;
1185
- totalWeight += duration;
1186
- weightedSum += duration * progress;
1273
+ let minAllowedStart = null;
1274
+ for (const dep of task.dependencies) {
1275
+ if (dep.type !== "FS") {
1276
+ continue;
1277
+ }
1278
+ const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
1279
+ if (!predecessor) {
1280
+ continue;
1281
+ }
1282
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1283
+ const predecessorDuration = getTaskDuration(
1284
+ predecessorStart,
1285
+ predecessorEnd,
1286
+ businessDays,
1287
+ weekendPredicate
1288
+ );
1289
+ const candidateMinStart = calculateSuccessorDate(
1290
+ predecessorStart,
1291
+ predecessorEnd,
1292
+ "FS",
1293
+ -predecessorDuration,
1294
+ businessDays,
1295
+ weekendPredicate,
1296
+ task.type
1297
+ );
1298
+ if (!minAllowedStart || candidateMinStart.getTime() > minAllowedStart.getTime()) {
1299
+ minAllowedStart = candidateMinStart;
1300
+ }
1187
1301
  }
1188
- if (totalWeight === 0) {
1189
- return 0;
1302
+ if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
1303
+ return { start: proposedStart, end: proposedEnd };
1190
1304
  }
1191
- return Math.round(weightedSum / totalWeight * 10) / 10;
1305
+ return buildTaskRangeFromStart(
1306
+ minAllowedStart,
1307
+ getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
1308
+ businessDays,
1309
+ weekendPredicate
1310
+ );
1192
1311
  }
1193
- function getAllDescendants(parentId, tasks) {
1194
- const descendants = [];
1195
- const visited = /* @__PURE__ */ new Set();
1196
- function collectChildren(taskId) {
1197
- if (visited.has(taskId)) return;
1198
- visited.add(taskId);
1199
- const children = getChildren(taskId, tasks);
1200
- for (const child of children) {
1201
- descendants.push(child);
1202
- collectChildren(child.id);
1312
+ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, businessDays = false, weekendPredicate, taskById) {
1313
+ if (!task.dependencies) return [];
1314
+ return task.dependencies.map((dep) => {
1315
+ const predecessor = taskById?.get(dep.taskId) ?? allTasks.find((candidate) => candidate.id === dep.taskId);
1316
+ if (!predecessor) {
1317
+ return { ...dep, lag: getDependencyLag(dep) };
1203
1318
  }
1319
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
1320
+ predecessor,
1321
+ (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
1322
+ );
1323
+ const nextLag = computeLagFromDates(
1324
+ dep.type,
1325
+ predecessorStart,
1326
+ predecessorEnd,
1327
+ newStartDate,
1328
+ newEndDate,
1329
+ businessDays,
1330
+ weekendPredicate,
1331
+ task.type
1332
+ );
1333
+ return { ...dep, lag: nextLag };
1334
+ });
1335
+ }
1336
+
1337
+ // src/core/scheduling/cascade.ts
1338
+ function parseCascadeDateInput(date) {
1339
+ if (date instanceof Date) {
1340
+ return normalizeUTCDate(date);
1204
1341
  }
1205
- collectChildren(parentId);
1206
- return descendants;
1342
+ return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
1207
1343
  }
1208
- function cascadeTaskProgress(task, tasks) {
1209
- const progress = task.progress;
1210
- if (progress !== 0 && progress !== 100) {
1211
- return [task];
1344
+ function projectDayDelta(from, to, businessDays, weekendPredicate) {
1345
+ if (businessDays && weekendPredicate) {
1346
+ return getBusinessDayOffset(from, to, weekendPredicate);
1212
1347
  }
1213
- const descendants = getAllDescendants(task.id, tasks);
1214
- if (descendants.length === 0) {
1215
- return [task];
1348
+ return Math.round((to.getTime() - from.getTime()) / DAY_MS);
1349
+ }
1350
+ function shiftTaskActivities(activities, dayDelta, businessDays, weekendPredicate) {
1351
+ if (!activities || activities.length === 0 || dayDelta === 0) return void 0;
1352
+ const shiftDate = (value) => {
1353
+ const base = parseDateOnly(value);
1354
+ const shifted = businessDays && weekendPredicate ? shiftBusinessDayOffset(base, dayDelta, weekendPredicate) : new Date(base.getTime() + dayDelta * DAY_MS);
1355
+ return typeof value === "string" ? shifted.toISOString().slice(0, 10) : shifted;
1356
+ };
1357
+ return activities.map((activity) => ({
1358
+ ...activity,
1359
+ startDate: shiftDate(activity.startDate),
1360
+ endDate: shiftDate(activity.endDate)
1361
+ }));
1362
+ }
1363
+ function shiftedActivitiesPatch(activities, dayDelta, businessDays, weekendPredicate) {
1364
+ const shifted = shiftTaskActivities(activities, dayDelta, businessDays, weekendPredicate);
1365
+ return shifted ? { activities: shifted } : {};
1366
+ }
1367
+ function createCascadeContext(allTasks) {
1368
+ const childrenByParentId = /* @__PURE__ */ new Map();
1369
+ const dependentsByPredecessorId = /* @__PURE__ */ new Map();
1370
+ const taskById = /* @__PURE__ */ new Map();
1371
+ for (const task of allTasks) {
1372
+ taskById.set(task.id, task);
1373
+ if (task.parentId) {
1374
+ const children = childrenByParentId.get(task.parentId) ?? [];
1375
+ children.push(task);
1376
+ childrenByParentId.set(task.parentId, children);
1377
+ }
1378
+ if (!task.dependencies) continue;
1379
+ task.dependencies.forEach((dependency, dependencyIndex) => {
1380
+ const dependents = dependentsByPredecessorId.get(dependency.taskId) ?? [];
1381
+ dependents.push({ task, dependencyIndex });
1382
+ dependentsByPredecessorId.set(dependency.taskId, dependents);
1383
+ });
1216
1384
  }
1217
- return [
1218
- { ...task, progress },
1219
- ...descendants.map((descendant) => ({ ...descendant, progress }))
1220
- ];
1385
+ return { childrenByParentId, dependentsByPredecessorId, taskById };
1221
1386
  }
1222
- function getAllDependencyEdges(tasks) {
1223
- const edges = [];
1224
- for (const task of tasks) {
1225
- if (task.dependencies) {
1226
- for (const dep of task.dependencies) {
1227
- edges.push({
1228
- predecessorId: dep.taskId,
1229
- successorId: task.id,
1230
- type: dep.type,
1231
- lag: dep.lag ?? 0
1232
- });
1387
+ function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
1388
+ const { dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1389
+ const visited = /* @__PURE__ */ new Set();
1390
+ const queue = [draggedTaskId];
1391
+ const chain = [];
1392
+ visited.add(draggedTaskId);
1393
+ while (queue.length > 0) {
1394
+ const current = queue.shift();
1395
+ const successors = dependentsByPredecessorId.get(current) ?? [];
1396
+ for (const { task } of successors) {
1397
+ if (!linkTypes.includes(task.dependencies?.find((dependency) => dependency.taskId === current)?.type ?? "FS")) {
1398
+ continue;
1399
+ }
1400
+ const sid = task.id;
1401
+ if (!visited.has(sid)) {
1402
+ visited.add(sid);
1403
+ const successorTask = taskById.get(sid);
1404
+ if (successorTask) {
1405
+ chain.push(successorTask);
1406
+ queue.push(sid);
1407
+ }
1233
1408
  }
1234
1409
  }
1235
1410
  }
1236
- return edges;
1411
+ return chain;
1237
1412
  }
1238
- function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1239
- return tasks.map((task) => {
1240
- if (task.id === taskId1 || task.id === taskId2) {
1241
- if (!task.dependencies) return task;
1242
- const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
1243
- const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
1244
- if (filteredDependencies.length === task.dependencies.length) {
1245
- return task;
1413
+ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascade = false) {
1414
+ const { childrenByParentId, dependentsByPredecessorId, taskById } = createCascadeContext(allTasks);
1415
+ const updatedDates = /* @__PURE__ */ new Map();
1416
+ updatedDates.set(movedTaskId, { start: newStart, end: newEnd });
1417
+ const result = [];
1418
+ const queue = [movedTaskId];
1419
+ const visited = /* @__PURE__ */ new Set([movedTaskId]);
1420
+ while (queue.length > 0) {
1421
+ const currentId = queue.shift();
1422
+ const { start: predStart, end: predEnd } = updatedDates.get(currentId);
1423
+ if (!skipChildCascade) {
1424
+ const children = childrenByParentId.get(currentId) ?? [];
1425
+ for (const child of children) {
1426
+ if (visited.has(child.id) || child.locked) continue;
1427
+ const origStart = new Date(child.startDate);
1428
+ const origEnd = new Date(child.endDate);
1429
+ const durationMs = origEnd.getTime() - origStart.getTime();
1430
+ const parentOrig = taskById.get(currentId);
1431
+ const parentOrigStart = new Date(parentOrig.startDate);
1432
+ const parentOrigEnd = new Date(parentOrig.endDate);
1433
+ const parentStartDelta = predStart.getTime() - parentOrigStart.getTime();
1434
+ const parentEndDelta = predEnd.getTime() - parentOrigEnd.getTime();
1435
+ const newChildStart = new Date(origStart.getTime() + parentStartDelta);
1436
+ const newChildEnd = new Date(origEnd.getTime() + parentEndDelta);
1437
+ visited.add(child.id);
1438
+ updatedDates.set(child.id, { start: newChildStart, end: newChildEnd });
1439
+ result.push(normalizeTaskDependencyLags({
1440
+ ...child,
1441
+ startDate: newChildStart.toISOString().split("T")[0],
1442
+ endDate: newChildEnd.toISOString().split("T")[0],
1443
+ ...shiftedActivitiesPatch(child.activities, Math.round(parentStartDelta / DAY_MS), false)
1444
+ }));
1445
+ queue.push(child.id);
1246
1446
  }
1247
- return {
1447
+ }
1448
+ const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1449
+ for (const { task, dependencyIndex } of dependents) {
1450
+ if (visited.has(task.id) || !task.dependencies || task.locked) continue;
1451
+ const dep = task.dependencies[dependencyIndex];
1452
+ if (!dep) continue;
1453
+ const orig = taskById.get(task.id);
1454
+ const origStart = new Date(orig.startDate);
1455
+ const origEnd = new Date(orig.endDate);
1456
+ const duration = getTaskDuration(origStart, origEnd);
1457
+ const currentTask = taskById.get(currentId);
1458
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1459
+ {
1460
+ startDate: predStart,
1461
+ endDate: predEnd,
1462
+ type: currentTask.type
1463
+ },
1464
+ parseCascadeDateInput
1465
+ );
1466
+ const constraintDate = calculateSuccessorDate(
1467
+ normalizedPredStart,
1468
+ normalizedPredEnd,
1469
+ dep.type,
1470
+ getDependencyLag(dep),
1471
+ false,
1472
+ void 0,
1473
+ task.type
1474
+ );
1475
+ let newSuccStart;
1476
+ let newSuccEnd;
1477
+ if (dep.type === "FS" || dep.type === "SS") {
1478
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromStart(constraintDate, duration));
1479
+ } else {
1480
+ ({ start: newSuccStart, end: newSuccEnd } = buildTaskRangeFromEnd(constraintDate, duration));
1481
+ }
1482
+ visited.add(task.id);
1483
+ updatedDates.set(task.id, { start: newSuccStart, end: newSuccEnd });
1484
+ result.push(normalizeTaskDependencyLags({
1248
1485
  ...task,
1249
- dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
1250
- };
1486
+ startDate: newSuccStart.toISOString().split("T")[0],
1487
+ endDate: newSuccEnd.toISOString().split("T")[0]
1488
+ }));
1489
+ queue.push(task.id);
1251
1490
  }
1252
- return task;
1253
- });
1254
- }
1255
- function findParentId(taskId, tasks) {
1256
- const task = tasks.find((t) => t.id === taskId);
1257
- return task?.parentId;
1491
+ }
1492
+ return result;
1258
1493
  }
1259
- function isAncestorTask(ancestorId, taskId, tasks) {
1260
- const taskById = new Map(tasks.map((task) => [task.id, task]));
1261
- const visited = /* @__PURE__ */ new Set();
1262
- let current = taskById.get(taskId);
1263
- while (current?.parentId) {
1264
- if (current.parentId === ancestorId) {
1265
- return true;
1494
+ function getTransitiveCascadeChain(changedTaskId, allTasks, firstLevelLinkTypes) {
1495
+ const { childrenByParentId, dependentsByPredecessorId } = createCascadeContext(allTasks);
1496
+ const directChildren = childrenByParentId.get(changedTaskId) ?? [];
1497
+ const directSuccessors = getSuccessorChain(changedTaskId, allTasks, firstLevelLinkTypes);
1498
+ const initialChain = [...directChildren, ...directSuccessors].filter(
1499
+ (task, index, arr) => arr.findIndex((candidate) => candidate.id === task.id) === index
1500
+ );
1501
+ const chain = [...initialChain];
1502
+ const visited = /* @__PURE__ */ new Set([changedTaskId, ...initialChain.map((t) => t.id)]);
1503
+ const queue = [...initialChain];
1504
+ while (queue.length > 0) {
1505
+ const current = queue.shift();
1506
+ const children = childrenByParentId.get(current.id) ?? [];
1507
+ for (const child of children) {
1508
+ if (!visited.has(child.id)) {
1509
+ visited.add(child.id);
1510
+ chain.push(child);
1511
+ queue.push(child);
1512
+ }
1266
1513
  }
1267
- if (visited.has(current.parentId)) {
1268
- return false;
1514
+ const successors = dependentsByPredecessorId.get(current.id) ?? [];
1515
+ for (const { task: successor } of successors) {
1516
+ if (!visited.has(successor.id)) {
1517
+ visited.add(successor.id);
1518
+ chain.push(successor);
1519
+ queue.push(successor);
1520
+ }
1269
1521
  }
1270
- visited.add(current.parentId);
1271
- current = taskById.get(current.parentId);
1272
- }
1273
- return false;
1274
- }
1275
- function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1276
- if (taskId1 === taskId2) {
1277
- return true;
1278
1522
  }
1279
- return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1280
- }
1281
-
1282
- // src/core/scheduling/execute.ts
1283
- function toIsoDate(date) {
1284
- return date.toISOString().split("T")[0];
1285
- }
1286
- function createChangedResult(snapshot, nextTasks) {
1287
- const originalById = new Map(snapshot.map((task) => [task.id, task]));
1288
- const changedTasks = nextTasks.filter((task) => JSON.stringify(originalById.get(task.id)) !== JSON.stringify(task));
1289
- return {
1290
- changedTasks,
1291
- changedIds: changedTasks.map((task) => task.id)
1292
- };
1523
+ return chain;
1293
1524
  }
1294
- function moveTaskWithCascade(taskId, newStart, snapshot, options) {
1295
- const task = snapshot.find((t) => t.id === taskId);
1296
- if (!task) {
1297
- return { changedTasks: [], changedIds: [] };
1298
- }
1299
- const businessDays = options?.businessDays ?? false;
1300
- const weekendPredicate = options?.weekendPredicate;
1301
- const newRange = moveTaskRange(
1302
- task.startDate,
1303
- task.endDate,
1304
- newStart,
1305
- businessDays,
1306
- weekendPredicate
1307
- );
1308
- const updatedDependencies = recalculateIncomingLags(
1309
- task,
1310
- newRange.start,
1311
- newRange.end,
1312
- snapshot,
1313
- businessDays,
1314
- weekendPredicate
1315
- );
1316
- const movedTask = {
1317
- ...task,
1318
- startDate: newRange.start.toISOString().split("T")[0],
1319
- endDate: newRange.end.toISOString().split("T")[0],
1320
- dependencies: updatedDependencies
1321
- };
1322
- if (options?.skipCascade) {
1323
- return {
1324
- changedTasks: [movedTask],
1325
- changedIds: [movedTask.id]
1326
- };
1525
+ function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays = false, weekendPredicate, context = createCascadeContext(allTasks)) {
1526
+ const { childrenByParentId, dependentsByPredecessorId, taskById } = context;
1527
+ const normalizedMovedTask = normalizeTaskDependencyLags({
1528
+ ...movedTask,
1529
+ startDate: newStart.toISOString().split("T")[0],
1530
+ endDate: newEnd.toISOString().split("T")[0]
1531
+ });
1532
+ if (!movedTask.parentId && !childrenByParentId.has(movedTask.id) && !dependentsByPredecessorId.has(movedTask.id)) {
1533
+ return [normalizedMovedTask];
1327
1534
  }
1328
- const cascadeResult = universalCascade(
1329
- movedTask,
1330
- newRange.start,
1331
- newRange.end,
1332
- snapshot,
1333
- businessDays,
1334
- weekendPredicate
1335
- );
1535
+ const updatedDates = /* @__PURE__ */ new Map();
1536
+ updatedDates.set(movedTask.id, { start: newStart, end: newEnd });
1336
1537
  const resultMap = /* @__PURE__ */ new Map();
1337
- resultMap.set(movedTask.id, movedTask);
1338
- for (const t of cascadeResult) {
1339
- resultMap.set(t.id, t);
1538
+ resultMap.set(movedTask.id, normalizedMovedTask);
1539
+ const queue = [[movedTask.id, "direct"]];
1540
+ const childShifted = /* @__PURE__ */ new Set();
1541
+ let iterations = 0;
1542
+ const MAX_ITERATIONS = allTasks.length * 3;
1543
+ while (queue.length > 0 && iterations < MAX_ITERATIONS) {
1544
+ iterations++;
1545
+ const [currentId, arrivalMode] = queue.shift();
1546
+ const { start: currStart, end: currEnd } = updatedDates.get(currentId);
1547
+ const currentOriginal = taskById.get(currentId);
1548
+ if (arrivalMode !== "parent-recalc") {
1549
+ const children = childrenByParentId.get(currentId) ?? [];
1550
+ for (const child of children) {
1551
+ if (childShifted.has(child.id) || child.locked) continue;
1552
+ const parentOrigStart = new Date(currentOriginal.startDate);
1553
+ const parentOrigEnd = new Date(currentOriginal.endDate);
1554
+ const childOrigStart = new Date(child.startDate);
1555
+ const childOrigEnd = new Date(child.endDate);
1556
+ const startDeltaMs = currStart.getTime() - parentOrigStart.getTime();
1557
+ const endDeltaMs = currEnd.getTime() - parentOrigEnd.getTime();
1558
+ let childNewStart;
1559
+ let childNewEnd;
1560
+ if (businessDays && weekendPredicate) {
1561
+ const proposedStart = new Date(childOrigStart.getTime() + startDeltaMs);
1562
+ const snapDirection = currStart.getTime() >= parentOrigStart.getTime() ? 1 : -1;
1563
+ const movedRange = moveTaskRange(
1564
+ child.startDate,
1565
+ child.endDate,
1566
+ proposedStart,
1567
+ true,
1568
+ weekendPredicate,
1569
+ snapDirection
1570
+ );
1571
+ childNewStart = movedRange.start;
1572
+ childNewEnd = movedRange.end;
1573
+ } else {
1574
+ childNewStart = new Date(childOrigStart.getTime() + startDeltaMs);
1575
+ childNewEnd = new Date(childOrigEnd.getTime() + endDeltaMs);
1576
+ }
1577
+ const prev = updatedDates.get(child.id);
1578
+ if (prev && prev.start.getTime() === childNewStart.getTime() && prev.end.getTime() === childNewEnd.getTime()) {
1579
+ continue;
1580
+ }
1581
+ updatedDates.set(child.id, { start: childNewStart, end: childNewEnd });
1582
+ childShifted.add(child.id);
1583
+ queue.push([child.id, "child-delta"]);
1584
+ resultMap.set(child.id, normalizeTaskDependencyLags({
1585
+ ...child,
1586
+ startDate: childNewStart.toISOString().split("T")[0],
1587
+ endDate: childNewEnd.toISOString().split("T")[0],
1588
+ // Дети едут параллельно: вместе с датами строки сдвигаются и её работы.
1589
+ ...shiftedActivitiesPatch(
1590
+ child.activities,
1591
+ projectDayDelta(childOrigStart, childNewStart, businessDays, weekendPredicate),
1592
+ businessDays,
1593
+ weekendPredicate
1594
+ )
1595
+ }));
1596
+ }
1597
+ }
1598
+ const parentId = currentOriginal.parentId;
1599
+ if (parentId && parentId !== movedTask.id) {
1600
+ const parent = taskById.get(parentId);
1601
+ if (parent && !parent.locked) {
1602
+ const siblings = childrenByParentId.get(parentId) ?? [];
1603
+ const siblingPositions = siblings.map((sib) => {
1604
+ if (updatedDates.has(sib.id)) return updatedDates.get(sib.id);
1605
+ return { start: new Date(sib.startDate), end: new Date(sib.endDate) };
1606
+ });
1607
+ const minStart = new Date(Math.min(...siblingPositions.map((p) => p.start.getTime())));
1608
+ const maxEnd = new Date(Math.max(...siblingPositions.map((p) => p.end.getTime())));
1609
+ const prev = updatedDates.get(parentId);
1610
+ if (!prev || prev.start.getTime() !== minStart.getTime() || prev.end.getTime() !== maxEnd.getTime()) {
1611
+ updatedDates.set(parentId, { start: minStart, end: maxEnd });
1612
+ queue.push([parentId, "parent-recalc"]);
1613
+ resultMap.set(parentId, normalizeTaskDependencyLags({
1614
+ ...parent,
1615
+ startDate: minStart.toISOString().split("T")[0],
1616
+ endDate: maxEnd.toISOString().split("T")[0]
1617
+ }));
1618
+ }
1619
+ }
1620
+ }
1621
+ const dependents = dependentsByPredecessorId.get(currentId) ?? [];
1622
+ for (const { task, dependencyIndex } of dependents) {
1623
+ if (task.locked || !task.dependencies) continue;
1624
+ if (childShifted.has(task.id)) continue;
1625
+ const dep = task.dependencies[dependencyIndex];
1626
+ if (!dep) continue;
1627
+ const origStart = new Date(task.startDate);
1628
+ const origEnd = new Date(task.endDate);
1629
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1630
+ {
1631
+ startDate: currStart,
1632
+ endDate: currEnd,
1633
+ type: currentOriginal.type
1634
+ },
1635
+ parseCascadeDateInput
1636
+ );
1637
+ const constraintDate = calculateSuccessorDate(
1638
+ normalizedPredStart,
1639
+ normalizedPredEnd,
1640
+ dep.type,
1641
+ getDependencyLag(dep),
1642
+ businessDays,
1643
+ weekendPredicate,
1644
+ task.type
1645
+ );
1646
+ let succNewStart;
1647
+ let succNewEnd;
1648
+ const duration = getTaskDuration(origStart, origEnd, businessDays, weekendPredicate);
1649
+ if (dep.type === "FS" || dep.type === "SS") {
1650
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromStart(
1651
+ constraintDate,
1652
+ duration,
1653
+ businessDays,
1654
+ weekendPredicate
1655
+ ));
1656
+ } else {
1657
+ ({ start: succNewStart, end: succNewEnd } = buildTaskRangeFromEnd(
1658
+ constraintDate,
1659
+ duration,
1660
+ businessDays,
1661
+ weekendPredicate
1662
+ ));
1663
+ }
1664
+ const prev = updatedDates.get(task.id);
1665
+ if (prev && prev.start.getTime() === succNewStart.getTime() && prev.end.getTime() === succNewEnd.getTime()) {
1666
+ continue;
1667
+ }
1668
+ updatedDates.set(task.id, { start: succNewStart, end: succNewEnd });
1669
+ queue.push([task.id, "dependency"]);
1670
+ resultMap.set(task.id, normalizeTaskDependencyLags({
1671
+ ...task,
1672
+ startDate: succNewStart.toISOString().split("T")[0],
1673
+ endDate: succNewEnd.toISOString().split("T")[0],
1674
+ ...shiftedActivitiesPatch(
1675
+ task.activities,
1676
+ projectDayDelta(origStart, succNewStart, businessDays, weekendPredicate),
1677
+ businessDays,
1678
+ weekendPredicate
1679
+ )
1680
+ }));
1681
+ }
1340
1682
  }
1341
- const changedTasks = Array.from(resultMap.values());
1683
+ return Array.from(resultMap.values());
1684
+ }
1685
+
1686
+ // src/core/scheduling/hierarchy.ts
1687
+ function getChildren(parentId, tasks) {
1688
+ return tasks.filter((t) => t.parentId === parentId);
1689
+ }
1690
+ function isTaskParent(taskId, tasks) {
1691
+ return tasks.some((t) => t.parentId === taskId);
1692
+ }
1693
+ function computeParentDates(parentId, tasks) {
1694
+ const children = getChildren(parentId, tasks);
1695
+ if (children.length === 0) {
1696
+ const parent = tasks.find((t) => t.id === parentId);
1697
+ const start = parent ? new Date(parent.startDate) : /* @__PURE__ */ new Date();
1698
+ const end = parent ? new Date(parent.endDate) : /* @__PURE__ */ new Date();
1699
+ return { startDate: start, endDate: end };
1700
+ }
1701
+ const startDates = children.map((c) => new Date(c.startDate));
1702
+ const endDates = children.map((c) => new Date(c.endDate));
1703
+ const minTime = Math.min(...startDates.map((d) => d.getTime()));
1704
+ const maxTime = Math.max(...endDates.map((d) => d.getTime()));
1342
1705
  return {
1343
- changedTasks,
1344
- changedIds: changedTasks.map((t) => t.id)
1706
+ startDate: new Date(minTime),
1707
+ endDate: new Date(maxTime)
1345
1708
  };
1346
1709
  }
1347
- function resizeTaskWithCascade(taskId, anchor, newDate, snapshot, options) {
1348
- const task = snapshot.find((t) => t.id === taskId);
1349
- if (!task) {
1350
- return { changedTasks: [], changedIds: [] };
1351
- }
1352
- const businessDays = options?.businessDays ?? false;
1353
- const weekendPredicate = options?.weekendPredicate;
1354
- const originalStart = parseDateOnly(task.startDate);
1355
- const originalEnd = parseDateOnly(task.endDate);
1356
- let newRange;
1357
- if (anchor === "start") {
1358
- newRange = { start: originalStart, end: newDate };
1359
- } else {
1360
- newRange = { start: newDate, end: originalEnd };
1710
+ function computeParentProgress(parentId, tasks) {
1711
+ const children = getChildren(parentId, tasks);
1712
+ if (children.length === 0) {
1713
+ return 0;
1361
1714
  }
1362
- const updatedDependencies = recalculateIncomingLags(
1363
- task,
1364
- newRange.start,
1365
- newRange.end,
1366
- snapshot,
1367
- businessDays,
1368
- weekendPredicate
1369
- );
1370
- const resizedTask = {
1371
- ...task,
1372
- startDate: newRange.start.toISOString().split("T")[0],
1373
- endDate: newRange.end.toISOString().split("T")[0],
1374
- dependencies: updatedDependencies
1375
- };
1376
- if (options?.skipCascade) {
1377
- return {
1378
- changedTasks: [resizedTask],
1379
- changedIds: [resizedTask.id]
1380
- };
1715
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
1716
+ let totalWeight = 0;
1717
+ let weightedSum = 0;
1718
+ for (const child of children) {
1719
+ const start = new Date(child.startDate).getTime();
1720
+ const end = new Date(child.endDate).getTime();
1721
+ const duration = (end - start + DAY_MS7) / DAY_MS7;
1722
+ const progress = child.progress ?? 0;
1723
+ totalWeight += duration;
1724
+ weightedSum += duration * progress;
1381
1725
  }
1382
- const cascadeResult = universalCascade(
1383
- resizedTask,
1384
- newRange.start,
1385
- newRange.end,
1386
- snapshot,
1387
- businessDays,
1388
- weekendPredicate
1389
- );
1390
- const resultMap = /* @__PURE__ */ new Map();
1391
- resultMap.set(resizedTask.id, resizedTask);
1392
- for (const t of cascadeResult) {
1393
- resultMap.set(t.id, t);
1726
+ if (totalWeight === 0) {
1727
+ return 0;
1394
1728
  }
1395
- const changedTasks = Array.from(resultMap.values());
1396
- return {
1397
- changedTasks,
1398
- changedIds: changedTasks.map((t) => t.id)
1399
- };
1729
+ return Math.round(weightedSum / totalWeight * 10) / 10;
1400
1730
  }
1401
- function recalculateTaskFromDependencies(taskId, snapshot, options) {
1402
- const task = snapshot.find((t) => t.id === taskId);
1403
- if (!task) {
1404
- return { changedTasks: [], changedIds: [] };
1731
+ function getAllDescendants(parentId, tasks) {
1732
+ const descendants = [];
1733
+ const visited = /* @__PURE__ */ new Set();
1734
+ function collectChildren(taskId) {
1735
+ if (visited.has(taskId)) return;
1736
+ visited.add(taskId);
1737
+ const children = getChildren(taskId, tasks);
1738
+ for (const child of children) {
1739
+ descendants.push(child);
1740
+ collectChildren(child.id);
1741
+ }
1405
1742
  }
1406
- const businessDays = options?.businessDays ?? false;
1743
+ collectChildren(parentId);
1744
+ return descendants;
1745
+ }
1746
+ function cascadeTaskProgress(task, tasks) {
1747
+ const progress = task.progress;
1748
+ if (progress !== 0 && progress !== 100) {
1749
+ return [task];
1750
+ }
1751
+ const descendants = getAllDescendants(task.id, tasks);
1752
+ if (descendants.length === 0) {
1753
+ return [task];
1754
+ }
1755
+ return [
1756
+ { ...task, progress },
1757
+ ...descendants.map((descendant) => ({ ...descendant, progress }))
1758
+ ];
1759
+ }
1760
+ function getAllDependencyEdges(tasks) {
1761
+ const edges = [];
1762
+ for (const task of tasks) {
1763
+ if (task.dependencies) {
1764
+ for (const dep of task.dependencies) {
1765
+ edges.push({
1766
+ predecessorId: dep.taskId,
1767
+ successorId: task.id,
1768
+ type: dep.type,
1769
+ lag: dep.lag ?? 0
1770
+ });
1771
+ }
1772
+ }
1773
+ }
1774
+ return edges;
1775
+ }
1776
+ function removeDependenciesBetweenTasks(taskId1, taskId2, tasks) {
1777
+ return tasks.map((task) => {
1778
+ if (task.id === taskId1 || task.id === taskId2) {
1779
+ if (!task.dependencies) return task;
1780
+ const otherTaskId = task.id === taskId1 ? taskId2 : taskId1;
1781
+ const filteredDependencies = task.dependencies.filter((dep) => dep.taskId !== otherTaskId);
1782
+ if (filteredDependencies.length === task.dependencies.length) {
1783
+ return task;
1784
+ }
1785
+ return {
1786
+ ...task,
1787
+ dependencies: filteredDependencies.length > 0 ? filteredDependencies : void 0
1788
+ };
1789
+ }
1790
+ return task;
1791
+ });
1792
+ }
1793
+ function findParentId(taskId, tasks) {
1794
+ const task = tasks.find((t) => t.id === taskId);
1795
+ return task?.parentId;
1796
+ }
1797
+ function isAncestorTask(ancestorId, taskId, tasks) {
1798
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
1799
+ const visited = /* @__PURE__ */ new Set();
1800
+ let current = taskById.get(taskId);
1801
+ while (current?.parentId) {
1802
+ if (current.parentId === ancestorId) {
1803
+ return true;
1804
+ }
1805
+ if (visited.has(current.parentId)) {
1806
+ return false;
1807
+ }
1808
+ visited.add(current.parentId);
1809
+ current = taskById.get(current.parentId);
1810
+ }
1811
+ return false;
1812
+ }
1813
+ function areTasksHierarchicallyRelated(taskId1, taskId2, tasks) {
1814
+ if (taskId1 === taskId2) {
1815
+ return true;
1816
+ }
1817
+ return isAncestorTask(taskId1, taskId2, tasks) || isAncestorTask(taskId2, taskId1, tasks);
1818
+ }
1819
+
1820
+ // src/core/scheduling/execute.ts
1821
+ function toIsoDate(date) {
1822
+ return date.toISOString().split("T")[0];
1823
+ }
1824
+ function createChangedResult(snapshot, nextTasks) {
1825
+ const originalById = new Map(snapshot.map((task) => [task.id, task]));
1826
+ const changedTasks = nextTasks.filter((task) => JSON.stringify(originalById.get(task.id)) !== JSON.stringify(task));
1827
+ return {
1828
+ changedTasks,
1829
+ changedIds: changedTasks.map((task) => task.id)
1830
+ };
1831
+ }
1832
+ function moveTaskWithCascade(taskId, newStart, snapshot, options) {
1833
+ const task = snapshot.find((t) => t.id === taskId);
1834
+ if (!task) {
1835
+ return { changedTasks: [], changedIds: [] };
1836
+ }
1837
+ const businessDays = options?.businessDays ?? false;
1838
+ const weekendPredicate = options?.weekendPredicate;
1839
+ const newRange = moveTaskRange(
1840
+ task.startDate,
1841
+ task.endDate,
1842
+ newStart,
1843
+ businessDays,
1844
+ weekendPredicate
1845
+ );
1846
+ const updatedDependencies = recalculateIncomingLags(
1847
+ task,
1848
+ newRange.start,
1849
+ newRange.end,
1850
+ snapshot,
1851
+ businessDays,
1852
+ weekendPredicate
1853
+ );
1854
+ const movedTask = {
1855
+ ...task,
1856
+ startDate: newRange.start.toISOString().split("T")[0],
1857
+ endDate: newRange.end.toISOString().split("T")[0],
1858
+ dependencies: updatedDependencies
1859
+ };
1860
+ if (options?.skipCascade) {
1861
+ return {
1862
+ changedTasks: [movedTask],
1863
+ changedIds: [movedTask.id]
1864
+ };
1865
+ }
1866
+ const cascadeResult = universalCascade(
1867
+ movedTask,
1868
+ newRange.start,
1869
+ newRange.end,
1870
+ snapshot,
1871
+ businessDays,
1872
+ weekendPredicate
1873
+ );
1874
+ const resultMap = /* @__PURE__ */ new Map();
1875
+ resultMap.set(movedTask.id, movedTask);
1876
+ for (const t of cascadeResult) {
1877
+ resultMap.set(t.id, t);
1878
+ }
1879
+ const changedTasks = Array.from(resultMap.values());
1880
+ return {
1881
+ changedTasks,
1882
+ changedIds: changedTasks.map((t) => t.id)
1883
+ };
1884
+ }
1885
+ function resizeTaskWithCascade(taskId, anchor, newDate, snapshot, options) {
1886
+ const task = snapshot.find((t) => t.id === taskId);
1887
+ if (!task) {
1888
+ return { changedTasks: [], changedIds: [] };
1889
+ }
1890
+ const businessDays = options?.businessDays ?? false;
1891
+ const weekendPredicate = options?.weekendPredicate;
1892
+ const originalStart = parseDateOnly(task.startDate);
1893
+ const originalEnd = parseDateOnly(task.endDate);
1894
+ let newRange;
1895
+ if (anchor === "start") {
1896
+ newRange = { start: originalStart, end: newDate };
1897
+ } else {
1898
+ newRange = { start: newDate, end: originalEnd };
1899
+ }
1900
+ const updatedDependencies = recalculateIncomingLags(
1901
+ task,
1902
+ newRange.start,
1903
+ newRange.end,
1904
+ snapshot,
1905
+ businessDays,
1906
+ weekendPredicate
1907
+ );
1908
+ const resizedTask = {
1909
+ ...task,
1910
+ startDate: newRange.start.toISOString().split("T")[0],
1911
+ endDate: newRange.end.toISOString().split("T")[0],
1912
+ dependencies: updatedDependencies
1913
+ };
1914
+ if (options?.skipCascade) {
1915
+ return {
1916
+ changedTasks: [resizedTask],
1917
+ changedIds: [resizedTask.id]
1918
+ };
1919
+ }
1920
+ const cascadeResult = universalCascade(
1921
+ resizedTask,
1922
+ newRange.start,
1923
+ newRange.end,
1924
+ snapshot,
1925
+ businessDays,
1926
+ weekendPredicate
1927
+ );
1928
+ const resultMap = /* @__PURE__ */ new Map();
1929
+ resultMap.set(resizedTask.id, resizedTask);
1930
+ for (const t of cascadeResult) {
1931
+ resultMap.set(t.id, t);
1932
+ }
1933
+ const changedTasks = Array.from(resultMap.values());
1934
+ return {
1935
+ changedTasks,
1936
+ changedIds: changedTasks.map((t) => t.id)
1937
+ };
1938
+ }
1939
+ function recalculateTaskFromDependencies(taskId, snapshot, options) {
1940
+ const task = snapshot.find((t) => t.id === taskId);
1941
+ if (!task) {
1942
+ return { changedTasks: [], changedIds: [] };
1943
+ }
1944
+ const businessDays = options?.businessDays ?? false;
1407
1945
  const weekendPredicate = options?.weekendPredicate;
1408
1946
  if (!task.dependencies || task.dependencies.length === 0) {
1409
1947
  return {
@@ -2886,7 +3424,11 @@ function flattenHierarchy(tasks) {
2886
3424
  }
2887
3425
  function normalizeHierarchyTasks(tasks) {
2888
3426
  const orderedTasks = flattenHierarchy(tasks).map((task) => {
2889
- const { startDate, endDate } = normalizeTaskDates(task.startDate, task.endDate);
3427
+ const bounds = activityBounds(task.activities);
3428
+ const { startDate, endDate } = normalizeTaskDates(
3429
+ bounds?.startDate ?? task.startDate,
3430
+ bounds?.endDate ?? task.endDate
3431
+ );
2890
3432
  return { ...task, startDate, endDate };
2891
3433
  });
2892
3434
  const childrenByParent = /* @__PURE__ */ new Map();
@@ -2897,7 +3439,7 @@ function normalizeHierarchyTasks(tasks) {
2897
3439
  children.push(index);
2898
3440
  childrenByParent.set(parentId, children);
2899
3441
  }
2900
- const DAY_MS6 = 24 * 60 * 60 * 1e3;
3442
+ const DAY_MS7 = 24 * 60 * 60 * 1e3;
2901
3443
  for (let index = orderedTasks.length - 1; index >= 0; index--) {
2902
3444
  const children = childrenByParent.get(orderedTasks[index].id);
2903
3445
  if (!children?.length) continue;
@@ -2911,7 +3453,7 @@ function normalizeHierarchyTasks(tasks) {
2911
3453
  const end = new Date(child.endDate).getTime();
2912
3454
  minStart = Math.min(minStart, start);
2913
3455
  maxEnd = Math.max(maxEnd, end);
2914
- const duration = (end - start + DAY_MS6) / DAY_MS6;
3456
+ const duration = (end - start + DAY_MS7) / DAY_MS7;
2915
3457
  totalWeight += duration;
2916
3458
  weightedProgress += duration * (child.progress ?? 0);
2917
3459
  }
@@ -3067,361 +3609,96 @@ var TimeScaleHeader = ({
3067
3609
  },
3068
3610
  `month-${index}`
3069
3611
  ))
3070
- )
3071
- }
3072
- ),
3073
- /* @__PURE__ */ jsx(
3074
- "div",
3075
- {
3076
- className: "gantt-tsh-dayRow",
3077
- style: {
3078
- height: `${rowHeight}px`,
3079
- gridTemplateColumns: viewMode === "week" ? weekGridTemplate : viewMode === "month" ? monthGridTemplate : dayGridTemplate
3080
- },
3081
- children: viewMode === "week" ? (
3082
- // Week-view row 2: week block start day numbers (variable width)
3083
- weekBlocks.map((block, index) => {
3084
- const prevBlock = weekBlocks[index - 1];
3085
- const isMonthBoundary = index > 0 && prevBlock && prevBlock.startDate.getUTCMonth() !== block.startDate.getUTCMonth();
3086
- const showDate = block.days === 7;
3087
- return /* @__PURE__ */ jsx(
3088
- "div",
3089
- {
3090
- className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3091
- children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: showDate ? String(block.startDate.getUTCDate()).padStart(2, "0") : "" })
3092
- },
3093
- `week-${index}`
3094
- );
3095
- })
3096
- ) : viewMode === "month" ? (
3097
- // Month-view row 2: one column per month, shows abbreviated month name
3098
- monthBlocks.map((block, index) => {
3099
- const MIN_DAYS_TO_SHOW_LABEL = 15;
3100
- const showLabel = block.days >= MIN_DAYS_TO_SHOW_LABEL;
3101
- const isYearBoundary = index > 0 && block.startDate.getUTCMonth() === 0;
3102
- return /* @__PURE__ */ jsx(
3103
- "div",
3104
- {
3105
- className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3106
- children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: showLabel ? (() => {
3107
- const s = block.startDate.toLocaleString("ru-RU", { month: "long", timeZone: "UTC" });
3108
- return s.charAt(0).toUpperCase() + s.slice(1);
3109
- })() : "" })
3110
- },
3111
- `mblock-${index}`
3112
- );
3113
- })
3114
- ) : (
3115
- // Day-view row 2: individual day numbers (existing code)
3116
- days.map((day, index) => {
3117
- const isWeekendDay = isCustomWeekend ? isCustomWeekend(day) : day.getUTCDay() === 0 || day.getUTCDay() === 6;
3118
- const prevDay = days[index - 1];
3119
- const isMonthBoundary = index > 0 && prevDay && prevDay.getUTCMonth() !== day.getUTCMonth();
3120
- const isTodayDate = day.getUTCFullYear() === today.getUTCFullYear() && day.getUTCMonth() === today.getUTCMonth() && day.getUTCDate() === today.getUTCDate();
3121
- const markerKey = `${day.getUTCFullYear()}-${day.getUTCMonth()}-${day.getUTCDate()}`;
3122
- const marker = markerByDayKey.get(markerKey);
3123
- const markerColor = marker?.color;
3124
- const formattedDate = `${String(day.getUTCDate()).padStart(2, "0")}.${String(day.getUTCMonth() + 1).padStart(2, "0")}.${String(day.getUTCFullYear()).slice(-2)}`;
3125
- const tooltipText = isTodayDate ? `${formattedDate} \u2014 \u0421\u0435\u0433\u043E\u0434\u043D\u044F` : marker?.name ? `${formattedDate} \u2014 ${marker.name}` : void 0;
3126
- return /* @__PURE__ */ jsx(
3127
- "div",
3128
- {
3129
- className: `gantt-tsh-dayCell ${isWeekendDay ? "gantt-tsh-weekendDay" : ""} ${isTodayDate ? "gantt-tsh-today" : ""} ${marker && !isTodayDate ? "gantt-tsh-markerDay" : ""} ${tooltipText ? "gantt-tsh-dayCell-tooltipTrigger" : ""}`,
3130
- style: markerColor && !isTodayDate ? {
3131
- ["--gantt-marker-day-color"]: markerColor
3132
- } : void 0,
3133
- "aria-label": tooltipText,
3134
- onMouseEnter: () => {
3135
- if (!tooltipText) return;
3136
- onTimelineHover?.({
3137
- label: tooltipText,
3138
- left: index * dayWidth,
3139
- color: isTodayDate ? "#dc2626" : markerColor || "var(--gantt-timeline-marker-color, #dc2626)"
3140
- });
3141
- },
3142
- onMouseLeave: onTimelineHoverEnd,
3143
- children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: format(day, "d") })
3144
- },
3145
- `day-${index}`
3146
- );
3147
- })
3148
- )
3149
- }
3150
- )
3151
- ]
3152
- }
3153
- );
3154
- };
3155
- var TimeScaleHeader_default = TimeScaleHeader;
3156
-
3157
- // src/components/TaskRow/TaskRow.tsx
3158
- import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
3159
- import { createPortal } from "react-dom";
3160
-
3161
- // src/utils/geometry.ts
3162
- var getUTCDayDifference = (date1, date2) => {
3163
- const ms1 = Date.UTC(
3164
- date1.getUTCFullYear(),
3165
- date1.getUTCMonth(),
3166
- date1.getUTCDate()
3167
- );
3168
- const ms2 = Date.UTC(
3169
- date2.getUTCFullYear(),
3170
- date2.getUTCMonth(),
3171
- date2.getUTCDate()
3172
- );
3173
- return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
3174
- };
3175
- var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
3176
- const startOffset = getUTCDayDifference(taskStartDate, monthStart);
3177
- const duration = getUTCDayDifference(taskEndDate, taskStartDate);
3178
- const left = Math.round(startOffset * dayWidth);
3179
- const width = Math.round((duration + 1) * dayWidth);
3180
- return { left, width };
3181
- };
3182
- var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
3183
- const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
3184
- const centerX = Math.round(left + width / 2);
3185
- const halfSize = Math.round(size / 2);
3186
- return {
3187
- centerX,
3188
- left: centerX - halfSize,
3189
- right: centerX + halfSize,
3190
- size
3191
- };
3192
- };
3193
- var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
3194
- const halfDiagonal = Math.round(size / Math.SQRT2);
3195
- const centerX = Math.round(dayLeft + dayWidth / 2);
3196
- return {
3197
- left: centerX - halfDiagonal,
3198
- right: centerX + halfDiagonal
3199
- };
3200
- };
3201
- var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
3202
- const startDate = new Date(task.startDate);
3203
- const endDate = new Date(task.endDate);
3204
- if (task.type === "milestone") {
3205
- const size = 14;
3206
- if (override) {
3207
- const bounds2 = calculateMilestoneConnectionBounds(override.left, dayWidth, size);
3208
- return {
3209
- ...bounds2,
3210
- centerX: Math.round(override.left + dayWidth / 2)
3211
- };
3212
- }
3213
- const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
3214
- const bounds = calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
3215
- return {
3216
- ...bounds,
3217
- centerX: Math.round(bar2.left + dayWidth / 2)
3218
- };
3219
- }
3220
- if (override) {
3221
- return {
3222
- left: override.left,
3223
- right: override.left + override.width,
3224
- centerX: Math.round(override.left + override.width / 2)
3225
- };
3226
- }
3227
- const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3228
- return {
3229
- left: bar.left,
3230
- right: bar.left + bar.width,
3231
- centerX: Math.round(bar.left + bar.width / 2)
3232
- };
3233
- };
3234
- var pixelsToDate = (pixels, monthStart, dayWidth) => {
3235
- const days = Math.round(pixels / dayWidth);
3236
- return new Date(Date.UTC(
3237
- monthStart.getUTCFullYear(),
3238
- monthStart.getUTCMonth(),
3239
- monthStart.getUTCDate() + days
3240
- ));
3241
- };
3242
- var calculateGridWidth = (daysInMonth, dayWidth) => {
3243
- return Math.round(daysInMonth * dayWidth);
3244
- };
3245
- var MIN_EDGE_ZONE_PX = 8;
3246
- var MIN_MOVE_ZONE_PX = 16;
3247
- var detectEdgeZone = (clientX, taskBarElement, edgeZoneWidth = 12) => {
3248
- const rect = taskBarElement.getBoundingClientRect();
3249
- const relativeX = Math.round(clientX - rect.left);
3250
- const width = Math.round(rect.width);
3251
- const effectiveZone = Math.max(
3252
- MIN_EDGE_ZONE_PX,
3253
- Math.min(edgeZoneWidth, (width - MIN_MOVE_ZONE_PX) / 2)
3254
- );
3255
- if (relativeX >= 0 && relativeX <= effectiveZone) {
3256
- return "left";
3257
- }
3258
- if (relativeX >= width - effectiveZone && relativeX <= width) {
3259
- return "right";
3260
- }
3261
- return "move";
3262
- };
3263
- var getCursorForPosition = (position) => {
3264
- switch (position) {
3265
- case "left":
3266
- case "right":
3267
- return "ew-resize";
3268
- case "move":
3269
- return "grab";
3270
- default:
3271
- return "default";
3272
- }
3273
- };
3274
- var calculateGridLines = (dateRange, dayWidth) => {
3275
- const lines = [];
3276
- for (let i = 0; i < dateRange.length; i++) {
3277
- const date = dateRange[i];
3278
- const x = Math.round(i * dayWidth);
3279
- const isMonthStart = i === 0 ? false : date.getUTCDate() === 1;
3280
- const isWeekStart = date.getUTCDay() === 1;
3281
- lines.push({ x, isMonthStart, isWeekStart });
3282
- }
3283
- if (dateRange.length > 0) {
3284
- lines.push({
3285
- x: Math.round(dateRange.length * dayWidth),
3286
- isMonthStart: false,
3287
- isWeekStart: false
3288
- });
3289
- }
3290
- return lines;
3291
- };
3292
- var calculateWeekendBlocks = (dateRange, dayWidth, isCustomWeekend) => {
3293
- const blocks = [];
3294
- let inWeekend = false;
3295
- let weekendStartIndex = -1;
3296
- for (let i = 0; i < dateRange.length; i++) {
3297
- const date = dateRange[i];
3298
- const isWeekend3 = isCustomWeekend ? isCustomWeekend(date) : date.getUTCDay() === 0 || date.getUTCDay() === 6;
3299
- if (isWeekend3 && !inWeekend) {
3300
- inWeekend = true;
3301
- weekendStartIndex = i;
3302
- } else if (!isWeekend3 && inWeekend) {
3303
- inWeekend = false;
3304
- const left = Math.round(weekendStartIndex * dayWidth);
3305
- const width = Math.round((i - weekendStartIndex) * dayWidth);
3306
- blocks.push({ left, width });
3307
- }
3308
- }
3309
- if (inWeekend && weekendStartIndex >= 0) {
3310
- const left = Math.round(weekendStartIndex * dayWidth);
3311
- const width = Math.round((dateRange.length - weekendStartIndex) * dayWidth);
3312
- blocks.push({ left, width });
3313
- }
3314
- return blocks;
3315
- };
3316
- var calculateBezierPath = (from, to) => {
3317
- const verticalDistance = Math.abs(to.y - from.y);
3318
- const cpOffset = Math.max(verticalDistance * 0.5, 20);
3319
- if (from.y === to.y) {
3320
- const arcHeight = 20;
3321
- const midX = (from.x + to.x) / 2;
3322
- return `M ${from.x} ${from.y} Q ${midX} ${from.y - arcHeight} ${to.x} ${to.y}`;
3323
- }
3324
- const cp1x = from.x;
3325
- const cp1y = from.y + (to.y > from.y ? cpOffset : -cpOffset);
3326
- const cp2x = to.x;
3327
- const cp2y = to.y - (to.y > from.y ? cpOffset : -cpOffset);
3328
- 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)}`;
3329
- };
3330
- var calculateDependencyPath = (from, to, arrivesFromRight) => {
3331
- const fx = Math.round(from.x);
3332
- const fy = Math.round(from.y);
3333
- const tx = Math.round(to.x);
3334
- const ty = Math.round(to.y);
3335
- if (fy === ty) {
3336
- return `M ${fx} ${fy} H ${tx}`;
3337
- }
3338
- if (fx === tx) {
3339
- return `M ${fx} ${fy} V ${ty}`;
3340
- }
3341
- const C = 2;
3342
- const goingDown = ty > fy;
3343
- const dirY = goingDown ? 1 : -1;
3344
- if (arrivesFromRight) {
3345
- const goingRight = tx >= fx;
3346
- const dirX = goingRight ? 1 : -1;
3347
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3348
- return [
3349
- `M ${fx} ${fy}`,
3350
- `H ${tx - dirX * C}`,
3351
- `L ${tx} ${fy + dirY * C}`,
3352
- `V ${ty}`
3353
- ].join(" ");
3354
- }
3355
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3356
- } else {
3357
- const goingRight = tx >= fx;
3358
- const dirX = goingRight ? 1 : -1;
3359
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3360
- return [
3361
- `M ${fx} ${fy}`,
3362
- `H ${tx - dirX * C}`,
3363
- `L ${tx} ${fy + dirY * C}`,
3364
- `V ${ty}`
3365
- ].join(" ");
3366
- }
3367
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3368
- }
3369
- };
3370
- var calculateWeekGridLines = (dateRange, dayWidth) => {
3371
- const blocks = getWeekBlocks(dateRange);
3372
- const lines = [];
3373
- let currentDayIndex = 0;
3374
- for (let i = 0; i < blocks.length; i++) {
3375
- const block = blocks[i];
3376
- currentDayIndex += block.days;
3377
- const blockEndMonth = dateRange[currentDayIndex - 1];
3378
- const nextBlockMonth = currentDayIndex < dateRange.length ? dateRange[currentDayIndex] : null;
3379
- const isMonthBoundary = nextBlockMonth && blockEndMonth.getUTCMonth() !== nextBlockMonth.getUTCMonth();
3380
- lines.push({
3381
- x: Math.round(currentDayIndex * dayWidth),
3382
- isMonthStart: isMonthBoundary || false
3383
- });
3384
- }
3385
- if (lines.length > 0 && lines[lines.length - 1].x === Math.round(dateRange.length * dayWidth)) {
3386
- lines.pop();
3387
- }
3388
- return lines;
3389
- };
3390
- var calculateMonthGridLines = (dateRange, dayWidth) => {
3391
- const lines = [];
3392
- for (let i = 1; i < dateRange.length; i++) {
3393
- const date = dateRange[i];
3394
- if (date.getUTCDate() === 1) {
3395
- const x = Math.round(i * dayWidth);
3396
- const isYearBoundary = date.getUTCMonth() === 0;
3397
- lines.push({ x, isMonthStart: isYearBoundary });
3612
+ )
3613
+ }
3614
+ ),
3615
+ /* @__PURE__ */ jsx(
3616
+ "div",
3617
+ {
3618
+ className: "gantt-tsh-dayRow",
3619
+ style: {
3620
+ height: `${rowHeight}px`,
3621
+ gridTemplateColumns: viewMode === "week" ? weekGridTemplate : viewMode === "month" ? monthGridTemplate : dayGridTemplate
3622
+ },
3623
+ children: viewMode === "week" ? (
3624
+ // Week-view row 2: week block start day numbers (variable width)
3625
+ weekBlocks.map((block, index) => {
3626
+ const prevBlock = weekBlocks[index - 1];
3627
+ const isMonthBoundary = index > 0 && prevBlock && prevBlock.startDate.getUTCMonth() !== block.startDate.getUTCMonth();
3628
+ const showDate = block.days === 7;
3629
+ return /* @__PURE__ */ jsx(
3630
+ "div",
3631
+ {
3632
+ className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3633
+ children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: showDate ? String(block.startDate.getUTCDate()).padStart(2, "0") : "" })
3634
+ },
3635
+ `week-${index}`
3636
+ );
3637
+ })
3638
+ ) : viewMode === "month" ? (
3639
+ // Month-view row 2: one column per month, shows abbreviated month name
3640
+ monthBlocks.map((block, index) => {
3641
+ const MIN_DAYS_TO_SHOW_LABEL = 15;
3642
+ const showLabel = block.days >= MIN_DAYS_TO_SHOW_LABEL;
3643
+ const isYearBoundary = index > 0 && block.startDate.getUTCMonth() === 0;
3644
+ return /* @__PURE__ */ jsx(
3645
+ "div",
3646
+ {
3647
+ className: "gantt-tsh-dayCell gantt-tsh-weekCell",
3648
+ children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: showLabel ? (() => {
3649
+ const s = block.startDate.toLocaleString("ru-RU", { month: "long", timeZone: "UTC" });
3650
+ return s.charAt(0).toUpperCase() + s.slice(1);
3651
+ })() : "" })
3652
+ },
3653
+ `mblock-${index}`
3654
+ );
3655
+ })
3656
+ ) : (
3657
+ // Day-view row 2: individual day numbers (existing code)
3658
+ days.map((day, index) => {
3659
+ const isWeekendDay = isCustomWeekend ? isCustomWeekend(day) : day.getUTCDay() === 0 || day.getUTCDay() === 6;
3660
+ const prevDay = days[index - 1];
3661
+ const isMonthBoundary = index > 0 && prevDay && prevDay.getUTCMonth() !== day.getUTCMonth();
3662
+ const isTodayDate = day.getUTCFullYear() === today.getUTCFullYear() && day.getUTCMonth() === today.getUTCMonth() && day.getUTCDate() === today.getUTCDate();
3663
+ const markerKey = `${day.getUTCFullYear()}-${day.getUTCMonth()}-${day.getUTCDate()}`;
3664
+ const marker = markerByDayKey.get(markerKey);
3665
+ const markerColor = marker?.color;
3666
+ const formattedDate = `${String(day.getUTCDate()).padStart(2, "0")}.${String(day.getUTCMonth() + 1).padStart(2, "0")}.${String(day.getUTCFullYear()).slice(-2)}`;
3667
+ const tooltipText = isTodayDate ? `${formattedDate} \u2014 \u0421\u0435\u0433\u043E\u0434\u043D\u044F` : marker?.name ? `${formattedDate} \u2014 ${marker.name}` : void 0;
3668
+ return /* @__PURE__ */ jsx(
3669
+ "div",
3670
+ {
3671
+ className: `gantt-tsh-dayCell ${isWeekendDay ? "gantt-tsh-weekendDay" : ""} ${isTodayDate ? "gantt-tsh-today" : ""} ${marker && !isTodayDate ? "gantt-tsh-markerDay" : ""} ${tooltipText ? "gantt-tsh-dayCell-tooltipTrigger" : ""}`,
3672
+ style: markerColor && !isTodayDate ? {
3673
+ ["--gantt-marker-day-color"]: markerColor
3674
+ } : void 0,
3675
+ "aria-label": tooltipText,
3676
+ onMouseEnter: () => {
3677
+ if (!tooltipText) return;
3678
+ onTimelineHover?.({
3679
+ label: tooltipText,
3680
+ left: index * dayWidth,
3681
+ color: isTodayDate ? "#dc2626" : markerColor || "var(--gantt-timeline-marker-color, #dc2626)"
3682
+ });
3683
+ },
3684
+ onMouseLeave: onTimelineHoverEnd,
3685
+ children: /* @__PURE__ */ jsx("span", { className: "gantt-tsh-dayLabel", children: format(day, "d") })
3686
+ },
3687
+ `day-${index}`
3688
+ );
3689
+ })
3690
+ )
3691
+ }
3692
+ )
3693
+ ]
3398
3694
  }
3399
- }
3400
- return lines;
3401
- };
3402
- var calculateOrthogonalPath = (from, to) => {
3403
- const fx = Math.round(from.x);
3404
- const fy = Math.round(from.y);
3405
- const tx = Math.round(to.x);
3406
- const ty = Math.round(to.y);
3407
- if (fy === ty) {
3408
- return `M ${fx} ${fy} H ${tx}`;
3409
- }
3410
- const C = 2;
3411
- const goingDown = ty > fy;
3412
- const goingRight = tx >= fx;
3413
- const dirY = goingDown ? 1 : -1;
3414
- const dirX = goingRight ? 1 : -1;
3415
- if (Math.abs(ty - fy) >= C && Math.abs(tx - fx) >= C) {
3416
- return [
3417
- `M ${fx} ${fy}`,
3418
- `H ${tx - dirX * C}`,
3419
- `L ${tx} ${fy + dirY * C}`,
3420
- `V ${ty}`
3421
- ].join(" ");
3422
- }
3423
- return `M ${fx} ${fy} H ${tx} V ${ty}`;
3695
+ );
3424
3696
  };
3697
+ var TimeScaleHeader_default = TimeScaleHeader;
3698
+
3699
+ // src/components/TaskRow/TaskRow.tsx
3700
+ import React2, { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
3701
+ import { createPortal } from "react-dom";
3425
3702
 
3426
3703
  // src/utils/expired.ts
3427
3704
  var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
@@ -4260,21 +4537,245 @@ var useTaskDrag = (options) => {
4260
4537
  }
4261
4538
  }
4262
4539
  };
4263
- };
4540
+ };
4541
+
4542
+ // src/components/GanttChart/previewStore.ts
4543
+ function positionsEqual(left, right) {
4544
+ return left?.left === right?.left && left?.width === right?.width;
4545
+ }
4546
+ function createTaskPreviewPositionStore() {
4547
+ let positions = /* @__PURE__ */ new Map();
4548
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4549
+ function notify(taskIds) {
4550
+ for (const taskId of taskIds) {
4551
+ const listeners = listenersByTaskId.get(taskId);
4552
+ if (!listeners) continue;
4553
+ for (const listener of listeners) {
4554
+ listener();
4555
+ }
4556
+ }
4557
+ }
4558
+ return {
4559
+ subscribeTask(taskId, listener) {
4560
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4561
+ listeners.add(listener);
4562
+ listenersByTaskId.set(taskId, listeners);
4563
+ return () => {
4564
+ listeners.delete(listener);
4565
+ if (listeners.size === 0) {
4566
+ listenersByTaskId.delete(taskId);
4567
+ }
4568
+ };
4569
+ },
4570
+ getTaskPosition(taskId) {
4571
+ return positions.get(taskId);
4572
+ },
4573
+ setPositions(nextPositions) {
4574
+ const changedTaskIds = /* @__PURE__ */ new Set();
4575
+ const mergedPositions = /* @__PURE__ */ new Map();
4576
+ for (const [taskId, nextPosition] of nextPositions) {
4577
+ const currentPosition = positions.get(taskId);
4578
+ if (positionsEqual(currentPosition, nextPosition)) {
4579
+ if (currentPosition) {
4580
+ mergedPositions.set(taskId, currentPosition);
4581
+ }
4582
+ continue;
4583
+ }
4584
+ changedTaskIds.add(taskId);
4585
+ mergedPositions.set(taskId, {
4586
+ left: nextPosition.left,
4587
+ width: nextPosition.width
4588
+ });
4589
+ }
4590
+ for (const taskId of positions.keys()) {
4591
+ if (!nextPositions.has(taskId)) {
4592
+ changedTaskIds.add(taskId);
4593
+ }
4594
+ }
4595
+ if (changedTaskIds.size === 0) {
4596
+ return;
4597
+ }
4598
+ positions = mergedPositions;
4599
+ notify(changedTaskIds);
4600
+ },
4601
+ clear() {
4602
+ if (positions.size === 0) {
4603
+ return;
4604
+ }
4605
+ const changedTaskIds = new Set(positions.keys());
4606
+ positions = /* @__PURE__ */ new Map();
4607
+ notify(changedTaskIds);
4608
+ }
4609
+ };
4610
+ }
4611
+ function createActivityPreviewStore() {
4612
+ let overridesByTask = /* @__PURE__ */ new Map();
4613
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4614
+ function notify(taskIds) {
4615
+ for (const taskId of taskIds) {
4616
+ for (const listener of listenersByTaskId.get(taskId) ?? []) {
4617
+ listener();
4618
+ }
4619
+ }
4620
+ }
4621
+ return {
4622
+ subscribeTask(taskId, listener) {
4623
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4624
+ listeners.add(listener);
4625
+ listenersByTaskId.set(taskId, listeners);
4626
+ return () => {
4627
+ listeners.delete(listener);
4628
+ if (listeners.size === 0) {
4629
+ listenersByTaskId.delete(taskId);
4630
+ }
4631
+ };
4632
+ },
4633
+ getTaskOverrides(taskId) {
4634
+ return overridesByTask.get(taskId);
4635
+ },
4636
+ setOverrides(nextOverrides) {
4637
+ const changedTaskIds = new Set(nextOverrides.keys());
4638
+ for (const taskId of overridesByTask.keys()) {
4639
+ changedTaskIds.add(taskId);
4640
+ }
4641
+ if ([...changedTaskIds].every((taskId) => {
4642
+ const current = overridesByTask.get(taskId);
4643
+ const next = nextOverrides.get(taskId);
4644
+ if (!current || !next || current.size !== next.size) return false;
4645
+ return [...next].every(([activityId, delta]) => current.get(activityId) === delta);
4646
+ })) {
4647
+ return;
4648
+ }
4649
+ overridesByTask = new Map([...nextOverrides].map(([taskId, deltas]) => [taskId, new Map(deltas)]));
4650
+ notify(changedTaskIds);
4651
+ },
4652
+ clear() {
4653
+ if (overridesByTask.size === 0) {
4654
+ return;
4655
+ }
4656
+ const changedTaskIds = new Set(overridesByTask.keys());
4657
+ overridesByTask = /* @__PURE__ */ new Map();
4658
+ notify(changedTaskIds);
4659
+ }
4660
+ };
4661
+ }
4662
+ function createActivityBlockStore() {
4663
+ let blockersByTask = /* @__PURE__ */ new Map();
4664
+ const listenersByTaskId = /* @__PURE__ */ new Map();
4665
+ function notify(taskIds) {
4666
+ for (const taskId of taskIds) {
4667
+ for (const listener of listenersByTaskId.get(taskId) ?? []) {
4668
+ listener();
4669
+ }
4670
+ }
4671
+ }
4672
+ return {
4673
+ subscribeTask(taskId, listener) {
4674
+ const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
4675
+ listeners.add(listener);
4676
+ listenersByTaskId.set(taskId, listeners);
4677
+ return () => {
4678
+ listeners.delete(listener);
4679
+ if (listeners.size === 0) {
4680
+ listenersByTaskId.delete(taskId);
4681
+ }
4682
+ };
4683
+ },
4684
+ getTaskBlockers(taskId) {
4685
+ return blockersByTask.get(taskId);
4686
+ },
4687
+ setBlockers(nextBlockers) {
4688
+ const changedTaskIds = new Set(nextBlockers.keys());
4689
+ for (const taskId of blockersByTask.keys()) {
4690
+ changedTaskIds.add(taskId);
4691
+ }
4692
+ if ([...changedTaskIds].every((taskId) => {
4693
+ const current = blockersByTask.get(taskId);
4694
+ const next = nextBlockers.get(taskId);
4695
+ if (!current || !next || current.size !== next.size) return false;
4696
+ return [...next].every((activityId) => current.has(activityId));
4697
+ })) {
4698
+ return;
4699
+ }
4700
+ blockersByTask = new Map([...nextBlockers].map(([taskId, ids]) => [taskId, new Set(ids)]));
4701
+ notify(changedTaskIds);
4702
+ },
4703
+ clear() {
4704
+ if (blockersByTask.size === 0) {
4705
+ return;
4706
+ }
4707
+ const changedTaskIds = new Set(blockersByTask.keys());
4708
+ blockersByTask = /* @__PURE__ */ new Map();
4709
+ notify(changedTaskIds);
4710
+ }
4711
+ };
4712
+ }
4713
+ var activityOwnerKey = (taskId, activityId) => `${taskId}\0${activityId}`;
4714
+ function createActivityDragOwner() {
4715
+ let ownerKey = null;
4716
+ return {
4717
+ get: () => ownerKey,
4718
+ set: (nextOwnerKey) => {
4719
+ ownerKey = nextOwnerKey;
4720
+ }
4721
+ };
4722
+ }
4723
+ function createActivityActivationStore() {
4724
+ let activeKey = null;
4725
+ const listeners = /* @__PURE__ */ new Set();
4726
+ return {
4727
+ subscribe(listener) {
4728
+ listeners.add(listener);
4729
+ return () => {
4730
+ listeners.delete(listener);
4731
+ };
4732
+ },
4733
+ getActiveKey: () => activeKey,
4734
+ setActiveKey(nextKey) {
4735
+ if (activeKey === nextKey) return;
4736
+ activeKey = nextKey;
4737
+ for (const listener of listeners) listener();
4738
+ }
4739
+ };
4740
+ }
4264
4741
 
4265
4742
  // src/components/TaskRow/TaskRow.tsx
4266
4743
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4267
4744
  var COMPOSITE_SEGMENT_CUT_MIN_PX = 10;
4745
+ var toIsoDay = (date) => date.toISOString().slice(0, 10);
4746
+ var shiftActivityDay = (value, days) => new Date(parseUTCDate(value).getTime() + days * DAY_MS);
4747
+ var activityDurationDays = (activity) => Math.max(1, Math.round((parseUTCDate(activity.endDate).getTime() - parseUTCDate(activity.startDate).getTime()) / DAY_MS) + 1);
4748
+ var ACTIVITY_TEXT_LUMINANCE_THRESHOLD = 0.55;
4749
+ var relativeLuminance = (hex) => {
4750
+ const channels = [1, 3, 5].map((index) => {
4751
+ const value = parseInt(hex.slice(index, index + 2), 16) / 255;
4752
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
4753
+ });
4754
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
4755
+ };
4756
+ var activityTextColor = (background) => {
4757
+ const lightText = "var(--gantt-task-bar-text-color, #ffffff)";
4758
+ if (!background) return lightText;
4759
+ const value = background.trim();
4760
+ if (value.startsWith("#")) {
4761
+ return relativeLuminance(value) > ACTIVITY_TEXT_LUMINANCE_THRESHOLD ? "#111111" : "#ffffff";
4762
+ }
4763
+ const oklch = /oklch\(\s*([0-9.]+)/i.exec(value);
4764
+ if (oklch) {
4765
+ return Number(oklch[1]) > 0.66 ? "#111111" : "#ffffff";
4766
+ }
4767
+ return lightText;
4768
+ };
4268
4769
  var formatCompositePreviewDate = (date) => {
4269
4770
  const month = new Intl.DateTimeFormat("ru-RU", { month: "short", timeZone: "UTC" }).format(date).replace(/\.$/, "");
4270
4771
  const yearSuffix = date.getUTCFullYear() === (/* @__PURE__ */ new Date()).getFullYear() ? "" : ` ${String(date.getUTCFullYear()).slice(-2)}`;
4271
4772
  return `${date.getUTCDate()} ${month}${yearSuffix}`;
4272
4773
  };
4273
4774
  var arePropsEqual = (prevProps, nextProps) => {
4274
- 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;
4775
+ 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;
4275
4776
  };
4276
4777
  var TaskRow = React2.memo(
4277
- ({ 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 }) => {
4778
+ ({ 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 }) => {
4278
4779
  const defaultParentBarColor = "#782FC4";
4279
4780
  const [showCompositePreview, setShowCompositePreview] = useState2(false);
4280
4781
  const [compositePreviewPosition, setCompositePreviewPosition] = useState2(null);
@@ -4484,6 +4985,347 @@ var TaskRow = React2.memo(
4484
4985
  const shouldRenderCompositeSegments = showCompositeSegmentsProp && task.composite && compositeSegments.length > 0 && visualWidth / compositeSegments.length >= 22;
4485
4986
  const shouldRenderBaseline = showBaseline && baselineGeometry !== null;
4486
4987
  const hasPreviewPosition = isDragging || effectiveOverridePosition !== void 0;
4988
+ const activityLayout = useMemo2(
4989
+ () => task.activities && task.activities.length > 0 ? computeActivityLanes(task.activities, monthStart, dayWidth) : null,
4990
+ [task.activities, monthStart, dayWidth]
4991
+ );
4992
+ const renderActivities = activityLayout !== null && !compactDetail;
4993
+ const activityTopOffset = activityLayout ? Math.max(0, (rowHeight - activityLayout.laneCount * ACTIVITY_LANE_STEP) / 2) : 0;
4994
+ const activityDragEnabled = !task.locked && !disableTaskDrag;
4995
+ const [activityDrag, setActivityDrag] = useState2(null);
4996
+ const [activityTipId, setActivityTipId] = useState2(null);
4997
+ const [activityTipPosition, setActivityTipPosition] = useState2(null);
4998
+ const activityTipAnchor = useRef2(null);
4999
+ const activityDragStart = useRef2(null);
5000
+ const activityConstraintRef = useRef2(null);
5001
+ const [activityBlockedBy, setActivityBlockedBy] = useState2(null);
5002
+ const visibleActivitySegments = useMemo2(() => {
5003
+ if (!activityLayout) return [];
5004
+ const window2 = horizontalWindow;
5005
+ if (!window2) return activityLayout.segments;
5006
+ const draggedId = activityDrag?.id;
5007
+ return activityLayout.segments.filter((segment) => segment.id === draggedId || segment.left <= window2.endPx && segment.left + segment.width >= window2.startPx);
5008
+ }, [activityLayout, horizontalWindow, activityDrag?.id]);
5009
+ const subscribeActivityOverrides = useCallback2(
5010
+ (listener) => activityPreviewStore?.subscribeTask(task.id, listener) ?? (() => {
5011
+ }),
5012
+ [activityPreviewStore, task.id]
5013
+ );
5014
+ const getActivityOverridesSnapshot = useCallback2(
5015
+ () => activityPreviewStore?.getTaskOverrides(task.id),
5016
+ [activityPreviewStore, task.id]
5017
+ );
5018
+ const activityOverrides = useSyncExternalStore(
5019
+ subscribeActivityOverrides,
5020
+ getActivityOverridesSnapshot,
5021
+ () => void 0
5022
+ );
5023
+ const subscribeActivityBlock = useCallback2(
5024
+ (listener) => activityBlockStore?.subscribeTask(task.id, listener) ?? (() => {
5025
+ }),
5026
+ [activityBlockStore, task.id]
5027
+ );
5028
+ const getActivityBlockSnapshot = useCallback2(
5029
+ () => activityBlockStore?.getTaskBlockers(task.id),
5030
+ [activityBlockStore, task.id]
5031
+ );
5032
+ const blockedActivityIds = useSyncExternalStore(
5033
+ subscribeActivityBlock,
5034
+ getActivityBlockSnapshot,
5035
+ () => void 0
5036
+ );
5037
+ const subscribeActivation = useCallback2(
5038
+ (listener) => activityActivationStore?.subscribe(listener) ?? (() => {
5039
+ }),
5040
+ [activityActivationStore]
5041
+ );
5042
+ const getActivationSnapshot = useCallback2(
5043
+ () => activityActivationStore?.getActiveKey() ?? null,
5044
+ [activityActivationStore]
5045
+ );
5046
+ const activeActivityKey = useSyncExternalStore(
5047
+ subscribeActivation,
5048
+ getActivationSnapshot,
5049
+ () => null
5050
+ );
5051
+ const activeWork = useMemo2(() => {
5052
+ if (!activeActivityKey) return null;
5053
+ const [activeTaskId, activeActivityId] = activeActivityKey.split("\0");
5054
+ const source = (allTasks ?? []).find((candidate) => candidate.id === activeTaskId);
5055
+ const activity = source?.activities?.find((candidate) => candidate.id === activeActivityId);
5056
+ return { id: activeActivityId, name: activity?.name ?? null };
5057
+ }, [activeActivityKey, allTasks]);
5058
+ const activationVisualEnabled = activityClickToDrag || activityActivationMode !== "none";
5059
+ const dimOthers = activityActivationMode === "dim";
5060
+ const highlightChain = activityActivationMode === "highlight";
5061
+ const chainEndDeltaDays = (mode, dayDelta, durationDays2) => {
5062
+ if (mode === "resize-left") return 0;
5063
+ if (mode === "resize-right") return Math.max(dayDelta, -(durationDays2 - 1));
5064
+ return dayDelta;
5065
+ };
5066
+ const publishChainPreview = useCallback2((draggedId, mode, dayDelta) => {
5067
+ if (!activityPreviewStore) return;
5068
+ if (!task.activityChain) {
5069
+ activityPreviewStore.clear();
5070
+ return;
5071
+ }
5072
+ let shifts;
5073
+ if (task.activityChain === "push") {
5074
+ let delta = dayDelta;
5075
+ if (mode !== "resize-right") {
5076
+ const minDelta = activityConstraintRef.current?.minStartDelta ?? null;
5077
+ if (minDelta !== null) delta = Math.max(delta, minDelta);
5078
+ }
5079
+ shifts = delta === 0 ? [] : pushActivityChain(allTasks ?? [], task.id, draggedId, delta).shifts;
5080
+ } else {
5081
+ const endDelta = chainEndDeltaDays(mode, dayDelta, activityDragStart.current?.durationDays ?? 1);
5082
+ shifts = endDelta === 0 ? [] : shiftActivityChain(allTasks ?? [], task.id, draggedId, endDelta);
5083
+ }
5084
+ if (shifts.length === 0) {
5085
+ activityPreviewStore.clear();
5086
+ return;
5087
+ }
5088
+ const overrides = /* @__PURE__ */ new Map();
5089
+ for (const shift of shifts) {
5090
+ const deltas = /* @__PURE__ */ new Map();
5091
+ for (const [id, d] of shift.deltas) {
5092
+ if (d !== 0) deltas.set(id, d);
5093
+ }
5094
+ if (deltas.size > 0) overrides.set(shift.taskId, deltas);
5095
+ }
5096
+ activityPreviewStore.setOverrides(overrides);
5097
+ }, [activityPreviewStore, allTasks, task.activityChain, task.id]);
5098
+ useEffect2(() => {
5099
+ if (!activityDrag) return;
5100
+ const startInfo = activityDragStart.current;
5101
+ if (!startInfo) return;
5102
+ const publishBlockers = (blocked) => {
5103
+ const constraint = activityConstraintRef.current;
5104
+ if (!activityBlockStore) return;
5105
+ if (!blocked || !constraint) {
5106
+ activityBlockStore.clear();
5107
+ setActivityBlockedBy((current) => current === null ? current : null);
5108
+ return;
5109
+ }
5110
+ const blockers = /* @__PURE__ */ new Map();
5111
+ const names = [];
5112
+ for (const blocker of constraint.blockers) {
5113
+ const set = blockers.get(blocker.taskId) ?? /* @__PURE__ */ new Set();
5114
+ set.add(blocker.activityId);
5115
+ blockers.set(blocker.taskId, set);
5116
+ const source = (allTasks ?? []).find((candidate) => candidate.id === blocker.taskId);
5117
+ const work = source?.activities?.find((activity) => activity.id === blocker.activityId);
5118
+ const workName = work?.name ?? source?.name ?? blocker.activityId;
5119
+ names.push(blocker.taskId === task.id ? workName : `${source?.name ?? blocker.taskId} \xB7 ${workName}`);
5120
+ }
5121
+ activityBlockStore.setBlockers(blockers);
5122
+ 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 });
5123
+ };
5124
+ const onMove = (event) => {
5125
+ const rawDelta = Math.round((event.clientX - startInfo.x) / dayWidth);
5126
+ let requested = rawDelta;
5127
+ if (activityDrag.mode === "resize-left") requested = Math.min(rawDelta, startInfo.durationDays - 1);
5128
+ if (activityDrag.mode === "resize-right") requested = Math.max(rawDelta, -(startInfo.durationDays - 1));
5129
+ const constraint = activityConstraintRef.current;
5130
+ let clamped = requested;
5131
+ let blocked = false;
5132
+ if (constraint && activityDrag.mode !== "resize-right" && requested < constraint.minStartDelta) {
5133
+ clamped = constraint.minStartDelta;
5134
+ blocked = true;
5135
+ }
5136
+ setActivityDrag((current) => current ? { ...current, dayDelta: clamped } : current);
5137
+ publishBlockers(blocked);
5138
+ const anchor = activityTipAnchor.current;
5139
+ if (anchor) {
5140
+ const shiftPx = activityDrag.mode === "resize-right" ? 0 : clamped * dayWidth;
5141
+ setActivityTipPosition({
5142
+ left: Math.max(8, Math.min(anchor.left + shiftPx, window.innerWidth - 160)),
5143
+ top: anchor.top,
5144
+ below: anchor.below
5145
+ });
5146
+ }
5147
+ publishChainPreview(activityDrag.id, activityDrag.mode, clamped);
5148
+ };
5149
+ const onUp = () => {
5150
+ activityPreviewStore?.clear();
5151
+ activityBlockStore?.clear();
5152
+ activityDragOwner?.set(null);
5153
+ setActivityBlockedBy(null);
5154
+ setActivityTipId(null);
5155
+ activityTipAnchor.current = null;
5156
+ setActivityTipPosition(null);
5157
+ if (activityDrag.dayDelta !== 0) {
5158
+ const chainPush = task.activityChain === "push";
5159
+ let delta = activityDrag.dayDelta;
5160
+ if (chainPush && activityDrag.mode !== "resize-right") {
5161
+ const minDelta = activityConstraintRef.current?.minStartDelta ?? null;
5162
+ if (minDelta !== null) delta = Math.max(delta, minDelta);
5163
+ }
5164
+ const clampLeft = Math.min(delta, startInfo.durationDays - 1);
5165
+ const clampRight = Math.max(delta, -(startInfo.durationDays - 1));
5166
+ const updatedActivities = (task.activities ?? []).map((activity) => {
5167
+ if (activity.id !== activityDrag.id) return activity;
5168
+ if (activityDrag.mode === "move") {
5169
+ return {
5170
+ ...activity,
5171
+ startDate: toIsoDay(shiftActivityDay(activity.startDate, delta)),
5172
+ endDate: toIsoDay(shiftActivityDay(activity.endDate, delta))
5173
+ };
5174
+ }
5175
+ if (activityDrag.mode === "resize-left") {
5176
+ return { ...activity, startDate: toIsoDay(shiftActivityDay(activity.startDate, clampLeft)) };
5177
+ }
5178
+ return { ...activity, endDate: toIsoDay(shiftActivityDay(activity.endDate, clampRight)) };
5179
+ });
5180
+ const chainShifts = chainPush ? delta !== 0 ? pushActivityChain(allTasks ?? [], task.id, activityDrag.id, delta).shifts : [] : (() => {
5181
+ const endDeltaDays = chainEndDeltaDays(activityDrag.mode, delta, startInfo.durationDays);
5182
+ return endDeltaDays !== 0 ? shiftActivityChain(allTasks ?? [], task.id, activityDrag.id, endDeltaDays) : [];
5183
+ })();
5184
+ const changedTasks = [];
5185
+ let hostHandled = false;
5186
+ for (const shift of chainShifts) {
5187
+ if (shift.taskId === task.id) {
5188
+ hostHandled = true;
5189
+ changedTasks.push({
5190
+ ...normalizedTask,
5191
+ activities: shift.activities.map((activity) => activity.id === activityDrag.id ? updatedActivities.find((item) => item.id === activityDrag.id) ?? activity : activity)
5192
+ });
5193
+ } else {
5194
+ const source = (allTasks ?? []).find((candidate) => candidate.id === shift.taskId);
5195
+ changedTasks.push({ ...source ?? { id: shift.taskId }, activities: shift.activities });
5196
+ }
5197
+ }
5198
+ if (!hostHandled) {
5199
+ changedTasks.unshift({ ...normalizedTask, activities: updatedActivities });
5200
+ }
5201
+ onTasksChange?.(changedTasks);
5202
+ }
5203
+ setActivityDrag(null);
5204
+ activityDragStart.current = null;
5205
+ activityConstraintRef.current = null;
5206
+ document.body.style.cursor = "";
5207
+ };
5208
+ document.body.style.cursor = activityDrag.mode === "move" ? "grabbing" : "ew-resize";
5209
+ window.addEventListener("mousemove", onMove);
5210
+ window.addEventListener("mouseup", onUp);
5211
+ return () => {
5212
+ window.removeEventListener("mousemove", onMove);
5213
+ window.removeEventListener("mouseup", onUp);
5214
+ document.body.style.cursor = "";
5215
+ };
5216
+ }, [activityDrag, activityPreviewStore, activityBlockStore, activityDragOwner, allTasks, dayWidth, normalizedTask, onTasksChange, publishChainPreview, task.activities]);
5217
+ const anchorActivityTip = (rect) => {
5218
+ const below = rect.top < 34;
5219
+ const anchor = {
5220
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - 160)),
5221
+ // Вплотную к полосе (1px зазора), сверху или снизу при нехватке места.
5222
+ top: below ? rect.bottom + 1 : rect.top - 1,
5223
+ below
5224
+ };
5225
+ activityTipAnchor.current = anchor;
5226
+ return anchor;
5227
+ };
5228
+ const handleActivityPointerDown = (segment, mode) => (event) => {
5229
+ if (!activityDragEnabled) return;
5230
+ event.preventDefault();
5231
+ event.stopPropagation();
5232
+ activityDragStart.current = {
5233
+ x: event.clientX,
5234
+ durationDays: Math.max(1, Math.round(segment.width / dayWidth) - 1)
5235
+ };
5236
+ activityConstraintRef.current = task.activityChain === "push" && mode !== "resize-right" ? activityStartConstraint(allTasks ?? [], task.id, segment.id) : null;
5237
+ activityDragOwner?.set(activityOwnerKey(task.id, segment.id));
5238
+ setActivityBlockedBy(null);
5239
+ setActivityTipPosition(anchorActivityTip(event.currentTarget.getBoundingClientRect()));
5240
+ setActivityTipId(segment.id);
5241
+ setActivityDrag({ id: segment.id, mode, dayDelta: 0 });
5242
+ };
5243
+ const liveActivityGeometry = (segment) => {
5244
+ if (!activityDrag || activityDrag.id !== segment.id || activityDrag.dayDelta === 0) return segment;
5245
+ if (activityDrag.mode === "move") {
5246
+ return { left: segment.left + activityDrag.dayDelta * dayWidth, width: segment.width };
5247
+ }
5248
+ if (activityDrag.mode === "resize-left") {
5249
+ return { left: segment.left + activityDrag.dayDelta * dayWidth, width: segment.width - activityDrag.dayDelta * dayWidth };
5250
+ }
5251
+ return { left: segment.left, width: segment.width + activityDrag.dayDelta * dayWidth };
5252
+ };
5253
+ const activityLinks = useMemo2(() => {
5254
+ const gaps = [];
5255
+ const elbows = [];
5256
+ if (!showActivityLinks || !renderActivities || !activityLayout) return { gaps, elbows };
5257
+ const activities = task.activities ?? [];
5258
+ const segmentById = new Map(activityLayout.segments.map((segment) => [segment.id, segment]));
5259
+ const visibleIds = new Set(visibleActivitySegments.map((segment) => segment.id));
5260
+ const laneTopY = (lane) => activityTopOffset + lane * ACTIVITY_LANE_STEP + (ACTIVITY_LANE_STEP - ACTIVITY_LANE_BAR_HEIGHT) / 2;
5261
+ const liveRect = (segment) => {
5262
+ const geometry = liveActivityGeometry(segment);
5263
+ const chainDelta = activityOverrides?.get(segment.id) ?? 0;
5264
+ const left2 = geometry.left + (chainDelta !== 0 ? chainDelta * dayWidth : 0);
5265
+ return { left: left2, right: left2 + geometry.width };
5266
+ };
5267
+ for (let index = 0; index < activities.length - 1; index += 1) {
5268
+ const predecessor = segmentById.get(activities[index].id);
5269
+ const successor = segmentById.get(activities[index + 1].id);
5270
+ if (!predecessor || !successor) continue;
5271
+ if (!visibleIds.has(predecessor.id) || !visibleIds.has(successor.id)) continue;
5272
+ const lag = activities[index + 1].lag ?? 0;
5273
+ const predRect = liveRect(predecessor);
5274
+ const succRect = liveRect(successor);
5275
+ if (predecessor.lane === successor.lane && succRect.left >= predRect.right) {
5276
+ const width2 = succRect.left - predRect.right;
5277
+ if (width2 >= 1) {
5278
+ gaps.push({ key: `${predecessor.id}->${successor.id}`, left: predRect.right, width: width2, lane: predecessor.lane, lag });
5279
+ }
5280
+ continue;
5281
+ }
5282
+ const predecessorLaneTop = laneTopY(predecessor.lane);
5283
+ const successorLaneTop = laneTopY(successor.lane);
5284
+ const sameLane = predecessor.lane === successor.lane;
5285
+ const reverseOrder = !sameLane && successorLaneTop < predecessorLaneTop;
5286
+ const fromY = reverseOrder ? predecessorLaneTop + 6 : predecessorLaneTop + ACTIVITY_LANE_BAR_HEIGHT - 6;
5287
+ const toY = reverseOrder ? successorLaneTop + ACTIVITY_LANE_BAR_HEIGHT - 6 : successorLaneTop + (sameLane ? ACTIVITY_LANE_STEP : 0) + 6;
5288
+ const fromX = predRect.right;
5289
+ const toX = succRect.left;
5290
+ elbows.push({
5291
+ key: `${predecessor.id}->${successor.id}`,
5292
+ d: calculateDependencyPath({ x: fromX, y: fromY }, { x: toX, y: toY }, false),
5293
+ lagX: toX - 14,
5294
+ lagY: reverseOrder ? fromY - 4 : fromY + 12,
5295
+ lag
5296
+ });
5297
+ }
5298
+ return { gaps, elbows };
5299
+ }, [showActivityLinks, renderActivities, activityLayout, visibleActivitySegments, activityTopOffset, activityOverrides, activityDrag, dayWidth, task.activities]);
5300
+ const activityTipData = useMemo2(() => {
5301
+ if (!activityTipId || !task.activities) return null;
5302
+ const activity = task.activities.find((item) => item.id === activityTipId);
5303
+ if (!activity) return null;
5304
+ let startDate = parseUTCDate(activity.startDate);
5305
+ let endDate = parseUTCDate(activity.endDate);
5306
+ if (activityDrag && activityDrag.id === activity.id && activityDrag.dayDelta !== 0) {
5307
+ startDate = shiftActivityDay(startDate, activityDrag.dayDelta);
5308
+ endDate = shiftActivityDay(endDate, activityDrag.dayDelta);
5309
+ }
5310
+ const progress = activity.progress === void 0 ? void 0 : Math.min(100, Math.max(0, Math.round(activity.progress)));
5311
+ return {
5312
+ floor: task.name,
5313
+ name: activity.name,
5314
+ // Готовность — первой строкой подсказки, дальше пользовательские поля.
5315
+ fields: [
5316
+ ...progress !== void 0 ? [{ label: "\u0413\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u044C", value: `${progress}%` }] : [],
5317
+ ...activity.tooltipFields ?? []
5318
+ ],
5319
+ context: {
5320
+ task,
5321
+ activity,
5322
+ startDate,
5323
+ endDate,
5324
+ durationDays: activityDurationDays({ startDate, endDate }),
5325
+ progress
5326
+ }
5327
+ };
5328
+ }, [activityDrag, activityTipId, task, task.activities, task.name]);
4487
5329
  const currentStartDate = hasPreviewPosition ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
4488
5330
  const currentEndDate = hasPreviewPosition ? milestone ? pixelsToDate(displayLeft, monthStart, dayWidth) : pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
4489
5331
  const dateRangeLabel = formatDateRangeLabel(currentStartDate, currentEndDate);
@@ -4516,7 +5358,7 @@ var TaskRow = React2.memo(
4516
5358
  children: [
4517
5359
  taskDivider === "top" && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-divider gantt-tr-divider-top" }),
4518
5360
  /* @__PURE__ */ jsxs2("div", { className: "gantt-tr-taskContainer", children: [
4519
- shouldRenderBaseline && /* @__PURE__ */ jsx2(
5361
+ shouldRenderBaseline && !renderActivities && /* @__PURE__ */ jsx2(
4520
5362
  "div",
4521
5363
  {
4522
5364
  className: `gantt-tr-baseline ${isParent ? "gantt-tr-baseline-parent" : ""} ${milestone ? "gantt-tr-baseline-milestone" : ""}`,
@@ -4526,7 +5368,122 @@ var TaskRow = React2.memo(
4526
5368
  }
4527
5369
  }
4528
5370
  ),
4529
- /* @__PURE__ */ jsxs2(
5371
+ renderActivities && activityLayout && /* @__PURE__ */ jsxs2("div", { className: "gantt-tr-activityLanes", children: [
5372
+ activityLinks.elbows.length > 0 && /* @__PURE__ */ jsxs2("svg", { className: "gantt-tr-activityLinksSvg", width: "100%", height: "100%", "aria-hidden": "true", children: [
5373
+ /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("marker", { id: "arrowhead", markerWidth: "8", markerHeight: "6", markerUnits: "userSpaceOnUse", refX: "7", refY: "3", orient: "auto", children: /* @__PURE__ */ jsx2("polygon", { points: "0 0, 8 3, 0 6", fill: "var(--gantt-dependency-line-color, #666666)" }) }) }),
5374
+ activityLinks.elbows.map((link) => /* @__PURE__ */ jsxs2("g", { children: [
5375
+ /* @__PURE__ */ jsx2("path", { className: "gantt-dependency-path", d: link.d, markerEnd: "url(#arrowhead)" }),
5376
+ showActivityLag && link.lag !== 0 && /* @__PURE__ */ jsx2(
5377
+ "text",
5378
+ {
5379
+ className: "gantt-dependency-lag-label",
5380
+ x: link.lagX,
5381
+ y: link.lagY,
5382
+ textAnchor: "middle",
5383
+ fontSize: "10",
5384
+ fill: "var(--gantt-dependency-line-color, #666666)",
5385
+ children: link.lag > 0 ? `+${link.lag}` : `${link.lag}`
5386
+ }
5387
+ )
5388
+ ] }, link.key))
5389
+ ] }),
5390
+ visibleActivitySegments.map((segment) => {
5391
+ const live = liveActivityGeometry(segment);
5392
+ const chainDelta = activityOverrides?.get(segment.id) ?? 0;
5393
+ const activityProgress = segment.progress === void 0 ? 0 : Math.min(100, Math.max(0, Math.round(segment.progress)));
5394
+ const liveLeft = live.left + (chainDelta !== 0 ? chainDelta * dayWidth : 0);
5395
+ const isDraggingActivity = activityDrag?.id === segment.id;
5396
+ const isBlockingActivity = blockedActivityIds?.has(segment.id) ?? false;
5397
+ const isAtLimit = isDraggingActivity && activityBlockedBy !== null;
5398
+ const isActiveActivity = activationVisualEnabled && activeActivityKey === activityOwnerKey(task.id, segment.id);
5399
+ const isSameWorkType = activeWork !== null && (segment.id === activeWork.id || activeWork.name !== null && segment.name === activeWork.name);
5400
+ const isDimmed = dimOthers && activeWork !== null && !isSameWorkType;
5401
+ const isChainHighlighted = highlightChain && activeWork !== null && isSameWorkType;
5402
+ return /* @__PURE__ */ jsxs2(
5403
+ "div",
5404
+ {
5405
+ "data-activity-id": segment.id,
5406
+ 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"}`,
5407
+ style: {
5408
+ left: `${liveLeft}px`,
5409
+ width: `${live.width}px`,
5410
+ top: `${activityTopOffset + segment.lane * ACTIVITY_LANE_STEP + (ACTIVITY_LANE_STEP - ACTIVITY_LANE_BAR_HEIGHT) / 2}px`,
5411
+ height: `${ACTIVITY_LANE_BAR_HEIGHT}px`,
5412
+ backgroundColor: segment.color || "var(--gantt-task-bar-default-color)"
5413
+ },
5414
+ onMouseDown: (event) => {
5415
+ if (!activityDragEnabled) return;
5416
+ const activationKey = activityOwnerKey(task.id, segment.id);
5417
+ const isActive = activityActivationStore?.getActiveKey() === activationKey;
5418
+ if (activationVisualEnabled && !isActive) {
5419
+ activityActivationStore?.setActiveKey(activationKey);
5420
+ }
5421
+ if (activityClickToDrag && !isActive) return;
5422
+ const target = event.target;
5423
+ const mode = target.closest(".gantt-tr-resizeHandleLeft") ? "resize-left" : target.closest(".gantt-tr-resizeHandleRight") ? "resize-right" : "move";
5424
+ handleActivityPointerDown(segment, mode)(event);
5425
+ },
5426
+ onMouseEnter: (event) => {
5427
+ if (activityDrag || activityDragOwner?.get()) return;
5428
+ setActivityTipPosition(anchorActivityTip(event.currentTarget.getBoundingClientRect()));
5429
+ setActivityTipId(segment.id);
5430
+ },
5431
+ onMouseLeave: () => {
5432
+ if (!activityDrag) {
5433
+ setActivityTipId((current) => current === segment.id ? null : current);
5434
+ activityTipAnchor.current = null;
5435
+ setActivityTipPosition(null);
5436
+ }
5437
+ },
5438
+ children: [
5439
+ activityProgress > 0 && /* @__PURE__ */ jsx2(
5440
+ "span",
5441
+ {
5442
+ className: "gantt-tr-activityProgress",
5443
+ style: { width: `${activityProgress}%` }
5444
+ }
5445
+ ),
5446
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityName", style: { color: activityTextColor(segment.color) }, children: segment.name }),
5447
+ activityDragEnabled && /* @__PURE__ */ jsxs2(Fragment, { children: [
5448
+ /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
5449
+ /* @__PURE__ */ jsx2("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
5450
+ ] })
5451
+ ]
5452
+ },
5453
+ segment.id
5454
+ );
5455
+ }),
5456
+ activityLinks.gaps.map((gap) => /* @__PURE__ */ jsxs2(React2.Fragment, { children: [
5457
+ /* @__PURE__ */ jsx2(
5458
+ "span",
5459
+ {
5460
+ className: "gantt-tr-activityLink",
5461
+ "aria-hidden": "true",
5462
+ style: {
5463
+ left: `${gap.left}px`,
5464
+ width: `${gap.width}px`,
5465
+ top: `${activityTopOffset + gap.lane * ACTIVITY_LANE_STEP + ACTIVITY_LANE_STEP / 2}px`
5466
+ }
5467
+ }
5468
+ ),
5469
+ showActivityLag && gap.lag > 0 && /* @__PURE__ */ jsxs2(
5470
+ "span",
5471
+ {
5472
+ className: "gantt-tr-activityLag",
5473
+ title: `\u0417\u0430\u0437\u043E\u0440 ${gap.lag} \u0434`,
5474
+ style: {
5475
+ left: `${gap.left + gap.width - 14}px`,
5476
+ top: `${activityTopOffset + gap.lane * ACTIVITY_LANE_STEP + ACTIVITY_LANE_STEP / 2}px`
5477
+ },
5478
+ children: [
5479
+ "+",
5480
+ gap.lag
5481
+ ]
5482
+ }
5483
+ )
5484
+ ] }, gap.key))
5485
+ ] }),
5486
+ !renderActivities && /* @__PURE__ */ jsxs2(
4530
5487
  "div",
4531
5488
  {
4532
5489
  "data-taskbar": true,
@@ -4682,7 +5639,33 @@ var TaskRow = React2.memo(
4682
5639
  ),
4683
5640
  document.body
4684
5641
  ),
4685
- !disableDependencyEditing && onDependencyPortPointerDown && /* @__PURE__ */ jsxs2(Fragment, { children: [
5642
+ activityTipData && activityTipPosition && typeof document !== "undefined" && createPortal(
5643
+ /* @__PURE__ */ jsxs2(
5644
+ "div",
5645
+ {
5646
+ className: `gantt-tr-activityTip${activityTipPosition.below ? " gantt-tr-activityTip-below" : ""}`,
5647
+ style: { left: `${activityTipPosition.left}px`, top: `${activityTipPosition.top}px` },
5648
+ role: "tooltip",
5649
+ children: [
5650
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityTipName", children: activityTipData.floor }),
5651
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityTipDot", "aria-hidden": "true", children: "\xB7" }),
5652
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityTipWork", children: activityTipData.name }),
5653
+ activityTooltip && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-activityTipExtra", children: activityTooltip(activityTipData.context) }),
5654
+ activityTipData.fields.length > 0 && /* @__PURE__ */ jsx2("div", { className: "gantt-tr-activityTipFields", children: activityTipData.fields.map((field) => /* @__PURE__ */ jsxs2("div", { className: "gantt-tr-activityTipField", children: [
5655
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityTipFieldLabel", children: field.label }),
5656
+ /* @__PURE__ */ jsx2("span", { className: "gantt-tr-activityTipFieldValue", children: field.value })
5657
+ ] }, field.label)) }),
5658
+ activityBlockedBy && /* @__PURE__ */ jsxs2("span", { className: "gantt-tr-activityTipReason", children: [
5659
+ "\xB7 \u0411\u043B\u043E\u043A: ",
5660
+ activityBlockedBy.names.join(", "),
5661
+ activityBlockedBy.lag > 0 ? ` \xB7 \u0437\u0430\u0437\u043E\u0440 ${activityBlockedBy.lag} \u0434` : ""
5662
+ ] })
5663
+ ]
5664
+ }
5665
+ ),
5666
+ document.body
5667
+ ),
5668
+ !disableDependencyEditing && onDependencyPortPointerDown && !renderActivities && /* @__PURE__ */ jsxs2(Fragment, { children: [
4686
5669
  /* @__PURE__ */ jsx2(
4687
5670
  "button",
4688
5671
  {
@@ -4712,7 +5695,7 @@ var TaskRow = React2.memo(
4712
5695
  }
4713
5696
  )
4714
5697
  ] }),
4715
- showTaskDateLabels && /* @__PURE__ */ jsx2(
5698
+ showTaskDateLabels && !renderActivities && /* @__PURE__ */ jsx2(
4716
5699
  "div",
4717
5700
  {
4718
5701
  className: `gantt-tr-leftLabels ${task.locked ? "gantt-tr-leftLabels-locked" : ""}`,
@@ -4722,7 +5705,7 @@ var TaskRow = React2.memo(
4722
5705
  children: /* @__PURE__ */ jsx2("span", { className: "gantt-tr-dateLabel gantt-tr-dateLabelLeft", children: dateRangeLabel })
4723
5706
  }
4724
5707
  ),
4725
- task.locked && /* @__PURE__ */ jsx2(
5708
+ task.locked && !renderActivities && /* @__PURE__ */ jsx2(
4726
5709
  "svg",
4727
5710
  {
4728
5711
  className: "gantt-tr-lockIcon",
@@ -4743,15 +5726,15 @@ var TaskRow = React2.memo(
4743
5726
  children: /* @__PURE__ */ jsx2("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" })
4744
5727
  }
4745
5728
  ),
4746
- !showDurationInside && !milestone && /* @__PURE__ */ jsx2(
5729
+ compactDetail && !milestone && /* @__PURE__ */ jsx2(
4747
5730
  "span",
4748
5731
  {
4749
- className: `gantt-tr-externalDuration gantt-tr-durationBeforeBar${compactDetail ? " gantt-tr-compositeDuration" : ""}`,
5732
+ className: "gantt-tr-externalDuration gantt-tr-durationBeforeBar gantt-tr-compositeDuration",
4750
5733
  style: { left: `${visualLeft - 46}px`, color: isParent ? task.color || defaultParentBarColor : barColor },
4751
5734
  children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434`
4752
5735
  }
4753
5736
  ),
4754
- /* @__PURE__ */ jsxs2(
5737
+ !renderActivities && /* @__PURE__ */ jsxs2(
4755
5738
  "div",
4756
5739
  {
4757
5740
  className: "gantt-tr-rightLabels",
@@ -4760,6 +5743,7 @@ var TaskRow = React2.memo(
4760
5743
  color: isParent ? task.color || defaultParentBarColor : barColor
4761
5744
  },
4762
5745
  children: [
5746
+ !compactDetail && !showDurationInside && !milestone && /* @__PURE__ */ jsx2("span", { className: "gantt-tr-externalDuration", children: isVisualParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
4763
5747
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ jsxs2("span", { className: "gantt-tr-externalProgress", children: [
4764
5748
  progressWidth,
4765
5749
  "%"
@@ -4887,10 +5871,10 @@ import React5, { useMemo as useMemo5 } from "react";
4887
5871
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
4888
5872
  var arePropsEqual2 = (prevProps, nextProps) => {
4889
5873
  return prevProps.dayWidth === nextProps.dayWidth && prevProps.dateRange.length === nextProps.dateRange.length && prevProps.totalHeight === nextProps.totalHeight && // skip re-render only when totalHeight unchanged
4890
- prevProps.viewMode === nextProps.viewMode && prevProps.isCustomWeekend === nextProps.isCustomWeekend;
5874
+ 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;
4891
5875
  };
4892
5876
  var GridBackground = React5.memo(
4893
- ({ dateRange, dayWidth, totalHeight, viewMode = "day", isCustomWeekend, className, showWeekendBlocks = true, showGridLines = true }) => {
5877
+ ({ dateRange, dayWidth, totalHeight, viewMode = "day", isCustomWeekend, className, showWeekendBlocks = true, showGridLines = true, horizontalWindow }) => {
4894
5878
  const weekGridLines = useMemo5(() => {
4895
5879
  if (viewMode !== "week") return [];
4896
5880
  return calculateWeekGridLines(dateRange, dayWidth);
@@ -4907,6 +5891,23 @@ var GridBackground = React5.memo(
4907
5891
  if (viewMode === "week" || viewMode === "month") return [];
4908
5892
  return calculateWeekendBlocks(dateRange, dayWidth, isCustomWeekend);
4909
5893
  }, [dateRange, dayWidth, viewMode, isCustomWeekend]);
5894
+ const inHorizontalWindow = (left, width = 1) => !horizontalWindow || left <= horizontalWindow.endPx && left + width >= horizontalWindow.startPx;
5895
+ const visibleWeekendBlocks = useMemo5(
5896
+ () => weekendBlocks.filter((block) => inHorizontalWindow(block.left, block.width)),
5897
+ [weekendBlocks, horizontalWindow?.startPx, horizontalWindow?.endPx]
5898
+ );
5899
+ const visibleWeekGridLines = useMemo5(
5900
+ () => weekGridLines.filter((line) => inHorizontalWindow(line.x)),
5901
+ [weekGridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
5902
+ );
5903
+ const visibleMonthGridLines = useMemo5(
5904
+ () => monthGridLines.filter((line) => inHorizontalWindow(line.x)),
5905
+ [monthGridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
5906
+ );
5907
+ const visibleGridLines = useMemo5(
5908
+ () => gridLines.filter((line) => inHorizontalWindow(line.x)),
5909
+ [gridLines, horizontalWindow?.startPx, horizontalWindow?.endPx]
5910
+ );
4910
5911
  const gridWidth = useMemo5(() => {
4911
5912
  return Math.round(dateRange.length * dayWidth);
4912
5913
  }, [dateRange.length, dayWidth]);
@@ -4919,7 +5920,7 @@ var GridBackground = React5.memo(
4919
5920
  height: `${totalHeight}px`
4920
5921
  },
4921
5922
  children: [
4922
- showWeekendBlocks && weekendBlocks.map((block, index) => /* @__PURE__ */ jsx5(
5923
+ showWeekendBlocks && visibleWeekendBlocks.map((block, index) => /* @__PURE__ */ jsx5(
4923
5924
  "div",
4924
5925
  {
4925
5926
  className: "gantt-gb-weekendBlock",
@@ -4932,7 +5933,7 @@ var GridBackground = React5.memo(
4932
5933
  )),
4933
5934
  showGridLines && (viewMode === "week" ? (
4934
5935
  // Week-view: one line per week column boundary
4935
- weekGridLines.map((line, index) => {
5936
+ visibleWeekGridLines.map((line, index) => {
4936
5937
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : "gantt-gb-weekSeparator";
4937
5938
  return /* @__PURE__ */ jsx5(
4938
5939
  "div",
@@ -4945,7 +5946,7 @@ var GridBackground = React5.memo(
4945
5946
  })
4946
5947
  ) : viewMode === "month" ? (
4947
5948
  // Month-view: thin line at each month boundary, thick at year boundary
4948
- monthGridLines.map((line, index) => {
5949
+ visibleMonthGridLines.map((line, index) => {
4949
5950
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : "gantt-gb-weekSeparator";
4950
5951
  return /* @__PURE__ */ jsx5(
4951
5952
  "div",
@@ -4957,8 +5958,8 @@ var GridBackground = React5.memo(
4957
5958
  );
4958
5959
  })
4959
5960
  ) : (
4960
- // Day-view: existing code (unchanged)
4961
- gridLines.map((line, index) => {
5961
+ // Day-view: render only the lines inside the horizontal window.
5962
+ visibleGridLines.map((line, index) => {
4962
5963
  const lineClass = line.isMonthStart ? "gantt-gb-monthSeparator" : line.isWeekStart ? "gantt-gb-weekSeparator" : "gantt-gb-dayLine";
4963
5964
  return /* @__PURE__ */ jsx5(
4964
5965
  "div",
@@ -5070,7 +6071,8 @@ var DependencyLines = React6.memo(({
5070
6071
  criticalTaskIds,
5071
6072
  businessDays = true,
5072
6073
  weekendPredicate,
5073
- onDependencyClick
6074
+ onDependencyClick,
6075
+ horizontalWindow
5074
6076
  }) => {
5075
6077
  const [hoveredLineId, setHoveredLineId] = useState3(null);
5076
6078
  const tasksForPositions = allTasks ?? tasks;
@@ -5234,6 +6236,10 @@ var DependencyLines = React6.memo(({
5234
6236
  }
5235
6237
  return lines2;
5236
6238
  }, [tasks, allTasks, taskPositions, taskIndices, cycleInfo, collapsedParentIds, criticalTaskIds]);
6239
+ const visibleLines = useMemo6(() => {
6240
+ if (!horizontalWindow) return lines;
6241
+ return lines.filter((line) => Math.max(line.fromX, line.toX) >= horizontalWindow.startPx && Math.min(line.fromX, line.toX) <= horizontalWindow.endPx);
6242
+ }, [lines, horizontalWindow?.startPx, horizontalWindow?.endPx]);
5237
6243
  const svgHeight = totalHeight ?? tasks.length * rowHeight;
5238
6244
  return /* @__PURE__ */ jsxs5(
5239
6245
  "svg",
@@ -5341,7 +6347,7 @@ var DependencyLines = React6.memo(({
5341
6347
  }
5342
6348
  )
5343
6349
  ] }),
5344
- lines.map(({ id, predecessorId, successorId, linkType, path, hasCycle, lag, fromX, toX, fromY, reverseOrder, isVirtual, isCritical }) => {
6350
+ visibleLines.map(({ id, predecessorId, successorId, linkType, path, hasCycle, lag, fromX, toX, fromY, reverseOrder, isVirtual, isCritical }) => {
5345
6351
  const isHovered = hoveredLineId === id;
5346
6352
  const isSelected = selectedDep != null && id === `${selectedDep.predecessorId}-${selectedDep.successorId}-${selectedDep.linkType}`;
5347
6353
  let pathClassName = "gantt-dependency-path";
@@ -6205,7 +7211,7 @@ var LINK_TYPE_LABELS = {
6205
7211
  };
6206
7212
 
6207
7213
  // src/components/TaskList/defaultTaskDates.ts
6208
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
7214
+ var DAY_MS3 = 24 * 60 * 60 * 1e3;
6209
7215
  var DEFAULT_TASK_DURATION_DAYS = 5;
6210
7216
  function toISODate(date) {
6211
7217
  return date.toISOString().split("T")[0];
@@ -6234,25 +7240,25 @@ function buildDefaultTaskDateRange(startDate, {
6234
7240
  }
6235
7241
  return {
6236
7242
  startDate: toISODate(start),
6237
- endDate: toISODate(new Date(start.getTime() + (durationDays - 1) * DAY_MS2))
7243
+ endDate: toISODate(new Date(start.getTime() + (durationDays - 1) * DAY_MS3))
6238
7244
  };
6239
7245
  }
6240
7246
 
6241
7247
  // src/components/TaskList/TaskListRow.tsx
6242
7248
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
6243
- var DAY_MS3 = 24 * 60 * 60 * 1e3;
7249
+ var DAY_MS4 = 24 * 60 * 60 * 1e3;
6244
7250
  var LINK_TYPE_ORDER = ["FS", "SS", "FF", "SF"];
6245
7251
  var getInclusiveDurationDays = (startDate, endDate) => {
6246
7252
  const start = parseUTCDate(startDate);
6247
7253
  const end = parseUTCDate(endDate);
6248
7254
  return Math.max(
6249
7255
  1,
6250
- Math.round((end.getTime() - start.getTime()) / DAY_MS3) + 1
7256
+ Math.round((end.getTime() - start.getTime()) / DAY_MS4) + 1
6251
7257
  );
6252
7258
  };
6253
7259
  var getEndDateFromDuration = (startDate, durationDays) => {
6254
7260
  const start = parseUTCDate(startDate);
6255
- return new Date(start.getTime() + (durationDays - 1) * DAY_MS3).toISOString().split("T")[0];
7261
+ return new Date(start.getTime() + (durationDays - 1) * DAY_MS4).toISOString().split("T")[0];
6256
7262
  };
6257
7263
  var TrashIcon = () => /* @__PURE__ */ jsxs10(
6258
7264
  "svg",
@@ -10358,7 +11364,11 @@ var TaskList = ({
10358
11364
  onActiveCustomCellChange: setActiveCustomCell,
10359
11365
  taskListMenuCommands,
10360
11366
  hideTaskListRowActions,
10361
- rowClassName: [task.parentId && compositeParentIds.has(task.parentId) ? "gantt-tl-row-composite-detail" : "", getTaskListRowClassName?.(task)].filter(Boolean).join(" "),
11367
+ rowClassName: [
11368
+ task.parentId && compositeParentIds.has(task.parentId) ? "gantt-tl-row-composite-detail" : "",
11369
+ (task.activities?.length ?? 0) > 0 && (rowHeights?.[index] ?? rowHeight) > rowHeight ? "gantt-tl-row-activities" : "",
11370
+ getTaskListRowClassName?.(task)
11371
+ ].filter(Boolean).join(" "),
10362
11372
  compositeParentColor: task.parentId ? compositeParentsById.get(task.parentId)?.color : void 0,
10363
11373
  getTaskListNamePrefixIcon,
10364
11374
  taskDateChangeMode,
@@ -12336,7 +13346,7 @@ import { useLayoutEffect, useMemo as useMemo11, useRef as useRef11, useState as
12336
13346
  import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
12337
13347
  var AUTO_COLUMN_MIN_WIDTH = 72;
12338
13348
  var AUTO_COLUMN_MAX_WIDTH = 180;
12339
- var DAY_MS4 = 24 * 60 * 60 * 1e3;
13349
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
12340
13350
  function joinClasses(...values) {
12341
13351
  return values.filter(Boolean).join(" ");
12342
13352
  }
@@ -12371,8 +13381,8 @@ function getOverlayWidthPercent(column, overlayDateMs) {
12371
13381
  if (overlayDateMs >= endMs) {
12372
13382
  return 100;
12373
13383
  }
12374
- const totalDays = Math.max(1, Math.round((endMs - startMs) / DAY_MS4) + 1);
12375
- const elapsedDays = Math.min(totalDays, Math.max(0, Math.round((overlayDateMs - startMs) / DAY_MS4) + 1));
13384
+ const totalDays = Math.max(1, Math.round((endMs - startMs) / DAY_MS5) + 1);
13385
+ const elapsedDays = Math.min(totalDays, Math.max(0, Math.round((overlayDateMs - startMs) / DAY_MS5) + 1));
12376
13386
  return elapsedDays / totalDays * 100;
12377
13387
  }
12378
13388
  function isOverlayDateInColumn(column, overlayDateMs) {
@@ -12702,7 +13712,7 @@ function TableMatrix({
12702
13712
  // src/components/PlanFactMatrix/PlanFactMatrix.tsx
12703
13713
  import React16, { useCallback as useCallback9, useEffect as useEffect10, useMemo as useMemo12, useRef as useRef12, useState as useState12 } from "react";
12704
13714
  import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
12705
- var DAY_MS5 = 24 * 60 * 60 * 1e3;
13715
+ var DAY_MS6 = 24 * 60 * 60 * 1e3;
12706
13716
  function joinClasses2(...values) {
12707
13717
  return values.filter(Boolean).join(" ");
12708
13718
  }
@@ -12732,8 +13742,8 @@ function getPlannedIndexRange(task, rangeStartMs, rangeLength) {
12732
13742
  if (rangeLength <= 0) return null;
12733
13743
  const start = parseUTCDate(task.startDate);
12734
13744
  const end = parseUTCDate(task.endDate);
12735
- const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS5);
12736
- const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS5);
13745
+ const startIndex = Math.ceil((getDateOnlyMs(start) - rangeStartMs) / DAY_MS6);
13746
+ const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) / DAY_MS6);
12737
13747
  const clampedStartIndex = Math.max(0, startIndex);
12738
13748
  const clampedEndIndex = Math.min(rangeLength - 1, endIndex);
12739
13749
  if (clampedStartIndex > clampedEndIndex) return null;
@@ -14102,80 +15112,13 @@ async function printGanttChart({
14102
15112
  }
14103
15113
  }
14104
15114
 
14105
- // src/components/GanttChart/previewStore.ts
14106
- function positionsEqual(left, right) {
14107
- return left?.left === right?.left && left?.width === right?.width;
14108
- }
14109
- function createTaskPreviewPositionStore() {
14110
- let positions = /* @__PURE__ */ new Map();
14111
- const listenersByTaskId = /* @__PURE__ */ new Map();
14112
- function notify(taskIds) {
14113
- for (const taskId of taskIds) {
14114
- const listeners = listenersByTaskId.get(taskId);
14115
- if (!listeners) continue;
14116
- for (const listener of listeners) {
14117
- listener();
14118
- }
14119
- }
14120
- }
14121
- return {
14122
- subscribeTask(taskId, listener) {
14123
- const listeners = listenersByTaskId.get(taskId) ?? /* @__PURE__ */ new Set();
14124
- listeners.add(listener);
14125
- listenersByTaskId.set(taskId, listeners);
14126
- return () => {
14127
- listeners.delete(listener);
14128
- if (listeners.size === 0) {
14129
- listenersByTaskId.delete(taskId);
14130
- }
14131
- };
14132
- },
14133
- getTaskPosition(taskId) {
14134
- return positions.get(taskId);
14135
- },
14136
- setPositions(nextPositions) {
14137
- const changedTaskIds = /* @__PURE__ */ new Set();
14138
- const mergedPositions = /* @__PURE__ */ new Map();
14139
- for (const [taskId, nextPosition] of nextPositions) {
14140
- const currentPosition = positions.get(taskId);
14141
- if (positionsEqual(currentPosition, nextPosition)) {
14142
- if (currentPosition) {
14143
- mergedPositions.set(taskId, currentPosition);
14144
- }
14145
- continue;
14146
- }
14147
- changedTaskIds.add(taskId);
14148
- mergedPositions.set(taskId, {
14149
- left: nextPosition.left,
14150
- width: nextPosition.width
14151
- });
14152
- }
14153
- for (const taskId of positions.keys()) {
14154
- if (!nextPositions.has(taskId)) {
14155
- changedTaskIds.add(taskId);
14156
- }
14157
- }
14158
- if (changedTaskIds.size === 0) {
14159
- return;
14160
- }
14161
- positions = mergedPositions;
14162
- notify(changedTaskIds);
14163
- },
14164
- clear() {
14165
- if (positions.size === 0) {
14166
- return;
14167
- }
14168
- const changedTaskIds = new Set(positions.keys());
14169
- positions = /* @__PURE__ */ new Map();
14170
- notify(changedTaskIds);
14171
- }
14172
- };
14173
- }
14174
-
14175
15115
  // src/components/GanttChart/GanttChart.tsx
14176
15116
  import { Fragment as Fragment6, jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
14177
15117
  var SCROLL_TO_ROW_CONTEXT_ROWS = 2;
14178
15118
  var TASK_ROW_OVERSCAN = 8;
15119
+ var SCROLL_TOP_BUCKET_PX = 64;
15120
+ var HORIZONTAL_WINDOW_BUCKET_PX = 480;
15121
+ var HORIZONTAL_WINDOW_OVERSCAN_PX = 960;
14179
15122
  var PLAN_FACT_COLUMN_OVERSCAN = 24;
14180
15123
  var PLAN_FACT_COLUMN_WINDOW_STEP = 14;
14181
15124
  var DEFAULT_INITIAL_VIEWPORT_HEIGHT = 768;
@@ -14369,6 +15312,11 @@ function TaskGanttChartInner(props, ref) {
14369
15312
  showTaskDateLabels = true,
14370
15313
  showTaskNames = true,
14371
15314
  showCompositeSegments = true,
15315
+ activityClickToDrag = false,
15316
+ activityActivationMode = "none",
15317
+ showActivityLinks = true,
15318
+ showActivityLag = true,
15319
+ activityTooltip,
14372
15320
  timelineMarkers,
14373
15321
  additionalColumns,
14374
15322
  hiddenTaskListColumns,
@@ -14402,17 +15350,41 @@ function TaskGanttChartInner(props, ref) {
14402
15350
  const clearSelectedTaskTimeoutRef = useRef13(null);
14403
15351
  const hasAutoScrolledToTodayRef = useRef13(false);
14404
15352
  const previewPositionStoreRef = useRef13(null);
15353
+ const activityPreviewStoreRef = useRef13(null);
15354
+ const activityBlockStoreRef = useRef13(null);
15355
+ const activityDragOwnerRef = useRef13(null);
15356
+ const activityActivationStoreRef = useRef13(null);
14405
15357
  const renderedTaskIdsRef = useRef13(/* @__PURE__ */ new Set());
14406
15358
  if (previewPositionStoreRef.current === null) {
14407
15359
  previewPositionStoreRef.current = createTaskPreviewPositionStore();
14408
15360
  }
15361
+ if (activityPreviewStoreRef.current === null) {
15362
+ activityPreviewStoreRef.current = createActivityPreviewStore();
15363
+ }
15364
+ if (activityBlockStoreRef.current === null) {
15365
+ activityBlockStoreRef.current = createActivityBlockStore();
15366
+ }
15367
+ if (activityDragOwnerRef.current === null) {
15368
+ activityDragOwnerRef.current = createActivityDragOwner();
15369
+ }
15370
+ if (activityActivationStoreRef.current === null) {
15371
+ activityActivationStoreRef.current = createActivityActivationStore();
15372
+ }
14409
15373
  const previewPositionStore = previewPositionStoreRef.current;
15374
+ const activityPreviewStore = activityPreviewStoreRef.current;
15375
+ const activityBlockStore = activityBlockStoreRef.current;
15376
+ const activityDragOwner = activityDragOwnerRef.current;
15377
+ const activityActivationStore = activityActivationStoreRef.current;
14410
15378
  const [selectedTaskId, setSelectedTaskId] = useState13(null);
14411
15379
  const [taskListHasRightShadow, setTaskListHasRightShadow] = useState13(false);
14412
15380
  const [internalTaskDateChangeMode, setInternalTaskDateChangeMode] = useState13("preserve-duration");
14413
15381
  const [scrollViewport, setScrollViewport] = useState13(() => ({
14414
15382
  scrollTop: 0,
14415
- viewportHeight: getInitialScrollViewportHeight(containerHeight, headerHeight + 1)
15383
+ viewportHeight: getInitialScrollViewportHeight(containerHeight, headerHeight + 1),
15384
+ // Ширина видимой области графика и «бакетированный» горизонтальный скролл —
15385
+ // основа горизонтального окна рендера (см. horizontalWindow).
15386
+ viewportWidth: 0,
15387
+ chartScrollLeft: 0
14416
15388
  }));
14417
15389
  const [forceFullRenderForPrint, setForceFullRenderForPrint] = useState13(false);
14418
15390
  const [planFactDateWindow, setPlanFactDateWindow] = useState13({ start: 0, end: 0 });
@@ -14650,14 +15622,21 @@ function TaskGanttChartInner(props, ref) {
14650
15622
  if (isPlanFactMode) {
14651
15623
  return getPlanFactRangeTasks(normalizedTasks);
14652
15624
  }
14653
- if (!showBaseline) {
14654
- return normalizedTasks;
15625
+ const rangeTasks = [];
15626
+ for (const task of normalizedTasks) {
15627
+ if (showBaseline) {
15628
+ rangeTasks.push({
15629
+ startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
15630
+ endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
15631
+ });
15632
+ } else {
15633
+ rangeTasks.push(task);
15634
+ }
15635
+ for (const activity of task.activities ?? []) {
15636
+ rangeTasks.push({ startDate: activity.startDate, endDate: activity.endDate });
15637
+ }
14655
15638
  }
14656
- return normalizedTasks.map((task) => ({
14657
- ...task,
14658
- startDate: task.baselineStartDate && parseUTCDate(task.baselineStartDate).getTime() < parseUTCDate(task.startDate).getTime() ? task.baselineStartDate : task.startDate,
14659
- endDate: task.baselineEndDate && parseUTCDate(task.baselineEndDate).getTime() > parseUTCDate(task.endDate).getTime() ? task.baselineEndDate : task.endDate
14660
- }));
15639
+ return rangeTasks;
14661
15640
  }, [isPlanFactMode, normalizedTasks, showBaseline]);
14662
15641
  const dateRange = useMemo13(
14663
15642
  () => {
@@ -14702,6 +15681,25 @@ function TaskGanttChartInner(props, ref) {
14702
15681
  }
14703
15682
  return tasks2;
14704
15683
  }, [normalizedTasks, collapsedParentIds, filterMode, taskFilter, criticalPathMode, criticalTaskIds]);
15684
+ const monthStart = useMemo13(() => {
15685
+ if (dateRange.length === 0) {
15686
+ return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
15687
+ }
15688
+ const firstDay = dateRange[0];
15689
+ if (explicitDateRange && !isPlanFactMode) {
15690
+ return new Date(firstDay);
15691
+ }
15692
+ return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
15693
+ }, [dateRange, explicitDateRange, isPlanFactMode]);
15694
+ const activityLaneCounts = useMemo13(() => {
15695
+ const counts = /* @__PURE__ */ new Map();
15696
+ for (const task of visibleTasks) {
15697
+ if (task.activities && task.activities.length > 0) {
15698
+ counts.set(task.id, computeActivityLanes(task.activities, monthStart, dayWidth).laneCount);
15699
+ }
15700
+ }
15701
+ return counts;
15702
+ }, [visibleTasks, monthStart, dayWidth]);
14705
15703
  const compositeRowLayout = useMemo13(() => {
14706
15704
  const compositeIds = new Set(normalizedTasks.filter((task) => task.composite).map((task) => task.id));
14707
15705
  const tops = [];
@@ -14709,12 +15707,12 @@ function TaskGanttChartInner(props, ref) {
14709
15707
  let top = 0;
14710
15708
  for (const task of visibleTasks) {
14711
15709
  tops.push(top);
14712
- const height = task.parentId && compositeIds.has(task.parentId) ? Math.min(effectiveRowHeight, 22) : effectiveRowHeight;
15710
+ const height = task.parentId && compositeIds.has(task.parentId) ? Math.min(effectiveRowHeight, 22) : Math.max(effectiveRowHeight, activityLanesExtraHeight(activityLaneCounts.get(task.id) ?? 0));
14713
15711
  heights.push(height);
14714
15712
  top += height;
14715
15713
  }
14716
15714
  return { tops, heights, totalHeight: top };
14717
- }, [normalizedTasks, visibleTasks, effectiveRowHeight]);
15715
+ }, [normalizedTasks, visibleTasks, effectiveRowHeight, activityLaneCounts]);
14718
15716
  const compositeParentsById = useMemo13(
14719
15717
  () => new Map(normalizedTasks.filter((task) => task.composite).map((task) => [task.id, task])),
14720
15718
  [normalizedTasks]
@@ -14753,16 +15751,6 @@ function TaskGanttChartInner(props, ref) {
14753
15751
  }
14754
15752
  return `calc(${containerHeight} - ${timelineHeaderHeight}px)`;
14755
15753
  }, [containerHeight, isPlanFactMode, isTableMatrixMode, timelineHeaderHeight]);
14756
- const monthStart = useMemo13(() => {
14757
- if (dateRange.length === 0) {
14758
- return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
14759
- }
14760
- const firstDay = dateRange[0];
14761
- if (explicitDateRange && !isPlanFactMode) {
14762
- return new Date(firstDay);
14763
- }
14764
- return new Date(Date.UTC(firstDay.getUTCFullYear(), firstDay.getUTCMonth(), 1));
14765
- }, [dateRange, explicitDateRange, isPlanFactMode]);
14766
15754
  const todayIndex = useMemo13(() => {
14767
15755
  const today = getTodayLocalUtcDate();
14768
15756
  return dateRange.findIndex((day) => day.getTime() === today.getTime());
@@ -14805,12 +15793,23 @@ function TaskGanttChartInner(props, ref) {
14805
15793
  const nextScrollTop = container.scrollTop;
14806
15794
  const nextScrollLeft = container.scrollLeft;
14807
15795
  const nextChartScrollLeft = Math.max(0, nextScrollLeft);
15796
+ const bucketedScrollTop = Math.floor(nextScrollTop / SCROLL_TOP_BUCKET_PX) * SCROLL_TOP_BUCKET_PX;
15797
+ const bucketedChartScrollLeft = Math.floor(nextChartScrollLeft / HORIZONTAL_WINDOW_BUCKET_PX) * HORIZONTAL_WINDOW_BUCKET_PX;
14808
15798
  setTaskListHasRightShadow(
14809
15799
  (previous) => previous === nextHasRightShadow ? previous : nextHasRightShadow
14810
15800
  );
14811
- setScrollViewport(
14812
- (previous) => previous.viewportHeight > 0 && nextViewportHeight === 0 ? { scrollTop: nextScrollTop, viewportHeight: previous.viewportHeight } : previous.scrollTop === nextScrollTop && previous.viewportHeight === nextViewportHeight ? previous : { scrollTop: nextScrollTop, viewportHeight: nextViewportHeight }
14813
- );
15801
+ setScrollViewport((previous) => {
15802
+ const resolvedViewportHeight = previous.viewportHeight > 0 && nextViewportHeight === 0 ? previous.viewportHeight : nextViewportHeight;
15803
+ if (previous.scrollTop === bucketedScrollTop && previous.viewportHeight === resolvedViewportHeight && previous.viewportWidth === nextViewportWidth && previous.chartScrollLeft === bucketedChartScrollLeft) {
15804
+ return previous;
15805
+ }
15806
+ return {
15807
+ scrollTop: bucketedScrollTop,
15808
+ viewportHeight: resolvedViewportHeight,
15809
+ viewportWidth: nextViewportWidth,
15810
+ chartScrollLeft: bucketedChartScrollLeft
15811
+ };
15812
+ });
14814
15813
  setPlanFactDateWindow((previous) => {
14815
15814
  if (!isPlanFactMode || dateRange.length === 0 || nextViewportWidth <= 0) {
14816
15815
  return previous;
@@ -14926,11 +15925,15 @@ function TaskGanttChartInner(props, ref) {
14926
15925
  return;
14927
15926
  }
14928
15927
  previewPositionStore.clear();
15928
+ activityPreviewStore.clear();
15929
+ activityBlockStore.clear();
15930
+ activityDragOwner.set(null);
15931
+ activityActivationStore.setActiveKey(null);
14929
15932
  setCascadeOverrides((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
14930
15933
  setPreviewTasksById((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
14931
15934
  setDraggedTaskOverride((current) => current === null ? current : null);
14932
15935
  setDragGuideLines((current) => current === null ? current : null);
14933
- }, [controlledScheduleIdentity, previewPositionStore]);
15936
+ }, [controlledScheduleIdentity, previewPositionStore, activityPreviewStore, activityBlockStore, activityDragOwner, activityActivationStore]);
14934
15937
  useEffect11(() => {
14935
15938
  const result = validateDependencies(tasks);
14936
15939
  setValidationResult(result);
@@ -14941,7 +15944,11 @@ function TaskGanttChartInner(props, ref) {
14941
15944
  window.clearTimeout(clearSelectedTaskTimeoutRef.current);
14942
15945
  }
14943
15946
  previewPositionStore.clear();
14944
- }, [previewPositionStore]);
15947
+ activityPreviewStore.clear();
15948
+ activityBlockStore.clear();
15949
+ activityDragOwner.set(null);
15950
+ activityActivationStore.setActiveKey(null);
15951
+ }, [previewPositionStore, activityPreviewStore, activityBlockStore, activityDragOwner, activityActivationStore]);
14945
15952
  const handleTaskChange = useCallback10((updatedTasks, durationEdit) => {
14946
15953
  let updatedTask = updatedTasks[0];
14947
15954
  if (!updatedTask) return;
@@ -14987,9 +15994,9 @@ function TaskGanttChartInner(props, ref) {
14987
15994
  return;
14988
15995
  }
14989
15996
  if (datesChanged && isTaskParent(updatedTask.id, tasks)) {
14990
- const projectDayDelta = (from, to) => businessDays && isCustomWeekend ? getBusinessDayOffset(from, to, isCustomWeekend) : Math.round((to.getTime() - from.getTime()) / DAY_MS);
14991
- const deltaStart = projectDayDelta(origStart, newStart);
14992
- const deltaEnd = projectDayDelta(origEnd, newEnd);
15997
+ const projectDayDelta2 = (from, to) => businessDays && isCustomWeekend ? getBusinessDayOffset(from, to, isCustomWeekend) : Math.round((to.getTime() - from.getTime()) / DAY_MS);
15998
+ const deltaStart = projectDayDelta2(origStart, newStart);
15999
+ const deltaEnd = projectDayDelta2(origEnd, newEnd);
14993
16000
  const isUniformMove = deltaStart === deltaEnd && deltaStart !== 0;
14994
16001
  if (!isUniformMove) {
14995
16002
  const anchor = deltaStart === 0 ? "start" : deltaEnd === 0 ? "end" : "start";
@@ -15177,6 +16184,15 @@ function TaskGanttChartInner(props, ref) {
15177
16184
  }
15178
16185
  return ids;
15179
16186
  }, [draggedTaskOverride]);
16187
+ const horizontalWindow = useMemo13(() => {
16188
+ const width = scrollViewport.viewportWidth;
16189
+ if (width <= 0) return void 0;
16190
+ const left = scrollViewport.chartScrollLeft;
16191
+ return {
16192
+ startPx: Math.max(0, left - HORIZONTAL_WINDOW_OVERSCAN_PX),
16193
+ endPx: left + width + HORIZONTAL_WINDOW_OVERSCAN_PX
16194
+ };
16195
+ }, [scrollViewport.chartScrollLeft, scrollViewport.viewportWidth]);
15180
16196
  const visibleTaskWindowIndices = useMemo13(() => {
15181
16197
  const totalTasks = visibleTasks.length;
15182
16198
  if (totalTasks === 0) {
@@ -15511,6 +16527,9 @@ function TaskGanttChartInner(props, ref) {
15511
16527
  const handlePanStart = useCallback10((e) => {
15512
16528
  if (e.button !== 0) return;
15513
16529
  const target = e.target;
16530
+ if (!target.closest(".gantt-tr-activityBar")) {
16531
+ activityActivationStore.setActiveKey(null);
16532
+ }
15514
16533
  if (target.closest("[data-taskbar]")) return;
15515
16534
  if (target.closest("input, button, textarea, [contenteditable]")) return;
15516
16535
  if (target.closest(".gantt-tl-overlay")) return;
@@ -15531,7 +16550,7 @@ function TaskGanttChartInner(props, ref) {
15531
16550
  }
15532
16551
  container.style.cursor = "grabbing";
15533
16552
  e.preventDefault();
15534
- }, []);
16553
+ }, [activityActivationStore]);
15535
16554
  useEffect11(() => {
15536
16555
  const flushPanMove = () => {
15537
16556
  const pan = panStateRef.current;
@@ -15761,7 +16780,8 @@ function TaskGanttChartInner(props, ref) {
15761
16780
  dayWidth,
15762
16781
  totalHeight: totalGridHeight,
15763
16782
  viewMode,
15764
- isCustomWeekend
16783
+ isCustomWeekend,
16784
+ horizontalWindow
15765
16785
  }
15766
16786
  ),
15767
16787
  todayInRange && /* @__PURE__ */ jsx21(
@@ -15802,6 +16822,7 @@ function TaskGanttChartInner(props, ref) {
15802
16822
  selectedDep: selectedChip,
15803
16823
  businessDays,
15804
16824
  weekendPredicate: isCustomWeekend,
16825
+ horizontalWindow,
15805
16826
  onDependencyClick: handleDependencyLineClick,
15806
16827
  criticalTaskIds: criticalPathMode ? criticalTaskIds : void 0
15807
16828
  }
@@ -15908,6 +16929,7 @@ function TaskGanttChartInner(props, ref) {
15908
16929
  setDragGuideLines((current) => current === null ? current : null);
15909
16930
  setDraggedTaskOverride((current) => current === null ? current : null);
15910
16931
  previewPositionStore.clear();
16932
+ activityPreviewStore.clear();
15911
16933
  setCascadeOverrides((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
15912
16934
  setPreviewTasksById((current) => current.size === 0 ? current : /* @__PURE__ */ new Map());
15913
16935
  }
@@ -15918,6 +16940,16 @@ function TaskGanttChartInner(props, ref) {
15918
16940
  disableConstraints: disableConstraints ?? false,
15919
16941
  deferCascadePreview: normalizedTasks.length >= 1e3,
15920
16942
  previewPositionStore,
16943
+ activityPreviewStore,
16944
+ activityBlockStore,
16945
+ activityDragOwner,
16946
+ activityActivationStore,
16947
+ activityClickToDrag,
16948
+ activityActivationMode,
16949
+ showActivityLinks,
16950
+ showActivityLag,
16951
+ activityTooltip,
16952
+ horizontalWindow,
15921
16953
  onCascadeProgress: handleCascadeProgress,
15922
16954
  onCascade: handleCascade,
15923
16955
  onScheduleIntent,
@@ -16024,6 +17056,9 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
16024
17056
  return target.includes(search);
16025
17057
  };
16026
17058
  export {
17059
+ ACTIVITY_LANES_PADDING,
17060
+ ACTIVITY_LANE_BAR_HEIGHT,
17061
+ ACTIVITY_LANE_STEP,
16027
17062
  Button,
16028
17063
  Calendar,
16029
17064
  DAY_MS,
@@ -16045,6 +17080,11 @@ export {
16045
17080
  TaskRow_default as TaskRow,
16046
17081
  TimeScaleHeader_default as TimeScaleHeader,
16047
17082
  TodayIndicator_default as TodayIndicator,
17083
+ activityBounds,
17084
+ activityChainMinStartDelta,
17085
+ activityLanesExtraHeight,
17086
+ activitySpanMs,
17087
+ activityStartConstraint,
16048
17088
  addBusinessDays2 as addBusinessDays,
16049
17089
  alignToWorkingDay,
16050
17090
  and,
@@ -16067,6 +17107,7 @@ export {
16067
17107
  cascadeByLinks,
16068
17108
  clampDateRangeForIncomingFS,
16069
17109
  clampTaskRangeForIncomingFS,
17110
+ computeActivityLanes,
16070
17111
  computeCriticalPath,
16071
17112
  computeLagFromDates,
16072
17113
  computeParentDates,
@@ -16121,10 +17162,12 @@ export {
16121
17162
  normalizeUTCDate,
16122
17163
  not,
16123
17164
  or,
17165
+ packIntervals,
16124
17166
  parseDateOnly,
16125
17167
  parseUTCDate,
16126
17168
  pixelsToDate,
16127
17169
  progressInRange,
17170
+ pushActivityChain,
16128
17171
  recalculateIncomingLags,
16129
17172
  recalculateProjectSchedule,
16130
17173
  recalculateTaskFromDependencies,
@@ -16134,6 +17177,8 @@ export {
16134
17177
  resolveDateRangeFromPixels,
16135
17178
  resolveTaskHorizontalGeometry,
16136
17179
  scaleTaskSubtreeDuration,
17180
+ shiftActivityChain,
17181
+ shiftActivityDate,
16137
17182
  shiftBusinessDayOffset,
16138
17183
  subtractBusinessDays2 as subtractBusinessDays,
16139
17184
  universalCascade,