iloom-flatten/src/hooks/useInventoryData.ts

410 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react';
import { supabase } from '../supabase/client';
import { useInventoryRealtime } from './useRealtime';
import type {
Product,
ProductWithInventory,
ProductYarnRatio,
FabricBatch,
ProductInRecord
} from '../types';
interface UseInventoryDataOptions {
companyId: string | null;
activeType: string;
}
interface InventoryDataState {
products: ProductWithInventory[];
productRatios: Record<string, ProductYarnRatio[]>;
batches: FabricBatch[];
loading: boolean;
error: Error | null;
}
/**
* 优化的库存数据获取 Hook
* 使用缓存和防抖来减少不必要的请求
*/
export function useInventoryData({ companyId, activeType }: UseInventoryDataOptions) {
const [state, setState] = useState<InventoryDataState>({
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 = 5000; // 5秒缓存
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 = batchesResult.data || [];
const outboundData = outboundResult.data || [];
// 获取纱线配比
const productIds = productsData.map(p => p.id);
let ratiosMap: Record<string, ProductYarnRatio[]> = {};
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 planIds = plansData?.map((p: any) => 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 factoryIds = [...new Set((planFactoriesData || []).map(pf => pf.factory_id).filter(Boolean))];
const { data: factoriesData } = await supabase
.from('companies')
.select('id, name')
.in('id', factoryIds);
// 建立映射
const planToFactoryName: Record<string, string> = {};
planFactoriesData?.forEach((pf: any) => {
const factory = factoriesData?.find(f => f.id === pf.factory_id);
if (factory) {
planToFactoryName[pf.plan_id] = factory.name;
}
});
// 获取入库记录
let textileInventoryRecords: any[] = [];
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 || [];
}
// 处理数据
const updatedProducts = processInventoryData({
products: productsData,
plansData: plansData || [],
textileInventoryRecords,
planToFactoryName,
batchesData,
outboundData
});
const newState: InventoryDataState = {
products: updatedProducts,
productRatios: ratiosMap,
batches: batchesData as FabricBatch[],
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: any[];
textileInventoryRecords: any[];
planToFactoryName: Record<string, string>;
batchesData: any[];
outboundData: any[];
}
function processInventoryData({
products,
plansData,
textileInventoryRecords,
planToFactoryName,
batchesData,
outboundData
}: ProcessInventoryDataParams): ProductWithInventory[] {
// 建立 plan_id 到 fabric_code 的映射
const planToFabricCode: Record<string, string> = {};
const planCompletedMap: Record<string, number> = {};
plansData?.forEach((plan: any) => {
planToFabricCode[plan.id] = plan.fabric_code;
planCompletedMap[plan.id] = plan.completed_quantity || 0;
});
// 按 plan_id 汇总库存
const planInventoryMap: Record<string, { meters: number; rolls: number; factory_id: string; factory_name: string }> = {};
textileInventoryRecords?.forEach((r: any) => {
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<string, { meters: number; rolls: number }> = {};
const fabricCodeFactoryInventory: Record<string, Record<string, any>> = {};
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<string, ProductInRecord[]> = {};
// 1. 先添加自录入的入库记录
batchesData?.forEach((b: any) => {
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<string, string> = {};
products.forEach((p: any) => {
fabricCodeToProductId[p.fabric_code] = p.id;
});
// 将纺织厂入库记录按 fabric_code 关联到对应产品
textileInventoryRecords?.forEach((r: any) => {
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: r.batch_no || '-',
rolls: Number(r.rolls) || 0,
meters: Number(r.quantity) || 0,
notes: r.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<string, { totalRolls: number; totalMeters: number; records: any[] }> = {};
outboundData?.forEach((r: any) => {
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({
id: r.id,
product_id: r.product_id,
batch_no: r.batch_no,
rolls: r.rolls,
meters: r.meters,
notes: r.notes,
outbound_type: r.outbound_type,
washing_plan_id: r.washing_plan_id,
created_at: r.created_at,
time: r.created_at,
// 新增字段:生产批号和接手公司
source_batch_id: r.source_batch_id,
recipient_company_id: r.recipient_company_id,
recipient_company_name: r.recipient_company_name
});
});
// 计算每个产品的自录入库总量(从 batchesData
const selfInventoryMap: Record<string, { rolls: number; meters: number }> = {};
batchesData?.forEach((b: any) => {
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: any) => {
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[];
}