439 lines
15 KiB
TypeScript
439 lines
15 KiB
TypeScript
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[];
|
||
}
|