diff --git a/src/App.tsx b/src/App.tsx index 6765b0a..8bde89b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ 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 { useState, useEffect, useRef } from 'react'; -import { addDays, format } from 'date-fns'; +import { addDays, format, differenceInCalendarDays } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService'; @@ -54,6 +54,14 @@ export default function App() { const [expectedDate, setExpectedDate] = useState(null); // 起始时间状态(从货期记录表获取,新记录则使用当前时间) const [startTime, setStartTime] = useState(null); + const [timelineDirection, setTimelineDirection] = useState<'forward' | 'backward'>('forward'); + const [calculatedRequiredStartTime, setCalculatedRequiredStartTime] = useState(null); + const [allocationVisible, setAllocationVisible] = useState(false); + const [allocationMode, setAllocationMode] = useState<'auto' | 'manual'>('auto'); + const [allocationExtraDays, setAllocationExtraDays] = useState(0); + const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({}); + const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState([]); + const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({}); // 预览相关状态(已移除未使用的 previewLoading 状态) @@ -121,6 +129,12 @@ export default function App() { setSelectedLabels({}); setExpectedDate(null); setStartTime(null); + setCalculatedRequiredStartTime(null); + setAllocationVisible(false); + setAllocationExtraDays(0); + setAllocationDraft({}); + setAllocationExcluded({}); + setAllocationNodesSnapshot([]); setTimelineVisible(false); setTimelineResults([]); setTimelineAdjustments({}); @@ -224,6 +238,7 @@ export default function App() { // 新表ID(批量生成表) const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY'; + const BATCH_ROW_NUMBER_FIELD_ID = 'fldiqlTVsU'; const fetchAllRecordsByPage = async (table: any, params?: any) => { let token: any = undefined; @@ -245,6 +260,14 @@ export default function App() { return res?.total || 0; }; + const parseBatchRowNumber = (raw: any): number | null => { + const text = extractText(raw); + const n = typeof raw === 'number' + ? raw + : (text && text.trim() !== '' ? Number(text) : NaN); + return Number.isFinite(n) ? n : null; + }; + // 已移除:调整模式不再加载货期记录列表 // 入口选择处理 @@ -259,10 +282,19 @@ export default function App() { try { const batchTable = await bitable.base.getTable(BATCH_TABLE_ID); const total = await getRecordTotalByPage(batchTable); - setBatchTotalRows(total); + let totalByRowNumber = 0; + try { + const rows = await fetchAllRecordsByPage(batchTable); + for (const r of rows) { + const no = parseBatchRowNumber((r?.fields || {})[BATCH_ROW_NUMBER_FIELD_ID]); + if (no !== null) totalByRowNumber = Math.max(totalByRowNumber, no); + } + } catch {} + const totalRows = totalByRowNumber > 0 ? totalByRowNumber : total; + setBatchTotalRows(totalRows); setBatchStartRow(1); - setBatchEndRow(total > 0 ? total : 1); - setBatchProcessingTotal(total); + setBatchEndRow(totalRows > 0 ? totalRows : 1); + setBatchProcessingTotal(totalRows); } catch { setBatchTotalRows(0); setBatchStartRow(1); @@ -271,8 +303,10 @@ export default function App() { } setBatchModalVisible(true); }; - + const restoreBaseBufferDaysFromSnapshot = (snapshot: any) => { + const snapshotDirection = snapshot?.timelineDirection; + if (snapshotDirection === 'backward') return; const candidates: any[] = [ snapshot?.bufferManagement?.baseDays, snapshot?.baseBufferDays, @@ -291,6 +325,21 @@ export default function App() { } }; + const restoreTimelineDirectionFromSnapshot = (snapshot: any) => { + const candidates: any[] = [ + snapshot?.timelineDirection, + snapshot?.timelineCalculationState?.timelineDirection, + snapshot?.timelineCalculationState?.calculationDirection, + snapshot?.calculationDirection, + ]; + for (const c of candidates) { + if (c === 'forward' || c === 'backward') { + setTimelineDirection(c); + return; + } + } + }; + // 根据货期记录ID读取节点详情并还原流程数据 const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => { if (!deliveryRecordId) { @@ -355,6 +404,7 @@ export default function App() { if (deliverySnapStr && deliverySnapStr.trim() !== '') { setIsRestoringSnapshot(true); const snapshot = JSON.parse(deliverySnapStr); + restoreTimelineDirectionFromSnapshot(snapshot); restoreBaseBufferDaysFromSnapshot(snapshot); // 恢复页面与全局状态 @@ -490,15 +540,16 @@ export default function App() { break; } } - if (snapStr && snapStr.trim() !== '') { - const snapshot = JSON.parse(snapStr); - if (Array.isArray(snapshot.timelineResults)) { - setIsRestoringSnapshot(true); - restoreBaseBufferDaysFromSnapshot(snapshot); + if (snapStr && snapStr.trim() !== '') { + const snapshot = JSON.parse(snapStr); + if (Array.isArray(snapshot.timelineResults)) { + setIsRestoringSnapshot(true); + restoreTimelineDirectionFromSnapshot(snapshot); + restoreBaseBufferDaysFromSnapshot(snapshot); - if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); - if (!mode && snapshot.mode) setMode(snapshot.mode); - if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId); + if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); + if (!mode && snapshot.mode) setMode(snapshot.mode); + if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId); if (snapshot.styleText) setCurrentStyleText(snapshot.styleText); if (snapshot.colorText) setCurrentColorText(snapshot.colorText); if (snapshot.text2) setCurrentText2(snapshot.text2); @@ -627,6 +678,7 @@ export default function App() { if (snapStr && snapStr.trim() !== '') { setIsRestoringSnapshot(true); // 开始快照还原 const snapshot = JSON.parse(snapStr); + restoreTimelineDirectionFromSnapshot(snapshot); restoreBaseBufferDaysFromSnapshot(snapshot); // 恢复页面状态 if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); @@ -967,6 +1019,7 @@ export default function App() { nodeSnapshots.length === globalSnapshotData.totalNodes) { // 重组完整的 timelineResults + restoreTimelineDirectionFromSnapshot(globalSnapshotData); restoreBaseBufferDaysFromSnapshot(globalSnapshotData); setTimelineResults(nodeSnapshots); setTimelineVisible(true); @@ -1855,6 +1908,7 @@ export default function App() { const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels; const currentExpectedDate = overrideData?.expectedDate || expectedDate; const currentStartTime = overrideData?.startTime || startTime; + const isBackward = timelineDirection === 'backward'; console.log('=== handleCalculateTimeline - 使用的数据 ==='); console.log('currentSelectedRecords:', currentSelectedRecords); @@ -1877,6 +1931,28 @@ export default function App() { } } + if (isBackward && !currentExpectedDate) { + if (showUI && bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' }); + } + return []; + } + + if (isBackward) { + if (!currentStartTime || isNaN(currentStartTime.getTime())) { + if (showUI && bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择起始日期' }); + } + return []; + } + if (!currentExpectedDate || isNaN(currentExpectedDate.getTime())) { + if (showUI && bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' }); + } + return []; + } + } + // 跳过验证(用于批量模式) if (!skipValidation) { // 检查是否选择了多条记录 @@ -2220,10 +2296,13 @@ export default function App() { const results: any[] = []; // 3. 按顺序为每个匹配的流程节点查找时效数据并计算累积时间 - let cumulativeStartTime = currentStartTime ? new Date(currentStartTime) : new Date(); // 累积开始时间 + let cumulativeTime = isBackward + ? new Date(currentExpectedDate as Date) + : (currentStartTime ? new Date(currentStartTime) : new Date()); + const nodesToProcess = isBackward ? [...matchedProcessNodes].reverse() : matchedProcessNodes; - for (let i = 0; i < matchedProcessNodes.length; i++) { - const processNode = matchedProcessNodes[i]; + for (let i = 0; i < nodesToProcess.length; i++) { + const processNode = nodesToProcess[i]; let timelineValue = null; let matchedTimelineRecord = null; @@ -2447,51 +2526,6 @@ export default function App() { }; }; - let nodeStartTime = new Date(cumulativeStartTime); - - // 应用起始日期调整规则 - if (processNode.startDateRule) { - let ruleJson = ''; - if (typeof processNode.startDateRule === 'string') { - ruleJson = processNode.startDateRule; - } else if (processNode.startDateRule && processNode.startDateRule.text) { - ruleJson = processNode.startDateRule.text; - } - - if (ruleJson.trim()) { - nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson); - console.log(`节点 ${processNode.nodeName} 应用起始日期调整规则后的开始时间:`, formatDate(nodeStartTime)); - } - } - - // 应用JSON格式日期调整规则 - let ruleDescription = ''; - if (processNode.dateAdjustmentRule) { - console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule); - let ruleText = ''; - if (typeof processNode.dateAdjustmentRule === 'string') { - ruleText = processNode.dateAdjustmentRule; - } else if (Array.isArray(processNode.dateAdjustmentRule)) { - // 处理富文本数组格式 - ruleText = processNode.dateAdjustmentRule - .filter((item: any) => item.type === 'text') - .map((item: any) => item.text) - .join(''); - } else if (processNode.dateAdjustmentRule && processNode.dateAdjustmentRule.text) { - ruleText = processNode.dateAdjustmentRule.text; - } - - console.log('解析后的ruleText:', ruleText); - console.log('调整前的nodeStartTime:', formatDate(nodeStartTime)); - - if (ruleText && ruleText.trim() !== '') { - const result = adjustStartDateByJsonRule(nodeStartTime, ruleText); - nodeStartTime = result.adjustedDate; - ruleDescription = result.description || ''; - console.log(`节点 ${processNode.nodeName} 应用JSON日期调整规则后的开始时间:`, formatDate(nodeStartTime)); - } - } - // 获取当前节点的计算方式 let nodeCalculationMethod = '外部'; // 默认值 if (matchedCandidates.length > 0) { @@ -2507,23 +2541,91 @@ export default function App() { } } - const timelineResult = timelineValue ? - calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod) : - { - startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'), - endDate: '未找到时效数据' - }; - + let nodeStartTime: Date; let nodeEndTime: Date; - if (timelineValue) { - const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); - if (nodeCalculationMethod === '内部') { - nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + let ruleDescription = ''; + let timelineResult: { startDate: string; endDate: string }; + + if (!isBackward) { + nodeStartTime = new Date(cumulativeTime); + + if (processNode.startDateRule) { + let ruleJson = ''; + if (typeof processNode.startDateRule === 'string') { + ruleJson = processNode.startDateRule; + } else if (processNode.startDateRule && processNode.startDateRule.text) { + ruleJson = processNode.startDateRule.text; + } + + if (ruleJson.trim()) { + nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson); + console.log(`节点 ${processNode.nodeName} 应用起始日期调整规则后的开始时间:`, formatDate(nodeStartTime)); + } + } + + if (processNode.dateAdjustmentRule) { + console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule); + let ruleText = ''; + if (typeof processNode.dateAdjustmentRule === 'string') { + ruleText = processNode.dateAdjustmentRule; + } else if (Array.isArray(processNode.dateAdjustmentRule)) { + ruleText = processNode.dateAdjustmentRule + .filter((item: any) => item.type === 'text') + .map((item: any) => item.text) + .join(''); + } else if (processNode.dateAdjustmentRule && processNode.dateAdjustmentRule.text) { + ruleText = processNode.dateAdjustmentRule.text; + } + + console.log('解析后的ruleText:', ruleText); + console.log('调整前的nodeStartTime:', formatDate(nodeStartTime)); + + if (ruleText && ruleText.trim() !== '') { + const result = adjustStartDateByJsonRule(nodeStartTime, ruleText); + nodeStartTime = result.adjustedDate; + ruleDescription = result.description || ''; + console.log(`节点 ${processNode.nodeName} 应用JSON日期调整规则后的开始时间:`, formatDate(nodeStartTime)); + } + } + + timelineResult = timelineValue + ? calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod) + : { + startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'), + endDate: '未找到时效数据' + }; + + if (timelineValue) { + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); + if (nodeCalculationMethod === '内部') { + nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } else { + nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } } else { - nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + nodeEndTime = new Date(nodeStartTime); } } else { - nodeEndTime = new Date(nodeStartTime); + nodeEndTime = new Date(cumulativeTime); + if (timelineValue) { + if (nodeCalculationMethod === '内部') { + nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } else { + nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } + } else { + nodeStartTime = new Date(nodeEndTime); + } + + timelineResult = timelineValue + ? { + startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'), + endDate: formatDate(nodeEndTime, 'STORAGE_FORMAT') + } + : { + startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'), + endDate: '未找到时效数据' + }; } // 计算跳过的天数 @@ -2533,7 +2635,7 @@ export default function App() { // 计算时间范围内实际跳过的自定义日期 const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []); - results.push({ + (isBackward ? results.unshift.bind(results) : results.push.bind(results))({ processOrder: processNode.processOrder, nodeName: processNode.nodeName, matchedLabels: processNode.matchedLabels, @@ -2569,7 +2671,7 @@ export default function App() { // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 if (timelineValue) { - cumulativeStartTime = new Date(nodeEndTime); + cumulativeTime = isBackward ? new Date(nodeStartTime) : new Date(nodeEndTime); } console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, { @@ -2579,10 +2681,43 @@ export default function App() { 计算方式: nodeCalculationMethod }); } + + if (isBackward && results.length > 0) { + const parsed = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null; + if (parsed && !isNaN(parsed.getTime())) { + setCalculatedRequiredStartTime(parsed); + } else { + setCalculatedRequiredStartTime(null); + } + } + + let delayShowTimelineModal = false; + if (isBackward) { + const required = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null; + setCalculatedRequiredStartTime(required && !isNaN(required.getTime()) ? required : null); + if (required && currentStartTime && !isNaN(currentStartTime.getTime())) { + const diffDays = differenceInCalendarDays(required, currentStartTime); + const extraDays = Math.max(0, diffDays); + + if (extraDays > 0) { + setAllocationNodesSnapshot(results); + setAllocationExtraDays(extraDays); + setAllocationMode('auto'); + setAllocationVisible(true); + delayShowTimelineModal = true; + } else if (required.getTime() < currentStartTime.getTime()) { + if (showUI && bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.warning, message: '当前起始日期晚于倒推要求起始日期,可能无法满足客户期望日期' }); + } + } + } + } setTimelineResults(results); - if (showUI) { + if (showUI && !delayShowTimelineModal) { setTimelineVisible(true); + } else if (showUI && delayShowTimelineModal) { + setTimelineVisible(false); } console.log('按流程顺序计算的时效结果:', results); @@ -2846,6 +2981,149 @@ export default function App() { return updatedResults; }; + const getRecalculatedTimelineBackward = (adjustments: { [key: number]: number }) => { + const updatedResults = [...timelineResults]; + const fallbackEnd = (() => { + if (expectedDate && !isNaN(expectedDate.getTime())) return new Date(expectedDate); + const lastEndStr = updatedResults.length > 0 ? updatedResults[updatedResults.length - 1]?.estimatedEnd : null; + const parsed = typeof lastEndStr === 'string' ? parseDate(lastEndStr) : null; + return parsed && !isNaN(parsed.getTime()) ? parsed : new Date(); + })(); + + let cumulativeEndTime = new Date(fallbackEnd); + + for (let i = updatedResults.length - 1; i >= 0; i--) { + const result = updatedResults[i]; + const baseTimelineValue = (typeof result.timelineValue === 'number') + ? result.timelineValue + : (typeof result.adjustedTimelineValue === 'number') + ? result.adjustedTimelineValue + : 0; + const adjustment = adjustments[i] || 0; + const adjustedTimelineValue = baseTimelineValue + adjustment; + + const nodeWeekendDays = result.weekendDaysConfig || []; + const nodeCalculationMethod = result.calculationMethod || '外部'; + const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : []; + + const nodeEndTime = new Date(cumulativeEndTime); + let nodeStartTime: Date; + + if (adjustedTimelineValue !== 0) { + if (nodeCalculationMethod === '内部') { + nodeStartTime = addInternalBusinessTime(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } else { + nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } + } else { + nodeStartTime = new Date(nodeEndTime); + } + + const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, nodeWeekendDays); + const estimatedStartStr = formatDate(nodeStartTime); + const estimatedEndStr = formatDate(nodeEndTime); + const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); + const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, nodeExcludedDates); + + updatedResults[i] = { + ...result, + adjustedTimelineValue, + estimatedStart: estimatedStartStr, + estimatedEnd: estimatedEndStr, + adjustment, + calculationMethod: nodeCalculationMethod, + skippedWeekends, + actualDays, + actualExcludedDates: excludedDatesInRange.dates, + actualExcludedDatesCount: excludedDatesInRange.count, + }; + + if (adjustedTimelineValue !== 0) { + cumulativeEndTime = new Date(nodeStartTime); + } + } + + return updatedResults; + }; + + const buildAutoAllocationDraft = (nodes: any[], extraDays: number, excluded?: {[key: number]: boolean}) => { + const roundedExtra = Math.max(0, Math.round(Number(extraDays) * 100) / 100); + if (!Array.isArray(nodes) || nodes.length === 0 || roundedExtra === 0) return {}; + + const isTurnover = (node: any) => node?.nodeName === '周转周期'; + const toBase = (node: any) => { + const v = typeof node?.timelineValue === 'number' + ? node.timelineValue + : (typeof node?.adjustedTimelineValue === 'number' ? node.adjustedTimelineValue : 0); + return Number.isFinite(v) ? v : 0; + }; + + const eligible = nodes + .map((n, i) => ({ i, base: toBase(n), turnover: isTurnover(n), excluded: !!excluded?.[i] })) + .filter(x => !x.turnover && !x.excluded); + + if (eligible.length === 0) return {}; + + const positive = eligible.filter(x => x.base > 0); + const pool = positive.length > 0 ? positive : eligible; + const totalBase = pool.reduce((s, x) => s + (x.base > 0 ? x.base : 1), 0); + + const draft: {[key: number]: number} = {}; + let remaining = roundedExtra; + for (let k = 0; k < pool.length; k++) { + const idx = pool[k].i; + if (k === pool.length - 1) { + draft[idx] = Math.max(0, Math.round(remaining * 100) / 100); + break; + } + const weight = pool[k].base > 0 ? pool[k].base : 1; + const raw = (roundedExtra * weight) / totalBase; + const val = Math.max(0, Math.round(raw * 100) / 100); + draft[idx] = val; + remaining = Math.max(0, Math.round((remaining - val) * 100) / 100); + } + + return draft; + }; + + useEffect(() => { + if (!allocationVisible) return; + if (allocationMode !== 'auto') return; + const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults; + const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded); + setAllocationDraft(draft); + }, [allocationVisible, allocationMode, allocationExtraDays, allocationNodesSnapshot, timelineResults, allocationExcluded]); + + const applyAllocationDraft = async () => { + const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0); + const roundedSum = Math.round(sum * 100) / 100; + const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100; + const diff = Math.round((roundedTarget - roundedSum) * 100) / 100; + + if (Math.abs(diff) > 0.01) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.warning, message: `当前分配合计${roundedSum}天,与盈余${roundedTarget}天不一致` }); + } + return; + } + + const merged: {[key: number]: number} = { ...(timelineAdjustments || {}) }; + for (const [k, v] of Object.entries(allocationDraft || {})) { + const idx = parseInt(k, 10); + const val = Math.max(0, Math.round((Number(v) || 0) * 100) / 100); + merged[idx] = Math.round(((Number(merged[idx]) || 0) + val) * 100) / 100; + } + + setTimelineAdjustments(merged); + setAllocationVisible(false); + recalculateTimeline(merged, true); + setTimelineVisible(true); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: ToastType.success, message: '已应用盈余分配' }); + } + }; + // 获取有效的最后完成日期(忽略 '时效值为0'),若不存在则返回最后一项的日期 const getLastValidCompletionDateFromResults = (results: any[]): Date | null => { if (!Array.isArray(results) || results.length === 0) return null; @@ -2864,6 +3142,7 @@ export default function App() { // 依据“最后流程节点的预计完成日期差(自然日)”计算剩余缓冲期 const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => { try { + if (timelineDirection === 'backward') return 0; const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整) const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整) @@ -2898,6 +3177,19 @@ export default function App() { // 重新计算时间线的函数 const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => { + if (timelineDirection === 'backward') { + const updated = getRecalculatedTimelineBackward(adjustments); + setTimelineResults(updated); + const firstStartStr = updated.length > 0 ? updated[0]?.estimatedStart : null; + const parsed = typeof firstStartStr === 'string' ? parseDate(firstStartStr) : null; + if (parsed && !isNaN(parsed.getTime())) { + setCalculatedRequiredStartTime(parsed); + } else { + setCalculatedRequiredStartTime(null); + } + return; + } + const updatedResults = [...timelineResults]; // 找到第一个被调整的节点索引 @@ -3085,10 +3377,19 @@ export default function App() { // 当起始时间变更时,重新以最新起始时间为基准重算全流程 useEffect(() => { + if (timelineDirection !== 'forward') return; if (timelineResults.length > 0 && !isRestoringSnapshot) { recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点 } - }, [startTime, isRestoringSnapshot]); + }, [startTime, isRestoringSnapshot, timelineDirection]); + + useEffect(() => { + if (timelineDirection !== 'backward') return; + if (!expectedDate) return; + if (timelineResults.length > 0 && !isRestoringSnapshot) { + recalculateTimeline(timelineAdjustments, true); + } + }, [expectedDate, isRestoringSnapshot, timelineDirection]); // 当实际完成日期变化时,以最新状态进行重算,避免首次选择不触发或使用旧值 useEffect(() => { @@ -3101,6 +3402,7 @@ export default function App() { useEffect(() => { if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) { initialSnapshotRef.current = { + timelineDirection, startTime, expectedDate, selectedLabels, @@ -3143,6 +3445,9 @@ export default function App() { } const s = initialSnapshotRef.current; setIsRestoringSnapshot(true); + if (s.timelineDirection === 'forward' || s.timelineDirection === 'backward') { + setTimelineDirection(s.timelineDirection); + } setStartTime(s.startTime || null); setExpectedDate(s.expectedDate || null); setSelectedLabels(s.selectedLabels || {}); @@ -3447,6 +3752,7 @@ export default function App() { colorText, text2, mode, + timelineDirection, selectedLabels: currentSelectedLabels, expectedDateTimestamp, expectedDateString, @@ -3465,14 +3771,18 @@ export default function App() { }), labelSelectionComplete: Object.keys(selectedLabels).length > 0 }, - bufferManagement: { - baseDays: baseBuferDays, - totalAdjustments, - dynamicBufferDays, - hasReachedFinalLimit, - hasAppliedSuggestedBuffer, - lastSuggestedApplied: lastSuggestedApplied ?? 0 - }, + ...(timelineDirection !== 'backward' + ? { + bufferManagement: { + baseDays: baseBuferDays, + totalAdjustments, + dynamicBufferDays, + hasReachedFinalLimit, + hasAppliedSuggestedBuffer, + lastSuggestedApplied: lastSuggestedApplied ?? 0 + }, + } + : {}), chainAdjustmentSystem: { enabled: true, lastCalculationTime: new Date().getTime(), @@ -3482,7 +3792,8 @@ export default function App() { calculationTimestamp: new Date().getTime(), totalNodes: timelineResults.length, hasValidResults: timelineResults.length > 0, - lastCalculationMode: mode + lastCalculationMode: mode, + timelineDirection }, totalNodes: timelineResults.length, isGlobalSnapshot: true @@ -4060,18 +4371,42 @@ export default function App() { } } const rows = await fetchAllRecordsByPage(batchTable); - const total = rows.length; - const startIndex1 = range?.start && range.start > 0 ? range.start : 1; - const endIndex1 = range?.end && range.end > 0 ? range.end : total; - const minIndex = Math.max(1, Math.min(startIndex1, total)); - const maxIndex = Math.max(minIndex, Math.min(endIndex1, total)); - setBatchProcessingTotal(maxIndex - minIndex + 1); + + const rowsWithNo = rows.map((row: any, idx: number) => { + const f = row?.fields || {}; + const no = parseBatchRowNumber(f[BATCH_ROW_NUMBER_FIELD_ID]); + return { row, idx, no }; + }); + const hasRowNo = rowsWithNo.some(r => typeof r.no === 'number'); + const ordered = hasRowNo + ? [...rowsWithNo].sort((a, b) => { + const na = typeof a.no === 'number' ? a.no : Infinity; + const nb = typeof b.no === 'number' ? b.no : Infinity; + if (na !== nb) return na - nb; + return a.idx - b.idx; + }) + : rowsWithNo; + + const allNos = hasRowNo ? ordered.map(r => (typeof r.no === 'number' ? r.no : null)).filter((v): v is number => v !== null) : []; + const minNo = allNos.length > 0 ? Math.min(...allNos) : 1; + const maxNo = allNos.length > 0 ? Math.max(...allNos) : ordered.length; + const requestedStart = range?.start && range.start > 0 ? range.start : (hasRowNo ? minNo : 1); + const requestedEnd = range?.end && range.end > 0 ? range.end : (hasRowNo ? maxNo : ordered.length); + const start = Math.min(requestedStart, requestedEnd); + const end = Math.max(requestedStart, requestedEnd); + + const selected = hasRowNo + ? ordered.filter(r => typeof r.no === 'number' && r.no >= Math.max(minNo, start) && r.no <= Math.min(maxNo, end)) + : ordered.slice(Math.max(0, Math.min(start, ordered.length) - 1), Math.max(0, Math.min(end, ordered.length))); + + setBatchProcessingTotal(selected.length); let processed = 0; - for (let i = minIndex - 1; i < maxIndex; i++) { + for (let j = 0; j < selected.length; j++) { if (batchAbortRef.current) { break; } - const row = rows[i]; + const { row, idx, no } = selected[j]; + const displayIndex = typeof no === 'number' ? no : (idx + 1); const f = row.fields || {}; const getText = (name: string) => extractText(f[nameToId.get(name) || '']); const foreignId = getText('foreignId'); @@ -4132,11 +4467,11 @@ export default function App() { if (missing.length > 0) { setBatchProcessedCount(p => p + 1); setBatchFailureCount(fCount => fCount + 1); - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]); + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]); continue; } } - setBatchCurrentRowInfo({ index: i + 1, foreignId: foreignId || '', style: styleText || '', color: colorText || '' }); + setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' }); setCurrentForeignId(foreignId || ''); setCurrentStyleText(styleText || ''); setCurrentColorText(colorText || ''); @@ -4176,15 +4511,15 @@ export default function App() { } } catch (e2) { throw e2; - } } - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]); + } + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]); } else { - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]); + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]); } } catch (statusErr: any) { console.warn('回写批量状态字段失败', statusErr); - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]); + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]); } processed++; setBatchProcessedCount(p => p + 1); @@ -4192,12 +4527,12 @@ export default function App() { } else { setBatchProcessedCount(p => p + 1); setBatchFailureCount(fCount => fCount + 1); - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]); + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]); } } catch (rowErr: any) { setBatchProcessedCount(p => p + 1); setBatchFailureCount(fCount => fCount + 1); - setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]); + setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]); } } if (bitable.ui.showToast) { @@ -5000,6 +5335,179 @@ export default function App() { )} + { + setAllocationVisible(false); + setTimelineVisible(true); + }} + footer={ +
+ + + + + + + + +
+ } + width={900} + > + {(() => { + const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults; + const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0); + const roundedSum = Math.round(sum * 100) / 100; + const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100; + const remainingRaw = Math.round((roundedTarget - roundedSum) * 100) / 100; + const remaining = Math.max(0, remainingRaw); + const overAllocated = Math.max(0, Math.round((0 - remainingRaw) * 100) / 100); + const statusColor = overAllocated > 0 ? '#dc2626' : (remaining === 0 ? '#16a34a' : '#d97706'); + + return ( +
+ +
+ 客户期望日期:{expectedDate ? formatDate(expectedDate, 'CHINESE_DATE') : '-'} + 业务起始日期:{startTime ? formatDate(startTime) : '-'} + 倒推要求起始日期:{calculatedRequiredStartTime ? formatDate(calculatedRequiredStartTime) : '-'} + + 盈余:{roundedTarget} 天(剩余 {remaining}{overAllocated > 0 ? `,已超配 ${overAllocated}` : ''}) + +
+
+ 分配方式 + setTimelineDirection(value as 'forward' | 'backward')} + > + 正推(从起始时间开始) + 倒推(以客户期望日期为目标) + +
+ +
+ 起始日期 + { + if (date instanceof Date) { + setStartTime(date); + } else if (typeof date === 'string') { + const parsed = new Date(date); + setStartTime(isNaN(parsed.getTime()) ? null : parsed); + } else { + setStartTime(null); + } + }} + type="dateTime" + format="yyyy-MM-dd HH:mm" + /> + {timelineDirection === 'backward' && ( + + 倒推模式必填 + + )} +
+ {/* 客户期望日期选择 */}
客户期望日期 @@ -6004,11 +6551,15 @@ export default function App() { ? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true) : handleCalculateTimeline()} loading={timelineLoading} - disabled={timelineLoading || Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => { - const val = (selectedLabels as any)[key]; - if (Array.isArray(val)) return val.length === 0; - return !(typeof val === 'string' && val.trim().length > 0); - })} + disabled={ + timelineLoading + || (timelineDirection === 'backward' && (!expectedDate || !startTime)) + || Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => { + const val = (selectedLabels as any)[key]; + if (Array.isArray(val)) return val.length === 0; + return !(typeof val === 'string' && val.trim().length > 0); + }) + } style={{ minWidth: '160px' }} > {labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'}