import { useState, useEffect, useCallback, useRef } from 'react'; import { supabase } from '../supabase/client'; import { CACHE_CONFIG } from '../config/app'; import { useInventoryRealtime } from './useRealtime'; import type { Tables } from '../supabase/types'; import type { Product, ProductWithInventory, ProductYarnRatio, FabricBatch, ProductInRecord } from '../types'; interface UseInventoryDataOptions { companyId: string | null; activeType: string; } interface InventoryDataState { products: ProductWithInventory[]; productRatios: Record; batches: FabricBatch[]; loading: boolean; error: Error | null; } // Supabase Row 类型别名 type InventoryRecord = Tables<'inventory_records'>; type ProductInventoryRecord = Tables<'product_inventory_records'>; type ProductOutboundRecord = Tables<'product_outbound_records'>; // 部分字段查询的局部类型(避免使用 any) interface PlanSummary { id: string; fabric_code: string; completed_quantity: number; } interface PlanFactoryRef { plan_id: string; factory_id: string; } interface CompanyRef { id: string; name: string; } /** * 优化的库存数据获取 Hook * 使用缓存和防抖来减少不必要的请求 */ export function useInventoryData({ companyId, activeType }: UseInventoryDataOptions) { const [state, setState] = useState({ products: [], productRatios: {}, batches: [], loading: true, error: null }); const cacheRef = useRef<{ data: InventoryDataState | null; timestamp: number; key: string; }>({ data: null, timestamp: 0, key: '' }); const CACHE_DURATION = CACHE_CONFIG.inventoryDataDuration; const fetchData = useCallback(async () => { if (!companyId || activeType !== 'raw_fabric') { setState(prev => ({ ...prev, loading: false })); return; } const cacheKey = `${companyId}-${activeType}`; const now = Date.now(); // 检查缓存 if ( cacheRef.current.data && cacheRef.current.key === cacheKey && now - cacheRef.current.timestamp < CACHE_DURATION ) { setState(cacheRef.current.data); return; } setState(prev => ({ ...prev, loading: true, error: null })); try { // 并行获取数据 const [productsResult, batchesResult, outboundResult] = await Promise.all([ // 获取产品列表 supabase .from('products') .select('*') .eq('company_id', companyId) .order('created_at', { ascending: false }), // 获取入库记录 supabase .from('product_inventory_records') .select('*') .eq('company_id', companyId) .order('created_at', { ascending: false }), // 获取出库记录 supabase .from('product_outbound_records') .select('*') .eq('company_id', companyId) .order('created_at', { ascending: false }) ]); if (productsResult.error) throw productsResult.error; if (batchesResult.error) throw batchesResult.error; if (outboundResult.error) throw outboundResult.error; const productsData = productsResult.data || []; const batchesData: ProductInventoryRecord[] = batchesResult.data || []; const outboundData: ProductOutboundRecord[] = outboundResult.data || []; // 获取纱线配比 const productIds = productsData.map(p => p.id); let ratiosMap: Record = {}; if (productIds.length > 0) { const { data: ratiosData, error: ratiosError } = await supabase .from('product_yarn_ratios') .select('*') .in('product_id', productIds); if (ratiosError) throw ratiosError; ratiosData?.forEach((r: ProductYarnRatio) => { if (!ratiosMap[r.product_id]) ratiosMap[r.product_id] = []; ratiosMap[r.product_id].push(r); }); } // 获取纺织厂入库数据 const fabricCodes = productsData.map(p => p.fabric_code); const { data: plansData } = await supabase .from('production_plans') .select('id, fabric_code, completed_quantity') .eq('purchaser_id', companyId) .in('fabric_code', fabricCodes); const typedPlansData: PlanSummary[] = plansData || []; const planIds = typedPlansData.map(p => p.id).filter(Boolean); // 获取工厂信息 const { data: planFactoriesData } = await supabase .from('plan_factories') .select('plan_id, factory_id') .in('plan_id', planIds) .eq('factory_type', 'textile'); const typedPlanFactories: PlanFactoryRef[] = planFactoriesData || []; const factoryIds = [...new Set(typedPlanFactories.map(pf => pf.factory_id).filter(Boolean))]; const { data: factoriesData } = await supabase .from('companies') .select('id, name') .in('id', factoryIds); const typedFactories: CompanyRef[] = factoriesData || []; // 建立映射 const planToFactoryName: Record = {}; typedPlanFactories.forEach((pf: PlanFactoryRef) => { const factory = typedFactories.find(f => f.id === pf.factory_id); if (factory) { planToFactoryName[pf.plan_id] = factory.name; } }); // 获取入库记录 let textileInventoryRecords: InventoryRecord[] = []; if (planIds.length > 0) { const { data: records } = await supabase .from('inventory_records') .select('plan_id, quantity, rolls, warehouse_location') .in('plan_id', planIds); textileInventoryRecords = (records || []) as InventoryRecord[]; } // 处理数据 const updatedProducts = processInventoryData({ products: productsData, plansData: typedPlansData, textileInventoryRecords, planToFactoryName, batchesData, outboundData }); // 为 batches 数据注入产品信息 const batchesWithProduct: FabricBatch[] = batchesData.map((batch: ProductInventoryRecord) => { const product = productsData.find((p: Product) => p.id === batch.product_id); return { id: batch.id, product_id: batch.product_id, company_id: batch.company_id || '', batch_no: batch.batch_no, rolls: batch.rolls, meters: batch.meters, notes: batch.notes, created_at: batch.created_at, product: product || undefined } as FabricBatch; }); const newState: InventoryDataState = { products: updatedProducts, productRatios: ratiosMap, batches: batchesWithProduct, loading: false, error: null }; // 更新缓存 cacheRef.current = { data: newState, timestamp: now, key: cacheKey }; setState(newState); } catch (err) { setState(prev => ({ ...prev, loading: false, error: err instanceof Error ? err : new Error('Unknown error') })); } }, [companyId, activeType]); useEffect(() => { fetchData(); }, [fetchData]); // 使用统一的实时订阅管理器 useInventoryRealtime(companyId, () => { // 清除缓存并刷新 cacheRef.current.timestamp = 0; fetchData(); }); return { ...state, refetch: fetchData }; } // 处理库存数据的辅助函数 interface ProcessInventoryDataParams { products: Product[]; plansData: PlanSummary[]; textileInventoryRecords: InventoryRecord[]; planToFactoryName: Record; batchesData: ProductInventoryRecord[]; outboundData: ProductOutboundRecord[]; } function processInventoryData({ products, plansData, textileInventoryRecords, planToFactoryName, batchesData, outboundData }: ProcessInventoryDataParams): ProductWithInventory[] { // 建立 plan_id 到 fabric_code 的映射 const planToFabricCode: Record = {}; const planCompletedMap: Record = {}; plansData.forEach((plan: PlanSummary) => { planToFabricCode[plan.id] = plan.fabric_code; planCompletedMap[plan.id] = plan.completed_quantity || 0; }); // 按 plan_id 汇总库存 const planInventoryMap: Record = {}; textileInventoryRecords.forEach((r: InventoryRecord) => { const factoryName = planToFactoryName[r.plan_id] || r.warehouse_location || '纺织厂'; const factoryId = planToFactoryName[r.plan_id] || r.warehouse_location || `factory_${r.plan_id}`; if (!planInventoryMap[r.plan_id]) { planInventoryMap[r.plan_id] = { meters: 0, rolls: 0, factory_id: factoryId, factory_name: factoryName }; } planInventoryMap[r.plan_id].meters += (Number(r.quantity) || 0); planInventoryMap[r.plan_id].rolls += (Number(r.rolls) || 0); }); // 汇总每个坯布码的库存 const fabricCodeInventory: Record = {}; const fabricCodeFactoryInventory: Record> = {}; Object.entries(planInventoryMap).forEach(([planId, inventory]) => { const fabricCode = planToFabricCode[planId]; if (fabricCode) { if (!fabricCodeInventory[fabricCode]) { fabricCodeInventory[fabricCode] = { meters: 0, rolls: 0 }; } fabricCodeInventory[fabricCode].meters += inventory.meters || 0; fabricCodeInventory[fabricCode].rolls += inventory.rolls || 0; if (!fabricCodeFactoryInventory[fabricCode]) { fabricCodeFactoryInventory[fabricCode] = {}; } const factoryKey = inventory.factory_name || 'unknown'; if (!fabricCodeFactoryInventory[fabricCode][factoryKey]) { fabricCodeFactoryInventory[fabricCode][factoryKey] = { factory_id: inventory.factory_id || 'unknown', factory_name: inventory.factory_name || '纺织厂', rolls: 0, meters: 0, records: [] }; } fabricCodeFactoryInventory[fabricCode][factoryKey].rolls += inventory.rolls || 0; fabricCodeFactoryInventory[fabricCode][factoryKey].meters += inventory.meters || 0; } }); // 处理入库记录 - 合并自录入和纺织厂入库 const inRecordsMap: Record = {}; // 1. 先添加自录入的入库记录 batchesData.forEach((b: ProductInventoryRecord) => { if (!inRecordsMap[b.product_id]) { inRecordsMap[b.product_id] = []; } inRecordsMap[b.product_id].push({ id: b.id, product_id: b.product_id, company_id: b.company_id, batch_no: b.batch_no, rolls: b.rolls || 0, meters: b.meters || 0, notes: b.notes, operator_id: b.operator_id, warehouse_id: b.warehouse_id, created_at: b.created_at, time: b.created_at, source: 'self' }); }); // 2. 添加纺织厂的入库记录 // 建立 fabric_code 到 product_id 的映射 const fabricCodeToProductId: Record = {}; products.forEach((p: Product) => { fabricCodeToProductId[p.fabric_code] = p.id; }); // 将纺织厂入库记录按 fabric_code 关联到对应产品 textileInventoryRecords.forEach((r: InventoryRecord) => { const fabricCode = planToFabricCode[r.plan_id]; const productId = fabricCodeToProductId[fabricCode]; if (productId) { if (!inRecordsMap[productId]) { inRecordsMap[productId] = []; } const factoryName = planToFactoryName[r.plan_id] || r.warehouse_location || '纺织厂'; inRecordsMap[productId].push({ id: r.id || `${r.plan_id}-${r.created_at}`, product_id: productId, company_id: r.company_id, batch_no: '-', rolls: Number(r.rolls) || 0, meters: Number(r.quantity) || 0, notes: `来自${factoryName}`, operator_id: r.operator_id, warehouse_id: r.warehouse_id, created_at: r.created_at, time: r.created_at, factory_name: factoryName, factory_id: r.plan_id, source: 'textile' }); } }); // 按时间排序 Object.keys(inRecordsMap).forEach(productId => { inRecordsMap[productId].sort((a, b) => new Date(b.time || b.created_at).getTime() - new Date(a.time || a.created_at).getTime()); }); // 处理出库记录 - 按 product_id 汇总 const outboundMap: Record = {}; outboundData.forEach((r: ProductOutboundRecord) => { if (!outboundMap[r.product_id]) { outboundMap[r.product_id] = { totalRolls: 0, totalMeters: 0, records: [] }; } outboundMap[r.product_id].totalRolls += Number(r.rolls) || 0; outboundMap[r.product_id].totalMeters += Number(r.meters) || 0; outboundMap[r.product_id].records.push(r); }); // 计算每个产品的自录入库总量(从 batchesData) const selfInventoryMap: Record = {}; batchesData.forEach((b: ProductInventoryRecord) => { if (!selfInventoryMap[b.product_id]) { selfInventoryMap[b.product_id] = { rolls: 0, meters: 0 }; } selfInventoryMap[b.product_id].rolls += Number(b.rolls) || 0; selfInventoryMap[b.product_id].meters += Number(b.meters) || 0; }); // 更新产品数据 - 扣除出库数量 return products.map((p: Product) => { const textileInventory = fabricCodeInventory[p.fabric_code] || { meters: 0, rolls: 0 }; const factoryInventories = fabricCodeFactoryInventory[p.fabric_code] || {}; // 使用实时计算的入库记录,而不是 products 表中的字段 const selfInventory = selfInventoryMap[p.id] || { rolls: 0, meters: 0 }; const outboundInfo = outboundMap[p.id] || { totalRolls: 0, totalMeters: 0, records: [] }; // 计算实际可用库存(入库 - 出库) const totalInRolls = selfInventory.rolls + textileInventory.rolls; const totalInMeters = selfInventory.meters + textileInventory.meters; const availableRolls = Math.max(0, totalInRolls - outboundInfo.totalRolls); const availableMeters = Math.max(0, totalInMeters - outboundInfo.totalMeters); return { ...p, production_price: p.production_price || 0, raw_fabric_rolls: availableRolls, raw_fabric_meters: availableMeters, total_stock: availableMeters, factory_inventories: factoryInventories, in_records: inRecordsMap[p.id] || [], out_records: outboundInfo.records, total_in_rolls: totalInRolls, total_in_meters: totalInMeters, total_out_rolls: outboundInfo.totalRolls, total_out_meters: outboundInfo.totalMeters }; }) as ProductWithInventory[]; }