gantt-lib 0.64.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -530,6 +530,8 @@ __export(index_exports, {
530
530
  calculateDependencyPath: () => calculateDependencyPath,
531
531
  calculateGridLines: () => calculateGridLines,
532
532
  calculateGridWidth: () => calculateGridWidth,
533
+ calculateMilestoneConnectionBounds: () => calculateMilestoneConnectionBounds,
534
+ calculateMilestoneGeometry: () => calculateMilestoneGeometry,
533
535
  calculateMonthGridLines: () => calculateMonthGridLines,
534
536
  calculateOrthogonalPath: () => calculateOrthogonalPath,
535
537
  calculateSuccessorDate: () => calculateSuccessorDate,
@@ -581,6 +583,7 @@ __export(index_exports, {
581
583
  nameContains: () => nameContains,
582
584
  normalizeDependencyLag: () => normalizeDependencyLag,
583
585
  normalizeHierarchyTasks: () => normalizeHierarchyTasks,
586
+ normalizePredecessorDates: () => normalizePredecessorDates,
584
587
  normalizeTaskDates: () => normalizeTaskDates,
585
588
  normalizeUTCDate: () => normalizeUTCDate,
586
589
  not: () => not,
@@ -596,6 +599,7 @@ __export(index_exports, {
596
599
  removeDependenciesBetweenTasks: () => removeDependenciesBetweenTasks,
597
600
  resizeTaskWithCascade: () => resizeTaskWithCascade,
598
601
  resolveDateRangeFromPixels: () => resolveDateRangeFromPixels,
602
+ resolveTaskHorizontalGeometry: () => resolveTaskHorizontalGeometry,
599
603
  shiftBusinessDayOffset: () => shiftBusinessDayOffset,
600
604
  subtractBusinessDays: () => subtractBusinessDays2,
601
605
  universalCascade: () => universalCascade,
@@ -614,6 +618,12 @@ init_dateMath();
614
618
 
615
619
  // src/core/scheduling/dependencies.ts
616
620
  init_dateMath();
621
+ function normalizePredecessorDates(predecessor, parseDateFn) {
622
+ const predStart = parseDateFn(predecessor.startDate);
623
+ const isMilestone = predecessor.type === "milestone";
624
+ const predEnd = isMilestone ? new Date(predStart.getTime() - DAY_MS) : parseDateFn(predecessor.endDate);
625
+ return { predStart, predEnd };
626
+ }
617
627
  function getDependencyLag(dep) {
618
628
  return Number.isFinite(dep.lag) ? dep.lag : 0;
619
629
  }
@@ -889,8 +899,7 @@ function clampTaskRangeForIncomingFS(task, proposedStart, proposedEnd, allTasks,
889
899
  if (!predecessor) {
890
900
  continue;
891
901
  }
892
- const predecessorStart = parseDateOnly(predecessor.startDate);
893
- const predecessorEnd = parseDateOnly(predecessor.endDate);
902
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
894
903
  const predecessorDuration = getTaskDuration(
895
904
  predecessorStart,
896
905
  predecessorEnd,
@@ -926,8 +935,10 @@ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, busin
926
935
  if (!predecessor) {
927
936
  return { ...dep, lag: getDependencyLag(dep) };
928
937
  }
929
- const predecessorStart = new Date(predecessor.startDate);
930
- const predecessorEnd = new Date(predecessor.endDate);
938
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(
939
+ predecessor,
940
+ (d) => new Date(d instanceof Date ? d.getTime() : `${String(d).split("T")[0]}T00:00:00.000Z`)
941
+ );
931
942
  const nextLag = computeLagFromDates(
932
943
  dep.type,
933
944
  predecessorStart,
@@ -942,6 +953,12 @@ function recalculateIncomingLags(task, newStartDate, newEndDate, allTasks, busin
942
953
  }
943
954
 
944
955
  // src/core/scheduling/cascade.ts
956
+ function parseCascadeDateInput(date) {
957
+ if (date instanceof Date) {
958
+ return normalizeUTCDate(date);
959
+ }
960
+ return normalizeUTCDate(/* @__PURE__ */ new Date(`${date.split("T")[0]}T00:00:00.000Z`));
961
+ }
945
962
  function getSuccessorChain(draggedTaskId, allTasks, linkTypes = ["FS"]) {
946
963
  const successorMap = /* @__PURE__ */ new Map();
947
964
  for (const task of allTasks) {
@@ -1020,7 +1037,21 @@ function cascadeByLinks(movedTaskId, newStart, newEnd, allTasks, skipChildCascad
1020
1037
  const origStart = new Date(orig.startDate);
1021
1038
  const origEnd = new Date(orig.endDate);
1022
1039
  const duration = getTaskDuration(origStart, origEnd);
1023
- const constraintDate = calculateSuccessorDate(predStart, predEnd, dep.type, getDependencyLag(dep));
1040
+ const currentTask = taskById.get(currentId);
1041
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1042
+ {
1043
+ startDate: predStart,
1044
+ endDate: predEnd,
1045
+ type: currentTask.type
1046
+ },
1047
+ parseCascadeDateInput
1048
+ );
1049
+ const constraintDate = calculateSuccessorDate(
1050
+ normalizedPredStart,
1051
+ normalizedPredEnd,
1052
+ dep.type,
1053
+ getDependencyLag(dep)
1054
+ );
1024
1055
  let newSuccStart;
1025
1056
  let newSuccEnd;
1026
1057
  if (dep.type === "FS" || dep.type === "SS") {
@@ -1175,9 +1206,17 @@ function universalCascade(movedTask, newStart, newEnd, allTasks, businessDays =
1175
1206
  if (!dep) continue;
1176
1207
  const origStart = new Date(task.startDate);
1177
1208
  const origEnd = new Date(task.endDate);
1209
+ const { predStart: normalizedPredStart, predEnd: normalizedPredEnd } = normalizePredecessorDates(
1210
+ {
1211
+ startDate: currStart,
1212
+ endDate: currEnd,
1213
+ type: currentOriginal.type
1214
+ },
1215
+ parseCascadeDateInput
1216
+ );
1178
1217
  const constraintDate = calculateSuccessorDate(
1179
- currStart,
1180
- currEnd,
1218
+ normalizedPredStart,
1219
+ normalizedPredEnd,
1181
1220
  dep.type,
1182
1221
  getDependencyLag(dep),
1183
1222
  businessDays,
@@ -1394,8 +1433,7 @@ function recalculateTaskFromDependencies(taskId, snapshot, options) {
1394
1433
  for (const dep of task.dependencies) {
1395
1434
  const predecessor = snapshot.find((t) => t.id === dep.taskId);
1396
1435
  if (!predecessor) continue;
1397
- const predStart = parseDateOnly(predecessor.startDate);
1398
- const predEnd = parseDateOnly(predecessor.endDate);
1436
+ const { predStart, predEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1399
1437
  const constraintDate = calculateSuccessorDate(
1400
1438
  predStart,
1401
1439
  predEnd,
@@ -1511,8 +1549,7 @@ function recalculateProjectSchedule(snapshot, options) {
1511
1549
  if (!predecessor) {
1512
1550
  continue;
1513
1551
  }
1514
- const predecessorStart = parseDateOnly(predecessor.startDate);
1515
- const predecessorEnd = parseDateOnly(predecessor.endDate);
1552
+ const { predStart: predecessorStart, predEnd: predecessorEnd } = normalizePredecessorDates(predecessor, parseDateOnly);
1516
1553
  const constraintDate = calculateSuccessorDate(
1517
1554
  predecessorStart,
1518
1555
  predecessorEnd,
@@ -1931,6 +1968,45 @@ var calculateTaskBar = (taskStartDate, taskEndDate, monthStart, dayWidth) => {
1931
1968
  const width = Math.round((duration + 1) * dayWidth);
1932
1969
  return { left, width };
1933
1970
  };
1971
+ var calculateMilestoneGeometry = (taskDate, monthStart, dayWidth, size = 14) => {
1972
+ const { left, width } = calculateTaskBar(taskDate, taskDate, monthStart, dayWidth);
1973
+ const centerX = Math.round(left + width / 2);
1974
+ const halfSize = Math.round(size / 2);
1975
+ return {
1976
+ centerX,
1977
+ left: centerX - halfSize,
1978
+ right: centerX + halfSize,
1979
+ size
1980
+ };
1981
+ };
1982
+ var calculateMilestoneConnectionBounds = (dayLeft, dayWidth, size = 14) => {
1983
+ const halfDiagonal = Math.round(size / Math.SQRT2);
1984
+ const visualNudge = 2;
1985
+ return {
1986
+ left: dayLeft + halfDiagonal + visualNudge,
1987
+ right: dayLeft + dayWidth - halfDiagonal - visualNudge
1988
+ };
1989
+ };
1990
+ var resolveTaskHorizontalGeometry = (task, monthStart, dayWidth, override) => {
1991
+ const startDate = new Date(task.startDate);
1992
+ const endDate = new Date(task.endDate);
1993
+ if (task.type === "milestone") {
1994
+ const size = 14;
1995
+ if (override) {
1996
+ return calculateMilestoneConnectionBounds(override.left, dayWidth, size);
1997
+ }
1998
+ const bar2 = calculateTaskBar(startDate, startDate, monthStart, dayWidth);
1999
+ return calculateMilestoneConnectionBounds(bar2.left, dayWidth, size);
2000
+ }
2001
+ if (override) {
2002
+ return {
2003
+ left: override.left,
2004
+ right: override.left + override.width
2005
+ };
2006
+ }
2007
+ const bar = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
2008
+ return { left: bar.left, right: bar.left + bar.width };
2009
+ };
1934
2010
  var pixelsToDate = (pixels, monthStart, dayWidth) => {
1935
2011
  const days = Math.round(pixels / dayWidth);
1936
2012
  return new Date(Date.UTC(
@@ -2034,6 +2110,9 @@ var calculateDependencyPath = (from, to, arrivesFromRight) => {
2034
2110
  if (fy === ty) {
2035
2111
  return `M ${fx} ${fy} H ${tx}`;
2036
2112
  }
2113
+ if (fx === tx) {
2114
+ return `M ${fx} ${fy} V ${ty}`;
2115
+ }
2037
2116
  const C = 2;
2038
2117
  const goingDown = ty > fy;
2039
2118
  const dirY = goingDown ? 1 : -1;
@@ -2141,6 +2220,37 @@ var isTaskExpired = (task, referenceDate = /* @__PURE__ */ new Date()) => {
2141
2220
  return actualProgress < expectedProgress;
2142
2221
  };
2143
2222
 
2223
+ // src/utils/taskType.ts
2224
+ init_dateUtils();
2225
+ var TASK_TYPE_DEFAULT = "task";
2226
+ function getTaskType(task) {
2227
+ return task.type ?? TASK_TYPE_DEFAULT;
2228
+ }
2229
+ function isMilestoneTask(task) {
2230
+ return getTaskType(task) === "milestone";
2231
+ }
2232
+ function normalizeMilestoneStartDate(startDateInput) {
2233
+ const parsedStartDate = parseUTCDate(startDateInput);
2234
+ if (startDateInput instanceof Date) {
2235
+ return new Date(Date.UTC(
2236
+ parsedStartDate.getUTCFullYear(),
2237
+ parsedStartDate.getUTCMonth(),
2238
+ parsedStartDate.getUTCDate()
2239
+ ));
2240
+ }
2241
+ return parsedStartDate.toISOString().split("T")[0];
2242
+ }
2243
+ function normalizeTaskDatesForType(task) {
2244
+ if (!isMilestoneTask(task)) {
2245
+ return task;
2246
+ }
2247
+ const startDate = normalizeMilestoneStartDate(task.startDate);
2248
+ return {
2249
+ ...task,
2250
+ endDate: startDate
2251
+ };
2252
+ }
2253
+
2144
2254
  // src/hooks/useTaskDrag.ts
2145
2255
  var import_react2 = require("react");
2146
2256
 
@@ -2159,6 +2269,17 @@ function resolveDateRangeFromPixels(mode, left, width, monthStart, dayWidth, tas
2159
2269
  monthStart.getUTCMonth(),
2160
2270
  monthStart.getUTCDate() + rawEndOffset
2161
2271
  ));
2272
+ const isMilestone = task.type === "milestone";
2273
+ if (isMilestone) {
2274
+ const anchorDate = mode === "resize-right" ? rawEndDate : rawStartDate;
2275
+ if (businessDays && weekendPredicate) {
2276
+ const originalAnchor = mode === "resize-right" ? new Date(task.endDate) : new Date(task.startDate);
2277
+ const snapDirection2 = anchorDate.getTime() >= originalAnchor.getTime() ? 1 : -1;
2278
+ const alignedDate = alignToWorkingDay(anchorDate, snapDirection2, weekendPredicate);
2279
+ return { start: alignedDate, end: alignedDate };
2280
+ }
2281
+ return { start: anchorDate, end: anchorDate };
2282
+ }
2162
2283
  if (!(businessDays && weekendPredicate)) {
2163
2284
  return { start: rawStartDate, end: rawEndDate };
2164
2285
  }
@@ -2249,8 +2370,10 @@ function handleGlobalMouseMove(e) {
2249
2370
  const activeDrag = globalActiveDrag;
2250
2371
  const { startX, initialLeft, initialWidth, mode, dayWidth, onProgress, allTasks } = activeDrag;
2251
2372
  const deltaX = e.clientX - startX;
2373
+ const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
2374
+ const effectiveWidth = draggedTask && isMilestoneTask(draggedTask) ? dayWidth : initialWidth;
2252
2375
  let newLeft = initialLeft;
2253
- let newWidth = initialWidth;
2376
+ let newWidth = effectiveWidth;
2254
2377
  switch (mode) {
2255
2378
  case "move":
2256
2379
  newLeft = snapToGrid(initialLeft + deltaX, dayWidth);
@@ -2266,7 +2389,6 @@ function handleGlobalMouseMove(e) {
2266
2389
  newWidth = Math.max(dayWidth, snappedWidth);
2267
2390
  break;
2268
2391
  }
2269
- const draggedTask = allTasks.find((t) => t.id === activeDrag.taskId);
2270
2392
  if (activeDrag.businessDays && activeDrag.weekendPredicate && draggedTask) {
2271
2393
  const previewRange = clampDateRangeForIncomingFS(
2272
2394
  draggedTask,
@@ -2308,6 +2430,9 @@ function handleGlobalMouseMove(e) {
2308
2430
  newLeft = Math.round(alignedStartDay * dayWidth);
2309
2431
  newWidth = Math.round((alignedEndDay - alignedStartDay + 1) * dayWidth);
2310
2432
  }
2433
+ if (draggedTask && isMilestoneTask(draggedTask)) {
2434
+ newWidth = dayWidth;
2435
+ }
2311
2436
  if (!activeDrag.disableConstraints && activeDrag.onCascadeProgress) {
2312
2437
  const { dayWidth: dayWidth2, monthStart: mStart, taskId: dragId } = activeDrag;
2313
2438
  const originalDraggedTask = draggedTask ?? allTasks.find((t) => t.id === dragId);
@@ -2344,7 +2469,8 @@ function handleGlobalMouseMove(e) {
2344
2469
  };
2345
2470
  })();
2346
2471
  const previewStartDate = previewRange.start;
2347
- const previewEndDate = previewRange.end;
2472
+ const isMilestone = originalDraggedTask ? isMilestoneTask(originalDraggedTask) : false;
2473
+ const previewEndDate = isMilestone ? previewRange.start : previewRange.end;
2348
2474
  const movedTaskData = originalDraggedTask ?? { id: dragId, name: "", startDate: "", endDate: "" };
2349
2475
  const cascadeResult = universalCascade(
2350
2476
  { ...movedTaskData, startDate: previewStartDate.toISOString(), endDate: previewEndDate.toISOString() },
@@ -2434,6 +2560,9 @@ var useTaskDrag = (options) => {
2434
2560
  businessDays = true,
2435
2561
  weekendPredicate
2436
2562
  } = options;
2563
+ const rawHookTask = allTasks.find((t) => t.id === taskId);
2564
+ const hookTask = rawHookTask ? normalizeTaskDatesForType(rawHookTask) : void 0;
2565
+ const hookTaskIsMilestone = hookTask ? isMilestoneTask(hookTask) : false;
2437
2566
  const isOwnerRef = (0, import_react2.useRef)(false);
2438
2567
  const effectiveLocked = locked || disableTaskDrag;
2439
2568
  const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
@@ -2455,11 +2584,11 @@ var useTaskDrag = (options) => {
2455
2584
  return Math.round((ms1 - ms2) / (1e3 * 60 * 60 * 24));
2456
2585
  };
2457
2586
  const startOffset = getUTCDayDifference2(initialStartDate, monthStart);
2458
- const duration = getUTCDayDifference2(initialEndDate, initialStartDate);
2587
+ const duration = hookTaskIsMilestone ? 0 : getUTCDayDifference2(initialEndDate, initialStartDate);
2459
2588
  const left = Math.round(startOffset * dayWidth);
2460
2589
  const width = Math.round((duration + 1) * dayWidth);
2461
2590
  return { left, width };
2462
- }, [initialStartDate, initialEndDate, monthStart, dayWidth]);
2591
+ }, [initialStartDate, initialEndDate, monthStart, dayWidth, hookTaskIsMilestone]);
2463
2592
  (0, import_react2.useEffect)(() => {
2464
2593
  if (isOwnerRef.current && globalActiveDrag) return;
2465
2594
  const { left, width } = getInitialPosition();
@@ -2494,7 +2623,8 @@ var useTaskDrag = (options) => {
2494
2623
  const handleComplete = (0, import_react2.useCallback)((finalLeft, finalWidth, finalMode) => {
2495
2624
  const wasOwner = isOwnerRef.current;
2496
2625
  isOwnerRef.current = false;
2497
- const currentTask = allTasks.find((t) => t.id === taskId);
2626
+ const currentTaskRaw = allTasks.find((t) => t.id === taskId);
2627
+ const currentTask = currentTaskRaw ? normalizeTaskDatesForType(currentTaskRaw) : void 0;
2498
2628
  const finalRange = currentTask ? clampDateRangeForIncomingFS(
2499
2629
  currentTask,
2500
2630
  resolveDateRangeFromPixels(
@@ -2528,7 +2658,7 @@ var useTaskDrag = (options) => {
2528
2658
  };
2529
2659
  })();
2530
2660
  const newStartDate = finalRange.start;
2531
- const newEndDate = finalRange.end;
2661
+ const newEndDate = currentTask && isMilestoneTask(currentTask) ? finalRange.start : finalRange.end;
2532
2662
  setIsDragging(false);
2533
2663
  setDragMode(null);
2534
2664
  if (onDragStateChange) {
@@ -2540,6 +2670,23 @@ var useTaskDrag = (options) => {
2540
2670
  });
2541
2671
  }
2542
2672
  if (wasOwner) {
2673
+ const startUnchanged = newStartDate.getTime() === Date.UTC(
2674
+ initialStartDate.getUTCFullYear(),
2675
+ initialStartDate.getUTCMonth(),
2676
+ initialStartDate.getUTCDate()
2677
+ );
2678
+ const baselineEndDate = hookTaskIsMilestone ? initialStartDate : initialEndDate;
2679
+ const endUnchanged = newEndDate.getTime() === Date.UTC(
2680
+ baselineEndDate.getUTCFullYear(),
2681
+ baselineEndDate.getUTCMonth(),
2682
+ baselineEndDate.getUTCDate()
2683
+ );
2684
+ if (startUnchanged && endUnchanged) {
2685
+ const { left, width } = getInitialPosition();
2686
+ setCurrentLeft(left);
2687
+ setCurrentWidth(width);
2688
+ return;
2689
+ }
2543
2690
  if (!disableConstraints && onCascade && allTasks.length > 0) {
2544
2691
  const draggedTaskData = currentTask;
2545
2692
  const movedTask = {
@@ -2577,7 +2724,8 @@ var useTaskDrag = (options) => {
2577
2724
  businessDays,
2578
2725
  weekendPredicate,
2579
2726
  initialStartDate,
2580
- initialEndDate
2727
+ initialEndDate,
2728
+ hookTaskIsMilestone
2581
2729
  ]);
2582
2730
  const handleCancel = (0, import_react2.useCallback)(() => {
2583
2731
  isOwnerRef.current = false;
@@ -2620,6 +2768,9 @@ var useTaskDrag = (options) => {
2620
2768
  if (currentTask2 && isTaskParent(taskId, allTasks)) {
2621
2769
  mode = "move";
2622
2770
  }
2771
+ if (currentTask2 && isMilestoneTask(currentTask2)) {
2772
+ mode = "move";
2773
+ }
2623
2774
  }
2624
2775
  if (!mode) {
2625
2776
  return;
@@ -2705,14 +2856,16 @@ var useTaskDrag = (options) => {
2705
2856
  // src/components/TaskRow/TaskRow.tsx
2706
2857
  var import_jsx_runtime2 = require("react/jsx-runtime");
2707
2858
  var arePropsEqual = (prevProps, nextProps) => {
2708
- 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.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.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.task.locked === nextProps.task.locked && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag;
2859
+ 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.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.allTasks === nextProps.allTasks && prevProps.disableConstraints === nextProps.disableConstraints && prevProps.task.locked === nextProps.task.locked && prevProps.task.divider === nextProps.task.divider && prevProps.highlightExpiredTasks === nextProps.highlightExpiredTasks && prevProps.isFilterMatch === nextProps.isFilterMatch && prevProps.businessDays === nextProps.businessDays && prevProps.customDays === nextProps.customDays && prevProps.isWeekend === nextProps.isWeekend && prevProps.disableTaskDrag === nextProps.disableTaskDrag;
2709
2860
  };
2710
2861
  var TaskRow = import_react3.default.memo(
2711
2862
  ({ task, monthStart, dayWidth, rowHeight, onTasksChange, onDragStateChange, rowIndex, allTasks, enableAutoSchedule, disableConstraints, overridePosition, onCascadeProgress, onCascade, divider, highlightExpiredTasks, isFilterMatch = false, businessDays, customDays, isWeekend: isWeekend3, disableTaskDrag = false }) => {
2712
2863
  const defaultParentBarColor = "#782FC4";
2713
2864
  const { divider: taskDivider } = task;
2714
- const taskStartDate = (0, import_react3.useMemo)(() => parseUTCDate(task.startDate), [task.startDate]);
2715
- const taskEndDate = (0, import_react3.useMemo)(() => parseUTCDate(task.endDate), [task.endDate]);
2865
+ const normalizedTask = (0, import_react3.useMemo)(() => normalizeTaskDatesForType(task), [task]);
2866
+ const milestone = (0, import_react3.useMemo)(() => isMilestoneTask(normalizedTask), [normalizedTask]);
2867
+ const taskStartDate = (0, import_react3.useMemo)(() => parseUTCDate(normalizedTask.startDate), [normalizedTask.startDate]);
2868
+ const taskEndDate = (0, import_react3.useMemo)(() => parseUTCDate(normalizedTask.endDate), [normalizedTask.endDate]);
2716
2869
  const isParent = (0, import_react3.useMemo)(() => {
2717
2870
  return allTasks ? isTaskParent(task.id, allTasks) : false;
2718
2871
  }, [allTasks, task.id]);
@@ -2721,12 +2874,16 @@ var TaskRow = import_react3.default.memo(
2721
2874
  }, [allTasks, task.id]);
2722
2875
  const isExpired = (0, import_react3.useMemo)(() => {
2723
2876
  if (!highlightExpiredTasks) return false;
2724
- return isTaskExpired(task);
2725
- }, [task.startDate, task.endDate, task.progress, highlightExpiredTasks]);
2877
+ return isTaskExpired(normalizedTask);
2878
+ }, [normalizedTask.startDate, normalizedTask.endDate, normalizedTask.progress, highlightExpiredTasks]);
2726
2879
  const { left, width } = (0, import_react3.useMemo)(
2727
2880
  () => calculateTaskBar(taskStartDate, taskEndDate, monthStart, dayWidth),
2728
2881
  [taskStartDate, taskEndDate, monthStart, dayWidth]
2729
2882
  );
2883
+ const milestoneGeometry = (0, import_react3.useMemo)(
2884
+ () => calculateMilestoneGeometry(taskStartDate, monthStart, dayWidth),
2885
+ [taskStartDate, monthStart, dayWidth]
2886
+ );
2730
2887
  const barColor = isExpired ? "var(--gantt-expired-color)" : task.color || "var(--gantt-task-bar-default-color)";
2731
2888
  const progressWidth = (0, import_react3.useMemo)(() => {
2732
2889
  if (task.progress === void 0 || task.progress <= 0) return 0;
@@ -2765,7 +2922,7 @@ var TaskRow = import_react3.default.memo(
2765
2922
  }, [defaultParentBarColor, isExpired, isParent, progressWidth, barColor, progressColor, task.color]);
2766
2923
  const handleDragEnd = (result) => {
2767
2924
  const updatedTask = {
2768
- ...task,
2925
+ ...normalizedTask,
2769
2926
  startDate: result.startDate.toISOString(),
2770
2927
  endDate: result.endDate.toISOString(),
2771
2928
  ...result.updatedDependencies !== void 0 && { dependencies: result.updatedDependencies }
@@ -2804,8 +2961,20 @@ var TaskRow = import_react3.default.memo(
2804
2961
  });
2805
2962
  const displayLeft = overridePosition?.left ?? (isDragging ? currentLeft : left);
2806
2963
  const displayWidth = overridePosition?.width ?? (isDragging ? currentWidth : width);
2964
+ const displayMilestoneGeometry = (0, import_react3.useMemo)(() => {
2965
+ const centerX = Math.round(displayLeft + dayWidth / 2);
2966
+ const halfSize = Math.round(milestoneGeometry.size / 2);
2967
+ return {
2968
+ centerX,
2969
+ left: centerX - halfSize,
2970
+ right: centerX + halfSize,
2971
+ size: milestoneGeometry.size
2972
+ };
2973
+ }, [displayLeft, dayWidth, milestoneGeometry.size]);
2974
+ const visualLeft = milestone ? displayMilestoneGeometry.left : displayLeft;
2975
+ const visualWidth = milestone ? displayMilestoneGeometry.size : displayWidth;
2807
2976
  const currentStartDate = isDragging ? pixelsToDate(displayLeft, monthStart, dayWidth) : taskStartDate;
2808
- const currentEndDate = isDragging ? pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
2977
+ const currentEndDate = isDragging ? milestone ? pixelsToDate(displayLeft, monthStart, dayWidth) : pixelsToDate(displayLeft + displayWidth - dayWidth, monthStart, dayWidth) : taskEndDate;
2809
2978
  const dateRangeLabel = formatDateRangeLabel(currentStartDate, currentEndDate);
2810
2979
  const durationDays = businessDays ? getBusinessDaysCount(currentStartDate, currentEndDate, weekendPredicate) : Math.round(
2811
2980
  (currentEndDate.getTime() - currentStartDate.getTime()) / (1e3 * 60 * 60 * 24)
@@ -2820,9 +2989,9 @@ var TaskRow = import_react3.default.memo(
2820
2989
  return `${count} \u0437\u0430\u0434\u0430\u0447`;
2821
2990
  };
2822
2991
  const estimatedTextWidth = isParent ? 120 : durationDays >= 10 ? 76 : 62;
2823
- const showProgressInside = progressWidth > 0 && displayWidth > estimatedTextWidth;
2992
+ const showProgressInside = !milestone && progressWidth > 0 && displayWidth > estimatedTextWidth;
2824
2993
  const MIN_DURATION_WIDTH = isParent ? 80 : 50;
2825
- const showDurationInside = durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
2994
+ const showDurationInside = !milestone && durationDays >= 2 && displayWidth > MIN_DURATION_WIDTH;
2826
2995
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
2827
2996
  "div",
2828
2997
  {
@@ -2836,18 +3005,24 @@ var TaskRow = import_react3.default.memo(
2836
3005
  "div",
2837
3006
  {
2838
3007
  "data-taskbar": true,
2839
- className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${isParent ? "gantt-tr-parentBar" : ""}`,
3008
+ className: `gantt-tr-taskBar ${isDragging ? "gantt-tr-dragging" : ""} ${task.locked ? "gantt-tr-locked" : ""} ${isParent ? "gantt-tr-parentBar" : ""} ${milestone ? "gantt-tr-milestone" : ""}`,
2840
3009
  style: {
2841
- left: `${displayLeft}px`,
2842
- width: `${displayWidth}px`,
3010
+ left: `${visualLeft}px`,
2843
3011
  ...barStyle,
2844
- height: isParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)",
3012
+ ...milestone ? {
3013
+ height: `${displayMilestoneGeometry.size}px`,
3014
+ width: `${displayMilestoneGeometry.size}px`,
3015
+ padding: 0
3016
+ } : {
3017
+ width: `${visualWidth}px`,
3018
+ height: isParent ? "var(--gantt-parent-bar-height, 14px)" : "var(--gantt-task-bar-height)"
3019
+ },
2845
3020
  cursor: dragHandleProps.style.cursor,
2846
3021
  userSelect: dragHandleProps.style.userSelect
2847
3022
  },
2848
3023
  onMouseDown: dragHandleProps.onMouseDown,
2849
3024
  children: [
2850
- progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3025
+ !milestone && progressWidth > 0 && progressWidth < 100 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2851
3026
  "div",
2852
3027
  {
2853
3028
  className: "gantt-tr-progressBar",
@@ -2860,13 +3035,13 @@ var TaskRow = import_react3.default.memo(
2860
3035
  }
2861
3036
  }
2862
3037
  ),
2863
- !isParent && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
3038
+ !isParent && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleLeft" }),
2864
3039
  showDurationInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-taskDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
2865
3040
  progressWidth > 0 && showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-progressText", children: [
2866
3041
  progressWidth,
2867
3042
  "%"
2868
3043
  ] }),
2869
- !isParent && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
3044
+ !isParent && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gantt-tr-resizeHandle gantt-tr-resizeHandleRight" })
2870
3045
  ]
2871
3046
  }
2872
3047
  ),
@@ -2875,7 +3050,7 @@ var TaskRow = import_react3.default.memo(
2875
3050
  {
2876
3051
  className: `gantt-tr-leftLabels ${task.locked ? "gantt-tr-leftLabels-locked" : ""}`,
2877
3052
  style: {
2878
- left: `${displayLeft}px`
3053
+ left: `${visualLeft}px`
2879
3054
  },
2880
3055
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-dateLabel gantt-tr-dateLabelLeft", children: dateRangeLabel })
2881
3056
  }
@@ -2886,7 +3061,7 @@ var TaskRow = import_react3.default.memo(
2886
3061
  className: "gantt-tr-lockIcon",
2887
3062
  style: {
2888
3063
  position: "absolute",
2889
- left: `${displayLeft - 16}px`,
3064
+ left: `${visualLeft - 16}px`,
2890
3065
  top: "50%",
2891
3066
  transform: "translateY(-50%)",
2892
3067
  width: "12px",
@@ -2906,11 +3081,11 @@ var TaskRow = import_react3.default.memo(
2906
3081
  {
2907
3082
  className: "gantt-tr-rightLabels",
2908
3083
  style: {
2909
- left: `${displayLeft + Math.max(displayWidth, 20) - Math.min(6, Math.max(displayWidth, 20) / 2) + 8}px`,
3084
+ left: `${visualLeft + Math.max(visualWidth, 20) - Math.min(6, Math.max(visualWidth, 20) / 2) + 8}px`,
2910
3085
  color: isParent ? task.color || defaultParentBarColor : barColor
2911
3086
  },
2912
3087
  children: [
2913
- !showDurationInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
3088
+ !showDurationInside && !milestone && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gantt-tr-externalDuration", children: isParent ? getChildCountLabel(childCount) : `${durationDays} \u0434` }),
2914
3089
  progressWidth > 0 && !showProgressInside && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gantt-tr-externalProgress", children: [
2915
3090
  progressWidth,
2916
3091
  "%"
@@ -3159,16 +3334,12 @@ var DependencyLines = import_react6.default.memo(({
3159
3334
  const taskMap = new Map(tasksForPositions.map((t) => [t.id, t]));
3160
3335
  const visibleTaskMap = new Map(tasks.map((t) => [t.id, t]));
3161
3336
  tasks.forEach((task, index) => {
3162
- const startDate = new Date(task.startDate);
3163
- const endDate = new Date(task.endDate);
3164
- const computed = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3165
3337
  const override = dragOverrides?.get(task.id);
3166
- const resolvedLeft = override?.left ?? computed.left;
3167
- const resolvedWidth = override?.width ?? computed.width;
3338
+ const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
3168
3339
  indices.set(task.id, index);
3169
3340
  positions.set(task.id, {
3170
- left: resolvedLeft,
3171
- right: resolvedLeft + resolvedWidth,
3341
+ left: computed.left,
3342
+ right: computed.right,
3172
3343
  rowTop: index * rowHeight,
3173
3344
  isVirtual: false
3174
3345
  });
@@ -3182,15 +3353,11 @@ var DependencyLines = import_react6.default.memo(({
3182
3353
  if (!visibleAncestor) continue;
3183
3354
  const ancestorPosition = positions.get(visibleAncestor.id);
3184
3355
  if (!ancestorPosition) continue;
3185
- const startDate = new Date(task.startDate);
3186
- const endDate = new Date(task.endDate);
3187
- const computed = calculateTaskBar(startDate, endDate, monthStart, dayWidth);
3188
3356
  const override = dragOverrides?.get(task.id);
3189
- const resolvedLeft = override?.left ?? computed.left;
3190
- const resolvedWidth = override?.width ?? computed.width;
3357
+ const computed = resolveTaskHorizontalGeometry(task, monthStart, dayWidth, override);
3191
3358
  positions.set(task.id, {
3192
- left: resolvedLeft,
3193
- right: resolvedLeft + resolvedWidth,
3359
+ left: computed.left,
3360
+ right: computed.right,
3194
3361
  rowTop: ancestorPosition.rowTop,
3195
3362
  isVirtual: true
3196
3363
  });
@@ -3206,6 +3373,7 @@ var DependencyLines = import_react6.default.memo(({
3206
3373
  }, [tasks, allTasks]);
3207
3374
  const lines = (0, import_react6.useMemo)(() => {
3208
3375
  const tasksForEdges = allTasks ?? tasks;
3376
+ const taskMap = new Map(tasksForEdges.map((task) => [task.id, task]));
3209
3377
  const edges = getAllDependencyEdges(tasksForEdges);
3210
3378
  const lines2 = [];
3211
3379
  for (const edge of edges) {
@@ -3216,9 +3384,11 @@ var DependencyLines = import_react6.default.memo(({
3216
3384
  if (!predecessor || !successor) {
3217
3385
  continue;
3218
3386
  }
3387
+ const predecessorTask = taskMap.get(edge.predecessorId);
3388
+ const successorTask = taskMap.get(edge.successorId);
3219
3389
  if (allTasks && collapsedParentIds.size > 0) {
3220
- const taskMap = new Map(allTasks.map((t) => [t.id, t]));
3221
- if (areBothHiddenInSameParent(edge.predecessorId, edge.successorId, collapsedParentIds, taskMap)) {
3390
+ const taskMap2 = new Map(allTasks.map((t) => [t.id, t]));
3391
+ if (areBothHiddenInSameParent(edge.predecessorId, edge.successorId, collapsedParentIds, taskMap2)) {
3222
3392
  continue;
3223
3393
  }
3224
3394
  }
@@ -3238,11 +3408,18 @@ var DependencyLines = import_react6.default.memo(({
3238
3408
  fromY = predecessor.rowTop + rowHeight - 10;
3239
3409
  toY = successor.rowTop + 6;
3240
3410
  }
3241
- const fromX = edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
3411
+ let fromX = edge.type === "SS" || edge.type === "SF" ? predecessor.left : predecessor.right;
3242
3412
  const toX = edge.type === "FF" || edge.type === "SF" ? successor.right : successor.left;
3413
+ const stackedMilestonesSameDay = Boolean(
3414
+ predecessorTask && successorTask && isMilestoneTask(predecessorTask) && isMilestoneTask(successorTask) && edge.lag === 0 && new Date(predecessorTask.startDate).toISOString().split("T")[0] === new Date(successorTask.startDate).toISOString().split("T")[0] && predecessor.rowTop !== successor.rowTop && edge.type === "FS"
3415
+ );
3416
+ const finalToX = stackedMilestonesSameDay ? Math.round(((predecessor.left + predecessor.right) / 2 + (successor.left + successor.right) / 2) / 2) : toX;
3417
+ if (stackedMilestonesSameDay) {
3418
+ fromX = finalToX;
3419
+ }
3243
3420
  const arrivesFromRight = edge.type === "FF" || edge.type === "SF";
3244
3421
  const from = { x: fromX, y: fromY };
3245
- const to = { x: toX, y: toY };
3422
+ const to = { x: finalToX, y: toY };
3246
3423
  const path = calculateDependencyPath(from, to, arrivesFromRight);
3247
3424
  const hasCycle = cycleInfo.has(edge.predecessorId) || cycleInfo.has(edge.successorId);
3248
3425
  lines2.push({
@@ -3251,7 +3428,7 @@ var DependencyLines = import_react6.default.memo(({
3251
3428
  hasCycle,
3252
3429
  lag: edge.lag,
3253
3430
  fromX,
3254
- toX,
3431
+ toX: finalToX,
3255
3432
  fromY,
3256
3433
  reverseOrder,
3257
3434
  isVirtual
@@ -3264,6 +3441,7 @@ var DependencyLines = import_react6.default.memo(({
3264
3441
  "svg",
3265
3442
  {
3266
3443
  className: "gantt-dependencies-svg",
3444
+ "data-testid": "dependency-lines-svg",
3267
3445
  width: gridWidth,
3268
3446
  height: svgHeight,
3269
3447
  xmlns: "http://www.w3.org/2000/svg",
@@ -4238,7 +4416,7 @@ var DepChip = ({
4238
4416
  const predecessor = taskById.get(dep.taskId);
4239
4417
  if (!predecessor) return;
4240
4418
  const predStart = parseUTCDate(predecessor.startDate);
4241
- const predEnd = parseUTCDate(predecessor.endDate);
4419
+ const predEnd = predecessor.type === "milestone" ? predStart : parseUTCDate(predecessor.endDate);
4242
4420
  const origStart = parseUTCDate(task.startDate);
4243
4421
  const origEnd = parseUTCDate(task.endDate);
4244
4422
  const durationMs = origEnd.getTime() - origStart.getTime();
@@ -4501,10 +4679,12 @@ var TaskListRow = import_react10.default.memo(
4501
4679
  const editingName = editingColumnId === "name";
4502
4680
  const editingDuration = editingColumnId === "duration";
4503
4681
  const editingProgress = editingColumnId === "progress";
4682
+ const normalizedTask = (0, import_react10.useMemo)(() => normalizeTaskDatesForType(task), [task]);
4683
+ const isMilestone = (0, import_react10.useMemo)(() => isMilestoneTask(normalizedTask), [normalizedTask]);
4504
4684
  const [nameValue, setNameValue] = (0, import_react10.useState)("");
4505
4685
  const nameInputRef = (0, import_react10.useRef)(null);
4506
4686
  const [durationValue, setDurationValue] = (0, import_react10.useState)(
4507
- () => getInclusiveDurationDays(task.startDate, task.endDate)
4687
+ () => getInclusiveDurationDays(normalizedTask.startDate, normalizedTask.endDate)
4508
4688
  );
4509
4689
  const durationInputRef = (0, import_react10.useRef)(null);
4510
4690
  const dependencySearchInputRef = (0, import_react10.useRef)(null);
@@ -4705,14 +4885,14 @@ var TaskListRow = import_react10.default.memo(
4705
4885
  e.stopPropagation();
4706
4886
  durationConfirmedRef.current = false;
4707
4887
  setDurationValue(
4708
- getDuration(task.startDate, task.endDate)
4888
+ isMilestone ? 0 : getDuration(normalizedTask.startDate, normalizedTask.endDate)
4709
4889
  );
4710
4890
  setEditingColumnId("duration");
4711
4891
  },
4712
- [task.locked, task.startDate, task.endDate, getDuration]
4892
+ [task.locked, normalizedTask.startDate, normalizedTask.endDate, getDuration, isMilestone]
4713
4893
  );
4714
4894
  const applyDurationChange = (0, import_react10.useCallback)((nextDuration) => {
4715
- const normalizedDuration = Math.max(1, Math.round(nextDuration) || 1);
4895
+ const normalizedDuration = Math.max(0, Math.round(nextDuration) || 0);
4716
4896
  setDurationValue(normalizedDuration);
4717
4897
  }, []);
4718
4898
  const handleDurationSave = (0, import_react10.useCallback)(() => {
@@ -4720,19 +4900,26 @@ var TaskListRow = import_react10.default.memo(
4720
4900
  durationConfirmedRef.current = false;
4721
4901
  return;
4722
4902
  }
4723
- const normalizedDuration = Math.max(1, Math.round(durationValue) || 1);
4724
- onTasksChange?.([
4725
- {
4726
- ...task,
4727
- endDate: getEndDate(task.startDate, normalizedDuration)
4728
- }
4729
- ]);
4903
+ const rounded = Math.round(durationValue) || 0;
4904
+ if (isMilestone && rounded > 0) {
4905
+ onTasksChange?.([
4906
+ { ...task, type: "task", endDate: getEndDate(task.startDate, rounded) }
4907
+ ]);
4908
+ } else if (!isMilestone && rounded === 0) {
4909
+ onTasksChange?.([
4910
+ { ...task, type: "milestone", endDate: task.startDate }
4911
+ ]);
4912
+ } else if (!isMilestone && rounded > 0) {
4913
+ onTasksChange?.([
4914
+ { ...task, endDate: getEndDate(task.startDate, rounded) }
4915
+ ]);
4916
+ }
4730
4917
  setEditingColumnId(null);
4731
- }, [durationValue, task, onTasksChange, getEndDate]);
4918
+ }, [durationValue, task, onTasksChange, getEndDate, isMilestone]);
4732
4919
  const handleDurationCancel = (0, import_react10.useCallback)(() => {
4733
- setDurationValue(getDuration(task.startDate, task.endDate));
4920
+ setDurationValue(isMilestone ? 0 : getDuration(normalizedTask.startDate, normalizedTask.endDate));
4734
4921
  setEditingColumnId(null);
4735
- }, [task.startDate, task.endDate, getDuration]);
4922
+ }, [normalizedTask.startDate, normalizedTask.endDate, getDuration, isMilestone]);
4736
4923
  const handleDurationAdjust = (0, import_react10.useCallback)(
4737
4924
  (delta) => {
4738
4925
  applyDurationChange(durationValue + delta);
@@ -4744,25 +4931,26 @@ var TaskListRow = import_react10.default.memo(
4744
4931
  e.stopPropagation();
4745
4932
  if (e.key === "Enter") {
4746
4933
  durationConfirmedRef.current = true;
4747
- const normalizedDuration = Math.max(
4748
- 1,
4749
- Math.round(durationValue) || 1
4750
- );
4751
- onTasksChange?.([
4752
- {
4753
- ...task,
4754
- endDate: getEndDate(
4755
- task.startDate,
4756
- normalizedDuration
4757
- )
4758
- }
4759
- ]);
4934
+ const rounded = Math.round(durationValue) || 0;
4935
+ if (isMilestone && rounded > 0) {
4936
+ onTasksChange?.([
4937
+ { ...task, type: "task", endDate: getEndDate(task.startDate, rounded) }
4938
+ ]);
4939
+ } else if (!isMilestone && rounded === 0) {
4940
+ onTasksChange?.([
4941
+ { ...task, type: "milestone", endDate: task.startDate }
4942
+ ]);
4943
+ } else if (!isMilestone && rounded > 0) {
4944
+ onTasksChange?.([
4945
+ { ...task, endDate: getEndDate(task.startDate, rounded) }
4946
+ ]);
4947
+ }
4760
4948
  setEditingColumnId(null);
4761
4949
  } else if (e.key === "Escape") {
4762
4950
  handleDurationCancel();
4763
4951
  }
4764
4952
  },
4765
- [durationValue, task, onTasksChange, handleDurationCancel, getEndDate]
4953
+ [durationValue, task, onTasksChange, handleDurationCancel, getEndDate, isMilestone]
4766
4954
  );
4767
4955
  const handleProgressClick = (0, import_react10.useCallback)(
4768
4956
  (e) => {
@@ -4833,17 +5021,54 @@ var TaskListRow = import_react10.default.memo(
4833
5021
  }
4834
5022
  }, [editingProgress]);
4835
5023
  (0, import_react10.useEffect)(() => {
4836
- setDurationValue(getDuration(task.startDate, task.endDate));
4837
- }, [task.startDate, task.endDate, getDuration]);
5024
+ setDurationValue(getDuration(normalizedTask.startDate, normalizedTask.endDate));
5025
+ }, [normalizedTask.startDate, normalizedTask.endDate, getDuration]);
4838
5026
  (0, import_react10.useEffect)(() => {
4839
5027
  if (editingDuration && durationInputRef.current) {
4840
5028
  durationInputRef.current.focus();
4841
5029
  durationInputRef.current.select();
4842
5030
  }
4843
5031
  }, [editingDuration]);
5032
+ const emitMilestoneDateChange = (0, import_react10.useCallback)((nextDateISO) => {
5033
+ const alignedDate = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${nextDateISO}T00:00:00.000Z`), 1, weekendPredicate) : /* @__PURE__ */ new Date(`${nextDateISO}T00:00:00.000Z`);
5034
+ const clampedRange = clampTaskRangeForIncomingFS(
5035
+ task,
5036
+ alignedDate,
5037
+ alignedDate,
5038
+ allTasks,
5039
+ businessDays,
5040
+ weekendPredicate
5041
+ );
5042
+ const normalized = normalizeTaskDatesForType({
5043
+ ...task,
5044
+ startDate: clampedRange.start.toISOString().split("T")[0],
5045
+ endDate: clampedRange.end.toISOString().split("T")[0]
5046
+ });
5047
+ const startDate = parseUTCDate(normalized.startDate);
5048
+ const endDate = parseUTCDate(normalized.endDate);
5049
+ onTasksChange?.([
5050
+ {
5051
+ ...normalized,
5052
+ ...task.dependencies && {
5053
+ dependencies: recalculateIncomingLags(
5054
+ task,
5055
+ startDate,
5056
+ endDate,
5057
+ allTasks,
5058
+ businessDays,
5059
+ weekendPredicate
5060
+ )
5061
+ }
5062
+ }
5063
+ ]);
5064
+ }, [task, onTasksChange, allTasks, businessDays, weekendPredicate]);
4844
5065
  const handleStartDateChange = (0, import_react10.useCallback)(
4845
5066
  (newDateISO) => {
4846
5067
  if (!newDateISO) return;
5068
+ if (isMilestone) {
5069
+ emitMilestoneDateChange(newDateISO);
5070
+ return;
5071
+ }
4847
5072
  let nextEndISO;
4848
5073
  const normalizedInputStart = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`), 1, weekendPredicate) : /* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`);
4849
5074
  if (businessDays) {
@@ -4890,11 +5115,15 @@ var TaskListRow = import_react10.default.memo(
4890
5115
  }
4891
5116
  ]);
4892
5117
  },
4893
- [task, onTasksChange, businessDays, getDuration, getEndDate, allTasks, weekendPredicate]
5118
+ [task, onTasksChange, businessDays, getDuration, getEndDate, allTasks, weekendPredicate, isMilestone, emitMilestoneDateChange]
4894
5119
  );
4895
5120
  const handleEndDateChange = (0, import_react10.useCallback)(
4896
5121
  (newDateISO) => {
4897
5122
  if (!newDateISO) return;
5123
+ if (isMilestone) {
5124
+ emitMilestoneDateChange(newDateISO);
5125
+ return;
5126
+ }
4898
5127
  let nextStartISO;
4899
5128
  const normalizedInputEnd = businessDays ? alignToWorkingDay(/* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`), -1, weekendPredicate) : /* @__PURE__ */ new Date(`${newDateISO}T00:00:00.000Z`);
4900
5129
  if (businessDays) {
@@ -4941,7 +5170,7 @@ var TaskListRow = import_react10.default.memo(
4941
5170
  }
4942
5171
  ]);
4943
5172
  },
4944
- [task, onTasksChange, businessDays, getDuration, weekendPredicate, allTasks]
5173
+ [task, onTasksChange, businessDays, getDuration, weekendPredicate, allTasks, isMilestone, emitMilestoneDateChange]
4945
5174
  );
4946
5175
  const handleRowClickInternal = (0, import_react10.useCallback)(() => {
4947
5176
  onRowClick?.(task.id);
@@ -5212,8 +5441,8 @@ var TaskListRow = import_react10.default.memo(
5212
5441
  },
5213
5442
  [selectedChip?.successorId, selectedChip?.predecessorId, selectedChip?.linkType, onRemoveDependency, onChipSelect]
5214
5443
  );
5215
- const startDateISO = toISODate(task.startDate);
5216
- const endDateISO = editingDuration ? getEndDate(task.startDate, durationValue) : toISODate(task.endDate);
5444
+ const startDateISO = toISODate(normalizedTask.startDate);
5445
+ const endDateISO = editingDuration ? getEndDate(normalizedTask.startDate, durationValue) : toISODate(normalizedTask.endDate);
5217
5446
  const numberCell = /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
5218
5447
  "div",
5219
5448
  {
@@ -5619,10 +5848,10 @@ var TaskListRow = import_react10.default.memo(
5619
5848
  {
5620
5849
  ref: durationInputRef,
5621
5850
  type: "number",
5622
- min: 1,
5851
+ min: 0,
5623
5852
  step: 1,
5624
5853
  value: durationValue,
5625
- onChange: (e) => applyDurationChange(parseInt(e.target.value, 10) || 1),
5854
+ onChange: (e) => applyDurationChange(parseInt(e.target.value, 10) || 0),
5626
5855
  onBlur: handleDurationSave,
5627
5856
  onKeyDown: handleDurationKeyDown,
5628
5857
  className: "gantt-tl-number-input"
@@ -5683,14 +5912,11 @@ var TaskListRow = import_react10.default.memo(
5683
5912
  ]
5684
5913
  }
5685
5914
  ),
5686
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
5915
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
5687
5916
  "span",
5688
5917
  {
5689
5918
  style: editingDuration ? { visibility: "hidden", pointerEvents: "none" } : void 0,
5690
- children: [
5691
- getDuration(task.startDate, task.endDate),
5692
- "\u0434"
5693
- ]
5919
+ children: isMilestone ? "0" : `${getDuration(normalizedTask.startDate, normalizedTask.endDate)}\u0434`
5694
5920
  }
5695
5921
  )
5696
5922
  ]
@@ -7180,7 +7406,8 @@ function GanttChartInner(props, ref) {
7180
7406
  }
7181
7407
  return;
7182
7408
  }
7183
- const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, tasks, businessDays, isCustomWeekend);
7409
+ const sourceTasks = tasks.map((task) => task.id === updatedTask.id ? updatedTask : task);
7410
+ const cascadedTasks = disableConstraints ? [updatedTask] : universalCascade(updatedTask, newStart, newEnd, sourceTasks, businessDays, isCustomWeekend);
7184
7411
  onTasksChange?.(cascadedTasks);
7185
7412
  }, [tasks, onTasksChange, disableConstraints, editingTaskId, businessDays, isCustomWeekend]);
7186
7413
  const handleDelete = (0, import_react13.useCallback)((taskId) => {
@@ -7634,6 +7861,8 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7634
7861
  calculateDependencyPath,
7635
7862
  calculateGridLines,
7636
7863
  calculateGridWidth,
7864
+ calculateMilestoneConnectionBounds,
7865
+ calculateMilestoneGeometry,
7637
7866
  calculateMonthGridLines,
7638
7867
  calculateOrthogonalPath,
7639
7868
  calculateSuccessorDate,
@@ -7685,6 +7914,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7685
7914
  nameContains,
7686
7915
  normalizeDependencyLag,
7687
7916
  normalizeHierarchyTasks,
7917
+ normalizePredecessorDates,
7688
7918
  normalizeTaskDates,
7689
7919
  normalizeUTCDate,
7690
7920
  not,
@@ -7700,6 +7930,7 @@ var nameContains = (substring, caseSensitive = false) => (task) => {
7700
7930
  removeDependenciesBetweenTasks,
7701
7931
  resizeTaskWithCascade,
7702
7932
  resolveDateRangeFromPixels,
7933
+ resolveTaskHorizontalGeometry,
7703
7934
  shiftBusinessDayOffset,
7704
7935
  subtractBusinessDays,
7705
7936
  universalCascade,