2026-05-26 05:15:10 +00:00
|
|
|
|
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,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
PlanFactory,
|
|
|
|
|
|
YarnRatio,
|
|
|
|
|
|
Product
|
2026-05-26 05:15:10 +00:00
|
|
|
|
} from '../types';
|
|
|
|
|
|
|
|
|
|
|
|
interface UsePlanDataOptions {
|
|
|
|
|
|
companyId: string | null;
|
|
|
|
|
|
pageSize?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PlanDataState {
|
|
|
|
|
|
plans: PlanWithSteps[];
|
|
|
|
|
|
factories: Company[];
|
|
|
|
|
|
planFactories: PlanFactory[];
|
|
|
|
|
|
operatorNames: Record<string, string>;
|
2026-05-27 03:17:46 +00:00
|
|
|
|
yarnRatios: Record<string, YarnRatio[]>;
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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: {},
|
2026-05-27 03:17:46 +00:00
|
|
|
|
yarnRatios: {},
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
priceHistoryData: any[] | null,
|
|
|
|
|
|
yarnRatiosData: any[] | null,
|
|
|
|
|
|
productsData: Product[] | null
|
2026-05-26 05:15:10 +00:00
|
|
|
|
): 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 }));
|
2026-05-27 03:17:46 +00:00
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
const yarnRatios = (yarnRatiosData || [])
|
|
|
|
|
|
.filter(y => y.plan_id === plan.id)
|
|
|
|
|
|
.map(y => ({
|
|
|
|
|
|
id: y.id,
|
|
|
|
|
|
plan_id: y.plan_id,
|
|
|
|
|
|
yarn_name: y.yarn_name,
|
|
|
|
|
|
ratio: y.ratio,
|
|
|
|
|
|
amount_per_meter: y.amount_per_meter,
|
|
|
|
|
|
total_amount: y.total_amount,
|
|
|
|
|
|
yarn_type: y.yarn_type,
|
|
|
|
|
|
created_at: y.created_at
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 查找关联的产品信息(通过fabric_code匹配)
|
|
|
|
|
|
const product = (productsData || []).find(p =>
|
|
|
|
|
|
p.fabric_code === plan.fabric_code && p.company_id === plan.purchaser_id
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
return {
|
|
|
|
|
|
...plan,
|
|
|
|
|
|
processSteps: (stepsData || []).filter(s => s.plan_id === plan.id),
|
|
|
|
|
|
inventoryRecords,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
priceHistory,
|
|
|
|
|
|
yarnRatios,
|
|
|
|
|
|
// 从产品表获取的字段
|
|
|
|
|
|
image_url: product?.image_url || null,
|
|
|
|
|
|
warp_weight: product?.warp_weight || null,
|
|
|
|
|
|
weft_weight: product?.weft_weight || null,
|
|
|
|
|
|
total_weight: product?.total_weight || null,
|
|
|
|
|
|
weight: product?.weight || null
|
2026-05-26 05:15:10 +00:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取数据
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
// 获取计划对应的采购商ID列表
|
|
|
|
|
|
const purchaserIds = [...new Set(planData.map(p => p.purchaser_id))];
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
// 并行获取关联数据
|
|
|
|
|
|
const [
|
|
|
|
|
|
stepsResult,
|
|
|
|
|
|
inventoryResult,
|
|
|
|
|
|
priceHistoryResult,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
pfResult,
|
|
|
|
|
|
yarnRatiosResult,
|
|
|
|
|
|
productsResult
|
2026-05-26 05:15:10 +00:00
|
|
|
|
] = 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),
|
2026-05-27 03:17:46 +00:00
|
|
|
|
supabase.from('plan_factories').select('*').in('plan_id', planIds),
|
|
|
|
|
|
supabase.from('yarn_ratios').select('*').in('plan_id', planIds),
|
|
|
|
|
|
// 获取采购商的产品信息(用于获取克重、图片等)
|
|
|
|
|
|
supabase
|
|
|
|
|
|
.from('products')
|
|
|
|
|
|
.select('*')
|
|
|
|
|
|
.in('company_id', purchaserIds)
|
2026-05-26 05:15:10 +00:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取操作人信息
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-06-04 13:57:36 +00:00
|
|
|
|
// 获取工厂信息(包括所有纺织厂和水洗厂,不仅限于有关联的)
|
|
|
|
|
|
const { data: allFactories } = await supabase
|
|
|
|
|
|
.from('companies')
|
|
|
|
|
|
.select('*')
|
|
|
|
|
|
.in('role', ['textile', 'washing']);
|
|
|
|
|
|
const factories = allFactories || [];
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
// 处理计划数据
|
|
|
|
|
|
const plansWithSteps = processPlansData(
|
|
|
|
|
|
planData,
|
|
|
|
|
|
stepsResult.data,
|
|
|
|
|
|
inventoryResult.data,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
priceHistoryResult.data,
|
|
|
|
|
|
yarnRatiosResult.data,
|
|
|
|
|
|
productsResult.data
|
2026-05-26 05:15:10 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
// 构建纱线配比映射
|
|
|
|
|
|
const yarnRatiosMap: Record<string, YarnRatio[]> = {};
|
|
|
|
|
|
(yarnRatiosResult.data || []).forEach((y: any) => {
|
|
|
|
|
|
if (!yarnRatiosMap[y.plan_id]) yarnRatiosMap[y.plan_id] = [];
|
|
|
|
|
|
yarnRatiosMap[y.plan_id].push(y as YarnRatio);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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 },
|
2026-05-27 03:17:46 +00:00
|
|
|
|
yarnRatios: isFirstPage ? yarnRatiosMap : { ...state.yarnRatios, ...yarnRatiosMap },
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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')
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-06-04 13:57:36 +00:00
|
|
|
|
}, [companyId, pageSize, fetchOperatorNames, processPlansData]);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
// 加载更多
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|