260 lines
7.1 KiB
TypeScript
260 lines
7.1 KiB
TypeScript
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||
|
|
import { supabase } from '../supabase/client';
|
||
|
|
import { statusMap } from '../utils/constants';
|
||
|
|
import { usePlanRealtime } from './useRealtime';
|
||
|
|
import type {
|
||
|
|
ProductionPlan,
|
||
|
|
PlanWithSteps,
|
||
|
|
PlanProcessStep,
|
||
|
|
Company,
|
||
|
|
PlanFactory
|
||
|
|
} from '../types';
|
||
|
|
|
||
|
|
interface UsePlanDataOptions {
|
||
|
|
companyId: string | null;
|
||
|
|
pageSize?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PlanDataState {
|
||
|
|
plans: PlanWithSteps[];
|
||
|
|
factories: Company[];
|
||
|
|
planFactories: PlanFactory[];
|
||
|
|
operatorNames: Record<string, string>;
|
||
|
|
loading: boolean;
|
||
|
|
loadingMore: boolean;
|
||
|
|
hasMore: boolean;
|
||
|
|
currentPage: number;
|
||
|
|
error: Error | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 优化的计划数据获取 Hook
|
||
|
|
* 支持分页加载和缓存
|
||
|
|
*/
|
||
|
|
export function usePlanData({ companyId, pageSize = 20 }: UsePlanDataOptions) {
|
||
|
|
const [state, setState] = useState<PlanDataState>({
|
||
|
|
plans: [],
|
||
|
|
factories: [],
|
||
|
|
planFactories: [],
|
||
|
|
operatorNames: {},
|
||
|
|
loading: true,
|
||
|
|
loadingMore: false,
|
||
|
|
hasMore: true,
|
||
|
|
currentPage: 1,
|
||
|
|
error: null
|
||
|
|
});
|
||
|
|
|
||
|
|
// 使用 ref 存储缓存,避免不必要的重渲染
|
||
|
|
const cacheRef = useRef<{
|
||
|
|
data: PlanDataState | null;
|
||
|
|
timestamp: number;
|
||
|
|
}>({ data: null, timestamp: 0 });
|
||
|
|
|
||
|
|
const CACHE_DURATION = 10000; // 10秒缓存
|
||
|
|
|
||
|
|
// 获取操作人名称
|
||
|
|
const fetchOperatorNames = useCallback(async (operatorIds: string[]) => {
|
||
|
|
if (operatorIds.length === 0) return {};
|
||
|
|
|
||
|
|
const { data: profilesData } = await supabase
|
||
|
|
.from('profiles')
|
||
|
|
.select('id, username')
|
||
|
|
.in('id', operatorIds);
|
||
|
|
|
||
|
|
const names: Record<string, string> = {};
|
||
|
|
profilesData?.forEach(p => {
|
||
|
|
names[p.id] = p.username;
|
||
|
|
});
|
||
|
|
return names;
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 处理计划数据
|
||
|
|
const processPlansData = useCallback((
|
||
|
|
planData: ProductionPlan[],
|
||
|
|
stepsData: PlanProcessStep[] | null,
|
||
|
|
inventoryData: any[] | null,
|
||
|
|
priceHistoryData: any[] | null
|
||
|
|
): PlanWithSteps[] => {
|
||
|
|
return planData.map(plan => {
|
||
|
|
const inventoryRecords = (inventoryData || [])
|
||
|
|
.filter(r => r.plan_id === plan.id)
|
||
|
|
.map(r => ({ time: r.created_at, quantity: r.quantity, rolls: r.rolls }));
|
||
|
|
|
||
|
|
const priceHistory = (priceHistoryData || [])
|
||
|
|
.filter(h => h.plan_id === plan.id)
|
||
|
|
.map(h => ({
|
||
|
|
id: h.id,
|
||
|
|
old_price: h.old_price,
|
||
|
|
new_price: h.new_price,
|
||
|
|
change_amount: h.change_amount,
|
||
|
|
operator_name: h.operator_name,
|
||
|
|
change_reason: h.change_reason,
|
||
|
|
created_at: h.created_at
|
||
|
|
}));
|
||
|
|
|
||
|
|
return {
|
||
|
|
...plan,
|
||
|
|
processSteps: (stepsData || []).filter(s => s.plan_id === plan.id),
|
||
|
|
inventoryRecords,
|
||
|
|
priceHistory
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// 获取数据
|
||
|
|
const fetchData = useCallback(async (page: number = 1, append: boolean = false) => {
|
||
|
|
if (!companyId) {
|
||
|
|
setState(prev => ({ ...prev, loading: false }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const isFirstPage = page === 1;
|
||
|
|
|
||
|
|
if (isFirstPage) {
|
||
|
|
// 检查缓存
|
||
|
|
const now = Date.now();
|
||
|
|
if (cacheRef.current.data && now - cacheRef.current.timestamp < CACHE_DURATION) {
|
||
|
|
setState(cacheRef.current.data);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setState(prev => ({ ...prev, loading: true, error: null }));
|
||
|
|
} else {
|
||
|
|
setState(prev => ({ ...prev, loadingMore: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const offset = (page - 1) * pageSize;
|
||
|
|
|
||
|
|
// 获取计划
|
||
|
|
const { data: planData, error: planError } = await supabase
|
||
|
|
.from('production_plans')
|
||
|
|
.select('*')
|
||
|
|
.eq('purchaser_id', companyId)
|
||
|
|
.order('created_at', { ascending: false })
|
||
|
|
.range(offset, offset + pageSize - 1);
|
||
|
|
|
||
|
|
if (planError) throw planError;
|
||
|
|
|
||
|
|
if (!planData || planData.length === 0) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
hasMore: false,
|
||
|
|
plans: isFirstPage ? [] : prev.plans
|
||
|
|
}));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const planIds = planData.map(p => p.id);
|
||
|
|
|
||
|
|
// 并行获取关联数据
|
||
|
|
const [
|
||
|
|
stepsResult,
|
||
|
|
inventoryResult,
|
||
|
|
priceHistoryResult,
|
||
|
|
pfResult
|
||
|
|
] = await Promise.all([
|
||
|
|
supabase.from('plan_process_steps').select('*').in('plan_id', planIds),
|
||
|
|
supabase
|
||
|
|
.from('inventory_records')
|
||
|
|
.select('plan_id, quantity, rolls, created_at')
|
||
|
|
.in('plan_id', planIds)
|
||
|
|
.order('created_at', { ascending: false })
|
||
|
|
.limit(100),
|
||
|
|
supabase
|
||
|
|
.from('production_price_history')
|
||
|
|
.select('*')
|
||
|
|
.in('plan_id', planIds)
|
||
|
|
.order('created_at', { ascending: false })
|
||
|
|
.limit(50),
|
||
|
|
supabase.from('plan_factories').select('*').in('plan_id', planIds)
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 获取操作人信息
|
||
|
|
const operatorIds = [...new Set(
|
||
|
|
(stepsResult.data || [])
|
||
|
|
.filter(s => s.operator_id)
|
||
|
|
.map(s => s.operator_id)
|
||
|
|
.filter(Boolean)
|
||
|
|
)] as string[];
|
||
|
|
|
||
|
|
const operatorNames = await fetchOperatorNames(operatorIds);
|
||
|
|
|
||
|
|
// 获取工厂信息
|
||
|
|
let factories: Company[] = [];
|
||
|
|
if (pfResult.data && pfResult.data.length > 0) {
|
||
|
|
const factoryIds = pfResult.data.map(pf => pf.factory_id);
|
||
|
|
const { data: factoryData } = await supabase
|
||
|
|
.from('companies')
|
||
|
|
.select('*')
|
||
|
|
.in('id', factoryIds);
|
||
|
|
factories = factoryData || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理计划数据
|
||
|
|
const plansWithSteps = processPlansData(
|
||
|
|
planData,
|
||
|
|
stepsResult.data,
|
||
|
|
inventoryResult.data,
|
||
|
|
priceHistoryResult.data
|
||
|
|
);
|
||
|
|
|
||
|
|
const newState: PlanDataState = {
|
||
|
|
plans: append ? [...state.plans, ...plansWithSteps] : plansWithSteps,
|
||
|
|
factories: isFirstPage ? factories : state.factories,
|
||
|
|
planFactories: isFirstPage ? (pfResult.data || []) : state.planFactories,
|
||
|
|
operatorNames: isFirstPage ? operatorNames : { ...state.operatorNames, ...operatorNames },
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
hasMore: planData.length === pageSize,
|
||
|
|
currentPage: page,
|
||
|
|
error: null
|
||
|
|
};
|
||
|
|
|
||
|
|
// 更新缓存
|
||
|
|
if (isFirstPage) {
|
||
|
|
cacheRef.current = { data: newState, timestamp: Date.now() };
|
||
|
|
}
|
||
|
|
|
||
|
|
setState(newState);
|
||
|
|
} catch (err) {
|
||
|
|
setState(prev => ({
|
||
|
|
...prev,
|
||
|
|
loading: false,
|
||
|
|
loadingMore: false,
|
||
|
|
error: err instanceof Error ? err : new Error('Unknown error')
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}, [companyId, pageSize, state.plans, fetchOperatorNames, processPlansData]);
|
||
|
|
|
||
|
|
// 加载更多
|
||
|
|
const loadMore = useCallback(() => {
|
||
|
|
if (state.loadingMore || !state.hasMore) return;
|
||
|
|
fetchData(state.currentPage + 1, true);
|
||
|
|
}, [fetchData, state.currentPage, state.hasMore, state.loadingMore]);
|
||
|
|
|
||
|
|
// 刷新数据
|
||
|
|
const refetch = useCallback(() => {
|
||
|
|
cacheRef.current.timestamp = 0;
|
||
|
|
fetchData(1, false);
|
||
|
|
}, [fetchData]);
|
||
|
|
|
||
|
|
// 初始加载
|
||
|
|
useEffect(() => {
|
||
|
|
fetchData(1, false);
|
||
|
|
}, [fetchData]);
|
||
|
|
|
||
|
|
// 使用统一的实时订阅管理器
|
||
|
|
usePlanRealtime(companyId, () => {
|
||
|
|
cacheRef.current.timestamp = 0;
|
||
|
|
fetchData(1, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
...state,
|
||
|
|
loadMore,
|
||
|
|
refetch
|
||
|
|
};
|
||
|
|
}
|