From 8768d8cee946c14ab2d246cabe25660da46787c0 Mon Sep 17 00:00:00 2001 From: mairuiming Date: Thu, 25 Dec 2025 10:31:01 +0800 Subject: [PATCH] 4 4 --- src/App.tsx | 625 +++++++++++++++++----------------------------------- 1 file changed, 208 insertions(+), 417 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 921b405..7dd3d24 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ 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 { addDays, format, differenceInCalendarDays } from 'date-fns'; import { zhCN } from 'date-fns/locale'; @@ -78,6 +78,8 @@ export default function App() { const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({}); // 基础缓冲期天数(可配置),用于计算动态缓冲期,默认14天 const [baseBufferDays, setBaseBufferDays] = useState(14); + const [lockedExpectedDeliveryDateTs, setLockedExpectedDeliveryDateTs] = useState(null); + const [isExpectedDeliveryDateLocked, setIsExpectedDeliveryDateLocked] = useState(false); // 快照回填来源(foreign_id、款式、颜色、文本2) const [currentForeignId, setCurrentForeignId] = useState(null); const [currentStyleText, setCurrentStyleText] = useState(''); @@ -103,6 +105,7 @@ export default function App() { 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 batchAbortRef = useRef(false); + const lastBufferDeficitRef = useRef(0); // 删除未使用的 deliveryRecords 状态 const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState(''); // 从货期记录读取到的record_ids(用于保存回写) @@ -145,12 +148,17 @@ export default function App() { setIsRestoringSnapshot(false); setRestoredRecordIds([]); setRestoredRecordIdsText(''); + setLockedExpectedDeliveryDateTs(null); + setIsExpectedDeliveryDateLocked(false); // 重置初始快照捕获状态 try { hasCapturedInitialSnapshotRef.current = false; initialSnapshotRef.current = null; } catch {} + try { + lastBufferDeficitRef.current = 0; + } catch {} // 移除:批量模式当前记录信息 @@ -275,6 +283,7 @@ export default function App() { // 切换功能时重置全局变量,但保留新的mode resetGlobalState({ resetMode: false }); setMode(m); + setIsExpectedDeliveryDateLocked(m === 'adjust'); setModeSelectionVisible(false); }; @@ -358,8 +367,26 @@ export default function App() { const recordIdsField: any = await deliveryTable.getField(DELIVERY_RECORD_IDS_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 expectedDeliveryDateField: any = await deliveryTable.getField(DELIVERY_EXPECTED_DATE_FIELD_ID); 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文本字段并保留原始文本(用于原样写回) try { const recordIdsTextVal = await recordIdsField.getValue(deliveryRecordId); @@ -439,6 +466,22 @@ export default function App() { } 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) { setExpectedDate(new Date(snapshot.expectedDateTimestamp)); } else if (snapshot.expectedDateString) { @@ -580,6 +623,22 @@ export default function App() { } 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) { setExpectedDate(new Date(snapshot.expectedDateTimestamp)); } else if (snapshot.expectedDateString) { @@ -913,6 +972,9 @@ export default function App() { colorText: nodeSnapshot.colorText, text2: nodeSnapshot.text2, mode: nodeSnapshot.mode, + timelineDirection: nodeSnapshot.timelineDirection, + lockedExpectedDeliveryDateTs: nodeSnapshot.lockedExpectedDeliveryDateTs, + isExpectedDeliveryDateLocked: nodeSnapshot.isExpectedDeliveryDateLocked, selectedLabels: nodeSnapshot.selectedLabels, expectedDateTimestamp: nodeSnapshot.expectedDateTimestamp, expectedDateString: nodeSnapshot.expectedDateString, @@ -1023,6 +1085,22 @@ export default function App() { restoreBaseBufferDaysFromSnapshot(globalSnapshotData); setTimelineResults(nodeSnapshots); 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) { @@ -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((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); if (showUI && !delayShowTimelineModal) { setTimelineVisible(true); @@ -2784,87 +2818,16 @@ export default function App() { // 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式 const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => { - // 计算当前状态 - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - const baseBuferDays = baseBufferDays; - - // 智能缓冲期计算逻辑(缓冲期扣减=最后完成时间变动的自然天数) - let dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); - - // 只有在缓冲期为0时才检查最终限制:最后节点预计完成时间是否已达到客户期望日期 - let hasReachedFinalLimit = false; - 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; - } + const nextAdjustments = handleTimelineAdjustment(nodeIndex, adjustment); + if (!nextAdjustments) return; + const deficit = computeBufferDeficitDaysUsingEndDelta(nextAdjustments); + const prevDeficit = lastBufferDeficitRef.current; + lastBufferDeficitRef.current = deficit; + if (adjustment > 0 && deficit > 0 && Math.ceil(deficit) > Math.ceil(prevDeficit)) { + Modal.warning({ + title: '缓冲期不足', + content: `当前缓冲期无法覆盖本次超期,缺口 ${Math.ceil(deficit)} 天`, + }); } }; @@ -2892,32 +2855,11 @@ export default function App() { 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); // 使用智能重算逻辑,只重算被调整的节点及其后续节点 recalculateTimeline(newAdjustments); - return timelineResults; + return newAdjustments; }; // 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径) @@ -3183,14 +3125,12 @@ export default function App() { return fallback && !isNaN(fallback.getTime()) ? fallback : null; }; - // 依据“最后流程节点的预计完成日期差(自然日)”计算剩余缓冲期 - const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => { + const computeLastNodeEndDeltaDays = (adjustments: { [key: number]: number }): number => { try { if (timelineDirection === 'backward') return 0; - const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整) - const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整) + const baseline = getRecalculatedTimeline({}); + const current = getRecalculatedTimeline(adjustments); - // 严格取最后流程节点的预计完成日期;若不可用则回退到最后一个有效完成日期 const pickLastNodeEnd = (results: any[]): Date | null => { if (!Array.isArray(results) || results.length === 0) return null; const endStr = results[results.length - 1]?.estimatedEnd; @@ -3203,19 +3143,54 @@ export default function App() { const baselineLast = pickLastNodeEnd(baseline); const currentLast = pickLastNodeEnd(current); - if (!baselineLast || !currentLast) { - return Math.max(0, Math.min(baseBufferDays, baseBufferDays)); - } + if (!baselineLast || !currentLast) return 0; - // 仅使用“预计完成日期”差,不叠加 completionDateAdjustment const dayMs = 1000 * 60 * 60 * 24; - const deltaDays = Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs); - // 缓冲期剩余 = clamp(基础缓冲期 - 差值, 0, 基础缓冲期) - const remaining = baseBufferDays - deltaDays; - return Math.max(0, Math.min(baseBufferDays, remaining)); + return Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs); } catch { - // 兜底:如出现异常,保持基础缓冲期 - return Math.max(0, Math.min(baseBufferDays, baseBufferDays)); + return 0; + } + }; + + 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, timelineAdjustments, baseBufferDays, + lockedExpectedDeliveryDateTs, + isExpectedDeliveryDateLocked, actualCompletionDates, completionDateAdjustment, hasAppliedSuggestedBuffer, @@ -3471,6 +3448,8 @@ export default function App() { setCompletionDateAdjustment(0); // 重置最后流程完成日期调整 setActualCompletionDates({}); // 重置实际完成日期 setBaseBufferDays(14); // 重置固定缓冲期为默认值 + setIsExpectedDeliveryDateLocked(false); + try { lastBufferDeficitRef.current = 0; } catch {} setHasAppliedSuggestedBuffer(false); // 重置建议缓冲期应用标志 setLastSuggestedApplied(null); // 清空上次建议值 recalculateTimeline({}, true); // 强制重算所有节点 @@ -3497,6 +3476,8 @@ export default function App() { setSelectedLabels(s.selectedLabels || {}); setTimelineAdjustments(s.timelineAdjustments || {}); setBaseBufferDays(s.baseBufferDays ?? 14); + setLockedExpectedDeliveryDateTs(s.lockedExpectedDeliveryDateTs ?? null); + setIsExpectedDeliveryDateLocked(!!s.isExpectedDeliveryDateLocked); setActualCompletionDates(s.actualCompletionDates || {}); setCompletionDateAdjustment(s.completionDateAdjustment || 0); setHasAppliedSuggestedBuffer(!!s.hasAppliedSuggestedBuffer && s.hasAppliedSuggestedBuffer); @@ -3684,36 +3665,9 @@ export default function App() { const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean); // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) - let expectedDeliveryDate = null; - if (timelineResults.length > 0) { - // 从后往前查找第一个有效的流程完成日期(与交期余量计算逻辑一致) - 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); - } - } + let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments); + if (mode === 'adjust' && isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null) { + expectedDeliveryDate = lockedExpectedDeliveryDateTs; } // 获取客户期望日期:批量模式优先使用传入的expectedDate @@ -3797,6 +3751,8 @@ export default function App() { text2, mode, timelineDirection, + lockedExpectedDeliveryDateTs, + isExpectedDeliveryDateLocked, selectedLabels: currentSelectedLabels, expectedDateTimestamp, expectedDateString, @@ -5269,7 +5225,11 @@ export default function App() { {mode !== null && (
- { + const next = v as any; + setMode(next); + setIsExpectedDeliveryDateLocked(next === 'adjust'); + }} optionList={[ { value: 'generate', label: '生成流程日期' }, { value: 'adjust', label: '调整流程日期' } @@ -5584,6 +5544,16 @@ export default function App() { > ✏️ + {mode === 'adjust' && ( + <> + 锁交付 + + + )}
} @@ -5593,14 +5563,18 @@ export default function App() { setTimelineAdjustments({}); // 关闭时重置调整 setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减 setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整 - setHasAppliedSuggestedBuffer(false); // 关闭时允许下次重新应用建议缓冲期 - setLastSuggestedApplied(null); // 关闭时清空上次建议值 setStyleColorEditable(false); // 关闭弹窗后恢复为锁定状态 + if (mode !== 'adjust') { + setLockedExpectedDeliveryDateTs(null); + setIsExpectedDeliveryDateLocked(false); + } }} footer={
- 缓冲期(天): + + {mode === 'adjust' && isExpectedDeliveryDateLocked ? '缓冲期(天)(不影响已锁交付):' : '缓冲期(天):'} + - {/* 建议缓冲期增量:根据实际完成偏差与交期余量自动反算 */} - {(() => { - 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 ( -
- 建议增加缓冲期:+{displayInt}天 - -
- ); - } - return null; - } catch { - return null; - } - })()} @@ -5807,105 +5663,8 @@ export default function App() { const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0; // 计算动态缓冲期(按“最后完成时间变动天数”扣减缓冲期) - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - const baseBuferDays = baseBufferDays; 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 (
@@ -6070,6 +5829,15 @@ export default function App() { if (!updated) { 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() {
+ {lockedDeliveryDate && ( +
+ 结束日期(已锁定) + + {formatDate(lockedDeliveryDate)}({getDayOfWeek(lockedDeliveryDate)}) + +
+ )} + {/* 第二行:客户期望日期(可更改,优化展示为标签样式) */}
客户期望日期(可更改)