
- Обновлена форма создания поставок расходников фулфилмента для использования v2 GraphQL API - Заменена мутация CREATE_SUPPLY_ORDER на CREATE_FULFILLMENT_CONSUMABLE_SUPPLY - Обновлена структура input данных под новый формат v2 - Сделано поле логистики опциональным - Добавлено поле notes для комментариев к поставке - Обновлены refetchQueries на новые v2 запросы - Исправлены TypeScript ошибки в интерфейсах - Удалена дублирующая страница consumables-v2 - Сохранен оригинальный богатый UI интерфейс формы (819 строк) - Подтверждена работа с новой таблицей FulfillmentConsumableSupplyOrder Технические изменения: - src/components/fulfillment-supplies/create-fulfillment-consumables-supply-v2.tsx - основная форма - src/components/fulfillment-supplies/fulfillment-supplies-layout.tsx - обновлена навигация - Добавлены недостающие поля quantity и ordered в интерфейсы продуктов - Исправлены импорты и зависимости Результат: форма полностью интегрирована с v2 системой поставок, которая использует отдельные таблицы для каждого типа поставок согласно новой архитектуре. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
309 lines
13 KiB
TypeScript
309 lines
13 KiB
TypeScript
'use client'
|
||
|
||
import { useQuery } from '@apollo/client'
|
||
import { Building2, ShoppingCart, Package, Wrench, RotateCcw, Clock, FileText, CheckCircle, ChevronRight, Home } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
import { usePathname } from 'next/navigation'
|
||
import React from 'react'
|
||
|
||
import { Sidebar } from '@/components/dashboard/sidebar'
|
||
import { GET_PENDING_SUPPLIES_COUNT } from '@/graphql/queries'
|
||
import { useRealtime } from '@/hooks/useRealtime'
|
||
import { useSidebar } from '@/hooks/useSidebar'
|
||
|
||
// Компонент для отображения бейджа с уведомлениями
|
||
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}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Breadcrumbs компонент
|
||
function Breadcrumbs({ pathname }: { pathname: string }) {
|
||
const getBreadcrumbs = (path: string) => {
|
||
const segments = path.split('/').filter(Boolean)
|
||
|
||
const breadcrumbs = [
|
||
{ name: 'Главная', href: '/dashboard', icon: Home }
|
||
]
|
||
|
||
if (segments[0] === 'fulfillment-supplies') {
|
||
breadcrumbs.push({ name: 'Входящие поставки', href: '/fulfillment-supplies', icon: Building2 })
|
||
|
||
if (segments[1]) {
|
||
const categoryMap: Record<string, string> = {
|
||
'goods': 'Товары',
|
||
'detailed-supplies': 'Расходники фулфилмента',
|
||
'consumables': 'Расходники селлеров',
|
||
'returns': 'Возвраты с ПВЗ'
|
||
}
|
||
|
||
const category = categoryMap[segments[1]]
|
||
if (category) {
|
||
breadcrumbs.push({
|
||
name: category,
|
||
href: segments[2] ? `/fulfillment-supplies/${segments[1]}` : path,
|
||
icon: Package
|
||
})
|
||
|
||
if (segments[1] === 'goods' && segments[2]) {
|
||
const subcategoryMap: Record<string, string> = {
|
||
'new': 'Новые',
|
||
'receiving': 'Приёмка',
|
||
'received': 'Принято'
|
||
}
|
||
|
||
const subcategory = subcategoryMap[segments[2]]
|
||
if (subcategory) {
|
||
breadcrumbs.push({ name: subcategory, href: path, icon: Clock })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return breadcrumbs
|
||
}
|
||
|
||
const breadcrumbs = getBreadcrumbs(pathname)
|
||
|
||
return (
|
||
<nav className="flex items-center space-x-2 text-sm text-white/60 mb-4">
|
||
{breadcrumbs.map((breadcrumb, index) => (
|
||
<React.Fragment key={breadcrumb.href}>
|
||
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
||
{index === breadcrumbs.length - 1 ? (
|
||
<span className="text-white font-medium flex items-center gap-1">
|
||
{breadcrumb.icon && <breadcrumb.icon className="h-4 w-4" />}
|
||
{breadcrumb.name}
|
||
</span>
|
||
) : (
|
||
<Link
|
||
href={breadcrumb.href}
|
||
className="hover:text-white transition-colors flex items-center gap-1"
|
||
>
|
||
{breadcrumb.icon && <breadcrumb.icon className="h-4 w-4" />}
|
||
{breadcrumb.name}
|
||
</Link>
|
||
)}
|
||
</React.Fragment>
|
||
))}
|
||
</nav>
|
||
)
|
||
}
|
||
|
||
export function FulfillmentSuppliesLayout({ children }: { children: React.ReactNode }) {
|
||
const { getSidebarMargin } = useSidebar()
|
||
const pathname = usePathname()
|
||
|
||
// Загружаем данные о непринятых поставках
|
||
const { data: pendingData, error: pendingError, refetch: refetchPending } = useQuery(GET_PENDING_SUPPLIES_COUNT, {
|
||
fetchPolicy: 'cache-first',
|
||
errorPolicy: 'ignore',
|
||
onError: (error) => {
|
||
console.error('❌ GET_PENDING_SUPPLIES_COUNT Error:', error)
|
||
},
|
||
})
|
||
|
||
// Realtime: обновление бейджа
|
||
useRealtime({
|
||
onEvent: (evt) => {
|
||
if (evt.type === 'supply-order:new' || evt.type === 'supply-order:updated') {
|
||
refetchPending()
|
||
}
|
||
},
|
||
})
|
||
|
||
// Логируем ошибку для диагностики
|
||
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
|
||
|
||
// Определяем активные табы на основе pathname
|
||
const getActiveTab = () => {
|
||
if (pathname.startsWith('/fulfillment-supplies/goods')) return 'fulfillment'
|
||
if (pathname.includes('/detailed-supplies')) return 'fulfillment'
|
||
if (pathname.includes('/consumables')) return 'fulfillment'
|
||
if (pathname.includes('/returns')) return 'fulfillment'
|
||
return 'fulfillment'
|
||
}
|
||
|
||
const getActiveSubTab = () => {
|
||
if (pathname.startsWith('/fulfillment-supplies/goods')) return 'goods'
|
||
if (pathname.includes('/detailed-supplies')) return 'detailed-supplies'
|
||
if (pathname.includes('/consumables')) return 'consumables'
|
||
if (pathname.includes('/returns')) return 'returns'
|
||
return 'goods'
|
||
}
|
||
|
||
const getActiveThirdTab = () => {
|
||
if (pathname.includes('/goods/new')) return 'new'
|
||
if (pathname.includes('/goods/receiving')) return 'receiving'
|
||
if (pathname.includes('/goods/received')) return 'received'
|
||
return 'new'
|
||
}
|
||
|
||
const activeTab = getActiveTab()
|
||
const activeSubTab = getActiveSubTab()
|
||
const activeThirdTab = getActiveThirdTab()
|
||
|
||
return (
|
||
<div className="h-screen flex overflow-hidden">
|
||
<Sidebar />
|
||
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
|
||
<div className="h-full w-full flex flex-col space-y-4">
|
||
{/* Breadcrumbs */}
|
||
<Breadcrumbs pathname={pathname} />
|
||
|
||
{/* БЛОК 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">
|
||
<Link
|
||
href="/fulfillment-supplies/goods/new"
|
||
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-4 w-4" />
|
||
<span className="hidden sm:inline">Поставки на фулфилмент</span>
|
||
<span className="sm:hidden">Фулфилмент</span>
|
||
<NotificationBadge count={pendingCount} />
|
||
</Link>
|
||
<button
|
||
className="flex items-center gap-2 text-sm font-semibold transition-all duration-200 rounded-lg px-3 text-white/40 cursor-not-allowed"
|
||
disabled
|
||
>
|
||
<ShoppingCart className="h-4 w-4" />
|
||
<span className="hidden sm:inline">Поставки на маркетплейсы</span>
|
||
<span className="sm:hidden">Маркетплейсы</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* УРОВЕНЬ 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">
|
||
<Link
|
||
href="/fulfillment-supplies/goods/new"
|
||
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>
|
||
</Link>
|
||
<Link
|
||
href="/fulfillment-supplies/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} />
|
||
</Link>
|
||
<Link
|
||
href="/fulfillment-supplies/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} />
|
||
</Link>
|
||
<Link
|
||
href="/fulfillment-supplies/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>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* УРОВЕНЬ 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">
|
||
<Link
|
||
href="/fulfillment-supplies/goods/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>
|
||
</Link>
|
||
<Link
|
||
href="/fulfillment-supplies/goods/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>
|
||
</Link>
|
||
<Link
|
||
href="/fulfillment-supplies/goods/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>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* БЛОК 2: ОСНОВНОЙ КОНТЕНТ */}
|
||
<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">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
} |