Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.

This commit is contained in:
Veronika Smirnova
2025-08-03 17:04:29 +03:00
parent a33adda9d7
commit 8407ca397c
34 changed files with 5382 additions and 1795 deletions

View File

@ -7,6 +7,11 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useAuth } from "@/hooks/useAuth";
@ -15,7 +20,8 @@ import {
GET_MY_COUNTERPARTIES,
GET_SUPPLY_ORDERS,
GET_WAREHOUSE_PRODUCTS,
GET_MY_SUPPLIES, // Расходники селлеров
GET_MY_SUPPLIES, // Расходники селлеров (старые данные заказов)
GET_SELLER_SUPPLIES_ON_WAREHOUSE, // Расходники селлеров на складе (новый API)
GET_MY_FULFILLMENT_SUPPLIES, // Расходники фулфилмента
GET_FULFILLMENT_WAREHOUSE_STATS, // Статистика склада с изменениями за сутки
} from "@/graphql/queries";
@ -55,6 +61,7 @@ interface ProductVariant {
defectsQuantity: number;
sellerSuppliesPlace?: string;
sellerSuppliesQuantity: number;
sellerSuppliesOwners?: string[]; // Владельцы расходников
pvzReturnsPlace?: string;
pvzReturnsQuantity: number;
}
@ -72,6 +79,7 @@ interface ProductItem {
defectsQuantity: number;
sellerSuppliesPlace?: string;
sellerSuppliesQuantity: number;
sellerSuppliesOwners?: string[]; // Владельцы расходников
pvzReturnsPlace?: string;
pvzReturnsQuantity: number;
// Третий уровень - варианты товара
@ -200,13 +208,13 @@ export function FulfillmentWarehouseDashboard() {
fetchPolicy: "cache-and-network",
});
// Загружаем расходники селлеров
// Загружаем расходники селлеров на складе фулфилмента
const {
data: suppliesData,
loading: suppliesLoading,
error: suppliesError,
refetch: refetchSupplies,
} = useQuery(GET_MY_SUPPLIES, {
data: sellerSuppliesData,
loading: sellerSuppliesLoading,
error: sellerSuppliesError,
refetch: refetchSellerSupplies,
} = useQuery(GET_SELLER_SUPPLIES_ON_WAREHOUSE, {
fetchPolicy: "cache-and-network",
});
@ -246,8 +254,10 @@ export function FulfillmentWarehouseDashboard() {
goods: warehouseStatsData.fulfillmentWarehouseStats.goods,
defects: warehouseStatsData.fulfillmentWarehouseStats.defects,
pvzReturns: warehouseStatsData.fulfillmentWarehouseStats.pvzReturns,
fulfillmentSupplies: warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies,
sellerSupplies: warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies,
fulfillmentSupplies:
warehouseStatsData.fulfillmentWarehouseStats.fulfillmentSupplies,
sellerSupplies:
warehouseStatsData.fulfillmentWarehouseStats.sellerSupplies,
});
}
@ -258,7 +268,7 @@ export function FulfillmentWarehouseDashboard() {
);
const supplyOrders: SupplyOrder[] = ordersData?.supplyOrders || [];
const allProducts = productsData?.warehouseProducts || [];
const mySupplies = suppliesData?.mySupplies || []; // Расходники селлеров
const sellerSupplies = sellerSuppliesData?.sellerSuppliesOnWarehouse || []; // Расходники селлеров на складе
const myFulfillmentSupplies =
fulfillmentSuppliesData?.myFulfillmentSupplies || []; // Расходники фулфилмента
@ -276,8 +286,8 @@ export function FulfillmentWarehouseDashboard() {
deliveredOrders: supplyOrders.filter((o) => o.status === "DELIVERED")
.length,
productsCount: allProducts.length,
suppliesCount: mySupplies.length, // Добавляем логирование расходников
supplies: mySupplies.map((s: any) => ({
suppliesCount: sellerSupplies.length, // Добавляем логирование расходников
supplies: sellerSupplies.map((s: any) => ({
id: s.id,
name: s.name,
currentStock: s.currentStock,
@ -293,7 +303,7 @@ export function FulfillmentWarehouseDashboard() {
})),
// Добавляем анализ соответствия товаров и расходников
productSupplyMatching: allProducts.map((product: any) => {
const matchingSupply = mySupplies.find((supply: any) => {
const matchingSupply = sellerSupplies.find((supply: any) => {
return (
supply.name.toLowerCase() === product.name.toLowerCase() ||
supply.name
@ -311,11 +321,11 @@ export function FulfillmentWarehouseDashboard() {
counterpartiesLoading,
ordersLoading,
productsLoading,
suppliesLoading, // Добавляем статус загрузки расходников
sellerSuppliesLoading, // Добавляем статус загрузки расходников селлеров
counterpartiesError: counterpartiesError?.message,
ordersError: ordersError?.message,
productsError: productsError?.message,
suppliesError: suppliesError?.message, // Добавляем ошибки загрузки расходников
sellerSuppliesError: sellerSuppliesError?.message, // Добавляем ошибки загрузки расходников селлеров
});
// Расчет поступлений расходников за сутки (выносим отдельно для использования в storeData)
@ -408,7 +418,7 @@ export function FulfillmentWarehouseDashboard() {
console.log("📊 Статистика расходников селлера:", {
suppliesReceivedToday,
suppliesUsedToday,
totalSellerSupplies: mySupplies.reduce(
totalSellerSupplies: sellerSupplies.reduce(
(sum: number, supply: any) => sum + (supply.currentStock || 0),
0
),
@ -418,7 +428,10 @@ export function FulfillmentWarehouseDashboard() {
// Получаем статистику склада из GraphQL (с реальными изменениями за сутки)
const warehouseStats: WarehouseStats = useMemo(() => {
// Если данные еще загружаются, возвращаем нули
if (warehouseStatsLoading || !warehouseStatsData?.fulfillmentWarehouseStats) {
if (
warehouseStatsLoading ||
!warehouseStatsData?.fulfillmentWarehouseStats
) {
return {
products: { current: 0, change: 0 },
goods: { current: 0, change: 0 },
@ -511,26 +524,64 @@ export function FulfillmentWarehouseDashboard() {
}
});
// Группируем расходники по названию
const groupedSupplies = new Map<string, number>();
mySupplies.forEach((supply: any) => {
// ИСПРАВЛЕНО: Группируем расходники по СЕЛЛЕРУ-ВЛАДЕЛЬЦУ, а не по названию
const suppliesByOwner = new Map<
string,
Map<string, { quantity: number; ownerName: string }>
>();
sellerSupplies.forEach((supply: any) => {
const ownerId = supply.sellerOwner?.id;
const ownerName =
supply.sellerOwner?.name ||
supply.sellerOwner?.fullName ||
"Неизвестный селлер";
const supplyName = supply.name;
const currentStock = supply.currentStock || 0;
const supplyType = supply.type;
if (groupedSupplies.has(supplyName)) {
groupedSupplies.set(
supplyName,
groupedSupplies.get(supplyName)! + currentStock
// ИСПРАВЛЕНО: Строгая проверка согласно правилам
if (!ownerId || supplyType !== "SELLER_CONSUMABLES") {
console.warn(
"⚠️ ОТФИЛЬТРОВАН расходник в компоненте (нарушение правил):",
{
id: supply.id,
name: supplyName,
type: supplyType,
ownerId,
ownerName,
reason: !ownerId
? "нет sellerOwner.id"
: "тип не SELLER_CONSUMABLES",
}
);
return; // Пропускаем согласно ПРАВИЛУ 6 из секции 11.6
}
// Инициализируем группу для селлера, если её нет
if (!suppliesByOwner.has(ownerId)) {
suppliesByOwner.set(ownerId, new Map());
}
const ownerSupplies = suppliesByOwner.get(ownerId)!;
if (ownerSupplies.has(supplyName)) {
// Суммируем количество, если расходник уже есть у этого селлера
const existing = ownerSupplies.get(supplyName)!;
existing.quantity += currentStock;
} else {
groupedSupplies.set(supplyName, currentStock);
// Добавляем новый расходник для этого селлера
ownerSupplies.set(supplyName, {
quantity: currentStock,
ownerName: ownerName,
});
}
});
// Логирование группировки
console.log("📊 Группировка товаров и расходников:", {
groupedProductsCount: groupedProducts.size,
groupedSuppliesCount: groupedSupplies.size,
suppliesByOwnerCount: suppliesByOwner.size,
groupedProducts: Array.from(groupedProducts.entries()).map(
([name, data]) => ({
name,
@ -539,10 +590,20 @@ export function FulfillmentWarehouseDashboard() {
uniqueSuppliers: [...new Set(data.suppliers)],
})
),
groupedSupplies: Array.from(groupedSupplies.entries()).map(
([name, quantity]) => ({
name,
totalQuantity: quantity,
suppliesByOwner: Array.from(suppliesByOwner.entries()).map(
([ownerId, ownerSupplies]) => ({
ownerId,
suppliesCount: ownerSupplies.size,
totalQuantity: Array.from(ownerSupplies.values()).reduce(
(sum, s) => sum + s.quantity,
0
),
ownerName:
Array.from(ownerSupplies.values())[0]?.ownerName || "Unknown",
supplies: Array.from(ownerSupplies.entries()).map(([name, data]) => ({
name,
quantity: data.quantity,
})),
})
),
});
@ -567,37 +628,56 @@ export function FulfillmentWarehouseDashboard() {
const productData = groupedProducts.get(productName)!;
const itemProducts = productData.totalQuantity;
// Ищем соответствующий расходник по названию
const matchingSupplyQuantity = groupedSupplies.get(productName) || 0;
// ИСПРАВЛЕНО: Ищем расходники конкретного селлера-владельца
let itemSuppliesQuantity = 0;
let suppliesOwners: string[] = [];
// Если нет точного совпадения, ищем частичное совпадение
let itemSuppliesQuantity = matchingSupplyQuantity;
if (itemSuppliesQuantity === 0) {
for (const [supplyName, quantity] of groupedSupplies.entries()) {
if (
supplyName.toLowerCase().includes(productName.toLowerCase()) ||
productName.toLowerCase().includes(supplyName.toLowerCase())
) {
itemSuppliesQuantity = quantity;
break;
// Получаем реального селлера для этого виртуального партнера
const realSeller = sellerPartners[index];
if (realSeller?.id && suppliesByOwner.has(realSeller.id)) {
const sellerSupplies = suppliesByOwner.get(realSeller.id)!;
// Ищем расходники этого селлера по названию товара
const matchingSupply = sellerSupplies.get(productName);
if (matchingSupply) {
itemSuppliesQuantity = matchingSupply.quantity;
suppliesOwners = [matchingSupply.ownerName];
} else {
// Если нет точного совпадения, ищем частичное среди расходников ЭТОГО селлера
for (const [supplyName, supplyData] of sellerSupplies.entries()) {
if (
supplyName
.toLowerCase()
.includes(productName.toLowerCase()) ||
productName.toLowerCase().includes(supplyName.toLowerCase())
) {
itemSuppliesQuantity = supplyData.quantity;
suppliesOwners = [supplyData.ownerName];
break;
}
}
}
}
// Fallback к процентному соотношению
if (itemSuppliesQuantity === 0) {
itemSuppliesQuantity = Math.floor(itemProducts * 0.1);
}
// Если у этого селлера нет расходников для данного товара - оставляем 0
// НЕ используем fallback, так как должны показывать только реальные данные
console.log(`📦 Товар "${productName}":`, {
totalQuantity: itemProducts,
suppliersCount: productData.suppliers.length,
uniqueSuppliers: [...new Set(productData.suppliers)],
matchingSupplyQuantity: matchingSupplyQuantity,
finalSuppliesQuantity: itemSuppliesQuantity,
usedFallback:
matchingSupplyQuantity === 0 && itemSuppliesQuantity > 0,
});
console.log(
`📦 Товар "${productName}" (партнер: ${
realSeller?.name || "Unknown"
}):`,
{
totalQuantity: itemProducts,
suppliersCount: productData.suppliers.length,
uniqueSuppliers: [...new Set(productData.suppliers)],
sellerSuppliesQuantity: itemSuppliesQuantity,
suppliesOwners: suppliesOwners,
sellerId: realSeller?.id,
hasSellerSupplies: itemSuppliesQuantity > 0,
}
);
return {
id: `grouped-${productName}-${itemIndex}`, // Уникальный ID для группированного товара
@ -615,6 +695,7 @@ export function FulfillmentWarehouseDashboard() {
defectsQuantity: 0, // Нет реальных данных о браке
sellerSuppliesPlace: `D${index + 1}-${itemIndex + 1}`,
sellerSuppliesQuantity: itemSuppliesQuantity, // Суммированное количество расходников (реальные данные)
sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО)
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}`,
pvzReturnsQuantity: 0, // Нет реальных данных о возвратах с ПВЗ
// Создаем варианты товара
@ -634,6 +715,7 @@ export function FulfillmentWarehouseDashboard() {
sellerSuppliesQuantity: Math.floor(
itemSuppliesQuantity * 0.4
), // Часть от расходников
sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО)
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-1`,
pvzReturnsQuantity: 0, // Нет реальных данных о возвратах
},
@ -650,6 +732,7 @@ export function FulfillmentWarehouseDashboard() {
sellerSuppliesQuantity: Math.floor(
itemSuppliesQuantity * 0.4
), // Часть от расходников
sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО)
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-2`,
pvzReturnsQuantity: 0, // Нет реальных данных о возвратах
},
@ -666,6 +749,7 @@ export function FulfillmentWarehouseDashboard() {
sellerSuppliesQuantity: Math.floor(
itemSuppliesQuantity * 0.2
), // Оставшаяся часть расходников
sellerSuppliesOwners: suppliesOwners, // Владельцы расходников (ИСПРАВЛЕНО)
pvzReturnsPlace: `E${index + 1}-${itemIndex + 1}-3`,
pvzReturnsQuantity: 0, // Нет реальных данных о возвратах
},
@ -738,7 +822,7 @@ export function FulfillmentWarehouseDashboard() {
totalSellerSupplies > 0
? Math.floor(
(totalSellerSupplies /
(mySupplies.reduce(
(sellerSupplies.reduce(
(sum: number, supply: any) =>
sum + (supply.currentStock || 0),
0
@ -774,7 +858,7 @@ export function FulfillmentWarehouseDashboard() {
items,
};
});
}, [sellerPartners, allProducts, mySupplies, suppliesReceivedToday]);
}, [sellerPartners, allProducts, sellerSupplies, suppliesReceivedToday]);
// Функции для аватаров магазинов
const getInitials = (name: string): string => {
@ -1007,9 +1091,14 @@ export function FulfillmentWarehouseDashboard() {
onClick?: () => void;
}) => {
// Используем percentChange из GraphQL, если доступно, иначе вычисляем локально
const displayPercentChange = percentChange !== undefined && percentChange !== null && !isNaN(percentChange)
? percentChange
: (current > 0 ? (change / current) * 100 : 0);
const displayPercentChange =
percentChange !== undefined &&
percentChange !== null &&
!isNaN(percentChange)
? percentChange
: current > 0
? (change / current) * 100
: 0;
return (
<div
@ -1125,7 +1214,7 @@ export function FulfillmentWarehouseDashboard() {
counterpartiesLoading ||
ordersLoading ||
productsLoading ||
suppliesLoading
sellerSuppliesLoading
) {
return (
<div className="h-screen flex overflow-hidden">
@ -1210,7 +1299,7 @@ export function FulfillmentWarehouseDashboard() {
counterpartiesLoading ||
ordersLoading ||
productsLoading ||
suppliesLoading
sellerSuppliesLoading
}
>
<RotateCcw className="h-3 w-3 mr-1" />
@ -1224,7 +1313,10 @@ export function FulfillmentWarehouseDashboard() {
icon={Box}
current={warehouseStats.products.current}
change={warehouseStats.products.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.products?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats?.products
?.percentChange
}
description="Готовые к отправке"
/>
<StatCard
@ -1232,7 +1324,10 @@ export function FulfillmentWarehouseDashboard() {
icon={Package}
current={warehouseStats.goods.current}
change={warehouseStats.goods.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.goods?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats?.goods
?.percentChange
}
description="В обработке"
/>
<StatCard
@ -1240,7 +1335,10 @@ export function FulfillmentWarehouseDashboard() {
icon={AlertTriangle}
current={warehouseStats.defects.current}
change={warehouseStats.defects.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.defects?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats?.defects
?.percentChange
}
description="Требует утилизации"
/>
<StatCard
@ -1248,7 +1346,10 @@ export function FulfillmentWarehouseDashboard() {
icon={RotateCcw}
current={warehouseStats.pvzReturns.current}
change={warehouseStats.pvzReturns.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.pvzReturns?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats?.pvzReturns
?.percentChange
}
description="К обработке"
/>
<StatCard
@ -1256,7 +1357,10 @@ export function FulfillmentWarehouseDashboard() {
icon={Wrench}
current={warehouseStats.fulfillmentSupplies.current}
change={warehouseStats.fulfillmentSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.fulfillmentSupplies?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats
?.fulfillmentSupplies?.percentChange
}
description="Расходники, этикетки"
onClick={() => router.push("/fulfillment-warehouse/supplies")}
/>
@ -1265,7 +1369,10 @@ export function FulfillmentWarehouseDashboard() {
icon={Users}
current={warehouseStats.sellerSupplies.current}
change={warehouseStats.sellerSupplies.change}
percentChange={warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies?.percentChange}
percentChange={
warehouseStatsData?.fulfillmentWarehouseStats?.sellerSupplies
?.percentChange
}
description="Материалы клиентов"
/>
</div>
@ -1935,11 +2042,43 @@ export function FulfillmentWarehouseDashboard() {
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-2 text-center text-xs text-white font-medium">
{formatNumber(
item.sellerSuppliesQuantity
)}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="px-1 py-2 text-center text-xs text-white font-medium cursor-help hover:bg-white/10 rounded">
{formatNumber(
item.sellerSuppliesQuantity
)}
</div>
</PopoverTrigger>
<PopoverContent className="w-64 glass-card">
<div className="text-xs">
<div className="font-medium mb-2 text-white">
Расходники селлеров:
</div>
{item.sellerSuppliesOwners &&
item.sellerSuppliesOwners.length >
0 ? (
<div className="space-y-1">
{item.sellerSuppliesOwners.map(
(owner, i) => (
<div
key={i}
className="text-white/80 flex items-center"
>
<div className="w-2 h-2 bg-purple-500 rounded-full mr-2 flex-shrink-0"></div>
{owner}
</div>
)
)}
</div>
) : (
<div className="text-white/60">
Нет данных о владельцах
</div>
)}
</div>
</PopoverContent>
</Popover>
<div className="px-1 py-2 text-center text-xs text-white/60">
{item.sellerSuppliesPlace || "-"}
</div>
@ -2065,11 +2204,45 @@ export function FulfillmentWarehouseDashboard() {
{/* Расходники селлера */}
<div className="grid grid-cols-2 gap-0">
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium">
{formatNumber(
variant.sellerSuppliesQuantity
)}
</div>
<Popover>
<PopoverTrigger asChild>
<div className="px-1 py-1.5 text-center text-[10px] text-white font-medium cursor-help hover:bg-white/10 rounded">
{formatNumber(
variant.sellerSuppliesQuantity
)}
</div>
</PopoverTrigger>
<PopoverContent className="w-64 glass-card">
<div className="text-xs">
<div className="font-medium mb-2 text-white">
Расходники селлеров:
</div>
{variant.sellerSuppliesOwners &&
variant
.sellerSuppliesOwners
.length > 0 ? (
<div className="space-y-1">
{variant.sellerSuppliesOwners.map(
(owner, i) => (
<div
key={i}
className="text-white/80 flex items-center"
>
<div className="w-2 h-2 bg-purple-500 rounded-full mr-2 flex-shrink-0"></div>
{owner}
</div>
)
)}
</div>
) : (
<div className="text-white/60">
Нет данных о
владельцах
</div>
)}
</div>
</PopoverContent>
</Popover>
<div className="px-1 py-1.5 text-center text-[10px] text-white/60">
{variant.sellerSuppliesPlace ||
"-"}