Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.
This commit is contained in:
@ -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(`❌ Недостаточно остатков!\nДоступно: ${availableStock} шт.\nЗапрашивается: ${quantity} шт.`);
|
||||
toast.error(
|
||||
`❌ Недостаточно остатков!\nДоступно: ${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>
|
||||
|
@ -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>
|
||||
|
@ -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) && (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user