feat: Implement real-time warehouse statistics with GraphQL resolver

- Added fulfillmentWarehouseStats GraphQL query and resolver
- Updated StatCard component to use percentChange from GraphQL
- Added comprehensive logging for debugging warehouse statistics
- Implemented 24-hour change tracking for warehouse metrics
- Added polling for real-time statistics updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Bivekich
2025-07-31 16:05:57 +03:00
parent 76a40e0eed
commit 8b66793ae7
2 changed files with 136 additions and 71 deletions

View File

@ -227,10 +227,30 @@ export function FulfillmentWarehouseDashboard() {
error: warehouseStatsError, error: warehouseStatsError,
refetch: refetchWarehouseStats, refetch: refetchWarehouseStats,
} = useQuery(GET_FULFILLMENT_WAREHOUSE_STATS, { } = useQuery(GET_FULFILLMENT_WAREHOUSE_STATS, {
fetchPolicy: "cache-and-network", fetchPolicy: "no-cache", // Принудительно обходим кеш
pollInterval: 60000, // Обновляем каждую минуту pollInterval: 60000, // Обновляем каждую минуту
}); });
// Логируем статистику склада для отладки
console.log("📊 WAREHOUSE STATS DEBUG:", {
loading: warehouseStatsLoading,
error: warehouseStatsError?.message,
data: warehouseStatsData,
hasData: !!warehouseStatsData?.fulfillmentWarehouseStats,
});
// Детальное логирование данных статистики
if (warehouseStatsData?.fulfillmentWarehouseStats) {
console.log("📈 DETAILED WAREHOUSE STATS:", {
products: warehouseStatsData.fulfillmentWarehouseStats.products,
goods: warehouseStatsData.fulfillmentWarehouseStats.goods,
defects: warehouseStatsData.fulfillmentWarehouseStats.defects,
pvzReturns: warehouseStatsData.fulfillmentWarehouseStats.pvzReturns,
fulfillmentSupplies: warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies,
sellerSupplies: warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies,
});
}
// Получаем данные магазинов, заказов и товаров // Получаем данные магазинов, заказов и товаров
const allCounterparties = counterpartiesData?.myCounterparties || []; const allCounterparties = counterpartiesData?.myCounterparties || [];
const sellerPartners = allCounterparties.filter( const sellerPartners = allCounterparties.filter(
@ -974,6 +994,7 @@ export function FulfillmentWarehouseDashboard() {
icon: Icon, icon: Icon,
current, current,
change, change,
percentChange,
description, description,
onClick, onClick,
}: { }: {
@ -981,10 +1002,14 @@ export function FulfillmentWarehouseDashboard() {
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
current: number; current: number;
change: number; change: number;
percentChange?: number;
description: string; description: string;
onClick?: () => void; onClick?: () => void;
}) => { }) => {
const percentChange = current > 0 ? (change / current) * 100 : 0; // Используем percentChange из GraphQL, если доступно, иначе вычисляем локально
const displayPercentChange = percentChange !== undefined && percentChange !== null && !isNaN(percentChange)
? percentChange
: (current > 0 ? (change / current) * 100 : 0);
return ( return (
<div <div
@ -1012,7 +1037,7 @@ export function FulfillmentWarehouseDashboard() {
change >= 0 ? "text-green-400" : "text-red-400" change >= 0 ? "text-green-400" : "text-red-400"
}`} }`}
> >
{percentChange.toFixed(1)}% {displayPercentChange.toFixed(1)}%
</span> </span>
</div> </div>
</div> </div>
@ -1199,6 +1224,7 @@ export function FulfillmentWarehouseDashboard() {
icon={Box} icon={Box}
current={warehouseStats.products.current} current={warehouseStats.products.current}
change={warehouseStats.products.change} change={warehouseStats.products.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.products?.percentChange}
description="Готовые к отправке" description="Готовые к отправке"
/> />
<StatCard <StatCard
@ -1206,6 +1232,7 @@ export function FulfillmentWarehouseDashboard() {
icon={Package} icon={Package}
current={warehouseStats.goods.current} current={warehouseStats.goods.current}
change={warehouseStats.goods.change} change={warehouseStats.goods.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.goods?.percentChange}
description="В обработке" description="В обработке"
/> />
<StatCard <StatCard
@ -1213,6 +1240,7 @@ export function FulfillmentWarehouseDashboard() {
icon={AlertTriangle} icon={AlertTriangle}
current={warehouseStats.defects.current} current={warehouseStats.defects.current}
change={warehouseStats.defects.change} change={warehouseStats.defects.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.defects?.percentChange}
description="Требует утилизации" description="Требует утилизации"
/> />
<StatCard <StatCard
@ -1220,6 +1248,7 @@ export function FulfillmentWarehouseDashboard() {
icon={RotateCcw} icon={RotateCcw}
current={warehouseStats.pvzReturns.current} current={warehouseStats.pvzReturns.current}
change={warehouseStats.pvzReturns.change} change={warehouseStats.pvzReturns.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.pvzReturns?.percentChange}
description="К обработке" description="К обработке"
/> />
<StatCard <StatCard
@ -1227,6 +1256,7 @@ export function FulfillmentWarehouseDashboard() {
icon={Wrench} icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current} current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change} change={warehouseStats.fulfillmentSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.fulfillmentSupplies?.percentChange}
description="Расходники, этикетки" description="Расходники, этикетки"
onClick={() => router.push("/fulfillment-warehouse/supplies")} onClick={() => router.push("/fulfillment-warehouse/supplies")}
/> />
@ -1235,6 +1265,7 @@ export function FulfillmentWarehouseDashboard() {
icon={Users} icon={Users}
current={warehouseStats.sellerSupplies.current} current={warehouseStats.sellerSupplies.current}
change={warehouseStats.sellerSupplies.change} change={warehouseStats.sellerSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies?.percentChange}
description="Материалы клиентов" description="Материалы клиентов"
/> />
</div> </div>

View File

@ -1010,6 +1010,7 @@ export const resolvers = {
// Статистика склада фулфилмента с изменениями за сутки // Статистика склада фулфилмента с изменениями за сутки
fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => { fulfillmentWarehouseStats: async (_: unknown, __: unknown, context: Context) => {
console.log("🔥 FULFILLMENT WAREHOUSE STATS RESOLVER CALLED");
if (!context.user) { if (!context.user) {
throw new GraphQLError("Требуется авторизация", { throw new GraphQLError("Требуется авторизация", {
extensions: { code: "UNAUTHENTICATED" }, extensions: { code: "UNAUTHENTICATED" },
@ -1035,35 +1036,66 @@ export const resolvers = {
const oneDayAgo = new Date(); const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1); oneDayAgo.setDate(oneDayAgo.getDate() - 1);
// Продукты (товары селлеров на складе) console.log(`🏢 Organization ID: ${organizationId}, Date 24h ago: ${oneDayAgo.toISOString()}`);
const products = await prisma.product.findMany({
// Сначала проверим ВСЕ заказы поставок
const allSupplyOrders = await prisma.supplyOrder.findMany({
where: { status: "DELIVERED" },
include: {
items: {
include: { product: true }
},
organization: { select: { id: true, name: true, type: true } }
}
});
console.log(`📦 ALL DELIVERED ORDERS: ${allSupplyOrders.length}`);
allSupplyOrders.forEach(order => {
console.log(` Order ${order.id}: org=${order.organizationId} (${order.organization?.name}), fulfillment=${order.fulfillmentCenterId}, items=${order.items.length}`);
});
// Продукты (товары от селлеров) - заказы К нам, но исключаем расходники фулфилмента
const allDeliveredOrders = await prisma.supplyOrder.findMany({
where: { where: {
organization: { fulfillmentCenterId: organizationId, // Доставлено к нам (фулфилменту)
type: "SELLER", status: "DELIVERED"
counterparties: { },
some: { include: {
OR: [ items: {
{ requesterId: organizationId }, include: { product: true }
{ receiverId: organizationId }
]
}
}
} }
} }
}); });
const productsCount = products.reduce((sum, p) => sum + p.quantity, 0); console.log(`🛒 ALL ORDERS TO FULFILLMENT: ${allDeliveredOrders.length}`);
const productsChangeToday = 0; // TODO: реальные изменения
const productsCount = sellerDeliveredOrders.reduce((sum, order) =>
// Товары (готовые товары для отправки) sum + order.items.reduce((itemSum, item) =>
const goods = await prisma.product.findMany({ itemSum + (item.product.type === "PRODUCT" ? item.quantity : 0), 0
), 0
);
// Изменения товаров за сутки (от селлеров)
const recentSellerDeliveredOrders = await prisma.supplyOrder.findMany({
where: { where: {
// Готовые товары - пока нет реальных данных fulfillmentCenterId: organizationId, // К нам
organizationId: organizationId, organizationId: { not: organizationId }, // От селлеров
type: "READY" status: "DELIVERED",
updatedAt: { gte: oneDayAgo }
},
include: {
items: {
include: { product: true }
}
} }
}); });
const goodsCount = goods.reduce((sum, p) => sum + p.quantity, 0);
const goodsChangeToday = 0; const productsChangeToday = recentSellerDeliveredOrders.reduce((sum, order) =>
sum + order.items.reduce((itemSum, item) =>
itemSum + (item.product.type === "PRODUCT" ? item.quantity : 0), 0
), 0
);
// Товары (готовые товары = все продукты, не расходники)
const goodsCount = productsCount; // Готовые товары = все продукты
const goodsChangeToday = productsChangeToday; // Изменения товаров = изменения продуктов
// Брак // Брак
const defectsCount = 0; // TODO: реальные данные о браке const defectsCount = 0; // TODO: реальные данные о браке
@ -1073,77 +1105,75 @@ export const resolvers = {
const pvzReturnsCount = 0; // TODO: реальные данные о возвратах const pvzReturnsCount = 0; // TODO: реальные данные о возвратах
const pvzReturnsChangeToday = 0; const pvzReturnsChangeToday = 0;
// Расходники фулфилмента // Расходники фулфилмента - заказы ОТ фулфилмента К поставщикам
const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({ const fulfillmentSupplyOrders = await prisma.supplyOrder.findMany({
where: { where: {
organizationId: organizationId, organizationId: organizationId, // Заказчик = фулфилмент
fulfillmentCenterId: organizationId, fulfillmentCenterId: null, // Не является доставкой к фулфилменту
status: "DELIVERED" status: "DELIVERED"
}, },
include: { items: true } include: {
items: {
include: { product: true }
}
}
}); });
console.log(`🏭 FULFILLMENT SUPPLY ORDERS: ${fulfillmentSupplyOrders.length}`);
const fulfillmentSuppliesCount = fulfillmentSupplyOrders.reduce( const fulfillmentSuppliesCount = fulfillmentSupplyOrders.reduce(
(sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + item.quantity, 0), (sum, order) => sum + order.items.reduce((itemSum, item) =>
0 itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
), 0
); );
console.log(`🔥 FULFILLMENT SUPPLIES DEBUG: organizationId=${organizationId}, totalOrders=${fulfillmentSupplyOrders.length}, totalCount=${fulfillmentSuppliesCount}`);
// Изменения расходников фулфилмента за сутки // Изменения расходников фулфилмента за сутки
const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({ const fulfillmentSuppliesReceivedToday = await prisma.supplyOrder.findMany({
where: { where: {
organizationId: organizationId, organizationId: organizationId, // Заказчик = фулфилмент
fulfillmentCenterId: organizationId, fulfillmentCenterId: null, // Не доставка к фулфилменту
status: "DELIVERED", status: "DELIVERED",
updatedAt: { gte: oneDayAgo } updatedAt: { gte: oneDayAgo }
}, },
include: { items: true } include: {
}); items: {
const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce( include: { product: true }
(sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + item.quantity, 0),
0
);
// Расходники селлеров
const sellerSupplies = await prisma.supply.findMany({
where: {
organizationId: {
not: organizationId
},
organization: {
counterparties: {
some: {
OR: [
{ requesterId: organizationId },
{ receiverId: organizationId }
]
}
}
} }
} }
}); });
const sellerSuppliesCount = sellerSupplies.reduce((sum, s) => sum + s.currentStock, 0); const fulfillmentSuppliesChangeToday = fulfillmentSuppliesReceivedToday.reduce(
(sum, order) => sum + order.items.reduce((itemSum, item) =>
// Изменения расходников селлеров за сутки itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
const sellerSuppliesReceivedToday = await prisma.supplyOrder.findMany({ ), 0
where: {
fulfillmentCenterId: organizationId,
organizationId: { not: organizationId },
status: "DELIVERED",
updatedAt: { gte: oneDayAgo }
},
include: { items: true }
});
const sellerSuppliesChangeToday = sellerSuppliesReceivedToday.reduce(
(sum, order) => sum + order.items.reduce((itemSum, item) => itemSum + item.quantity, 0),
0
); );
console.log(`📊 FULFILLMENT SUPPLIES RECEIVED TODAY: ${fulfillmentSuppliesReceivedToday.length} orders, ${fulfillmentSuppliesChangeToday} items`);
// Расходники селлеров - получаем из заказов от селлеров (расходники = CONSUMABLE)
const sellerSuppliesCount = sellerDeliveredOrders.reduce((sum, order) =>
sum + order.items.reduce((itemSum, item) =>
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
), 0
);
console.log(`💼 SELLER SUPPLIES DEBUG: totalCount=${sellerSuppliesCount} (from delivered orders)`);
// Изменения расходников селлеров за сутки - используем уже полученные данные
const sellerSuppliesChangeToday = recentSellerDeliveredOrders.reduce((sum, order) =>
sum + order.items.reduce((itemSum, item) =>
itemSum + (item.product.type === "CONSUMABLE" ? item.quantity : 0), 0
), 0
);
console.log(`📊 SELLER SUPPLIES RECEIVED TODAY: ${recentSellerDeliveredOrders.length} orders, ${sellerSuppliesChangeToday} items`);
// Вычисляем процентные изменения // Вычисляем процентные изменения
const calculatePercentChange = (current: number, change: number): number => { const calculatePercentChange = (current: number, change: number): number => {
if (current === 0) return change > 0 ? 100 : 0; if (current === 0) return change > 0 ? 100 : 0;
return (change / current) * 100; return (change / current) * 100;
}; };
return { const result = {
products: { products: {
current: productsCount, current: productsCount,
change: productsChangeToday, change: productsChangeToday,
@ -1175,6 +1205,10 @@ export const resolvers = {
percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday) percentChange: calculatePercentChange(sellerSuppliesCount, sellerSuppliesChangeToday)
} }
}; };
console.log(`🏁 FINAL WAREHOUSE STATS RESULT:`, JSON.stringify(result, null, 2));
return result;
}, },
// Логистика организации // Логистика организации