diff --git a/src/App.tsx b/src/App.tsx index 77d3c0e..37d6b6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -865,6 +865,11 @@ export default function App() { // 记录ID文本字段(货期记录表新增) const DELIVERY_RECORD_IDS_FIELD_ID = 'fldq3u7h7H'; + // OMS看板表相关常量(新增) + const OMS_BOARD_TABLE_ID = 'tbl7j8bCpUbFmGuk'; // OMS看板表ID + const OMS_PLAN_TEXT_FIELD_ID = 'fldnGV2GLl'; // OMS看板:货期计划(文本结构) + const OMS_PLAN_VERSION_FIELD_ID = 'fldwlIUf4z'; // OMS看板:计划版本(公式数字) + // 已移除中国法定节假日相关常量和配置 // 这个变量声明也不需要了 @@ -1192,59 +1197,92 @@ export default function App() { // 内部工作时间计算函数 const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => { const result = new Date(startDate); - let remainingDays = businessDays; + if (!businessDays || businessDays === 0) return result; + const isNegative = businessDays < 0; + const absDays = Math.abs(businessDays); - // 处理整数天 - const wholeDays = Math.floor(remainingDays); - let addedDays = 0; - - while (addedDays < wholeDays) { - result.setDate(result.getDate() + 1); + // 处理整数天(仅计入工作日) + const wholeDays = Math.floor(absDays); + let processedDays = 0; + while (processedDays < wholeDays) { + result.setDate(result.getDate() + (isNegative ? -1 : 1)); if (isBusinessDay(result, weekendDays, excludedDates)) { - addedDays++; + processedDays++; } } // 处理小数部分(按9小时工作制) - const fractionalDays = remainingDays - wholeDays; + const fractionalDays = absDays - wholeDays; if (fractionalDays > 0) { - const workingHoursToAdd = fractionalDays * 9; // 内部按9小时工作制 + const workingHours = fractionalDays * 9; // 内部按9小时工作制 let currentHour = result.getHours(); let currentMinute = result.getMinutes(); - // 确保在工作时间内开始 - if (currentHour < 9) { - currentHour = 9; - currentMinute = 0; - } else if (currentHour >= 18) { - // 跳到下一个工作日的9:00 - result.setDate(result.getDate() + 1); - while (!isBusinessDay(result, weekendDays, excludedDates)) { - result.setDate(result.getDate() + 1); - } - currentHour = 9; - currentMinute = 0; - } - - // 添加工作小时 - const totalMinutes = currentHour * 60 + currentMinute + workingHoursToAdd * 60; - const finalHour = Math.floor(totalMinutes / 60); - const finalMinute = totalMinutes % 60; - - // 如果超过18:00,需要跨到下一个工作日 - if (finalHour >= 18) { - const overflowHours = finalHour - 18; - const overflowMinutes = finalMinute; - - // 跳到下一个工作日 - result.setDate(result.getDate() + 1); - while (!isBusinessDay(result, weekendDays, excludedDates)) { + if (!isNegative) { + // 正向:确保在工作时间内开始 + if (currentHour < 9) { + currentHour = 9; + currentMinute = 0; + } else if (currentHour >= 18) { + // 跳到下一个工作日的9:00 result.setDate(result.getDate() + 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() + 1); + } + currentHour = 9; + currentMinute = 0; } - result.setHours(9 + overflowHours, overflowMinutes, 0, 0); + const totalMinutes = currentHour * 60 + currentMinute + workingHours * 60; + const finalHour = Math.floor(totalMinutes / 60); + const finalMinute = totalMinutes % 60; + + if (finalHour >= 18) { + const overflowHours = finalHour - 18; + const overflowMinutes = finalMinute; + result.setDate(result.getDate() + 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() + 1); + } + result.setHours(9 + overflowHours, overflowMinutes, 0, 0); + } else { + result.setHours(finalHour, finalMinute, 0, 0); + } } else { - result.setHours(finalHour, finalMinute, 0, 0); + // 负向:从当前时间向前回退工作小时,规范到工作时间窗口 + if (currentHour > 18) { + // 当天超过18:00,先归位到18:00 + result.setHours(18, 0, 0, 0); + currentHour = 18; + currentMinute = 0; + } else if (currentHour < 9) { + // 早于9:00,跳到前一个工作日的18:00 + result.setDate(result.getDate() - 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() - 1); + } + result.setHours(18, 0, 0, 0); + currentHour = 18; + currentMinute = 0; + } + + const totalMinutes = currentHour * 60 + currentMinute - workingHours * 60; + if (totalMinutes >= 9 * 60) { + const finalHour = Math.floor(totalMinutes / 60); + const finalMinute = totalMinutes % 60; + result.setHours(finalHour, finalMinute, 0, 0); + } else { + // 需要跨到前一个工作日,计算欠缺分钟数 + let deficit = 9 * 60 - totalMinutes; // 需要从前一工作日的18:00再退回的分钟数 + // 跳到前一个工作日 + result.setDate(result.getDate() - 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() - 1); + } + // 从18:00开始退 deficit 分钟 + result.setHours(18, 0, 0, 0); + result.setMinutes(result.getMinutes() - deficit); + } } } @@ -1254,24 +1292,25 @@ export default function App() { // 添加工作日 - 使用表格配置的休息日与节点自定义跳过日期 const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => { const result = new Date(startDate); - let addedDays = 0; + if (!businessDays || businessDays === 0) return result; + const isNegative = businessDays < 0; + const absDays = Math.abs(businessDays); + let processedDays = 0; - // 处理小数天数:先添加整数天,再处理小数部分 - const wholeDays = Math.floor(businessDays); - const fractionalDays = businessDays - wholeDays; - - // 添加整数工作日 - while (addedDays < wholeDays) { - result.setDate(result.getDate() + 1); + // 先处理整数工作日 + const wholeDays = Math.floor(absDays); + while (processedDays < wholeDays) { + result.setDate(result.getDate() + (isNegative ? -1 : 1)); if (isBusinessDay(result, weekendDays, excludedDates)) { - addedDays++; + processedDays++; } } - // 处理小数部分(转换为小时,按24小时制) + // 再处理小数部分(按24小时制) + const fractionalDays = absDays - wholeDays; if (fractionalDays > 0) { - const hoursToAdd = fractionalDays * 24; // 1天=24小时 - result.setHours(result.getHours() + hoursToAdd); + const hours = fractionalDays * 24; + result.setHours(result.getHours() + (isNegative ? -hours : hours)); } return result; @@ -1398,6 +1437,53 @@ export default function App() { return; } } + } else if (recordId && tableId === OMS_BOARD_TABLE_ID) { + // 从OMS看板匹配对应的货期记录后,尝试获取其起始时间 + try { + const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID); + const omsRecord = await omsTable.getRecordById(recordId); + const planTextRaw = omsRecord?.fields?.[OMS_PLAN_TEXT_FIELD_ID]; + const planVersionRaw = omsRecord?.fields?.[OMS_PLAN_VERSION_FIELD_ID]; + const planText = extractText(planTextRaw)?.trim(); + let planVersion: number | null = null; + if (typeof planVersionRaw === 'number') { + planVersion = planVersionRaw; + } else if (typeof planVersionRaw === 'string') { + const m = planVersionRaw.match(/\d+/); + if (m) planVersion = parseInt(m[0], 10); + } else if (planVersionRaw && typeof planVersionRaw === 'object') { + const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text; + if (typeof v === 'number') planVersion = v; + else if (typeof v === 'string') { + const m = v.match(/\d+/); + if (m) planVersion = parseInt(m[0], 10); + } + } + + if (planText && planVersion !== null) { + const deliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion); + if (deliveryRecordId) { + const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + const deliveryRecord = await deliveryTable.getRecordById(deliveryRecordId); + const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID]; + if (startTimeValue) { + let extractedStartTime: Date | null = null; + if (typeof startTimeValue === 'number') { + extractedStartTime = new Date(startTimeValue); + } else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) { + const timestamp = startTimeValue[0]; + if (typeof timestamp === 'number') extractedStartTime = new Date(timestamp); + } + if (extractedStartTime && !isNaN(extractedStartTime.getTime())) { + setStartTime(extractedStartTime); + return; + } + } + } + } + } catch (e) { + console.warn('从OMS看板匹配起始时间失败,使用当前时间:', e); + } } // 如果没有找到有效的起始时间,使用当前时间 @@ -1409,6 +1495,42 @@ export default function App() { } }; + // 根据OMS看板的“货期计划”和“计划版本”匹配货期记录ID + const findDeliveryRecordIdByPlan = async (planText: string, planVersion: number): Promise => { + try { + const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + // 拉取一定数量的记录进行匹配(如需可优化为分页/索引) + const recordsResult = await deliveryTable.getRecords({ pageSize: 5000 }); + const records = recordsResult.records || []; + for (const rec of records) { + const fields = rec?.fields || {}; + const recordIdsTextVal = fields[DELIVERY_RECORD_IDS_FIELD_ID]; + const versionVal = fields[DELIVERY_VERSION_FIELD_ID]; + const recordIdsText = extractText(recordIdsTextVal)?.trim(); + let versionNum: number | null = null; + if (typeof versionVal === 'number') versionNum = versionVal; + else if (typeof versionVal === 'string') { + const m = versionVal.match(/\d+/); + if (m) versionNum = parseInt(m[0], 10); + } else if (versionVal && typeof versionVal === 'object') { + const v = (versionVal as any).value ?? (versionVal as any).text; + if (typeof v === 'number') versionNum = v; + else if (typeof v === 'string') { + const m = v.match(/\d+/); + if (m) versionNum = parseInt(m[0], 10); + } + } + if (recordIdsText && versionNum !== null && recordIdsText === planText && versionNum === planVersion) { + return rec.id || rec.recordId || null; + } + } + return null; + } catch (error) { + console.error('匹配货期记录失败:', error); + return null; + } + }; + // 加载可用表列表 const loadAvailableTables = async () => { try { @@ -2331,15 +2453,12 @@ export default function App() { const currentAdjustment = newAdjustments[nodeIndex] || 0; const newAdjustment = currentAdjustment + adjustment; - // 防止调整后的时效值小于0(优先使用原始时效值,其次使用上次重算后的值) + // 允许调整后的时效值为负数,用于向前回退结束时间 const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number') ? timelineResults[nodeIndex]!.timelineValue : (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number') ? timelineResults[nodeIndex]!.adjustedTimelineValue : 0; - if (baseValue + newAdjustment < 0) { - return null; - } // 检查当前调整的节点是否为周转周期节点 const currentNodeName = timelineResults[nodeIndex]?.nodeName; @@ -2406,9 +2525,9 @@ export default function App() { // 获取调整规则描述 const ruleDescription = result.ruleDescription || ''; - // 计算节点的结束时间 + // 计算节点的结束时间(允许负时效值向前回退) let nodeEndTime; - if (adjustedTimelineValue > 0) { + if (adjustedTimelineValue !== 0) { const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); if (nodeCalculationMethod === 'internal') { nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); @@ -2420,10 +2539,10 @@ export default function App() { } // 计算跳过的天数 - const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime; + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); const estimatedStartStr = formatDate(adjustedStartTime); - const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0'; + const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0'; const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); // 计算时间范围内实际跳过的自定义日期 @@ -2447,7 +2566,7 @@ export default function App() { }; // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 - if (adjustedTimelineValue > 0) { + if (adjustedTimelineValue !== 0) { cumulativeStartTime = new Date(nodeEndTime); } } @@ -2569,7 +2688,7 @@ export default function App() { let nodeEndTime: Date; const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : []; - if (adjustedTimelineValue > 0) { + if (adjustedTimelineValue !== 0) { const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); if (nodeCalculationMethod === '内部') { nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); @@ -2581,10 +2700,10 @@ export default function App() { } // 计算跳过的天数 - const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime; + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); const estimatedStartStr = formatDate(adjustedStartTime); - const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0'; + const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0'; const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); // 计算时间范围内实际跳过的自定义日期 @@ -2608,7 +2727,7 @@ export default function App() { }; // 更新累积时间:优先使用当前节点的实际完成时间,否则使用预计完成时间 - if (adjustedTimelineValue > 0) { + if (adjustedTimelineValue !== 0) { if (actualCompletionDates[i]) { // 如果当前节点有实际完成时间,使用实际完成时间 cumulativeStartTime = new Date(actualCompletionDates[i]!); @@ -3029,12 +3148,12 @@ export default function App() { startDateRule: selectedResult.startDateRule, dateAdjustmentRule: selectedResult.dateAdjustmentRule, nodeCalculationState: { - hasValidTimelineValue: selectedResult.timelineValue > 0, + hasValidTimelineValue: typeof selectedResult.timelineValue === 'number' && selectedResult.timelineValue !== 0, hasValidStartTime: Boolean(nodeStartTs), hasValidEndTime: Boolean(nodeEndTs), calculationTimestamp: new Date().getTime(), originalTimelineValue: selectedResult.timelineValue, - finalAdjustedValue: selectedResult.adjustedTimelineValue || selectedResult.timelineValue + finalAdjustedValue: (selectedResult.adjustedTimelineValue ?? selectedResult.timelineValue) }, chainAdjustmentNode: { nodeIndex: selectedIndex, @@ -4448,7 +4567,7 @@ export default function App() { } } - if (timelineValue && timelineValue > 0) { + if (timelineValue && timelineValue !== 0) { // 根据计算方式调整开始时间 const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); @@ -4492,7 +4611,7 @@ export default function App() { }); // 更新累积时间 - if (timelineValue && timelineValue > 0) { + if (timelineValue && timelineValue !== 0) { cumulativeStartTime = new Date(nodeEndTime); } } @@ -5245,14 +5364,53 @@ export default function App() { } return; } - if (tableId && tableId !== DELIVERY_RECORD_TABLE_ID) { + if (tableId === DELIVERY_RECORD_TABLE_ID) { + setSelectedDeliveryRecordId(recordId); + await loadProcessDataFromDeliveryRecord(recordId); + } else if (tableId === OMS_BOARD_TABLE_ID) { + // 支持在OMS看板选中记录后读取:通过货期计划 + 计划版本匹配货期记录 + const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID); + const omsRecord = await omsTable.getRecordById(recordId); + const planTextRaw = omsRecord?.fields?.[OMS_PLAN_TEXT_FIELD_ID]; + const planVersionRaw = omsRecord?.fields?.[OMS_PLAN_VERSION_FIELD_ID]; + const planText = extractText(planTextRaw)?.trim(); + let planVersion: number | null = null; + if (typeof planVersionRaw === 'number') { + planVersion = planVersionRaw; + } else if (typeof planVersionRaw === 'string') { + const m = planVersionRaw.match(/\d+/); + if (m) planVersion = parseInt(m[0], 10); + } else if (planVersionRaw && typeof planVersionRaw === 'object') { + const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text; + if (typeof v === 'number') planVersion = v; + else if (typeof v === 'string') { + const m = v.match(/\d+/); + if (m) planVersion = parseInt(m[0], 10); + } + } + + if (!planText || planVersion === null) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: 'OMS看板记录缺少货期计划或计划版本' }); + } + return; + } + + const matchedDeliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion); + if (!matchedDeliveryRecordId) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: '未能在货期记录表中匹配到对应记录' }); + } + return; + } + setSelectedDeliveryRecordId(matchedDeliveryRecordId); + await loadProcessDataFromDeliveryRecord(matchedDeliveryRecordId); + } else { if (bitable.ui.showToast) { - await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录表中选择记录' }); + await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录或OMS看板表中选择记录' }); } return; } - setSelectedDeliveryRecordId(recordId); - await loadProcessDataFromDeliveryRecord(recordId); } catch (e) { console.error('读取当前选中记录失败:', e); if (bitable.ui.showToast) { @@ -5579,16 +5737,16 @@ export default function App() { // 使用二分搜索反推工作日数(按0.5天粒度),使得正向计算的结束时间尽量贴近目标日期 const dayMs = 1000 * 60 * 60 * 24; - const approxNatural = Math.max(0, (targetDate.getTime() - adjustedStart.getTime()) / dayMs); + const approxNatural = (targetDate.getTime() - adjustedStart.getTime()) / dayMs; const endFor = (bd: number): Date => { - if (bd <= 0) return new Date(adjustedStart); + if (bd === 0) return new Date(adjustedStart); return calcMethod === '内部' ? addInternalBusinessTime(new Date(adjustedStart), bd, weekendDays, excludedDates) : addBusinessDaysWithHolidays(new Date(adjustedStart), bd, weekendDays, excludedDates); }; - let lo = 0; - let hi = Math.max(1, approxNatural + 50); + let lo = approxNatural - 50; + let hi = approxNatural + 50; for (let it = 0; it < 40; it++) { const mid = (lo + hi) / 2; const end = endFor(mid);