10
10
This commit is contained in:
657
src/App.tsx
657
src/App.tsx
@ -1,7 +1,7 @@
|
|||||||
import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk';
|
import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk';
|
||||||
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress } from '@douyinfe/semi-ui';
|
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress } from '@douyinfe/semi-ui';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
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 { zhCN } from 'date-fns/locale';
|
||||||
import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService';
|
import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService';
|
||||||
|
|
||||||
@ -54,6 +54,14 @@ export default function App() {
|
|||||||
const [expectedDate, setExpectedDate] = useState<Date | null>(null);
|
const [expectedDate, setExpectedDate] = useState<Date | null>(null);
|
||||||
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
|
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
|
||||||
const [startTime, setStartTime] = useState<Date | null>(null);
|
const [startTime, setStartTime] = useState<Date | null>(null);
|
||||||
|
const [timelineDirection, setTimelineDirection] = useState<'forward' | 'backward'>('forward');
|
||||||
|
const [calculatedRequiredStartTime, setCalculatedRequiredStartTime] = useState<Date | null>(null);
|
||||||
|
const [allocationVisible, setAllocationVisible] = useState(false);
|
||||||
|
const [allocationMode, setAllocationMode] = useState<'auto' | 'manual'>('auto');
|
||||||
|
const [allocationExtraDays, setAllocationExtraDays] = useState<number>(0);
|
||||||
|
const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({});
|
||||||
|
const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState<any[]>([]);
|
||||||
|
const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({});
|
||||||
|
|
||||||
// 预览相关状态(已移除未使用的 previewLoading 状态)
|
// 预览相关状态(已移除未使用的 previewLoading 状态)
|
||||||
|
|
||||||
@ -121,6 +129,12 @@ export default function App() {
|
|||||||
setSelectedLabels({});
|
setSelectedLabels({});
|
||||||
setExpectedDate(null);
|
setExpectedDate(null);
|
||||||
setStartTime(null);
|
setStartTime(null);
|
||||||
|
setCalculatedRequiredStartTime(null);
|
||||||
|
setAllocationVisible(false);
|
||||||
|
setAllocationExtraDays(0);
|
||||||
|
setAllocationDraft({});
|
||||||
|
setAllocationExcluded({});
|
||||||
|
setAllocationNodesSnapshot([]);
|
||||||
setTimelineVisible(false);
|
setTimelineVisible(false);
|
||||||
setTimelineResults([]);
|
setTimelineResults([]);
|
||||||
setTimelineAdjustments({});
|
setTimelineAdjustments({});
|
||||||
@ -224,6 +238,7 @@ export default function App() {
|
|||||||
|
|
||||||
// 新表ID(批量生成表)
|
// 新表ID(批量生成表)
|
||||||
const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY';
|
const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY';
|
||||||
|
const BATCH_ROW_NUMBER_FIELD_ID = 'fldiqlTVsU';
|
||||||
|
|
||||||
const fetchAllRecordsByPage = async (table: any, params?: any) => {
|
const fetchAllRecordsByPage = async (table: any, params?: any) => {
|
||||||
let token: any = undefined;
|
let token: any = undefined;
|
||||||
@ -245,6 +260,14 @@ export default function App() {
|
|||||||
return res?.total || 0;
|
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 {
|
try {
|
||||||
const batchTable = await bitable.base.getTable(BATCH_TABLE_ID);
|
const batchTable = await bitable.base.getTable(BATCH_TABLE_ID);
|
||||||
const total = await getRecordTotalByPage(batchTable);
|
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);
|
setBatchStartRow(1);
|
||||||
setBatchEndRow(total > 0 ? total : 1);
|
setBatchEndRow(totalRows > 0 ? totalRows : 1);
|
||||||
setBatchProcessingTotal(total);
|
setBatchProcessingTotal(totalRows);
|
||||||
} catch {
|
} catch {
|
||||||
setBatchTotalRows(0);
|
setBatchTotalRows(0);
|
||||||
setBatchStartRow(1);
|
setBatchStartRow(1);
|
||||||
@ -273,6 +305,8 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const restoreBaseBufferDaysFromSnapshot = (snapshot: any) => {
|
const restoreBaseBufferDaysFromSnapshot = (snapshot: any) => {
|
||||||
|
const snapshotDirection = snapshot?.timelineDirection;
|
||||||
|
if (snapshotDirection === 'backward') return;
|
||||||
const candidates: any[] = [
|
const candidates: any[] = [
|
||||||
snapshot?.bufferManagement?.baseDays,
|
snapshot?.bufferManagement?.baseDays,
|
||||||
snapshot?.baseBufferDays,
|
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读取节点详情并还原流程数据
|
// 根据货期记录ID读取节点详情并还原流程数据
|
||||||
const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => {
|
const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => {
|
||||||
if (!deliveryRecordId) {
|
if (!deliveryRecordId) {
|
||||||
@ -355,6 +404,7 @@ export default function App() {
|
|||||||
if (deliverySnapStr && deliverySnapStr.trim() !== '') {
|
if (deliverySnapStr && deliverySnapStr.trim() !== '') {
|
||||||
setIsRestoringSnapshot(true);
|
setIsRestoringSnapshot(true);
|
||||||
const snapshot = JSON.parse(deliverySnapStr);
|
const snapshot = JSON.parse(deliverySnapStr);
|
||||||
|
restoreTimelineDirectionFromSnapshot(snapshot);
|
||||||
restoreBaseBufferDaysFromSnapshot(snapshot);
|
restoreBaseBufferDaysFromSnapshot(snapshot);
|
||||||
|
|
||||||
// 恢复页面与全局状态
|
// 恢复页面与全局状态
|
||||||
@ -494,6 +544,7 @@ export default function App() {
|
|||||||
const snapshot = JSON.parse(snapStr);
|
const snapshot = JSON.parse(snapStr);
|
||||||
if (Array.isArray(snapshot.timelineResults)) {
|
if (Array.isArray(snapshot.timelineResults)) {
|
||||||
setIsRestoringSnapshot(true);
|
setIsRestoringSnapshot(true);
|
||||||
|
restoreTimelineDirectionFromSnapshot(snapshot);
|
||||||
restoreBaseBufferDaysFromSnapshot(snapshot);
|
restoreBaseBufferDaysFromSnapshot(snapshot);
|
||||||
|
|
||||||
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
|
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
|
||||||
@ -627,6 +678,7 @@ export default function App() {
|
|||||||
if (snapStr && snapStr.trim() !== '') {
|
if (snapStr && snapStr.trim() !== '') {
|
||||||
setIsRestoringSnapshot(true); // 开始快照还原
|
setIsRestoringSnapshot(true); // 开始快照还原
|
||||||
const snapshot = JSON.parse(snapStr);
|
const snapshot = JSON.parse(snapStr);
|
||||||
|
restoreTimelineDirectionFromSnapshot(snapshot);
|
||||||
restoreBaseBufferDaysFromSnapshot(snapshot);
|
restoreBaseBufferDaysFromSnapshot(snapshot);
|
||||||
// 恢复页面状态
|
// 恢复页面状态
|
||||||
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
|
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
|
||||||
@ -967,6 +1019,7 @@ export default function App() {
|
|||||||
nodeSnapshots.length === globalSnapshotData.totalNodes) {
|
nodeSnapshots.length === globalSnapshotData.totalNodes) {
|
||||||
|
|
||||||
// 重组完整的 timelineResults
|
// 重组完整的 timelineResults
|
||||||
|
restoreTimelineDirectionFromSnapshot(globalSnapshotData);
|
||||||
restoreBaseBufferDaysFromSnapshot(globalSnapshotData);
|
restoreBaseBufferDaysFromSnapshot(globalSnapshotData);
|
||||||
setTimelineResults(nodeSnapshots);
|
setTimelineResults(nodeSnapshots);
|
||||||
setTimelineVisible(true);
|
setTimelineVisible(true);
|
||||||
@ -1855,6 +1908,7 @@ export default function App() {
|
|||||||
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
|
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
|
||||||
const currentExpectedDate = overrideData?.expectedDate || expectedDate;
|
const currentExpectedDate = overrideData?.expectedDate || expectedDate;
|
||||||
const currentStartTime = overrideData?.startTime || startTime;
|
const currentStartTime = overrideData?.startTime || startTime;
|
||||||
|
const isBackward = timelineDirection === 'backward';
|
||||||
|
|
||||||
console.log('=== handleCalculateTimeline - 使用的数据 ===');
|
console.log('=== handleCalculateTimeline - 使用的数据 ===');
|
||||||
console.log('currentSelectedRecords:', currentSelectedRecords);
|
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) {
|
if (!skipValidation) {
|
||||||
// 检查是否选择了多条记录
|
// 检查是否选择了多条记录
|
||||||
@ -2220,10 +2296,13 @@ export default function App() {
|
|||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
// 3. 按顺序为每个匹配的流程节点查找时效数据并计算累积时间
|
// 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++) {
|
for (let i = 0; i < nodesToProcess.length; i++) {
|
||||||
const processNode = matchedProcessNodes[i];
|
const processNode = nodesToProcess[i];
|
||||||
let timelineValue = null;
|
let timelineValue = null;
|
||||||
let matchedTimelineRecord = null;
|
let matchedTimelineRecord = null;
|
||||||
|
|
||||||
@ -2447,9 +2526,29 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let nodeStartTime = new Date(cumulativeStartTime);
|
// 获取当前节点的计算方式
|
||||||
|
let nodeCalculationMethod = '外部'; // 默认值
|
||||||
|
if (matchedCandidates.length > 0) {
|
||||||
|
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
|
||||||
|
} else if (matchedTimelineRecord) {
|
||||||
|
const calculationMethodField = matchedTimelineRecord.fields[calculationMethodFieldId];
|
||||||
|
if (calculationMethodField) {
|
||||||
|
if (typeof calculationMethodField === 'string') {
|
||||||
|
nodeCalculationMethod = calculationMethodField;
|
||||||
|
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
|
||||||
|
nodeCalculationMethod = calculationMethodField.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeStartTime: Date;
|
||||||
|
let nodeEndTime: Date;
|
||||||
|
let ruleDescription = '';
|
||||||
|
let timelineResult: { startDate: string; endDate: string };
|
||||||
|
|
||||||
|
if (!isBackward) {
|
||||||
|
nodeStartTime = new Date(cumulativeTime);
|
||||||
|
|
||||||
// 应用起始日期调整规则
|
|
||||||
if (processNode.startDateRule) {
|
if (processNode.startDateRule) {
|
||||||
let ruleJson = '';
|
let ruleJson = '';
|
||||||
if (typeof processNode.startDateRule === 'string') {
|
if (typeof processNode.startDateRule === 'string') {
|
||||||
@ -2464,15 +2563,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用JSON格式日期调整规则
|
|
||||||
let ruleDescription = '';
|
|
||||||
if (processNode.dateAdjustmentRule) {
|
if (processNode.dateAdjustmentRule) {
|
||||||
console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule);
|
console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule);
|
||||||
let ruleText = '';
|
let ruleText = '';
|
||||||
if (typeof processNode.dateAdjustmentRule === 'string') {
|
if (typeof processNode.dateAdjustmentRule === 'string') {
|
||||||
ruleText = processNode.dateAdjustmentRule;
|
ruleText = processNode.dateAdjustmentRule;
|
||||||
} else if (Array.isArray(processNode.dateAdjustmentRule)) {
|
} else if (Array.isArray(processNode.dateAdjustmentRule)) {
|
||||||
// 处理富文本数组格式
|
|
||||||
ruleText = processNode.dateAdjustmentRule
|
ruleText = processNode.dateAdjustmentRule
|
||||||
.filter((item: any) => item.type === 'text')
|
.filter((item: any) => item.type === 'text')
|
||||||
.map((item: any) => item.text)
|
.map((item: any) => item.text)
|
||||||
@ -2492,29 +2588,13 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前节点的计算方式
|
timelineResult = timelineValue
|
||||||
let nodeCalculationMethod = '外部'; // 默认值
|
? calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod)
|
||||||
if (matchedCandidates.length > 0) {
|
: {
|
||||||
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
|
|
||||||
} else if (matchedTimelineRecord) {
|
|
||||||
const calculationMethodField = matchedTimelineRecord.fields[calculationMethodFieldId];
|
|
||||||
if (calculationMethodField) {
|
|
||||||
if (typeof calculationMethodField === 'string') {
|
|
||||||
nodeCalculationMethod = calculationMethodField;
|
|
||||||
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
|
|
||||||
nodeCalculationMethod = calculationMethodField.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const timelineResult = timelineValue ?
|
|
||||||
calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod) :
|
|
||||||
{
|
|
||||||
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
|
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
|
||||||
endDate: '未找到时效数据'
|
endDate: '未找到时效数据'
|
||||||
};
|
};
|
||||||
|
|
||||||
let nodeEndTime: Date;
|
|
||||||
if (timelineValue) {
|
if (timelineValue) {
|
||||||
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
|
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
|
||||||
if (nodeCalculationMethod === '内部') {
|
if (nodeCalculationMethod === '内部') {
|
||||||
@ -2525,6 +2605,28 @@ export default function App() {
|
|||||||
} else {
|
} else {
|
||||||
nodeEndTime = new Date(nodeStartTime);
|
nodeEndTime = new Date(nodeStartTime);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
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: '未找到时效数据'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 计算跳过的天数
|
// 计算跳过的天数
|
||||||
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays);
|
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays);
|
||||||
@ -2533,7 +2635,7 @@ export default function App() {
|
|||||||
// 计算时间范围内实际跳过的自定义日期
|
// 计算时间范围内实际跳过的自定义日期
|
||||||
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []);
|
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []);
|
||||||
|
|
||||||
results.push({
|
(isBackward ? results.unshift.bind(results) : results.push.bind(results))({
|
||||||
processOrder: processNode.processOrder,
|
processOrder: processNode.processOrder,
|
||||||
nodeName: processNode.nodeName,
|
nodeName: processNode.nodeName,
|
||||||
matchedLabels: processNode.matchedLabels,
|
matchedLabels: processNode.matchedLabels,
|
||||||
@ -2569,7 +2671,7 @@ export default function App() {
|
|||||||
|
|
||||||
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
|
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
|
||||||
if (timelineValue) {
|
if (timelineValue) {
|
||||||
cumulativeStartTime = new Date(nodeEndTime);
|
cumulativeTime = isBackward ? new Date(nodeStartTime) : new Date(nodeEndTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, {
|
console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, {
|
||||||
@ -2580,9 +2682,42 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
setTimelineResults(results);
|
||||||
if (showUI) {
|
if (showUI && !delayShowTimelineModal) {
|
||||||
setTimelineVisible(true);
|
setTimelineVisible(true);
|
||||||
|
} else if (showUI && delayShowTimelineModal) {
|
||||||
|
setTimelineVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('按流程顺序计算的时效结果:', results);
|
console.log('按流程顺序计算的时效结果:', results);
|
||||||
@ -2846,6 +2981,149 @@ export default function App() {
|
|||||||
return updatedResults;
|
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'),若不存在则返回最后一项的日期
|
// 获取有效的最后完成日期(忽略 '时效值为0'),若不存在则返回最后一项的日期
|
||||||
const getLastValidCompletionDateFromResults = (results: any[]): Date | null => {
|
const getLastValidCompletionDateFromResults = (results: any[]): Date | null => {
|
||||||
if (!Array.isArray(results) || results.length === 0) return 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 => {
|
const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => {
|
||||||
try {
|
try {
|
||||||
|
if (timelineDirection === 'backward') return 0;
|
||||||
const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整)
|
const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整)
|
||||||
const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整)
|
const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整)
|
||||||
|
|
||||||
@ -2898,6 +3177,19 @@ export default function App() {
|
|||||||
|
|
||||||
// 重新计算时间线的函数
|
// 重新计算时间线的函数
|
||||||
const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => {
|
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];
|
const updatedResults = [...timelineResults];
|
||||||
|
|
||||||
// 找到第一个被调整的节点索引
|
// 找到第一个被调整的节点索引
|
||||||
@ -3085,10 +3377,19 @@ export default function App() {
|
|||||||
|
|
||||||
// 当起始时间变更时,重新以最新起始时间为基准重算全流程
|
// 当起始时间变更时,重新以最新起始时间为基准重算全流程
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (timelineDirection !== 'forward') return;
|
||||||
if (timelineResults.length > 0 && !isRestoringSnapshot) {
|
if (timelineResults.length > 0 && !isRestoringSnapshot) {
|
||||||
recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点
|
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(() => {
|
useEffect(() => {
|
||||||
@ -3101,6 +3402,7 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) {
|
if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) {
|
||||||
initialSnapshotRef.current = {
|
initialSnapshotRef.current = {
|
||||||
|
timelineDirection,
|
||||||
startTime,
|
startTime,
|
||||||
expectedDate,
|
expectedDate,
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
@ -3143,6 +3445,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
const s = initialSnapshotRef.current;
|
const s = initialSnapshotRef.current;
|
||||||
setIsRestoringSnapshot(true);
|
setIsRestoringSnapshot(true);
|
||||||
|
if (s.timelineDirection === 'forward' || s.timelineDirection === 'backward') {
|
||||||
|
setTimelineDirection(s.timelineDirection);
|
||||||
|
}
|
||||||
setStartTime(s.startTime || null);
|
setStartTime(s.startTime || null);
|
||||||
setExpectedDate(s.expectedDate || null);
|
setExpectedDate(s.expectedDate || null);
|
||||||
setSelectedLabels(s.selectedLabels || {});
|
setSelectedLabels(s.selectedLabels || {});
|
||||||
@ -3447,6 +3752,7 @@ export default function App() {
|
|||||||
colorText,
|
colorText,
|
||||||
text2,
|
text2,
|
||||||
mode,
|
mode,
|
||||||
|
timelineDirection,
|
||||||
selectedLabels: currentSelectedLabels,
|
selectedLabels: currentSelectedLabels,
|
||||||
expectedDateTimestamp,
|
expectedDateTimestamp,
|
||||||
expectedDateString,
|
expectedDateString,
|
||||||
@ -3465,6 +3771,8 @@ export default function App() {
|
|||||||
}),
|
}),
|
||||||
labelSelectionComplete: Object.keys(selectedLabels).length > 0
|
labelSelectionComplete: Object.keys(selectedLabels).length > 0
|
||||||
},
|
},
|
||||||
|
...(timelineDirection !== 'backward'
|
||||||
|
? {
|
||||||
bufferManagement: {
|
bufferManagement: {
|
||||||
baseDays: baseBuferDays,
|
baseDays: baseBuferDays,
|
||||||
totalAdjustments,
|
totalAdjustments,
|
||||||
@ -3473,6 +3781,8 @@ export default function App() {
|
|||||||
hasAppliedSuggestedBuffer,
|
hasAppliedSuggestedBuffer,
|
||||||
lastSuggestedApplied: lastSuggestedApplied ?? 0
|
lastSuggestedApplied: lastSuggestedApplied ?? 0
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
chainAdjustmentSystem: {
|
chainAdjustmentSystem: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lastCalculationTime: new Date().getTime(),
|
lastCalculationTime: new Date().getTime(),
|
||||||
@ -3482,7 +3792,8 @@ export default function App() {
|
|||||||
calculationTimestamp: new Date().getTime(),
|
calculationTimestamp: new Date().getTime(),
|
||||||
totalNodes: timelineResults.length,
|
totalNodes: timelineResults.length,
|
||||||
hasValidResults: timelineResults.length > 0,
|
hasValidResults: timelineResults.length > 0,
|
||||||
lastCalculationMode: mode
|
lastCalculationMode: mode,
|
||||||
|
timelineDirection
|
||||||
},
|
},
|
||||||
totalNodes: timelineResults.length,
|
totalNodes: timelineResults.length,
|
||||||
isGlobalSnapshot: true
|
isGlobalSnapshot: true
|
||||||
@ -4060,18 +4371,42 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rows = await fetchAllRecordsByPage(batchTable);
|
const rows = await fetchAllRecordsByPage(batchTable);
|
||||||
const total = rows.length;
|
|
||||||
const startIndex1 = range?.start && range.start > 0 ? range.start : 1;
|
const rowsWithNo = rows.map((row: any, idx: number) => {
|
||||||
const endIndex1 = range?.end && range.end > 0 ? range.end : total;
|
const f = row?.fields || {};
|
||||||
const minIndex = Math.max(1, Math.min(startIndex1, total));
|
const no = parseBatchRowNumber(f[BATCH_ROW_NUMBER_FIELD_ID]);
|
||||||
const maxIndex = Math.max(minIndex, Math.min(endIndex1, total));
|
return { row, idx, no };
|
||||||
setBatchProcessingTotal(maxIndex - minIndex + 1);
|
});
|
||||||
|
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;
|
let processed = 0;
|
||||||
for (let i = minIndex - 1; i < maxIndex; i++) {
|
for (let j = 0; j < selected.length; j++) {
|
||||||
if (batchAbortRef.current) {
|
if (batchAbortRef.current) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const row = rows[i];
|
const { row, idx, no } = selected[j];
|
||||||
|
const displayIndex = typeof no === 'number' ? no : (idx + 1);
|
||||||
const f = row.fields || {};
|
const f = row.fields || {};
|
||||||
const getText = (name: string) => extractText(f[nameToId.get(name) || '']);
|
const getText = (name: string) => extractText(f[nameToId.get(name) || '']);
|
||||||
const foreignId = getText('foreignId');
|
const foreignId = getText('foreignId');
|
||||||
@ -4132,11 +4467,11 @@ export default function App() {
|
|||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
setBatchProcessedCount(p => p + 1);
|
setBatchProcessedCount(p => p + 1);
|
||||||
setBatchFailureCount(fCount => fCount + 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setBatchCurrentRowInfo({ index: i + 1, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
|
setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
|
||||||
setCurrentForeignId(foreignId || '');
|
setCurrentForeignId(foreignId || '');
|
||||||
setCurrentStyleText(styleText || '');
|
setCurrentStyleText(styleText || '');
|
||||||
setCurrentColorText(colorText || '');
|
setCurrentColorText(colorText || '');
|
||||||
@ -4178,13 +4513,13 @@ export default function App() {
|
|||||||
throw 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 {
|
} 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) {
|
} catch (statusErr: any) {
|
||||||
console.warn('回写批量状态字段失败', statusErr);
|
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++;
|
processed++;
|
||||||
setBatchProcessedCount(p => p + 1);
|
setBatchProcessedCount(p => p + 1);
|
||||||
@ -4192,12 +4527,12 @@ export default function App() {
|
|||||||
} else {
|
} else {
|
||||||
setBatchProcessedCount(p => p + 1);
|
setBatchProcessedCount(p => p + 1);
|
||||||
setBatchFailureCount(fCount => fCount + 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) {
|
} catch (rowErr: any) {
|
||||||
setBatchProcessedCount(p => p + 1);
|
setBatchProcessedCount(p => p + 1);
|
||||||
setBatchFailureCount(fCount => fCount + 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) {
|
if (bitable.ui.showToast) {
|
||||||
@ -5000,6 +5335,179 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="倒推盈余分配"
|
||||||
|
visible={allocationVisible}
|
||||||
|
maskClosable={false}
|
||||||
|
onCancel={() => {
|
||||||
|
setAllocationVisible(false);
|
||||||
|
setTimelineVisible(true);
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
|
||||||
|
const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded);
|
||||||
|
setAllocationDraft(draft);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
按比例重算
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAllocationDraft({});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清空分配
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => { setAllocationVisible(false); setTimelineVisible(true); }}>暂不分配</Button>
|
||||||
|
<Button type="primary" onClick={applyAllocationDraft}>应用分配</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
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 (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
<Card>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center' }}>
|
||||||
|
<Text>客户期望日期:{expectedDate ? formatDate(expectedDate, 'CHINESE_DATE') : '-'}</Text>
|
||||||
|
<Text>业务起始日期:{startTime ? formatDate(startTime) : '-'}</Text>
|
||||||
|
<Text>倒推要求起始日期:{calculatedRequiredStartTime ? formatDate(calculatedRequiredStartTime) : '-'}</Text>
|
||||||
|
<Text strong style={{ color: statusColor }}>
|
||||||
|
盈余:{roundedTarget} 天(剩余 {remaining}{overAllocated > 0 ? `,已超配 ${overAllocated}` : ''})
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
<Text strong>分配方式</Text>
|
||||||
|
<Select
|
||||||
|
style={{ width: 240 }}
|
||||||
|
value={allocationMode}
|
||||||
|
onChange={(v) => setAllocationMode(v as 'auto' | 'manual')}
|
||||||
|
optionList={[
|
||||||
|
{ value: 'auto', label: '系统自动分配(按时效比例)' },
|
||||||
|
{ value: 'manual', label: '业务自行分配' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '节点',
|
||||||
|
dataIndex: 'nodeName',
|
||||||
|
key: 'nodeName',
|
||||||
|
width: 220,
|
||||||
|
render: (_: any, row: any) => {
|
||||||
|
const idx = row.index as number;
|
||||||
|
const nodeName = row.nodeName as string;
|
||||||
|
const isTurnoverNode = nodeName === '周转周期';
|
||||||
|
const excluded = !!allocationExcluded?.[idx] || isTurnoverNode;
|
||||||
|
return (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<span style={{ opacity: excluded ? 0.5 : 1 }}>{nodeName}</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="tertiary"
|
||||||
|
disabled={isTurnoverNode}
|
||||||
|
onClick={() => {
|
||||||
|
const currentlyExcluded = excluded;
|
||||||
|
setAllocationExcluded(prev => {
|
||||||
|
const next = { ...(prev || {}) };
|
||||||
|
if (currentlyExcluded) {
|
||||||
|
delete next[idx];
|
||||||
|
} else {
|
||||||
|
next[idx] = true;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setAllocationDraft(prev => {
|
||||||
|
const next = { ...(prev || {}) };
|
||||||
|
if (!currentlyExcluded) {
|
||||||
|
next[idx] = 0;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{excluded ? '+' : '-'}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ title: '基准时效(天)', dataIndex: 'base', key: 'base', width: 120, render: (v) => (Number(v) || 0).toFixed(2) },
|
||||||
|
{
|
||||||
|
title: allocationMode === 'manual' ? '分配盈余(天)' : '系统分配(天)',
|
||||||
|
dataIndex: 'allocated',
|
||||||
|
key: 'allocated',
|
||||||
|
width: 160,
|
||||||
|
render: (_: any, row: any) => {
|
||||||
|
const idx = row.index as number;
|
||||||
|
const isTurnoverNode = row.nodeName === '周转周期';
|
||||||
|
const isExcluded = !!allocationExcluded?.[idx];
|
||||||
|
const val = Number(allocationDraft[idx]) || 0;
|
||||||
|
|
||||||
|
if (allocationMode === 'manual') {
|
||||||
|
const otherSum = Math.round((roundedSum - val) * 100) / 100;
|
||||||
|
const maxAllowed = Math.max(0, Math.round((roundedTarget - otherSum) * 100) / 100);
|
||||||
|
const maxForControl = maxAllowed < val ? val : maxAllowed;
|
||||||
|
return (
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={maxForControl}
|
||||||
|
step={0.1}
|
||||||
|
value={val}
|
||||||
|
disabled={isTurnoverNode || isExcluded}
|
||||||
|
onChange={(v) => {
|
||||||
|
const raw = Math.round((Number(v) || 0) * 100) / 100;
|
||||||
|
const n = Math.max(0, Math.min(raw, maxAllowed));
|
||||||
|
setAllocationDraft(prev => ({ ...(prev || {}), [idx]: n }));
|
||||||
|
}}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{val.toFixed(2)}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataSource={nodes.map((n, index) => {
|
||||||
|
const base = typeof n?.timelineValue === 'number'
|
||||||
|
? n.timelineValue
|
||||||
|
: (typeof n?.adjustedTimelineValue === 'number' ? n.adjustedTimelineValue : 0);
|
||||||
|
return {
|
||||||
|
key: index,
|
||||||
|
index,
|
||||||
|
nodeName: n?.nodeName || `节点${index + 1}`,
|
||||||
|
base: Number.isFinite(base) ? base : 0,
|
||||||
|
allocated: Number(allocationDraft[index]) || 0,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* 时效计算结果模态框 */}
|
{/* 时效计算结果模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
@ -5953,6 +6461,45 @@ export default function App() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '24px' }}>
|
||||||
|
<Text strong style={{ display: 'block', marginBottom: '8px' }}>计算方向</Text>
|
||||||
|
<Select
|
||||||
|
style={{ width: '300px' }}
|
||||||
|
value={timelineDirection}
|
||||||
|
onChange={(value) => setTimelineDirection(value as 'forward' | 'backward')}
|
||||||
|
>
|
||||||
|
<Select.Option value="forward">正推(从起始时间开始)</Select.Option>
|
||||||
|
<Select.Option value="backward">倒推(以客户期望日期为目标)</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '24px' }}>
|
||||||
|
<Text strong style={{ display: 'block', marginBottom: '8px' }}>起始日期</Text>
|
||||||
|
<DatePicker
|
||||||
|
className="input-enhanced"
|
||||||
|
style={{ width: '300px' }}
|
||||||
|
placeholder="请选择起始日期"
|
||||||
|
value={startTime ?? undefined}
|
||||||
|
onChange={(date) => {
|
||||||
|
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' && (
|
||||||
|
<Text type="secondary" style={{ marginLeft: '12px' }}>
|
||||||
|
倒推模式必填
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 客户期望日期选择 */}
|
{/* 客户期望日期选择 */}
|
||||||
<div style={{ marginTop: '24px' }}>
|
<div style={{ marginTop: '24px' }}>
|
||||||
<Text strong style={{ display: 'block', marginBottom: '8px' }}>客户期望日期</Text>
|
<Text strong style={{ display: 'block', marginBottom: '8px' }}>客户期望日期</Text>
|
||||||
@ -6004,11 +6551,15 @@ export default function App() {
|
|||||||
? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true)
|
? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true)
|
||||||
: handleCalculateTimeline()}
|
: handleCalculateTimeline()}
|
||||||
loading={timelineLoading}
|
loading={timelineLoading}
|
||||||
disabled={timelineLoading || Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => {
|
disabled={
|
||||||
|
timelineLoading
|
||||||
|
|| (timelineDirection === 'backward' && (!expectedDate || !startTime))
|
||||||
|
|| Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => {
|
||||||
const val = (selectedLabels as any)[key];
|
const val = (selectedLabels as any)[key];
|
||||||
if (Array.isArray(val)) return val.length === 0;
|
if (Array.isArray(val)) return val.length === 0;
|
||||||
return !(typeof val === 'string' && val.trim().length > 0);
|
return !(typeof val === 'string' && val.trim().length > 0);
|
||||||
})}
|
})
|
||||||
|
}
|
||||||
style={{ minWidth: '160px' }}
|
style={{ minWidth: '160px' }}
|
||||||
>
|
>
|
||||||
{labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'}
|
{labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'}
|
||||||
|
|||||||
Reference in New Issue
Block a user