feat(fulfillment-supplies): миграция формы создания поставок расходников на v2 систему

- Обновлена форма создания поставок расходников фулфилмента для использования 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>
This commit is contained in:
Veronika Smirnova
2025-08-25 07:52:46 +03:00
parent d05f0a6a93
commit 0e3ffc179c
34 changed files with 5795 additions and 565 deletions

View File

@ -0,0 +1,309 @@
'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>
)
}