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/README.md +80 -0
- package/dist/core/scheduling/index.d.mts +1 -1
- package/dist/core/scheduling/index.d.ts +1 -1
- package/dist/core/scheduling/index.js +40 -3
- package/dist/core/scheduling/index.js.map +1 -1
- package/dist/core/scheduling/index.mjs +40 -3
- package/dist/core/scheduling/index.mjs.map +1 -1
- package/dist/{index-D99kbU4p.d.mts → index-C3mZqSTf.d.mts} +67 -1
- package/dist/{index-D99kbU4p.d.ts → index-C3mZqSTf.d.ts} +67 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +258 -3
- package/dist/index.d.ts +258 -3
- package/dist/index.js +2389 -1331
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2376 -1331
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +176 -1
- package/package.json +1 -1
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/
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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 (
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
);
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
|
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/
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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:
|
|
756
|
-
end:
|
|
789
|
+
start: parseUTCDate(activity.startDate).getTime(),
|
|
790
|
+
end: parseUTCDate(activity.endDate).getTime() + DAY_MS2
|
|
757
791
|
};
|
|
758
792
|
}
|
|
759
|
-
function
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
|
778
|
-
if (
|
|
779
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
task.
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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
|
|
805
|
-
minAllowedStart,
|
|
806
|
-
getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
|
|
807
|
-
businessDays,
|
|
808
|
-
weekendPredicate
|
|
809
|
-
);
|
|
854
|
+
return visited;
|
|
810
855
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
if (!
|
|
816
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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/
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
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
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
);
|
|
976
|
-
const
|
|
977
|
-
const
|
|
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
|
-
|
|
982
|
-
for (const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
|
1001
|
-
const
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
1228
|
+
return result;
|
|
1146
1229
|
}
|
|
1147
1230
|
|
|
1148
|
-
// src/core/scheduling/
|
|
1149
|
-
function
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
|
1156
|
-
const
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
|
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
|
-
|
|
1169
|
-
|
|
1256
|
+
start: new Date(normalizedEnd.getTime() - (Math.max(1, duration) - 1) * DAY_MS7),
|
|
1257
|
+
end: normalizedEnd
|
|
1170
1258
|
};
|
|
1171
1259
|
}
|
|
1172
|
-
function
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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 (
|
|
1189
|
-
return
|
|
1302
|
+
if (!minAllowedStart || proposedStart.getTime() >= minAllowedStart.getTime()) {
|
|
1303
|
+
return { start: proposedStart, end: proposedEnd };
|
|
1190
1304
|
}
|
|
1191
|
-
return
|
|
1305
|
+
return buildTaskRangeFromStart(
|
|
1306
|
+
minAllowedStart,
|
|
1307
|
+
getTaskDuration(proposedStart, proposedEnd, businessDays, weekendPredicate),
|
|
1308
|
+
businessDays,
|
|
1309
|
+
weekendPredicate
|
|
1310
|
+
);
|
|
1192
1311
|
}
|
|
1193
|
-
function
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (
|
|
1198
|
-
|
|
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
|
-
|
|
1206
|
-
return descendants;
|
|
1342
|
+
return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
|
|
1207
1343
|
}
|
|
1208
|
-
function
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
return [task];
|
|
1344
|
+
function projectDayDelta(from, to, businessDays, weekendPredicate) {
|
|
1345
|
+
if (businessDays && weekendPredicate) {
|
|
1346
|
+
return getBusinessDayOffset(from, to, weekendPredicate);
|
|
1212
1347
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
|
1411
|
+
return chain;
|
|
1237
1412
|
}
|
|
1238
|
-
function
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1486
|
+
startDate: newSuccStart.toISOString().split("T")[0],
|
|
1487
|
+
endDate: newSuccEnd.toISOString().split("T")[0]
|
|
1488
|
+
}));
|
|
1489
|
+
queue.push(task.id);
|
|
1251
1490
|
}
|
|
1252
|
-
|
|
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
|
|
1260
|
-
const
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
|
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
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1329
|
-
|
|
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,
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1706
|
+
startDate: new Date(minTime),
|
|
1707
|
+
endDate: new Date(maxTime)
|
|
1345
1708
|
};
|
|
1346
1709
|
}
|
|
1347
|
-
function
|
|
1348
|
-
const
|
|
1349
|
-
if (
|
|
1350
|
-
return
|
|
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
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1396
|
-
return {
|
|
1397
|
-
changedTasks,
|
|
1398
|
-
changedIds: changedTasks.map((t) => t.id)
|
|
1399
|
-
};
|
|
1729
|
+
return Math.round(weightedSum / totalWeight * 10) / 10;
|
|
1400
1730
|
}
|
|
1401
|
-
function
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
5729
|
+
compactDetail && !milestone && /* @__PURE__ */ jsx2(
|
|
4747
5730
|
"span",
|
|
4748
5731
|
{
|
|
4749
|
-
className:
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4961
|
-
|
|
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
|
-
|
|
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
|
|
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) *
|
|
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
|
|
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()) /
|
|
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) *
|
|
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: [
|
|
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
|
|
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) /
|
|
12375
|
-
const elapsedDays = Math.min(totalDays, Math.max(0, Math.round((overlayDateMs - startMs) /
|
|
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
|
|
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) /
|
|
12736
|
-
const endIndex = Math.floor((getDateOnlyMs(end) - rangeStartMs) /
|
|
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
|
-
|
|
14654
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14991
|
-
const deltaStart =
|
|
14992
|
-
const deltaEnd =
|
|
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,
|