diff --git a/src/App.tsx b/src/App.tsx index 7dd3d24..5385e80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1976,7 +1976,8 @@ export default function App() { recordDetails?: any[], selectedLabels?: {[key: string]: string | string[]}, expectedDate?: Date | null, - startTime?: Date | null + startTime?: Date | null, + excludedDates?: string[] }, showUI: boolean = true // 新增参数控制是否显示UI ) => { @@ -1986,6 +1987,7 @@ export default function App() { const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels; const currentExpectedDate = overrideData?.expectedDate || expectedDate; const currentStartTime = overrideData?.startTime || startTime; + const currentExcludedDates = Array.isArray(overrideData?.excludedDates) ? overrideData!.excludedDates : []; const isBackward = timelineDirection === 'backward'; console.log('=== handleCalculateTimeline - 使用的数据 ==='); @@ -2381,6 +2383,7 @@ export default function App() { for (let i = 0; i < nodesToProcess.length; i++) { const processNode = nodesToProcess[i]; + const nodeExcludedDates = Array.from(new Set([...(processNode.excludedDates || []), ...currentExcludedDates])).filter(Boolean); let timelineValue = null; let matchedTimelineRecord = null; @@ -2587,15 +2590,15 @@ export default function App() { // 计算当前节点的开始和完成时间(使用工作日计算) const calculateTimeline = (startDate: Date, timelineValue: number, calculationMethod: string = '外部') => { // 根据计算方式调整开始时间 - const adjustedStartDate = adjustToNextWorkingHour(startDate, calculationMethod, processNode.weekendDays, processNode.excludedDates || []); + const adjustedStartDate = adjustToNextWorkingHour(startDate, calculationMethod, processNode.weekendDays, nodeExcludedDates); let endDate: Date; if (calculationMethod === '内部') { // 使用内部工作时间计算 - endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays, nodeExcludedDates); } else { // 使用原有的24小时制计算 - endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays, nodeExcludedDates); } return { @@ -2674,11 +2677,11 @@ export default function App() { }; if (timelineValue) { - const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, nodeExcludedDates); if (nodeCalculationMethod === '内部') { - nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, nodeExcludedDates); } else { - nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, nodeExcludedDates); } } else { nodeEndTime = new Date(nodeStartTime); @@ -2687,9 +2690,9 @@ export default function App() { nodeEndTime = new Date(cumulativeTime); if (timelineValue) { if (nodeCalculationMethod === '内部') { - nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []); + nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, nodeExcludedDates); } else { - nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []); + nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -timelineValue, processNode.weekendDays, nodeExcludedDates); } } else { nodeStartTime = new Date(nodeEndTime); @@ -2711,7 +2714,7 @@ export default function App() { const actualDays = calculateActualDays(timelineResult.startDate, timelineResult.endDate); // 计算时间范围内实际跳过的自定义日期 - const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []); + const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, nodeExcludedDates); (isBackward ? results.unshift.bind(results) : results.push.bind(results))({ processOrder: processNode.processOrder, @@ -2733,7 +2736,7 @@ export default function App() { // 新增:标识是否为累加处理 isAccumulated: processingRule === '累加值' && matchedCandidates.length > 1, weekendDaysConfig: processNode.weekendDays, // 新增:保存休息日配置用于显示 - excludedDates: processNode.excludedDates || [], // 新增:保存不参与计算日期用于显示与快照 + excludedDates: nodeExcludedDates, // 新增:保存不参与计算日期用于显示与快照 // 新增:保存时间范围内实际跳过的日期 actualExcludedDates: excludedDatesInRange.dates, actualExcludedDatesCount: excludedDatesInRange.count, @@ -2792,6 +2795,43 @@ export default function App() { } setTimelineResults(results); + if (!isBackward && mode === 'generate' && showUI && currentExpectedDate && results.length > 0) { + let lastCompletionDate: Date | null = null; + for (let i = results.length - 1; i >= 0; i--) { + const end = results[i]?.estimatedEnd; + if (!end || end === '时效值为0') continue; + const parsed = typeof end === 'string' ? parseDate(end) : (end as any as Date); + if (parsed && !isNaN(parsed.getTime())) { + lastCompletionDate = parsed; + break; + } + } + + if (lastCompletionDate) { + const bufferDays = Math.max(0, Math.ceil(baseBufferDays)); + const computedDeliveryDate = new Date(lastCompletionDate); + computedDeliveryDate.setDate(computedDeliveryDate.getDate() + bufferDays); + const slackDays = differenceInCalendarDays(currentExpectedDate, computedDeliveryDate); + if (slackDays > 0) { + const suggested = bufferDays + slackDays; + await new Promise((resolve) => { + Modal.confirm({ + title: '建议增加缓冲期', + content: `客户交期 ${formatDate(currentExpectedDate)}(${getDayOfWeek(currentExpectedDate)})晚于当前预计交付日期 ${formatDate(computedDeliveryDate)}(${getDayOfWeek(computedDeliveryDate)}),建议将缓冲期从 ${bufferDays} 天调整为 ${suggested} 天以对齐。`, + okText: '增加缓冲期', + cancelText: '不调整', + onOk: () => { + setBaseBufferDays(suggested); + setHasAppliedSuggestedBuffer(true); + setLastSuggestedApplied(suggested); + resolve(); + }, + onCancel: () => resolve() + }); + }); + } + } + } if (showUI && !delayShowTimelineModal) { setTimelineVisible(true); } else if (showUI && delayShowTimelineModal) { @@ -3160,12 +3200,13 @@ export default function App() { const computeExpectedDeliveryDateTsFromResults = ( results: any[], - adjustments: { [key: number]: number } + adjustments: { [key: number]: number }, + baseBufferDaysOverride?: number ): number | null => { if (timelineDirection === 'backward') return null; const lastCompletionDate = getLastValidCompletionDateFromResults(results); if (!lastCompletionDate) return null; - const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(adjustments); + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(adjustments, baseBufferDaysOverride); const deliveryDate = new Date(lastCompletionDate); deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays); const ts = deliveryDate.getTime(); @@ -3181,15 +3222,15 @@ export default function App() { if (computed !== null) setLockedExpectedDeliveryDateTs(computed); }; - const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => { + const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }, baseBufferDaysOverride?: number): number => { try { if (timelineDirection === 'backward') return 0; const deltaDays = computeLastNodeEndDeltaDays(adjustments); - const base = Math.max(0, Math.ceil(baseBufferDays)); + const base = Math.max(0, Math.ceil(baseBufferDaysOverride ?? baseBufferDays)); const remaining = base - deltaDays; return Math.max(0, Math.min(base, remaining)); } catch { - const base = Math.max(0, Math.ceil(baseBufferDays)); + const base = Math.max(0, Math.ceil(baseBufferDaysOverride ?? baseBufferDays)); return base; } }; @@ -3502,7 +3543,7 @@ export default function App() { timelineResults: any[], processRecordIds: string[], timelineAdjustments: {[key: number]: number} = {}, - overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]} } + overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number } ) => { let recordCells: any[] | undefined; try { @@ -3664,8 +3705,10 @@ export default function App() { // 获取标签汇总:批量模式优先使用传入的labels const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean); + const baseBufferDaysToUse = Math.max(0, Math.ceil(overrides?.baseBufferDays ?? baseBufferDays)); + // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) - let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments); + let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments, baseBufferDaysToUse); if (mode === 'adjust' && isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null) { expectedDeliveryDate = lockedExpectedDeliveryDateTs; } @@ -3721,7 +3764,7 @@ export default function App() { const styleText = style || ''; const colorText = color || ''; - const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments, baseBufferDaysToUse); // 检查是否达到最终限制 let hasReachedFinalLimit = false; @@ -3740,7 +3783,7 @@ export default function App() { } // 为快照提供基础缓冲与节点调整总量(用于兼容历史字段),尽管动态缓冲期已改为“自然日差”口径 - const baseBuferDays = baseBufferDays; + const baseBuferDays = baseBufferDaysToUse; const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); const globalSnapshot = { @@ -4436,6 +4479,45 @@ export default function App() { const v = (rawExpected as any).value || (rawExpected as any).text || (rawExpected as any).name || ''; if (typeof v === 'number') expectedDateObj = new Date(v); else expectedDateObj = parseDate(v); } + + const normalizeExcludedDates = (raw: any): string[] => { + const out: string[] = []; + const pushToken = (token: any) => { + if (token == null) return; + if (typeof token === 'number' && Number.isFinite(token)) { + const d = new Date(token); + if (!isNaN(d.getTime())) out.push(format(d, 'yyyy-MM-dd')); + return; + } + const text = (typeof token === 'string') ? token : extractText(token); + if (!text) return; + const parts = String(text).split(/[,,;;、\n\r\t ]+/).map(s => s.trim()).filter(Boolean); + for (const p of parts) { + const head = p.slice(0, 10); + if (/^\d{4}-\d{2}-\d{2}$/.test(head)) { + out.push(head); + continue; + } + const parsed = parseDate(p); + if (parsed && !isNaN(parsed.getTime())) out.push(format(parsed, 'yyyy-MM-dd')); + } + }; + const walk = (v: any) => { + if (Array.isArray(v)) { + v.forEach(walk); + return; + } + if (v && typeof v === 'object' && Array.isArray((v as any).value)) { + walk((v as any).value); + return; + } + pushToken(v); + }; + walk(raw); + return Array.from(new Set(out)).filter(Boolean); + }; + + const batchExcludedDates = normalizeExcludedDates(f['fldQNxtHnd']); const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean); const normalizeToStringList = (raw: any): string[] => { if (!raw) return []; @@ -4479,10 +4561,33 @@ export default function App() { setStartTime(startDate || null); setSelectedLabels(labels); try { - const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null }, false); + const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null, excludedDates: batchExcludedDates }, false); if (results && results.length > 0) { + const baseForSuggestion = 14; + const suggestedBaseBufferDays = (() => { + if (!expectedDateObj || isNaN(expectedDateObj.getTime())) return baseForSuggestion; + const lastCompletionDate = getLastValidCompletionDateFromResults(results); + if (!lastCompletionDate) return baseForSuggestion; + const computedDeliveryDate = new Date(lastCompletionDate); + computedDeliveryDate.setDate(computedDeliveryDate.getDate() + baseForSuggestion); + const slackDays = differenceInCalendarDays(expectedDateObj, computedDeliveryDate); + return slackDays > 0 ? (baseForSuggestion + slackDays) : baseForSuggestion; + })(); const processRecordIds = await writeToProcessDataTable(results, { foreignId, style: styleText, color: colorText }); - const deliveryRecordId = await writeToDeliveryRecordTable(results, processRecordIds, {}, { foreignId, style: styleText, color: colorText, expectedDate: expectedDateObj || null, startTime: startDate || null, selectedLabels: labels }); + const deliveryRecordId = await writeToDeliveryRecordTable( + results, + processRecordIds, + {}, + { + foreignId, + style: styleText, + color: colorText, + expectedDate: expectedDateObj || null, + startTime: startDate || null, + selectedLabels: labels, + baseBufferDays: suggestedBaseBufferDays + } + ); try { const candidateNames = ['状态','record_id','记录ID','货期记录ID','deliveryRecordId']; let statusFieldId = '';