iloom-flatten/src/hooks/useInventoryData.ts

410 lines
13 KiB
TypeScript
Raw Normal View History

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[];
}