Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели 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

@ -99,14 +99,44 @@ export function CreateFulfillmentConsumablesSupplyPage() {
GET_MY_COUNTERPARTIES
);
// ОТЛАДКА: Логируем состояние перед запросом товаров
console.log("🔍 ДИАГНОСТИКА ЗАПРОСА ТОВАРОВ:", {
selectedSupplier: selectedSupplier
? {
id: selectedSupplier.id,
name: selectedSupplier.name || selectedSupplier.fullName,
type: selectedSupplier.type,
}
: null,
skipQuery: !selectedSupplier,
productSearchQuery,
});
// Загружаем товары для выбранного поставщика
const { data: productsData, loading: productsLoading } = useQuery(
GET_ALL_PRODUCTS,
{
skip: !selectedSupplier,
variables: { search: productSearchQuery || null, category: null },
}
);
const {
data: productsData,
loading: productsLoading,
error: productsError,
} = useQuery(GET_ALL_PRODUCTS, {
skip: !selectedSupplier,
variables: { search: productSearchQuery || null, category: null },
onCompleted: (data) => {
console.log("✅ GET_ALL_PRODUCTS COMPLETED:", {
totalProducts: data?.allProducts?.length || 0,
products:
data?.allProducts?.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
orgId: p.organization?.id,
orgName: p.organization?.name,
})) || [],
});
},
onError: (error) => {
console.error("❌ GET_ALL_PRODUCTS ERROR:", error);
},
});
// Мутация для создания заказа поставки расходников
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
@ -117,9 +147,9 @@ export function CreateFulfillmentConsumablesSupplyPage() {
).filter((org: FulfillmentConsumableSupplier) => org.type === "WHOLESALE");
// Фильтруем только логистические компании
const logisticsPartners = (
counterpartiesData?.myCounterparties || []
).filter((org: FulfillmentConsumableSupplier) => org.type === "LOGIST");
const logisticsPartners = (counterpartiesData?.myCounterparties || []).filter(
(org: FulfillmentConsumableSupplier) => org.type === "LOGIST"
);
// Фильтруем поставщиков по поисковому запросу
const filteredSuppliers = consumableSuppliers.filter(
@ -150,6 +180,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
}
: null,
productsLoading,
productsError: productsError?.message,
allProductsCount: productsData?.allProducts?.length || 0,
supplierProductsCount: supplierProducts.length,
allProducts:
@ -160,14 +191,20 @@ export function CreateFulfillmentConsumablesSupplyPage() {
organizationName: p.organization.name,
type: p.type || "NO_TYPE",
})) || [],
supplierProducts: supplierProducts.map((p) => ({
supplierProductsDetails: supplierProducts.slice(0, 5).map((p) => ({
id: p.id,
name: p.name,
organizationId: p.organization.id,
organizationName: p.organization.name,
})),
});
}, [selectedSupplier, productsData, productsLoading, supplierProducts]);
}, [
selectedSupplier,
productsData,
productsLoading,
productsError,
supplierProducts.length,
]);
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
@ -198,10 +235,13 @@ export function CreateFulfillmentConsumablesSupplyPage() {
// 🔒 ВАЛИДАЦИЯ ОСТАТКОВ согласно правилам (раздел 6.2)
if (quantity > 0) {
const availableStock = (product.stock || product.quantity || 0) - (product.ordered || 0);
const availableStock =
(product.stock || product.quantity || 0) - (product.ordered || 0);
if (quantity > availableStock) {
toast.error(`❌ Недостаточно остатков!\оступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`);
toast.error(
`❌ Недостаточно остатков!\оступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`
);
return;
}
}
@ -265,7 +305,15 @@ export function CreateFulfillmentConsumablesSupplyPage() {
!deliveryDate ||
!selectedLogistics
) {
toast.error("Заполните все обязательные поля");
toast.error(
"Заполните все обязательные поля: поставщик, расходники, дата доставки и логистика"
);
return;
}
// Дополнительная проверка ID логистики
if (!selectedLogistics.id) {
toast.error("Выберите логистическую компанию");
return;
}
@ -279,7 +327,7 @@ export function CreateFulfillmentConsumablesSupplyPage() {
deliveryDate: deliveryDate,
// Для фулфилмента указываем себя как получателя (поставка на свой склад)
fulfillmentCenterId: user?.organization?.id,
logisticsPartnerId: selectedLogistics?.id,
logisticsPartnerId: selectedLogistics.id,
// 🏷️ КЛАССИФИКАЦИЯ согласно правилам (раздел 2.2)
consumableType: "FULFILLMENT_CONSUMABLES", // Расходники фулфилмента
items: selectedConsumables.map((consumable) => ({
@ -574,15 +622,19 @@ export function CreateFulfillmentConsumablesSupplyPage() {
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden relative flex-shrink-0">
{/* 🚫 ОВЕРЛЕЙ НЕДОСТУПНОСТИ */}
{(() => {
const totalStock = product.stock || product.quantity || 0;
const totalStock =
product.stock || product.quantity || 0;
const orderedStock = product.ordered || 0;
const availableStock = totalStock - orderedStock;
const availableStock =
totalStock - orderedStock;
if (availableStock <= 0) {
return (
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-10">
<div className="text-center">
<div className="text-red-400 font-bold text-xs">НЕТ В НАЛИЧИИ</div>
<div className="text-red-400 font-bold text-xs">
НЕТ В НАЛИЧИИ
</div>
</div>
</div>
);
@ -636,10 +688,12 @@ export function CreateFulfillmentConsumablesSupplyPage() {
)}
{/* 🚨 ИНДИКАТОР НИЗКИХ ОСТАТКОВ согласно правилам (раздел 6.3) */}
{(() => {
const totalStock = product.stock || product.quantity || 0;
const totalStock =
product.stock || product.quantity || 0;
const orderedStock = product.ordered || 0;
const availableStock = totalStock - orderedStock;
const availableStock =
totalStock - orderedStock;
if (availableStock <= 0) {
return (
<Badge className="bg-red-500/30 text-red-300 border-red-500/50 text-xs px-2 py-1 animate-pulse">
@ -663,19 +717,26 @@ export function CreateFulfillmentConsumablesSupplyPage() {
{/* 📊 АКТУАЛЬНЫЙ ОСТАТОК согласно правилам (раздел 6.4.2) */}
<div className="text-right">
{(() => {
const totalStock = product.stock || product.quantity || 0;
const orderedStock = product.ordered || 0;
const availableStock = totalStock - orderedStock;
const totalStock =
product.stock ||
product.quantity ||
0;
const orderedStock =
product.ordered || 0;
const availableStock =
totalStock - orderedStock;
return (
<div className="flex flex-col items-end">
<span className={`text-xs font-medium ${
availableStock <= 0
? 'text-red-400'
: availableStock <= 10
? 'text-yellow-400'
: 'text-white/80'
}`}>
<span
className={`text-xs font-medium ${
availableStock <= 0
? "text-red-400"
: availableStock <= 10
? "text-yellow-400"
: "text-white/80"
}`}
>
Доступно: {availableStock}
</span>
{orderedStock > 0 && (
@ -693,10 +754,12 @@ export function CreateFulfillmentConsumablesSupplyPage() {
{/* Управление количеством */}
<div className="flex flex-col items-center space-y-2 mt-auto">
{(() => {
const totalStock = product.stock || product.quantity || 0;
const totalStock =
product.stock || product.quantity || 0;
const orderedStock = product.ordered || 0;
const availableStock = totalStock - orderedStock;
const availableStock =
totalStock - orderedStock;
return (
<div className="flex items-center space-x-2">
<Button
@ -713,81 +776,92 @@ export function CreateFulfillmentConsumablesSupplyPage() {
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={
selectedQuantity === 0
? ""
: selectedQuantity.toString()
}
onChange={(e) => {
let inputValue = e.target.value;
<Input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={
selectedQuantity === 0
? ""
: selectedQuantity.toString()
}
onChange={(e) => {
let inputValue = e.target.value;
// Удаляем все нецифровые символы
inputValue = inputValue.replace(
/[^0-9]/g,
""
);
// Удаляем все нецифровые символы
inputValue = inputValue.replace(
/[^0-9]/g,
""
);
// Удаляем ведущие нули
inputValue = inputValue.replace(
/^0+/,
""
);
// Удаляем ведущие нули
inputValue = inputValue.replace(
/^0+/,
""
);
// Если строка пустая после удаления нулей, устанавливаем 0
const numericValue =
inputValue === ""
? 0
: parseInt(inputValue);
// Если строка пустая после удаления нулей, устанавливаем 0
const numericValue =
inputValue === ""
? 0
: parseInt(inputValue);
// Ограничиваем значение максимумом доступного остатка
const clampedValue = Math.min(
numericValue,
availableStock,
99999
);
// Ограничиваем значение максимумом доступного остатка
const clampedValue = Math.min(
numericValue,
availableStock,
99999
);
updateConsumableQuantity(
product.id,
clampedValue
);
}}
onBlur={(e) => {
// При потере фокуса, если поле пустое, устанавливаем 0
if (e.target.value === "") {
updateConsumableQuantity(
product.id,
0
);
}
}}
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
placeholder="0"
/>
updateConsumableQuantity(
product.id,
clampedValue
);
}}
onBlur={(e) => {
// При потере фокуса, если поле пустое, устанавливаем 0
if (e.target.value === "") {
updateConsumableQuantity(
product.id,
0
);
}
}}
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
placeholder="0"
/>
<Button
variant="ghost"
size="sm"
onClick={() =>
updateConsumableQuantity(
product.id,
Math.min(selectedQuantity + 1, availableStock, 99999)
Math.min(
selectedQuantity + 1,
availableStock,
99999
)
)
}
className={`h-6 w-6 p-0 rounded-full transition-all duration-300 ${
selectedQuantity >= availableStock || availableStock <= 0
? 'text-white/30 cursor-not-allowed'
: 'text-white/60 hover:text-white hover:bg-white/20'
selectedQuantity >=
availableStock ||
availableStock <= 0
? "text-white/30 cursor-not-allowed"
: "text-white/60 hover:text-white hover:bg-white/20"
}`}
disabled={selectedQuantity >= availableStock || availableStock <= 0}
disabled={
selectedQuantity >=
availableStock ||
availableStock <= 0
}
title={
availableStock <= 0
? 'Товар отсутствует на складе'
: selectedQuantity >= availableStock
? `Максимум доступно: ${availableStock}`
: 'Увеличить количество'
availableStock <= 0
? "Товар отсутствует на складе"
: selectedQuantity >=
availableStock
? `Максимум доступно: ${availableStock}`
: "Увеличить количество"
}
>
<Plus className="h-3 w-3" />
@ -903,7 +977,9 @@ export function CreateFulfillmentConsumablesSupplyPage() {
value={selectedLogistics?.id || ""}
onChange={(e) => {
const logisticsId = e.target.value;
const logistics = logisticsPartners.find(p => p.id === logisticsId);
const logistics = logisticsPartners.find(
(p) => p.id === logisticsId
);
setSelectedLogistics(logistics || null);
}}
className="w-full bg-white/10 border border-white/20 rounded-md px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent appearance-none"
@ -912,8 +988,8 @@ export function CreateFulfillmentConsumablesSupplyPage() {
Выберите логистику
</option>
{logisticsPartners.map((partner) => (
<option
key={partner.id}
<option
key={partner.id}
value={partner.id}
className="bg-gray-800 text-white"
>
@ -922,8 +998,18 @@ export function CreateFulfillmentConsumablesSupplyPage() {
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
<svg
className="w-4 h-4 text-white/60"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>

View File

@ -1,22 +1,34 @@
"use client";
import { useState } from "react";
import React, { useState } from "react";
import { useQuery } from "@apollo/client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { GET_PENDING_SUPPLIES_COUNT } from "@/graphql/queries";
import { Building2, ShoppingCart } from "lucide-react";
import {
Building2,
ShoppingCart,
Package,
Wrench,
RotateCcw,
Clock,
FileText,
CheckCircle,
} from "lucide-react";
// Импорты компонентов подразделов
import { FulfillmentSuppliesTab } from "./fulfillment-supplies/fulfillment-supplies-tab";
import { MarketplaceSuppliesTab } from "./marketplace-supplies/marketplace-supplies-tab";
import { FulfillmentDetailedSuppliesTab } from "./fulfillment-supplies/fulfillment-detailed-supplies-tab";
import { FulfillmentConsumablesOrdersTab } from "./fulfillment-supplies/fulfillment-consumables-orders-tab";
import { PvzReturnsTab } from "./fulfillment-supplies/pvz-returns-tab";
// Компонент для отображения бейджа с уведомлениями
function NotificationBadge({ count }: { count: number }) {
if (count === 0) return null;
return (
<div className="ml-1 bg-red-500 text-white text-xs font-bold rounded-full min-w-[16px] h-4 flex items-center justify-center px-1">
{count > 99 ? "99+" : count}
@ -27,72 +39,390 @@ function NotificationBadge({ count }: { count: number }) {
export function FulfillmentSuppliesDashboard() {
const { getSidebarMargin } = useSidebar();
const [activeTab, setActiveTab] = useState("fulfillment");
const [activeSubTab, setActiveSubTab] = useState("goods"); // товар
const [activeThirdTab, setActiveThirdTab] = useState("new"); // новые
// Загружаем данные о непринятых поставках
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: "cache-first",
errorPolicy: "ignore",
});
const { data: pendingData, error: pendingError } = useQuery(
GET_PENDING_SUPPLIES_COUNT,
{
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: "cache-first",
errorPolicy: "ignore",
onError: (error) => {
console.error("❌ GET_PENDING_SUPPLIES_COUNT Error:", error);
},
}
);
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
// Логируем ошибку для диагностики
React.useEffect(() => {
if (pendingError) {
console.error("🚨 Ошибка загрузки счетчиков поставок:", pendingError);
}
}, [pendingError]);
// ✅ ПРАВИЛЬНО: Для фулфилмента считаем только поставки, НЕ заявки на партнерство
const pendingCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0;
const ourSupplyOrdersCount =
pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0;
const sellerSupplyOrdersCount =
pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0;
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-2 xl:px-4 py-2 xl:py-3 overflow-hidden transition-all duration-300`}
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="h-full w-full flex flex-col">
{/* Основной контент с табами */}
<div className="flex-1 overflow-hidden">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="h-full flex flex-col"
>
<TabsList className="grid w-full grid-cols-2 bg-white/5 backdrop-blur border-white/10 flex-shrink-0 h-8 xl:h-10">
<TabsTrigger
value="fulfillment"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs xl:text-sm relative"
<div className="h-full w-full flex flex-col space-y-4">
{/* БЛОК 1: ТАБЫ ВСЕХ УРОВНЕЙ */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6">
{/* УРОВЕНЬ 1: Главные табы */}
<div className="mb-4">
<div className="grid w-full grid-cols-2 bg-white/15 backdrop-blur border-white/30 rounded-xl h-11 p-2">
<button
onClick={() => setActiveTab("fulfillment")}
className={`flex items-center gap-2 text-sm font-semibold transition-all duration-200 rounded-lg px-3 ${
activeTab === "fulfillment"
? "bg-gradient-to-r from-purple-500/40 to-pink-500/40 text-white shadow-lg"
: "text-white/80 hover:text-white"
}`}
>
<Building2 className="h-3 w-3" />
<Building2 className="h-4 w-4" />
<span className="hidden sm:inline">
Поставки на фулфилмент
</span>
<span className="sm:hidden">Фулфилмент</span>
<NotificationBadge count={pendingCount} />
</TabsTrigger>
<TabsTrigger
value="marketplace"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-xs xl:text-sm"
</button>
<button
onClick={() => setActiveTab("marketplace")}
className={`flex items-center gap-2 text-sm font-semibold transition-all duration-200 rounded-lg px-3 ${
activeTab === "marketplace"
? "bg-gradient-to-r from-purple-500/40 to-pink-500/40 text-white shadow-lg"
: "text-white/80 hover:text-white"
}`}
>
<ShoppingCart className="h-3 w-3" />
<ShoppingCart className="h-4 w-4" />
<span className="hidden sm:inline">
Поставки на маркетплейсы
</span>
<span className="sm:hidden">Маркетплейсы</span>
</TabsTrigger>
</TabsList>
</button>
</div>
</div>
<TabsContent
value="fulfillment"
className="flex-1 overflow-hidden mt-2 xl:mt-3"
>
<Card className="glass-card h-full overflow-hidden p-0">
<FulfillmentSuppliesTab />
</Card>
</TabsContent>
{/* УРОВЕНЬ 2: Подтабы */}
{activeTab === "fulfillment" && (
<div className="ml-4 mb-3">
<div className="grid w-full grid-cols-4 bg-white/8 backdrop-blur border-white/20 h-9 rounded-lg p-1">
<button
onClick={() => setActiveSubTab("goods")}
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
activeSubTab === "goods"
? "bg-white/15 text-white border-white/20"
: "text-white/60 hover:text-white/80"
}`}
>
<Package className="h-3 w-3" />
<span className="hidden sm:inline">Товар</span>
<span className="sm:hidden">Т</span>
</button>
<button
onClick={() => setActiveSubTab("detailed-supplies")}
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 relative ${
activeSubTab === "detailed-supplies"
? "bg-white/15 text-white border-white/20"
: "text-white/60 hover:text-white/80"
}`}
>
<Building2 className="h-3 w-3" />
<span className="hidden md:inline">
Расходники фулфилмента
</span>
<span className="md:hidden hidden sm:inline">
Фулфилмент
</span>
<span className="sm:hidden">Ф</span>
<NotificationBadge count={ourSupplyOrdersCount} />
</button>
<button
onClick={() => setActiveSubTab("consumables")}
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 relative ${
activeSubTab === "consumables"
? "bg-white/15 text-white border-white/20"
: "text-white/60 hover:text-white/80"
}`}
>
<Wrench className="h-3 w-3" />
<span className="hidden md:inline">
Расходники селлеров
</span>
<span className="md:hidden hidden sm:inline">Селлеры</span>
<span className="sm:hidden">С</span>
<NotificationBadge count={sellerSupplyOrdersCount} />
</button>
<button
onClick={() => setActiveSubTab("returns")}
className={`flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md px-2 ${
activeSubTab === "returns"
? "bg-white/15 text-white border-white/20"
: "text-white/60 hover:text-white/80"
}`}
>
<RotateCcw className="h-3 w-3" />
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
<span className="sm:hidden">В</span>
</button>
</div>
</div>
)}
<TabsContent
value="marketplace"
className="flex-1 overflow-hidden mt-2 xl:mt-3"
>
<Card className="glass-card h-full overflow-hidden p-0">
<MarketplaceSuppliesTab />
</Card>
</TabsContent>
</Tabs>
{/* УРОВЕНЬ 3: Подподтабы */}
{activeTab === "fulfillment" && activeSubTab === "goods" && (
<div className="ml-8">
<div className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border-white/15 h-8 rounded-md p-1">
<button
onClick={() => setActiveThirdTab("new")}
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
activeThirdTab === "new"
? "bg-white/10 text-white"
: "text-white/50 hover:text-white/70"
}`}
>
<Clock className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Новые</span>
<span className="sm:hidden">Н</span>
</button>
<button
onClick={() => setActiveThirdTab("receiving")}
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
activeThirdTab === "receiving"
? "bg-white/10 text-white"
: "text-white/50 hover:text-white/70"
}`}
>
<FileText className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Приёмка</span>
<span className="sm:hidden">П</span>
</button>
<button
onClick={() => setActiveThirdTab("received")}
className={`flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm px-2 ${
activeThirdTab === "received"
? "bg-white/10 text-white"
: "text-white/50 hover:text-white/70"
}`}
>
<CheckCircle className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Принято</span>
<span className="sm:hidden">Пр</span>
</button>
</div>
</div>
)}
</div>
{/* БЛОК 2: МОДУЛИ СТАТИСТИКИ */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6">
<h3 className="text-white font-semibold mb-4">Статистика</h3>
{/* Статистика для расходников фулфилмента */}
{activeTab === "fulfillment" &&
activeSubTab === "detailed-supplies" && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Building2 className="h-5 w-5 text-blue-400" />
<div>
<p className="text-xs text-white/60">Наши заказы</p>
<p className="text-lg font-semibold text-white">
{ourSupplyOrdersCount}
</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Package className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs text-white/60">Всего позиций</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Wrench className="h-5 w-5 text-purple-400" />
<div>
<p className="text-xs text-white/60">На складе</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-emerald-400" />
<div>
<p className="text-xs text-white/60">Доставлено</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
</div>
)}
{/* Статистика для расходников селлеров */}
{activeTab === "fulfillment" && activeSubTab === "consumables" && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Wrench className="h-5 w-5 text-orange-400" />
<div>
<p className="text-xs text-white/60">От селлеров</p>
<p className="text-lg font-semibold text-white">
{sellerSupplyOrdersCount}
</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-yellow-400" />
<div>
<p className="text-xs text-white/60">В обработке</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Package className="h-5 w-5 text-blue-400" />
<div>
<p className="text-xs text-white/60">Принято</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs text-white/60">Использовано</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
</div>
)}
{/* Статистика для товаров */}
{activeTab === "fulfillment" && activeSubTab === "goods" && (
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-blue-400" />
<div>
<p className="text-xs text-white/60">Новые</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<FileText className="h-5 w-5 text-yellow-400" />
<div>
<p className="text-xs text-white/60">Приёмка</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
<div className="bg-white/5 backdrop-blur rounded-lg p-4">
<div className="flex items-center space-x-2">
<CheckCircle className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs text-white/60">Принято</p>
<p className="text-lg font-semibold text-white">-</p>
</div>
</div>
</div>
</div>
)}
{/* Общая статистика для других разделов */}
{activeTab === "fulfillment" && activeSubTab === "returns" && (
<div className="text-white/70">Статистика возвратов с ПВЗ</div>
)}
{activeTab === "marketplace" && (
<div className="text-white/70">
Статистика поставок на маркетплейсы
</div>
)}
</div>
{/* БЛОК 3: ОСНОВНОЙ КОНТЕНТ */}
<div className="flex-1 overflow-hidden">
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl h-full overflow-hidden p-6">
<div className="h-full">
<h3 className="text-white font-semibold mb-4">
Контент: {activeTab} {activeSubTab} {activeThirdTab}
</h3>
{/* КОНТЕНТ ДЛЯ ТОВАРОВ */}
{activeTab === "fulfillment" &&
activeSubTab === "goods" &&
activeThirdTab === "new" && (
<div className="text-white/80">
Здесь отображаются НОВЫЕ поставки товаров на фулфилмент
</div>
)}
{activeTab === "fulfillment" &&
activeSubTab === "goods" &&
activeThirdTab === "receiving" && (
<div className="text-white/80">
Здесь отображаются товары в ПРИЁМКЕ
</div>
)}
{activeTab === "fulfillment" &&
activeSubTab === "goods" &&
activeThirdTab === "received" && (
<div className="text-white/80">
Здесь отображаются ПРИНЯТЫЕ товары
</div>
)}
{/* КОНТЕНТ ДЛЯ РАСХОДНИКОВ ФУЛФИЛМЕНТА */}
{activeTab === "fulfillment" &&
activeSubTab === "detailed-supplies" && (
<div className="h-full overflow-hidden">
<FulfillmentDetailedSuppliesTab />
</div>
)}
{/* КОНТЕНТ ДЛЯ РАСХОДНИКОВ СЕЛЛЕРОВ */}
{activeTab === "fulfillment" &&
activeSubTab === "consumables" && (
<div className="h-full overflow-hidden">
<FulfillmentConsumablesOrdersTab />
</div>
)}
{/* КОНТЕНТ ДЛЯ ВОЗВРАТОВ С ПВЗ */}
{activeTab === "fulfillment" && activeSubTab === "returns" && (
<div className="h-full overflow-hidden">
<PvzReturnsTab />
</div>
)}
{/* КОНТЕНТ ДЛЯ МАРКЕТПЛЕЙСОВ */}
{activeTab === "marketplace" && (
<div className="text-white/80">
Содержимое поставок на маркетплейсы
</div>
)}
</div>
</div>
</div>
</div>
</main>

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import React, { useState } from "react";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -12,8 +12,14 @@ import {
GET_MY_SUPPLIES,
GET_PENDING_SUPPLIES_COUNT,
GET_WAREHOUSE_PRODUCTS,
GET_MY_EMPLOYEES,
GET_LOGISTICS_PARTNERS,
} from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import {
UPDATE_SUPPLY_ORDER_STATUS,
ASSIGN_LOGISTICS_TO_SUPPLY,
FULFILLMENT_RECEIVE_ORDER,
} from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import {
@ -34,16 +40,36 @@ import {
Store,
Bell,
AlertTriangle,
UserPlus,
Settings,
} from "lucide-react";
interface SupplyOrder {
id: string;
partnerId: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
status:
| "PENDING"
| "SUPPLIER_APPROVED"
| "CONFIRMED"
| "LOGISTICS_CONFIRMED"
| "SHIPPED"
| "IN_TRANSIT"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
fulfillmentCenter?: {
id: string;
name: string;
fullName: string;
};
organization?: {
id: string;
name: string;
fullName: string;
};
partner: {
id: string;
inn: string;
@ -83,31 +109,106 @@ interface SupplyOrder {
export function FulfillmentConsumablesOrdersTab() {
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
const [assigningOrders, setAssigningOrders] = useState<Set<string>>(
new Set()
);
const [selectedLogistics, setSelectedLogistics] = useState<{
[orderId: string]: string;
}>({});
const [selectedEmployees, setSelectedEmployees] = useState<{
[orderId: string]: string;
}>({});
const { user } = useAuth();
// Запросы данных
const {
data: employeesData,
loading: employeesLoading,
error: employeesError,
} = useQuery(GET_MY_EMPLOYEES);
const {
data: logisticsData,
loading: logisticsLoading,
error: logisticsError,
} = useQuery(GET_LOGISTICS_PARTNERS);
// Отладочная информация
console.log("DEBUG EMPLOYEES:", {
loading: employeesLoading,
error: employeesError?.message,
errorDetails: employeesError,
data: employeesData,
employees: employeesData?.myEmployees,
});
console.log("DEBUG LOGISTICS:", {
loading: logisticsLoading,
error: logisticsError?.message,
errorDetails: logisticsError,
data: logisticsData,
partners: logisticsData?.logisticsPartners,
});
// Логируем ошибки отдельно
if (employeesError) {
console.error("EMPLOYEES ERROR:", employeesError);
}
if (logisticsError) {
console.error("LOGISTICS ERROR:", logisticsError);
}
// Загружаем заказы поставок
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS);
// Мутация для обновления статуса заказа
const [updateSupplyOrderStatus, { loading: updating }] = useMutation(
UPDATE_SUPPLY_ORDER_STATUS,
// Мутация для приемки поставки фулфилментом
const [fulfillmentReceiveOrder, { loading: receiving }] = useMutation(
FULFILLMENT_RECEIVE_ORDER,
{
onCompleted: (data) => {
if (data.updateSupplyOrderStatus.success) {
toast.success(data.updateSupplyOrderStatus.message);
if (data.fulfillmentReceiveOrder.success) {
toast.success(data.fulfillmentReceiveOrder.message);
refetch(); // Обновляем список заказов
} else {
toast.error(data.updateSupplyOrderStatus.message);
toast.error(data.fulfillmentReceiveOrder.message);
}
},
refetchQueries: [
{ query: GET_SUPPLY_ORDERS }, // Обновляем заказы поставок
{ query: GET_MY_SUPPLIES }, // Обновляем склад фулфилмента (расходники фулфилмента)
{ query: GET_WAREHOUSE_PRODUCTS }, // Обновляем товары склада
{ query: GET_PENDING_SUPPLIES_COUNT }, // Обновляем счетчики уведомлений
],
onError: (error) => {
console.error("Error updating supply order status:", error);
toast.error("Ошибка при обновлении статуса заказа");
console.error("Error receiving supply order:", error);
toast.error("Ошибка при приеме заказа поставки");
},
}
);
// Мутация для назначения логистики и ответственного
const [assignLogisticsToSupply, { loading: assigning }] = useMutation(
ASSIGN_LOGISTICS_TO_SUPPLY,
{
onCompleted: (data) => {
if (data.assignLogisticsToSupply.success) {
toast.success("Логистика и ответственный назначены успешно");
refetch(); // Обновляем список заказов
// Сбрасываем состояние назначения
setAssigningOrders((prev) => {
const newSet = new Set(prev);
newSet.delete(data.assignLogisticsToSupply.supplyOrder.id);
return newSet;
});
} else {
toast.error(
data.assignLogisticsToSupply.message ||
"Ошибка при назначении логистики"
);
}
},
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onError: (error) => {
console.error("Error assigning logistics:", error);
toast.error("Ошибка при назначении логистики");
},
}
);
@ -144,6 +245,19 @@ export function FulfillmentConsumablesOrdersTab() {
number: fulfillmentOrders.length - index, // Обратный порядок для новых заказов сверху
}));
// Автоматически открываем режим назначения для заказов, которые требуют назначения логистики
React.useEffect(() => {
fulfillmentOrders.forEach((order) => {
if (canAssignLogistics(order) && !assigningOrders.has(order.id)) {
setAssigningOrders((prev) => {
const newSet = new Set(prev);
newSet.add(order.id);
return newSet;
});
}
});
}, [fulfillmentOrders]);
const getStatusBadge = (status: SupplyOrder["status"]) => {
const statusMap = {
PENDING: {
@ -151,11 +265,26 @@ export function FulfillmentConsumablesOrdersTab() {
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
icon: Clock,
},
CONFIRMED: {
label: "Подтверждена",
SUPPLIER_APPROVED: {
label: "Одобрено",
color: "bg-green-500/20 text-green-300 border-green-500/30",
icon: CheckCircle,
},
CONFIRMED: {
label: "Подтверждена",
color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
icon: CheckCircle,
},
LOGISTICS_CONFIRMED: {
label: "Логистика OK",
color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
icon: Truck,
},
SHIPPED: {
label: "Отгружено",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
icon: Package,
},
IN_TRANSIT: {
label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
@ -181,24 +310,69 @@ export function FulfillmentConsumablesOrdersTab() {
);
};
const handleStatusUpdate = async (
orderId: string,
newStatus: SupplyOrder["status"]
) => {
// Функция для приема заказа фулфилментом
const handleReceiveOrder = async (orderId: string) => {
try {
await updateSupplyOrderStatus({
variables: {
id: orderId,
status: newStatus,
},
await fulfillmentReceiveOrder({
variables: { id: orderId },
});
} catch (error) {
console.error("Error updating status:", error);
console.error("Error receiving order:", error);
}
};
const canMarkAsDelivered = (status: SupplyOrder["status"]) => {
return status === "IN_TRANSIT";
// Проверяем, можно ли принять заказ (для фулфилмента)
const canReceiveOrder = (status: SupplyOrder["status"]) => {
return status === "SHIPPED";
};
const toggleAssignmentMode = (orderId: string) => {
setAssigningOrders((prev) => {
const newSet = new Set(prev);
if (newSet.has(orderId)) {
newSet.delete(orderId);
} else {
newSet.add(orderId);
}
return newSet;
});
};
const handleAssignLogistics = async (orderId: string) => {
const logisticsId = selectedLogistics[orderId];
const employeeId = selectedEmployees[orderId];
if (!logisticsId) {
toast.error("Выберите логистическую компанию");
return;
}
if (!employeeId) {
toast.error("Выберите ответственного сотрудника");
return;
}
try {
await assignLogisticsToSupply({
variables: {
supplyOrderId: orderId,
logisticsPartnerId: logisticsId,
responsibleId: employeeId,
},
});
} catch (error) {
console.error("Error assigning logistics:", error);
}
};
const canAssignLogistics = (order: SupplyOrder) => {
// Можем назначать логистику если:
// 1. Статус SUPPLIER_APPROVED (одобрено поставщиком) или CONFIRMED (подтвержден фулфилментом)
// 2. Логистика еще не назначена
return (
(order.status === "SUPPLIER_APPROVED" || order.status === "CONFIRMED") &&
!order.logisticsPartner
);
};
const formatDate = (dateString: string) => {
@ -247,15 +421,15 @@ export function FulfillmentConsumablesOrdersTab() {
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-blue-500/20 rounded">
<Clock className="h-3 w-3 text-blue-400" />
<div className="p-1 bg-green-500/20 rounded">
<CheckCircle className="h-3 w-3 text-green-400" />
</div>
<div>
<p className="text-white/60 text-xs">Ожидание</p>
<p className="text-white/60 text-xs">Одобрено</p>
<p className="text-sm font-bold text-white">
{
fulfillmentOrders.filter(
(order) => order.status === "PENDING"
(order) => order.status === "SUPPLIER_APPROVED"
).length
}
</p>
@ -265,8 +439,8 @@ export function FulfillmentConsumablesOrdersTab() {
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1 bg-green-500/20 rounded">
<CheckCircle className="h-3 w-3 text-green-400" />
<div className="p-1 bg-emerald-500/20 rounded">
<CheckCircle className="h-3 w-3 text-emerald-400" />
</div>
<div>
<p className="text-white/60 text-xs">Подтверждено</p>
@ -336,8 +510,18 @@ export function FulfillmentConsumablesOrdersTab() {
ordersWithNumbers.map((order) => (
<Card
key={order.id}
className="bg-white/10 backdrop-blur border-white/20 overflow-hidden hover:bg-white/15 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
className={`bg-white/10 backdrop-blur border-white/20 overflow-hidden hover:bg-white/15 transition-colors ${
canAssignLogistics(order) && assigningOrders.has(order.id)
? "cursor-default"
: "cursor-pointer"
}`}
onClick={() => {
if (
!(canAssignLogistics(order) && assigningOrders.has(order.id))
) {
toggleOrderExpansion(order.id);
}
}}
>
{/* Компактная основная информация */}
<div className="px-3 py-2">
@ -427,53 +611,20 @@ export function FulfillmentConsumablesOrdersTab() {
{/* Правая часть - статус и действия */}
<div className="flex items-center space-x-2 flex-shrink-0">
<Badge
className={`${
order.status === "PENDING"
? "bg-blue-500/20 text-blue-300 border-blue-500/30"
: order.status === "CONFIRMED"
? "bg-green-500/20 text-green-300 border-green-500/30"
: order.status === "IN_TRANSIT"
? "bg-yellow-500/20 text-yellow-300 border-yellow-500/30"
: order.status === "DELIVERED"
? "bg-purple-500/20 text-purple-300 border-purple-500/30"
: "bg-red-500/20 text-red-300 border-red-500/30"
} border flex items-center gap-1 text-xs px-2 py-1`}
>
{order.status === "PENDING" && (
<Clock className="h-3 w-3" />
)}
{order.status === "CONFIRMED" && (
<CheckCircle className="h-3 w-3" />
)}
{order.status === "IN_TRANSIT" && (
<Truck className="h-3 w-3" />
)}
{order.status === "DELIVERED" && (
<Package className="h-3 w-3" />
)}
{order.status === "CANCELLED" && (
<XCircle className="h-3 w-3" />
)}
{order.status === "PENDING" && "Ожидание"}
{order.status === "CONFIRMED" && "Подтверждена"}
{order.status === "IN_TRANSIT" && "В пути"}
{order.status === "DELIVERED" && "Доставлена"}
{order.status === "CANCELLED" && "Отменена"}
</Badge>
{getStatusBadge(order.status)}
{canMarkAsDelivered(order.status) && (
{canReceiveOrder(order.status) && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleStatusUpdate(order.id, "DELIVERED");
handleReceiveOrder(order.id);
}}
disabled={updating}
disabled={receiving}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-7"
>
<CheckCircle className="h-3 w-3 mr-1" />
Получено
Принять
</Button>
)}
</div>
@ -528,6 +679,100 @@ export function FulfillmentConsumablesOrdersTab() {
</div>
</div>
{/* Назначение логистики и ответственного в одной строке */}
{assigningOrders.has(order.id) && canAssignLogistics(order) && (
<div className="mt-2 p-2 bg-blue-500/10 border border-blue-500/20 rounded">
<div className="flex items-center gap-3">
{/* Иконка и заголовок */}
<div className="flex items-center text-blue-300 text-xs font-medium whitespace-nowrap">
<Settings className="h-3 w-3 mr-1" />
Назначить:
</div>
{/* Выбор логистики */}
<div className="flex-1 min-w-0">
<select
value={selectedLogistics[order.id] || ""}
onChange={(e) => {
setSelectedLogistics((prev) => ({
...prev,
[order.id]: e.target.value,
}));
}}
className="w-full bg-white/10 border border-white/20 text-white text-xs rounded px-2 py-1 focus:ring-2 focus:ring-blue-400/50 focus:border-blue-400/50 appearance-none"
>
<option value="" className="bg-gray-800 text-white">
{logisticsData?.logisticsPartners?.length > 0
? "Выберите логистику"
: "Нет логистики"}
</option>
{logisticsData?.logisticsPartners?.map(
(logistics: any) => (
<option
key={logistics.id}
value={logistics.id}
className="bg-gray-800 text-white"
>
{logistics.name || logistics.fullName}
</option>
)
) || []}
</select>
</div>
{/* Выбор ответственного */}
<div className="flex-1 min-w-0">
<select
value={selectedEmployees[order.id] || ""}
onChange={(e) => {
setSelectedEmployees((prev) => ({
...prev,
[order.id]: e.target.value,
}));
}}
className="w-full bg-white/10 border border-white/20 text-white text-xs rounded px-2 py-1 focus:ring-2 focus:ring-blue-400/50 focus:border-blue-400/50 appearance-none"
>
<option value="" className="bg-gray-800 text-white">
{employeesData?.myEmployees?.length > 0
? "Выберите ответственного"
: "Нет сотрудников"}
</option>
{employeesData?.myEmployees?.map((employee: any) => (
<option
key={employee.id}
value={employee.id}
className="bg-gray-800 text-white"
>
{employee.fullName || employee.name}
</option>
)) || []}
</select>
</div>
{/* Кнопки действий */}
<div className="flex gap-1 flex-shrink-0">
<Button
size="sm"
onClick={() => handleAssignLogistics(order.id)}
disabled={assigning}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-6"
>
<UserPlus className="h-3 w-3 mr-1" />
Принять
</Button>
<Button
size="sm"
variant="outline"
onClick={() => toggleAssignmentMode(order.id)}
className="border-white/20 text-white/60 hover:bg-white/10 text-xs px-2 py-1 h-6"
>
</Button>
</div>
</div>
</div>
)}
{/* Развернутые детали заказа */}
{expandedOrders.has(order.id) && (
<>

View File

@ -33,8 +33,6 @@ import {
CheckCircle,
} from "lucide-react";
// Интерфейс для заказа
interface SupplyOrder {
id: string;
@ -174,10 +172,14 @@ export function FulfillmentDetailedSuppliesTab() {
// "Расходники фулфилмента" = расходники, которые МЫ (фулфилмент-центр) заказали для себя
// Критерии: создатель = мы И получатель = мы (ОБА условия)
const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
(order: SupplyOrder) => {
(order: any) => {
// Защита от null/undefined значений
return (
order.organizationId === currentOrganizationId && // Создали мы
order.fulfillmentCenterId === currentOrganizationId // Получатель - мы
order?.organizationId === currentOrganizationId && // Создали мы
order?.fulfillmentCenterId === currentOrganizationId && // Получатель - мы
order?.organization && // Проверяем наличие organization
order?.partner && // Проверяем наличие partner
Array.isArray(order?.items) // Проверяем наличие items
);
}
);
@ -248,7 +250,9 @@ export function FulfillmentDetailedSuppliesTab() {
{/* Заголовок с кнопкой создания поставки */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white mb-1">Расходники фулфилмента</h2>
<h2 className="text-xl font-bold text-white mb-1">
Расходники фулфилмента
</h2>
<p className="text-white/60 text-sm">
Поставки расходников, поступающие на склад фулфилмент-центра
</p>

View File

@ -40,8 +40,6 @@ import {
} from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Интерфейсы для данных
interface Employee {
id: string;
@ -662,52 +660,50 @@ export function FulfillmentGoodsTab() {
};
return (
<div className="h-full flex flex-col p-2 xl:p-4">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="h-full flex flex-col"
>
{/* Вкладки товаров */}
<TabsList className="grid w-full grid-cols-3 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-8 xl:h-10 mb-2 xl:mb-4">
<TabsTrigger
value="new"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<Clock className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Новые</span>
<span className="sm:hidden">Н</span>
</TabsTrigger>
<TabsTrigger
value="receiving"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<FileText className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Приёмка</span>
<span className="sm:hidden">П</span>
</TabsTrigger>
<TabsTrigger
value="received"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 xl:gap-2 text-xs xl:text-sm"
>
<CheckCircle className="h-3 w-3 xl:h-4 xl:w-4" />
<span className="hidden sm:inline">Принято</span>
<span className="sm:hidden">Пр</span>
</TabsTrigger>
</TabsList>
<div className="space-y-3">
{/* УРОВЕНЬ 3: Подподтабы (маленький размер, больший отступ) */}
<div className="ml-8">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-white/5 backdrop-blur border-white/15 h-8 rounded-md p-1 mb-3">
<TabsTrigger
value="new"
className="data-[state=active]:bg-white/10 data-[state=active]:text-white text-white/50 hover:text-white/70 flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm"
>
<Clock className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Новые</span>
<span className="sm:hidden">Н</span>
</TabsTrigger>
<TabsTrigger
value="receiving"
className="data-[state=active]:bg-white/10 data-[state=active]:text-white text-white/50 hover:text-white/70 flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm"
>
<FileText className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Приёмка</span>
<span className="sm:hidden">П</span>
</TabsTrigger>
<TabsTrigger
value="received"
className="data-[state=active]:bg-white/10 data-[state=active]:text-white text-white/50 hover:text-white/70 flex items-center gap-1 text-xs font-normal transition-all duration-150 rounded-sm"
>
<CheckCircle className="h-2.5 w-2.5" />
<span className="hidden sm:inline">Принято</span>
<span className="sm:hidden">Пр</span>
</TabsTrigger>
</TabsList>
<TabsContent value="new" className="flex-1 overflow-hidden">
<TabContent tabName="new" />
</TabsContent>
<TabsContent value="new" className="space-y-0">
<TabContent tabName="new" />
</TabsContent>
<TabsContent value="receiving" className="flex-1 overflow-hidden">
<TabContent tabName="receiving" />
</TabsContent>
<TabsContent value="receiving" className="space-y-0">
<TabContent tabName="receiving" />
</TabsContent>
<TabsContent value="received" className="flex-1 overflow-hidden">
<TabContent tabName="received" />
</TabsContent>
</Tabs>
<TabsContent value="received" className="space-y-0">
<TabContent tabName="received" />
</TabsContent>
</Tabs>
</div>
</div>
);

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useQuery } from "@apollo/client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@ -32,13 +32,33 @@ export function FulfillmentSuppliesTab() {
const [activeTab, setActiveTab] = useState("goods");
// Загружаем данные о непринятых поставках
const { data: pendingData } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: "cache-first",
errorPolicy: "ignore",
});
const { data: pendingData, error: pendingError } = useQuery(
GET_PENDING_SUPPLIES_COUNT,
{
pollInterval: 30000, // Обновляем каждые 30 секунд
fetchPolicy: "cache-first",
errorPolicy: "ignore",
onError: (error) => {
console.error(
"❌ GET_PENDING_SUPPLIES_COUNT Error in FulfillmentSuppliesTab:",
error
);
},
}
);
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
// Логируем ошибку для диагностики
React.useEffect(() => {
if (pendingError) {
console.error(
"🚨 Ошибка загрузки счетчиков в FulfillmentSuppliesTab:",
pendingError
);
}
}, [pendingError]);
// ✅ ПРАВИЛЬНО: Для фулфилмента считаем только поставки, НЕ заявки на партнерство
const pendingCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0;
const ourSupplyOrdersCount =
pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0;
const sellerSupplyOrdersCount =
@ -66,74 +86,74 @@ export function FulfillmentSuppliesTab() {
};
return (
<div className="h-full flex flex-col">
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="h-full flex flex-col"
>
<TabsList className="grid w-full grid-cols-4 bg-white/10 backdrop-blur border-white/10 flex-shrink-0 h-8 xl:h-10 mb-2 xl:mb-3 mx-2 xl:mx-4 mt-2 xl:mt-4">
<TabsTrigger
value="goods"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs"
>
<Package className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
<span className="hidden sm:inline">Товар</span>
<span className="sm:hidden">Т</span>
</TabsTrigger>
<TabsTrigger
value="detailed-supplies"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs relative"
>
<Building2 className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
<span className="hidden md:inline">Расходники фулфилмента</span>
<span className="md:hidden hidden sm:inline">Фулфилмент</span>
<span className="sm:hidden">Ф</span>
<NotificationBadge count={ourSupplyOrdersCount} />
</TabsTrigger>
<TabsTrigger
value="consumables"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs relative"
>
<Wrench className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
<span className="hidden md:inline">Расходники селлеров</span>
<span className="md:hidden hidden sm:inline">Селлеры</span>
<span className="sm:hidden">С</span>
<NotificationBadge count={sellerSupplyOrdersCount} />
</TabsTrigger>
<TabsTrigger
value="returns"
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs"
>
<RotateCcw className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
<span className="sm:hidden">В</span>
</TabsTrigger>
</TabsList>
<TabsContent value="goods" className="flex-1 overflow-hidden">
<FulfillmentGoodsTab />
</TabsContent>
<TabsContent
value="detailed-supplies"
className="flex-1 overflow-hidden"
<div className="space-y-3">
{/* УРОВЕНЬ 2: Подтабы (средний размер, отступ показывает иерархию) */}
<div className="ml-4">
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="w-full"
>
<div className="h-full p-2 xl:p-4 overflow-y-auto">
<FulfillmentDetailedSuppliesTab />
</div>
</TabsContent>
<TabsList className="grid w-full grid-cols-4 bg-white/8 backdrop-blur border-white/20 h-9 rounded-lg p-1 mb-3">
<TabsTrigger
value="goods"
className="data-[state=active]:bg-white/15 data-[state=active]:text-white data-[state=active]:border-white/20 text-white/60 hover:text-white/80 flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md"
>
<Package className="h-3 w-3" />
<span className="hidden sm:inline">Товар</span>
<span className="sm:hidden">Т</span>
</TabsTrigger>
<TabsTrigger
value="detailed-supplies"
className="data-[state=active]:bg-white/15 data-[state=active]:text-white data-[state=active]:border-white/20 text-white/60 hover:text-white/80 flex items-center gap-1 text-xs font-medium transition-all duration-150 relative rounded-md"
>
<Building2 className="h-3 w-3" />
<span className="hidden md:inline">Расходники фулфилмента</span>
<span className="md:hidden hidden sm:inline">Фулфилмент</span>
<span className="sm:hidden">Ф</span>
<NotificationBadge count={ourSupplyOrdersCount} />
</TabsTrigger>
<TabsTrigger
value="consumables"
className="data-[state=active]:bg-white/15 data-[state=active]:text-white data-[state=active]:border-white/20 text-white/60 hover:text-white/80 flex items-center gap-1 text-xs font-medium transition-all duration-150 relative rounded-md"
>
<Wrench className="h-3 w-3" />
<span className="hidden md:inline">Расходники селлеров</span>
<span className="md:hidden hidden sm:inline">Селлеры</span>
<span className="sm:hidden">С</span>
<NotificationBadge count={sellerSupplyOrdersCount} />
</TabsTrigger>
<TabsTrigger
value="returns"
className="data-[state=active]:bg-white/15 data-[state=active]:text-white data-[state=active]:border-white/20 text-white/60 hover:text-white/80 flex items-center gap-1 text-xs font-medium transition-all duration-150 rounded-md"
>
<RotateCcw className="h-3 w-3" />
<span className="hidden sm:inline">Возвраты с ПВЗ</span>
<span className="sm:hidden">В</span>
</TabsTrigger>
</TabsList>
<TabsContent value="consumables" className="flex-1 overflow-hidden">
<div className="h-full p-2 xl:p-4 overflow-y-auto">
<FulfillmentConsumablesOrdersTab />
</div>
</TabsContent>
<TabsContent value="goods" className="space-y-0">
<FulfillmentGoodsTab />
</TabsContent>
<TabsContent value="returns" className="flex-1 overflow-hidden">
<PvzReturnsTab />
</TabsContent>
</Tabs>
<TabsContent value="detailed-supplies" className="space-y-0">
<div className="p-4">
<FulfillmentDetailedSuppliesTab />
</div>
</TabsContent>
<TabsContent value="consumables" className="space-y-0">
<div className="p-4">
<FulfillmentConsumablesOrdersTab />
</div>
</TabsContent>
<TabsContent value="returns" className="space-y-0">
<PvzReturnsTab />
</TabsContent>
</Tabs>
</div>
</div>
);
}