iloom-flatten/src/hooks/useInventoryData.ts

439 lines
15 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 { 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<string, ProductYarnRatio[]>;
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<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 = 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<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 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<string, string> = {};
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<string, string>;
batchesData: ProductInventoryRecord[];
outboundData: ProductOutboundRecord[];
}
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: PlanSummary) => {
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: 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<string, { meters: number; rolls: number }> = {};
const fabricCodeFactoryInventory: Record<string, Record<string, { factory_id: string; factory_name: string; rolls: number; meters: number; records: ProductInRecord[] }>> = {};
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: 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<string, string> = {};
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<string, { totalRolls: number; totalMeters: number; records: ProductOutboundRecord[] }> = {};
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<string, { rolls: number; meters: number }> = {};
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[];
}