4
This commit is contained in:
2025-12-25 10:31:01 +08:00
parent b819397c2b
commit 8768d8cee9

View File

@ -1,5 +1,5 @@
import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk'; import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk';
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress } from '@douyinfe/semi-ui'; import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress, Switch } from '@douyinfe/semi-ui';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { addDays, format, differenceInCalendarDays } from 'date-fns'; import { addDays, format, differenceInCalendarDays } from 'date-fns';
import { zhCN } from 'date-fns/locale'; import { zhCN } from 'date-fns/locale';
@ -78,6 +78,8 @@ export default function App() {
const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({}); const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({});
// 基础缓冲期天数可配置用于计算动态缓冲期默认14天 // 基础缓冲期天数可配置用于计算动态缓冲期默认14天
const [baseBufferDays, setBaseBufferDays] = useState<number>(14); const [baseBufferDays, setBaseBufferDays] = useState<number>(14);
const [lockedExpectedDeliveryDateTs, setLockedExpectedDeliveryDateTs] = useState<number | null>(null);
const [isExpectedDeliveryDateLocked, setIsExpectedDeliveryDateLocked] = useState<boolean>(false);
// 快照回填来源foreign_id、款式、颜色、文本2 // 快照回填来源foreign_id、款式、颜色、文本2
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null); const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
const [currentStyleText, setCurrentStyleText] = useState<string>(''); const [currentStyleText, setCurrentStyleText] = useState<string>('');
@ -103,6 +105,7 @@ export default function App() {
const [batchProgressList, setBatchProgressList] = useState<{ index: number; foreignId: string; status: 'success' | 'failed'; message?: string }[]>([]); const [batchProgressList, setBatchProgressList] = useState<{ index: number; foreignId: string; status: 'success' | 'failed'; message?: string }[]>([]);
const [batchCurrentRowInfo, setBatchCurrentRowInfo] = useState<{ index: number; foreignId: string; style: string; color: string } | null>(null); const [batchCurrentRowInfo, setBatchCurrentRowInfo] = useState<{ index: number; foreignId: string; style: string; color: string } | null>(null);
const batchAbortRef = useRef<boolean>(false); const batchAbortRef = useRef<boolean>(false);
const lastBufferDeficitRef = useRef<number>(0);
// 删除未使用的 deliveryRecords 状态 // 删除未使用的 deliveryRecords 状态
const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>(''); const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>('');
// 从货期记录读取到的record_ids用于保存回写 // 从货期记录读取到的record_ids用于保存回写
@ -145,12 +148,17 @@ export default function App() {
setIsRestoringSnapshot(false); setIsRestoringSnapshot(false);
setRestoredRecordIds([]); setRestoredRecordIds([]);
setRestoredRecordIdsText(''); setRestoredRecordIdsText('');
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
// 重置初始快照捕获状态 // 重置初始快照捕获状态
try { try {
hasCapturedInitialSnapshotRef.current = false; hasCapturedInitialSnapshotRef.current = false;
initialSnapshotRef.current = null; initialSnapshotRef.current = null;
} catch {} } catch {}
try {
lastBufferDeficitRef.current = 0;
} catch {}
// 移除:批量模式当前记录信息 // 移除:批量模式当前记录信息
@ -275,6 +283,7 @@ export default function App() {
// 切换功能时重置全局变量但保留新的mode // 切换功能时重置全局变量但保留新的mode
resetGlobalState({ resetMode: false }); resetGlobalState({ resetMode: false });
setMode(m); setMode(m);
setIsExpectedDeliveryDateLocked(m === 'adjust');
setModeSelectionVisible(false); setModeSelectionVisible(false);
}; };
@ -358,8 +367,26 @@ export default function App() {
const recordIdsField: any = await deliveryTable.getField(DELIVERY_RECORD_IDS_FIELD_ID); const recordIdsField: any = await deliveryTable.getField(DELIVERY_RECORD_IDS_FIELD_ID);
const snapshotField: any = await deliveryTable.getField(DELIVERY_SNAPSHOT_JSON_FIELD_ID); const snapshotField: any = await deliveryTable.getField(DELIVERY_SNAPSHOT_JSON_FIELD_ID);
const startTimeField: any = await deliveryTable.getField(DELIVERY_START_TIME_FIELD_ID); const startTimeField: any = await deliveryTable.getField(DELIVERY_START_TIME_FIELD_ID);
const expectedDeliveryDateField: any = await deliveryTable.getField(DELIVERY_EXPECTED_DATE_FIELD_ID);
const nodeDetailsVal = await nodeDetailsField.getValue(deliveryRecordId); const nodeDetailsVal = await nodeDetailsField.getValue(deliveryRecordId);
try {
const expectedDeliveryVal = await expectedDeliveryDateField.getValue(deliveryRecordId);
let ts: number | null = null;
if (typeof expectedDeliveryVal === 'number') {
ts = expectedDeliveryVal;
} else if (Array.isArray(expectedDeliveryVal) && expectedDeliveryVal.length > 0 && typeof expectedDeliveryVal[0] === 'number') {
ts = expectedDeliveryVal[0];
} else {
const extracted = extractText(expectedDeliveryVal);
if (extracted && extracted.trim() !== '') {
const parsed = new Date(extracted);
if (!isNaN(parsed.getTime())) ts = parsed.getTime();
}
}
setLockedExpectedDeliveryDateTs(ts);
setIsExpectedDeliveryDateLocked(true);
} catch {}
// 读取record_ids文本字段并保留原始文本用于原样写回 // 读取record_ids文本字段并保留原始文本用于原样写回
try { try {
const recordIdsTextVal = await recordIdsField.getValue(deliveryRecordId); const recordIdsTextVal = await recordIdsField.getValue(deliveryRecordId);
@ -439,6 +466,22 @@ export default function App() {
} }
if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments);
{
const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs;
if (snapLockedTs === null) {
setLockedExpectedDeliveryDateTs(null);
} else {
const hasLockedTs = snapLockedTs !== undefined && snapLockedTs !== null;
if (hasLockedTs) setLockedExpectedDeliveryDateTs(snapLockedTs);
}
const hasLockedTs = snapLockedTs !== undefined && snapLockedTs !== null;
const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked;
if (typeof snapLockedFlag === 'boolean') {
setIsExpectedDeliveryDateLocked(snapLockedFlag);
} else if (hasLockedTs) {
setIsExpectedDeliveryDateLocked(true);
}
}
if (snapshot.expectedDateTimestamp) { if (snapshot.expectedDateTimestamp) {
setExpectedDate(new Date(snapshot.expectedDateTimestamp)); setExpectedDate(new Date(snapshot.expectedDateTimestamp));
} else if (snapshot.expectedDateString) { } else if (snapshot.expectedDateString) {
@ -580,6 +623,22 @@ export default function App() {
} }
if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments);
{
const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs;
if (snapLockedTs === null) {
setLockedExpectedDeliveryDateTs(null);
} else {
const hasLockedTs = snapLockedTs !== undefined && snapLockedTs !== null;
if (hasLockedTs) setLockedExpectedDeliveryDateTs(snapLockedTs);
}
const hasLockedTs = snapLockedTs !== undefined && snapLockedTs !== null;
const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked;
if (typeof snapLockedFlag === 'boolean') {
setIsExpectedDeliveryDateLocked(snapLockedFlag);
} else if (hasLockedTs) {
setIsExpectedDeliveryDateLocked(true);
}
}
if (snapshot.expectedDateTimestamp) { if (snapshot.expectedDateTimestamp) {
setExpectedDate(new Date(snapshot.expectedDateTimestamp)); setExpectedDate(new Date(snapshot.expectedDateTimestamp));
} else if (snapshot.expectedDateString) { } else if (snapshot.expectedDateString) {
@ -913,6 +972,9 @@ export default function App() {
colorText: nodeSnapshot.colorText, colorText: nodeSnapshot.colorText,
text2: nodeSnapshot.text2, text2: nodeSnapshot.text2,
mode: nodeSnapshot.mode, mode: nodeSnapshot.mode,
timelineDirection: nodeSnapshot.timelineDirection,
lockedExpectedDeliveryDateTs: nodeSnapshot.lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked: nodeSnapshot.isExpectedDeliveryDateLocked,
selectedLabels: nodeSnapshot.selectedLabels, selectedLabels: nodeSnapshot.selectedLabels,
expectedDateTimestamp: nodeSnapshot.expectedDateTimestamp, expectedDateTimestamp: nodeSnapshot.expectedDateTimestamp,
expectedDateString: nodeSnapshot.expectedDateString, expectedDateString: nodeSnapshot.expectedDateString,
@ -1024,6 +1086,22 @@ export default function App() {
setTimelineResults(nodeSnapshots); setTimelineResults(nodeSnapshots);
setTimelineVisible(true); setTimelineVisible(true);
{
const snapLockedTs = (globalSnapshotData as any)?.lockedExpectedDeliveryDateTs;
if (snapLockedTs === null) {
setLockedExpectedDeliveryDateTs(null);
} else {
const hasLockedTs = snapLockedTs !== undefined && snapLockedTs !== null;
if (hasLockedTs) setLockedExpectedDeliveryDateTs(snapLockedTs);
}
const snapLockedFlag = (globalSnapshotData as any)?.isExpectedDeliveryDateLocked;
if (typeof snapLockedFlag === 'boolean') {
setIsExpectedDeliveryDateLocked(snapLockedFlag);
} else if (snapLockedTs !== undefined && snapLockedTs !== null) {
setIsExpectedDeliveryDateLocked(true);
}
}
// 恢复智能缓冲期状态(如果存在) // 恢复智能缓冲期状态(如果存在)
if (globalSnapshotData.bufferManagement) { if (globalSnapshotData.bufferManagement) {
console.log('恢复智能缓冲期状态:', globalSnapshotData.bufferManagement); console.log('恢复智能缓冲期状态:', globalSnapshotData.bufferManagement);
@ -2713,50 +2791,6 @@ export default function App() {
} }
} }
if (!isBackward && mode === 'generate' && showUI && currentExpectedDate && !isNaN(currentExpectedDate.getTime()) && results.length > 0) {
const findLastCompletionDate = (): Date | null => {
for (let i = results.length - 1; i >= 0; i--) {
const endStr = results[i]?.estimatedEnd;
if (typeof endStr !== 'string' || endStr.trim() === '' || endStr.includes('未找到') || endStr.includes('时效值为0')) continue;
const d = parseDate(endStr);
if (d && !isNaN(d.getTime())) return d;
}
return null;
};
const lastCompletionDate = findLastCompletionDate();
if (lastCompletionDate) {
const deliveryDate = new Date(lastCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + Math.max(0, Math.ceil(baseBufferDays)));
const increaseDays = differenceInCalendarDays(currentExpectedDate, deliveryDate);
if (increaseDays > 0) {
const shouldApply = await new Promise<boolean>((resolve) => {
Modal.confirm({
title: '是否增加缓冲期使其一致?',
content: `请业务确认OMS 获取小ERP的预计交付日期。\n当前预计交付日期${formatDate(deliveryDate, 'CHINESE_DATE')}\n客户期望日期${formatDate(currentExpectedDate, 'CHINESE_DATE')}\n是否增加缓冲期 +${increaseDays} 天使其一致?`,
okText: '确认增加',
cancelText: '不调整',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
});
if (shouldApply) {
setBaseBufferDays((prev) => Math.max(0, Math.ceil(prev) + increaseDays));
setHasAppliedSuggestedBuffer(true);
setLastSuggestedApplied(increaseDays);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: `已自动增加缓冲期 +${increaseDays} 天,使预计交付日期对齐客户期望日期`,
});
}
}
}
}
}
setTimelineResults(results); setTimelineResults(results);
if (showUI && !delayShowTimelineModal) { if (showUI && !delayShowTimelineModal) {
setTimelineVisible(true); setTimelineVisible(true);
@ -2784,87 +2818,16 @@ export default function App() {
// 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式 // 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式
const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => { const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => {
// 计算当前状态 const nextAdjustments = handleTimelineAdjustment(nodeIndex, adjustment);
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); if (!nextAdjustments) return;
const baseBuferDays = baseBufferDays; const deficit = computeBufferDeficitDaysUsingEndDelta(nextAdjustments);
const prevDeficit = lastBufferDeficitRef.current;
// 智能缓冲期计算逻辑(缓冲期扣减=最后完成时间变动的自然天数) lastBufferDeficitRef.current = deficit;
let dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); if (adjustment > 0 && deficit > 0 && Math.ceil(deficit) > Math.ceil(prevDeficit)) {
Modal.warning({
// 只有在缓冲期为0时才检查最终限制最后节点预计完成时间是否已达到客户期望日期 title: '缓冲期不足',
let hasReachedFinalLimit = false; content: `当前缓冲期无法覆盖本次超期,缺口 ${Math.ceil(deficit)}`,
if (dynamicBufferDays === 0 && expectedDate && timelineResults.length > 0) { });
// 获取有效的最后流程完成日期
let effectiveLastProcess = null;
let lastCompletionDate = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 计算最后流程完成日期(包含调整)
const adjustedCompletionDate = new Date(lastCompletionDate as Date);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
// 检查是否已达到客户期望日期
const timeDiffToExpected = expectedDate.getTime() - adjustedCompletionDate.getTime();
const daysToExpected = Math.ceil(timeDiffToExpected / (1000 * 60 * 60 * 24));
hasReachedFinalLimit = daysToExpected <= 0;
}
// 如果已达到最终限制且是正向调整,则禁止操作
if (hasReachedFinalLimit && adjustment > 0) {
return;
}
// 调整逻辑:
// 1. 优先使用缓冲期
// 2. 缓冲期为0时通过调整节点时效值实现连锁传递
if (adjustment > 0) {
// 正数调整:需要扣减
if (dynamicBufferDays > 0) {
// 1. 缓冲期 > 0优先扣减缓冲期通过调整节点时间
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
} else {
// 2. 缓冲期 = 0通过调整节点时效值实现连锁传递
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
} else if (adjustment < 0) {
// 负数调整:需要恢复
if (completionDateAdjustment > 0) {
// 1. 优先恢复最后流程完成日期的调整
const restoreAmount = Math.min(-adjustment, completionDateAdjustment);
setCompletionDateAdjustment(completionDateAdjustment - restoreAmount);
const remainingRestore = -adjustment - restoreAmount;
if (remainingRestore > 0) {
// 2. 如果还有剩余恢复量,恢复缓冲期(通过调整节点时间)
const updatedResults = handleTimelineAdjustment(nodeIndex, -remainingRestore);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
} else if (totalAdjustments > 0) {
// 直接恢复缓冲期
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
} }
}; };
@ -2892,32 +2855,11 @@ export default function App() {
newAdjustments[nodeIndex] = newAdjustment; newAdjustments[nodeIndex] = newAdjustment;
// 查找周转周期节点的索引
const turnoverNodeIndex = timelineResults.findIndex(result => result.nodeName === '周转周期');
// 如果存在周转周期节点,进行反向调整
if (turnoverNodeIndex !== -1 && turnoverNodeIndex !== nodeIndex) {
const turnoverCurrentAdjustment = newAdjustments[turnoverNodeIndex] || 0;
const turnoverBaseValue = (typeof timelineResults[turnoverNodeIndex]?.timelineValue === 'number')
? timelineResults[turnoverNodeIndex]!.timelineValue
: (typeof timelineResults[turnoverNodeIndex]?.adjustedTimelineValue === 'number')
? timelineResults[turnoverNodeIndex]!.adjustedTimelineValue
: 0;
// 计算周转周期的反向调整值
const turnoverNewAdjustment = turnoverCurrentAdjustment - adjustment;
// 确保周转周期调整后的值不小于0
if (turnoverBaseValue + turnoverNewAdjustment >= 0) {
newAdjustments[turnoverNodeIndex] = turnoverNewAdjustment;
}
}
setTimelineAdjustments(newAdjustments); setTimelineAdjustments(newAdjustments);
// 使用智能重算逻辑,只重算被调整的节点及其后续节点 // 使用智能重算逻辑,只重算被调整的节点及其后续节点
recalculateTimeline(newAdjustments); recalculateTimeline(newAdjustments);
return timelineResults; return newAdjustments;
}; };
// 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径) // 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径)
@ -3183,14 +3125,12 @@ export default function App() {
return fallback && !isNaN(fallback.getTime()) ? fallback : null; return fallback && !isNaN(fallback.getTime()) ? fallback : null;
}; };
// 依据“最后流程节点的预计完成日期差(自然日)”计算剩余缓冲期 const computeLastNodeEndDeltaDays = (adjustments: { [key: number]: number }): number => {
const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => {
try { try {
if (timelineDirection === 'backward') return 0; if (timelineDirection === 'backward') return 0;
const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整) const baseline = getRecalculatedTimeline({});
const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整) const current = getRecalculatedTimeline(adjustments);
// 严格取最后流程节点的预计完成日期;若不可用则回退到最后一个有效完成日期
const pickLastNodeEnd = (results: any[]): Date | null => { const pickLastNodeEnd = (results: any[]): Date | null => {
if (!Array.isArray(results) || results.length === 0) return null; if (!Array.isArray(results) || results.length === 0) return null;
const endStr = results[results.length - 1]?.estimatedEnd; const endStr = results[results.length - 1]?.estimatedEnd;
@ -3203,19 +3143,54 @@ export default function App() {
const baselineLast = pickLastNodeEnd(baseline); const baselineLast = pickLastNodeEnd(baseline);
const currentLast = pickLastNodeEnd(current); const currentLast = pickLastNodeEnd(current);
if (!baselineLast || !currentLast) { if (!baselineLast || !currentLast) return 0;
return Math.max(0, Math.min(baseBufferDays, baseBufferDays));
}
// 仅使用“预计完成日期”差,不叠加 completionDateAdjustment
const dayMs = 1000 * 60 * 60 * 24; const dayMs = 1000 * 60 * 60 * 24;
const deltaDays = Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs); return Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs);
// 缓冲期剩余 = clamp(基础缓冲期 - 差值, 0, 基础缓冲期)
const remaining = baseBufferDays - deltaDays;
return Math.max(0, Math.min(baseBufferDays, remaining));
} catch { } catch {
// 兜底:如出现异常,保持基础缓冲期 return 0;
return Math.max(0, Math.min(baseBufferDays, baseBufferDays)); }
};
const computeBufferDeficitDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => {
const deltaDays = computeLastNodeEndDeltaDays(adjustments);
const base = Math.max(0, Math.ceil(baseBufferDays));
return Math.max(0, deltaDays - base);
};
const computeExpectedDeliveryDateTsFromResults = (
results: any[],
adjustments: { [key: number]: number }
): number | null => {
if (timelineDirection === 'backward') return null;
const lastCompletionDate = getLastValidCompletionDateFromResults(results);
if (!lastCompletionDate) return null;
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(adjustments);
const deliveryDate = new Date(lastCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
const ts = deliveryDate.getTime();
return Number.isFinite(ts) ? ts : null;
};
const handleExpectedDeliveryDateLockChange = (checked: boolean) => {
const nextLocked = !!checked;
setIsExpectedDeliveryDateLocked(nextLocked);
if (!nextLocked) return;
if (lockedExpectedDeliveryDateTs !== null && lockedExpectedDeliveryDateTs !== undefined) return;
const computed = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments);
if (computed !== null) setLockedExpectedDeliveryDateTs(computed);
};
const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => {
try {
if (timelineDirection === 'backward') return 0;
const deltaDays = computeLastNodeEndDeltaDays(adjustments);
const base = Math.max(0, Math.ceil(baseBufferDays));
const remaining = base - deltaDays;
return Math.max(0, Math.min(base, remaining));
} catch {
const base = Math.max(0, Math.ceil(baseBufferDays));
return base;
} }
}; };
@ -3453,6 +3428,8 @@ export default function App() {
timelineResults, timelineResults,
timelineAdjustments, timelineAdjustments,
baseBufferDays, baseBufferDays,
lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked,
actualCompletionDates, actualCompletionDates,
completionDateAdjustment, completionDateAdjustment,
hasAppliedSuggestedBuffer, hasAppliedSuggestedBuffer,
@ -3471,6 +3448,8 @@ export default function App() {
setCompletionDateAdjustment(0); // 重置最后流程完成日期调整 setCompletionDateAdjustment(0); // 重置最后流程完成日期调整
setActualCompletionDates({}); // 重置实际完成日期 setActualCompletionDates({}); // 重置实际完成日期
setBaseBufferDays(14); // 重置固定缓冲期为默认值 setBaseBufferDays(14); // 重置固定缓冲期为默认值
setIsExpectedDeliveryDateLocked(false);
try { lastBufferDeficitRef.current = 0; } catch {}
setHasAppliedSuggestedBuffer(false); // 重置建议缓冲期应用标志 setHasAppliedSuggestedBuffer(false); // 重置建议缓冲期应用标志
setLastSuggestedApplied(null); // 清空上次建议值 setLastSuggestedApplied(null); // 清空上次建议值
recalculateTimeline({}, true); // 强制重算所有节点 recalculateTimeline({}, true); // 强制重算所有节点
@ -3497,6 +3476,8 @@ export default function App() {
setSelectedLabels(s.selectedLabels || {}); setSelectedLabels(s.selectedLabels || {});
setTimelineAdjustments(s.timelineAdjustments || {}); setTimelineAdjustments(s.timelineAdjustments || {});
setBaseBufferDays(s.baseBufferDays ?? 14); setBaseBufferDays(s.baseBufferDays ?? 14);
setLockedExpectedDeliveryDateTs(s.lockedExpectedDeliveryDateTs ?? null);
setIsExpectedDeliveryDateLocked(!!s.isExpectedDeliveryDateLocked);
setActualCompletionDates(s.actualCompletionDates || {}); setActualCompletionDates(s.actualCompletionDates || {});
setCompletionDateAdjustment(s.completionDateAdjustment || 0); setCompletionDateAdjustment(s.completionDateAdjustment || 0);
setHasAppliedSuggestedBuffer(!!s.hasAppliedSuggestedBuffer && s.hasAppliedSuggestedBuffer); setHasAppliedSuggestedBuffer(!!s.hasAppliedSuggestedBuffer && s.hasAppliedSuggestedBuffer);
@ -3684,36 +3665,9 @@ export default function App() {
const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean); const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean);
// 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期)
let expectedDeliveryDate = null; let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments);
if (timelineResults.length > 0) { if (mode === 'adjust' && isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null) {
// 从后往前查找第一个有效的流程完成日期(与交期余量计算逻辑一致) expectedDeliveryDate = lockedExpectedDeliveryDateTs;
let effectiveLastProcess = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
if (process.estimatedEnd &&
!process.estimatedEnd.includes('未找到') &&
!process.estimatedEnd.includes('时效值为0') &&
process.estimatedEnd !== 'Invalid Date') {
effectiveLastProcess = process;
break;
}
}
if (effectiveLastProcess) {
try {
const lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
// 计算动态缓冲期:按“最后完成时间自然日差”扣减基础缓冲期
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments);
// 加上动态缓冲期
const deliveryDate = new Date(lastCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
expectedDeliveryDate = deliveryDate.getTime();
} catch (error) {
console.error('转换预计交付日期失败:', error);
}
}
} }
// 获取客户期望日期批量模式优先使用传入的expectedDate // 获取客户期望日期批量模式优先使用传入的expectedDate
@ -3797,6 +3751,8 @@ export default function App() {
text2, text2,
mode, mode,
timelineDirection, timelineDirection,
lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked,
selectedLabels: currentSelectedLabels, selectedLabels: currentSelectedLabels,
expectedDateTimestamp, expectedDateTimestamp,
expectedDateString, expectedDateString,
@ -5269,7 +5225,11 @@ export default function App() {
{mode !== null && ( {mode !== null && (
<div style={{ margin: '18px 0 14px' }}> <div style={{ margin: '18px 0 14px' }}>
<Space spacing={16} align='center'> <Space spacing={16} align='center'>
<Select value={mode} onChange={(v) => setMode(v as any)} <Select value={mode} onChange={(v) => {
const next = v as any;
setMode(next);
setIsExpectedDeliveryDateLocked(next === 'adjust');
}}
optionList={[ optionList={[
{ value: 'generate', label: '生成流程日期' }, { value: 'generate', label: '生成流程日期' },
{ value: 'adjust', label: '调整流程日期' } { value: 'adjust', label: '调整流程日期' }
@ -5584,6 +5544,16 @@ export default function App() {
> >
<span style={{ fontSize: 14, lineHeight: 1 }}></span> <span style={{ fontSize: 14, lineHeight: 1 }}></span>
</Button> </Button>
{mode === 'adjust' && (
<>
<Text type="tertiary" style={{ marginLeft: 8 }}></Text>
<Switch
checked={isExpectedDeliveryDateLocked}
onChange={handleExpectedDeliveryDateLockChange}
style={{ marginLeft: 4 }}
/>
</>
)}
</div> </div>
</div> </div>
} }
@ -5593,14 +5563,18 @@ export default function App() {
setTimelineAdjustments({}); // 关闭时重置调整 setTimelineAdjustments({}); // 关闭时重置调整
setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减 setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减
setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整 setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整
setHasAppliedSuggestedBuffer(false); // 关闭时允许下次重新应用建议缓冲期
setLastSuggestedApplied(null); // 关闭时清空上次建议值
setStyleColorEditable(false); // 关闭弹窗后恢复为锁定状态 setStyleColorEditable(false); // 关闭弹窗后恢复为锁定状态
if (mode !== 'adjust') {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
}} }}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space align="center"> <Space align="center">
<Text>()</Text> <Text>
{mode === 'adjust' && isExpectedDeliveryDateLocked ? '缓冲期(天)(不影响已锁交付)' : '缓冲期(天)'}
</Text>
<InputNumber <InputNumber
min={0} min={0}
step={1} step={1}
@ -5611,124 +5585,6 @@ export default function App() {
}} }}
style={{ width: 90 }} style={{ width: 90 }}
/> />
{/* 建议缓冲期增量:根据实际完成偏差与交期余量自动反算 */}
{(() => {
try {
// 仅当用户本次会话发生改动时显示建议
const initialSnap = initialSnapshotRef.current;
const hasAdjustmentChanges = Object.values(timelineAdjustments || {}).some(v => (Number(v) || 0) !== 0) || (Number(completionDateAdjustment) || 0) !== 0;
const toMillisMap = (obj: any) => {
const entries = Object.entries(obj || {});
return Object.fromEntries(entries.map(([k, v]) => [k, v ? new Date(v as any).getTime() : null]));
};
const currentActualMap = toMillisMap(actualCompletionDates);
const initialActualMap = toMillisMap(initialSnap?.actualCompletionDates || {});
const hasActualChanges = JSON.stringify(currentActualMap) !== JSON.stringify(initialActualMap);
const hasUserSessionChanges = hasAdjustmentChanges || hasActualChanges;
if (!hasUserSessionChanges) {
return null;
}
// 计算动态缓冲期(按“最后完成时间自然日差”扣减基础缓冲期)
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments);
// 获取有效的最后流程完成日期
let effectiveLastProcess: any = null;
let lastCompletionDate: Date | null = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess && timelineResults.length > 0) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 基线计划(不考虑实际完成、也不考虑调整):使用原始时效值重算
const baseline = getRecalculatedTimeline({});
let baselineLast: Date | null = null;
for (let i = baseline.length - 1; i >= 0; i--) {
const p = baseline[i];
const d = new Date(p.estimatedEnd);
if (!isNaN(d.getTime()) && p.estimatedEnd !== '时效值为0') { baselineLast = d; break; }
}
if (!baselineLast && baseline.length > 0) {
const d = new Date(baseline[baseline.length - 1].estimatedEnd);
if (!isNaN(d.getTime())) baselineLast = d;
}
// 当前计划考虑实际完成使用现有timelineResults
let currentLast: Date | null = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const p = timelineResults[i];
const d = new Date(p.estimatedEnd);
if (!isNaN(d.getTime()) && p.estimatedEnd !== '时效值为0') { currentLast = d; break; }
}
if (!currentLast && timelineResults.length > 0) {
const d = new Date(timelineResults[timelineResults.length - 1].estimatedEnd);
if (!isNaN(d.getTime())) currentLast = d;
}
// 建议口径1自然日偏差当前计划 vs 原始计划)扣除默认缓冲期
let suggestedInt = 0;
let slipDays = 0;
if (baselineLast && currentLast) {
const dayMs = 1000 * 60 * 60 * 24;
slipDays = Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs);
const deficitCalendar = Math.max(0, slipDays - baseBuferDays);
suggestedInt = Math.ceil(deficitCalendar);
}
// 建议口径2工作日总调整量仅统计 external 节点的天数调整)扣除默认缓冲期
const totalWorkingAdjustments = Object.entries(timelineAdjustments).reduce((sum, [k, adj]) => {
const i = parseInt(k, 10);
const method = timelineResults[i]?.calculationMethod || 'external';
const val = Number(adj) || 0;
return method === 'internal' ? sum : sum + val;
}, 0);
const deficitWorking = Math.max(0, totalWorkingAdjustments - baseBuferDays);
const suggestedByWorking = Math.ceil(deficitWorking);
// 取两种口径的较大值,避免出现工作日调整很大但自然日差较小的误差
suggestedInt = Math.max(suggestedInt, suggestedByWorking);
const displayInt = Math.max(0, suggestedInt);
if (displayInt > 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text style={{ color: '#fa8c16' }}>+{displayInt}</Text>
<Button
size="small"
onClick={() => {
if (!hasAppliedSuggestedBuffer) {
// 应用当前建议值
setBaseBufferDays(Math.max(0, Math.ceil(baseBufferDays) + displayInt));
setHasAppliedSuggestedBuffer(true);
setLastSuggestedApplied(displayInt);
} else {
// 撤销上次应用的建议值(使用记录的 lastSuggestedApplied 兜底当前显示值)
const rollback = (lastSuggestedApplied ?? displayInt);
setBaseBufferDays(prev => Math.max(0, Math.ceil(prev) - rollback));
setHasAppliedSuggestedBuffer(false);
setLastSuggestedApplied(null);
}
}}
>
{hasAppliedSuggestedBuffer ? '撤销建议' : '应用建议'}
</Button>
</div>
);
}
return null;
} catch {
return null;
}
})()}
<Button onClick={resetToInitialState}> <Button onClick={resetToInitialState}>
</Button> </Button>
@ -5807,105 +5663,8 @@ export default function App() {
const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0; const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0;
// 计算动态缓冲期(按“最后完成时间变动天数”扣减缓冲期) // 计算动态缓冲期(按“最后完成时间变动天数”扣减缓冲期)
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments);
// 新的复合限制逻辑:
// 1. 如果缓冲期 > 0允许操作
// 2. 如果缓冲期 = 0进一步判断交期余量
// 3. 如果缓冲期 = 0 且交期余量 <= 0禁止操作
let hasNegativeBuffer = false;
// 计算交期余量(仅用于显示,不用于限制)
let deliveryMargin = 0;
if (timelineResults.length > 0) {
// 获取有效的最后流程完成日期(与交期余量计算逻辑保持一致)
let effectiveLastProcess = null;
let lastCompletionDate: Date | null = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
if (lastCompletionDate && !isNaN(lastCompletionDate.getTime())) {
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const deliveryDate = new Date(adjustedCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
if (expectedDate) {
const timeDiff = expectedDate.getTime() - deliveryDate.getTime();
const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
} else {
const today = new Date();
today.setHours(0, 0, 0, 0);
const timeDiff = deliveryDate.getTime() - today.getTime();
const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
}
}
}
// 执行复合限制判断 - 只有在缓冲期为0时才检查最终限制
let canIncrease = false;
if (dynamicBufferDays > 0) {
// 1. 缓冲期 > 0允许操作
canIncrease = true;
} else {
// 2. 缓冲期 = 0检查最终限制最后节点预计完成时间是否已达到客户期望日期
if (expectedDate && timelineResults.length > 0) {
// 获取有效的最后流程完成日期
let effectiveLastProcess = null;
let lastCompletionDate: Date | null = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
if (lastCompletionDate && !isNaN(lastCompletionDate.getTime())) {
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const timeDiffToExpected = expectedDate.getTime() - adjustedCompletionDate.getTime();
const daysToExpected = Math.ceil(timeDiffToExpected / (1000 * 60 * 60 * 24));
canIncrease = daysToExpected > 0;
} else {
canIncrease = true;
}
} else {
// 无客户期望日期时,理论上可以无限调整
canIncrease = true;
}
}
hasNegativeBuffer = !canIncrease;
return ( return (
<Card key={index} style={{ marginBottom: '8px', padding: '12px', position: 'relative' }}> <Card key={index} style={{ marginBottom: '8px', padding: '12px', position: 'relative' }}>
<div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 1, display: 'flex', alignItems: 'center', gap: '16px' }}> <div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 1, display: 'flex', alignItems: 'center', gap: '16px' }}>
@ -6070,6 +5829,15 @@ export default function App() {
if (!updated) { if (!updated) {
return; return;
} }
const deficit = computeBufferDeficitDaysUsingEndDelta(updated);
const prevDeficit = lastBufferDeficitRef.current;
lastBufferDeficitRef.current = deficit;
if (deficit > 0 && Math.ceil(deficit) > Math.ceil(prevDeficit)) {
Modal.warning({
title: '缓冲期不足',
content: `当前缓冲期无法覆盖本次超期,缺口 ${Math.ceil(deficit)}`,
});
}
} }
} }
} }
@ -6132,13 +5900,13 @@ export default function App() {
<Button <Button
size="small" size="small"
onClick={() => handleComplexAdjustment(index, 1)} onClick={() => handleComplexAdjustment(index, 1)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer} disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }} style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={ title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' : isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' : (hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' : (isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : '' ''
} }
> >
+1 +1
@ -6146,13 +5914,13 @@ export default function App() {
<Button <Button
size="small" size="small"
onClick={() => handleComplexAdjustment(index, 0.5)} onClick={() => handleComplexAdjustment(index, 0.5)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer} disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }} style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={ title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' : isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' : (hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' : (isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : '' ''
} }
> >
+0.5 +0.5
@ -6293,6 +6061,9 @@ export default function App() {
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment); adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const deliveryDate = new Date(adjustedCompletionDate); const deliveryDate = new Date(adjustedCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays); deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
const lockedDeliveryDate = (mode === 'adjust' && isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null)
? new Date(lockedExpectedDeliveryDateTs)
: null;
return ( return (
<> <>
@ -6332,7 +6103,9 @@ export default function App() {
{dynamicBufferDays} {dynamicBufferDays}
</span> </span>
<span style={{ fontSize: 16, color: '#1890ff', fontWeight: 700 }}>=</span> <span style={{ fontSize: 16, color: '#1890ff', fontWeight: 700 }}>=</span>
<span style={{ fontSize: 13, color: '#666', fontWeight: 600 }}></span> <span style={{ fontSize: 13, color: '#666', fontWeight: 600 }}>
{lockedDeliveryDate ? '结束日期(未锁)' : '结束日期'}
</span>
<span style={{ <span style={{
fontSize: 13, fontSize: 13,
color: '#52c41a', color: '#52c41a',
@ -6346,6 +6119,24 @@ export default function App() {
</span> </span>
</div> </div>
{lockedDeliveryDate && (
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 8, flexWrap: 'nowrap', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Text style={{ fontSize: 13, color: '#333', fontWeight: 600, whiteSpace: 'nowrap' }}></Text>
<span style={{
fontSize: 13,
color: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.08)',
border: '1px solid rgba(114, 46, 209, 0.24)',
borderRadius: 6,
padding: '3px 8px',
fontWeight: 700,
whiteSpace: 'nowrap'
}}>
{formatDate(lockedDeliveryDate)}{getDayOfWeek(lockedDeliveryDate)}
</span>
</div>
)}
{/* 第二行:客户期望日期(可更改,优化展示为标签样式) */} {/* 第二行:客户期望日期(可更改,优化展示为标签样式) */}
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 8, flexWrap: 'nowrap', whiteSpace: 'nowrap', overflowX: 'auto' }}> <div style={{ marginTop: 8, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 8, flexWrap: 'nowrap', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Text style={{ fontSize: 13, color: '#333', fontWeight: 600, whiteSpace: 'nowrap' }}></Text> <Text style={{ fontSize: 13, color: '#333', fontWeight: 600, whiteSpace: 'nowrap' }}></Text>