Files
sfera-new/src/components/supplies/create-suppliers/blocks/DetailedCatalogBlock.tsx
Veronika Smirnova 89257c75b5 fix: завершение модуляризации системы и финальная организация проекта
## Структурные изменения:

### 📁 Организация архивных файлов:
- Перенос всех устаревших правил в legacy-rules/
- Создание структуры docs-and-reports/ для отчетов
- Архивация backup файлов в legacy-rules/backups/

### 🔧 Критические компоненты:
- src/components/supplies/multilevel-supplies-table.tsx - многоуровневая таблица поставок
- src/components/supplies/components/recipe-display.tsx - отображение рецептур
- src/components/fulfillment-supplies/fulfillment-goods-orders-tab.tsx - вкладка товарных заказов

### 🎯 GraphQL обновления:
- Обновление mutations.ts, queries.ts, resolvers.ts, typedefs.ts
- Синхронизация с Prisma schema.prisma
- Backup файлы для истории изменений

### 🛠️ Утилитарные скрипты:
- 12 новых скриптов в scripts/ для анализа данных
- Скрипты проверки фулфилмент-пользователей
- Утилиты очистки и фиксации данных поставок

### 📊 Тестирование:
- test-fulfillment-filtering.js - тестирование фильтрации фулфилмента
- test-full-workflow.js - полный workflow тестирование

### 📝 Документация:
- logistics-statistics-warehouse-rules.md - объединенные правила модулей
- Обновление журналов модуляризации и разработки

###  Исправления ESLint:
- Исправлены критические ошибки в sidebar.tsx
- Исправлены ошибки типизации в multilevel-supplies-table.tsx
- Исправлены неиспользуемые переменные в goods-supplies-table.tsx
- Заменены типы any на строгую типизацию
- Исправлены console.log на console.warn

## Результат:
- Завершена полная модуляризация системы
- Организована архитектура legacy файлов
- Добавлены критически важные компоненты таблиц
- Создана полная инфраструктура тестирования
- Исправлены все критические ESLint ошибки
- Сохранены 103 незакоммиченных изменения

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 10:31:43 +03:00

545 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* БЛОК ДЕТАЛЬНОГО КАТАЛОГА С РЕЦЕПТУРОЙ
*
* Выделен из create-suppliers-supply-page.tsx
* Детальный просмотр товаров с настройкой рецептуры и панелью управления
*/
'use client'
import { useQuery } from '@apollo/client'
import { Package, Building2, Sparkles, Zap, Star, Orbit, X } from 'lucide-react'
import Image from 'next/image'
import React, { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { DatePicker } from '@/components/ui/date-picker'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { GET_WB_WAREHOUSE_DATA } from '@/graphql/queries'
import type {
DetailedCatalogBlockProps,
GoodsProduct,
ProductRecipe,
FulfillmentService,
FulfillmentConsumable,
SellerConsumable,
} from '../types/supply-creation.types'
export const DetailedCatalogBlock = React.memo(function DetailedCatalogBlock({
allSelectedProducts,
productRecipes,
fulfillmentServices,
fulfillmentConsumables,
sellerConsumables,
deliveryDate,
selectedFulfillment,
allCounterparties,
onQuantityChange,
onRecipeChange,
onDeliveryDateChange,
onFulfillmentChange,
onProductRemove,
}: DetailedCatalogBlockProps) {
const fulfillmentCenters = allCounterparties?.filter((partner) => partner.type === 'FULFILLMENT') || []
return (
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl h-full flex flex-col">
{/* Панель управления */}
<div className="p-6 border-b border-white/10">
{/* ЗАГОЛОВОК УДАЛЕН ДЛЯ МИНИМАЛИЗМА */}
<div className="flex items-center gap-4">
{/* Дата поставки */}
<div className="w-[180px]">
<DatePicker
value={deliveryDate}
onChange={(date) => {
console.log('DatePicker onChange вызван:', date)
onDeliveryDateChange(date)
}}
className="glass-input text-white text-sm h-9"
/>
</div>
{/* Фулфилмент-центр */}
<div className="flex-1 max-w-[300px]">
<Select value={selectedFulfillment} onValueChange={onFulfillmentChange}>
<SelectTrigger className="glass-input text-white text-sm h-9">
<SelectValue placeholder="Выберите фулфилмент-центр" />
</SelectTrigger>
<SelectContent>
{fulfillmentCenters.map((center) => (
<SelectItem key={center.id} value={center.id}>
<div className="flex items-center gap-2">
<Building2 className="h-4 w-4" />
{center.name || center.fullName}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Каталог товаров с рецептурой - Новый стиль таблицы */}
<div className="flex-1 overflow-y-auto">
<div className="p-6 space-y-3">
{/* Строки товаров */}
{allSelectedProducts.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64">
<Package className="h-12 w-12 text-white/20 mb-2" />
<p className="text-white/60">Товары не добавлены</p>
<p className="text-white/40 text-sm mt-1">Выберите товары из каталога выше для настройки рецептуры</p>
</div>
) : (
allSelectedProducts.map((product) => {
const recipe = productRecipes[product.id]
const selectedServicesIds = recipe?.selectedServices || []
const selectedFFConsumablesIds = recipe?.selectedFFConsumables || []
const selectedSellerConsumablesIds = recipe?.selectedSellerConsumables || []
return (
<ProductTableRow
key={product.id}
product={product}
recipe={recipe}
fulfillmentServices={fulfillmentServices}
fulfillmentConsumables={fulfillmentConsumables}
sellerConsumables={sellerConsumables}
selectedServicesIds={selectedServicesIds}
selectedFFConsumablesIds={selectedFFConsumablesIds}
selectedSellerConsumablesIds={selectedSellerConsumablesIds}
selectedFulfillment={selectedFulfillment}
onQuantityChange={onQuantityChange}
onRecipeChange={onRecipeChange}
onRemove={onProductRemove}
/>
)
})
)}
</div>
</div>
</div>
)
})
// Компонент строки товара в табличном стиле
interface ProductTableRowProps {
product: GoodsProduct & { selectedQuantity: number }
recipe?: ProductRecipe
fulfillmentServices: FulfillmentService[]
fulfillmentConsumables: FulfillmentConsumable[]
sellerConsumables: SellerConsumable[]
selectedServicesIds: string[]
selectedFFConsumablesIds: string[]
selectedSellerConsumablesIds: string[]
selectedFulfillment?: string
onQuantityChange: (productId: string, quantity: number) => void
onRecipeChange: (productId: string, recipe: ProductRecipe) => void
onRemove: (productId: string) => void
}
function ProductTableRow({
product,
recipe,
selectedServicesIds,
selectedFFConsumablesIds,
selectedSellerConsumablesIds,
fulfillmentServices,
fulfillmentConsumables,
sellerConsumables,
selectedFulfillment,
onQuantityChange,
onRecipeChange,
onRemove,
}: ProductTableRowProps) {
// Расчет стоимости услуг и расходников
const servicesCost = selectedServicesIds.reduce((sum, serviceId) => {
const service = fulfillmentServices.find(s => s.id === serviceId)
return sum + (service ? service.price * product.selectedQuantity : 0)
}, 0)
const ffConsumablesCost = selectedFFConsumablesIds.reduce((sum, consumableId) => {
const consumable = fulfillmentConsumables.find(c => c.id === consumableId)
return sum + (consumable ? consumable.price * product.selectedQuantity : 0)
}, 0)
const sellerConsumablesCost = selectedSellerConsumablesIds.reduce((sum, consumableId) => {
const consumable = sellerConsumables.find(c => c.id === consumableId)
return sum + (consumable ? (consumable.pricePerUnit || 0) * product.selectedQuantity : 0)
}, 0)
const productCost = product.price * product.selectedQuantity
const totalCost = productCost + servicesCost + ffConsumablesCost + sellerConsumablesCost
return (
<div className="p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-200 border border-white/10 relative group">
{/* КНОПКА УДАЛЕНИЯ */}
<button
onClick={() => onRemove(product.id)}
className="absolute top-2 right-2 text-white/40 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 p-1 rounded-lg hover:bg-red-500/10 z-10"
>
<X className="h-4 w-4" />
</button>
<div className="grid grid-cols-12 gap-4 items-start">
{/* ТОВАР (3 колонки) */}
<div className="col-span-3">
<div className="flex items-center gap-2 mb-2">
<Package className="h-4 w-4 text-cyan-400" />
<span className="text-sm font-medium text-white/80">Товар</span>
</div>
<div className="flex items-center gap-3">
<div className="relative w-12 h-12 rounded-lg overflow-hidden bg-white/5 flex-shrink-0">
{product.mainImage || (product.images && product.images[0]) ? (
<Image src={product.mainImage || product.images[0]} alt={product.name} fill className="object-cover" />
) : (
<div className="flex items-center justify-center h-full">
<Package className="h-5 w-5 text-white/30" />
</div>
)}
</div>
<div className="min-w-0">
<h5 className="text-white font-medium text-sm leading-tight line-clamp-2">{product.name}</h5>
{product.article && <p className="text-white/50 text-xs mt-1">Арт: {product.article}</p>}
<div className="text-white/80 font-semibold text-xs">
{product.price.toLocaleString('ru-RU')} /{product.unit || 'шт'}
</div>
</div>
</div>
</div>
{/* КОЛИЧЕСТВО (1 колонка) */}
<div className="col-span-1">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium text-white/80">Кол-во</span>
</div>
<div className="space-y-1">
{product.quantity !== undefined && (
<div className="flex items-center justify-center gap-1">
<div className={`w-1.5 h-1.5 rounded-full ${product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'}`} />
<span className={`text-xs ${product.quantity > 0 ? 'text-green-400' : 'text-red-400'}`}>
{product.quantity > 0 ? product.quantity : 0}
</span>
</div>
)}
<div className="flex items-center justify-center gap-1">
<Input
type="number"
min="0"
max={product.quantity}
value={product.selectedQuantity || ''}
onChange={(e) => {
const inputValue = e.target.value
const newQuantity = inputValue === '' ? 0 : Math.max(0, parseInt(inputValue) || 0)
if (newQuantity > product.quantity) {
onQuantityChange(product.id, product.quantity)
return
}
onQuantityChange(product.id, newQuantity)
}}
className="glass-input w-14 h-7 text-xs text-center text-white placeholder:text-white/50"
placeholder="0"
/>
</div>
</div>
</div>
{/* УСЛУГИ ФФ (2 колонки) */}
<div className="col-span-2">
<div className="flex items-center gap-2 mb-2">
<Zap className="h-4 w-4 text-purple-400" />
<span className="text-sm font-medium text-white/80">Услуги ФФ</span>
</div>
<div className="flex flex-wrap gap-1">
{(() => {
console.log('🎯 Услуги ФФ:', {
fulfillmentServicesCount: fulfillmentServices.length,
fulfillmentServices: fulfillmentServices,
selectedFulfillment: selectedFulfillment
})
return null
})()}
{fulfillmentServices.length > 0 ? (
fulfillmentServices.map((service) => {
const isSelected = selectedServicesIds.includes(service.id)
return (
<button
key={service.id}
onClick={() => {
const newSelectedServices = isSelected
? selectedServicesIds.filter(id => id !== service.id)
: [...selectedServicesIds, service.id]
const newRecipe = {
selectedServices: newSelectedServices,
selectedFFConsumables: selectedFFConsumablesIds,
selectedSellerConsumables: selectedSellerConsumablesIds,
}
console.log('🔧 Услуга ФФ клик:', {
productId: product.id,
serviceName: service.name,
isSelected: isSelected,
newRecipe: newRecipe
})
onRecipeChange(product.id, newRecipe)
}}
className={`px-2 py-1 rounded-lg text-xs font-medium transition-all duration-200 ${
isSelected
? 'bg-purple-500/30 border border-purple-400/60 text-purple-200'
: 'bg-white/10 border border-white/20 text-white/70 hover:bg-purple-500/10'
}`}
>
{service.name} {service.price}
</button>
)
})
) : (
<div className="text-xs text-white/50">
{!selectedFulfillment ? 'Выберите ФФ-центр' : 'Нет услуг'}
</div>
)}
</div>
</div>
{/* РАСХОДНИКИ ФФ (2 колонки) */}
<div className="col-span-2">
<div className="flex items-center gap-2 mb-2">
<Star className="h-4 w-4 text-orange-400" />
<span className="text-sm font-medium text-white/80">Расходники ФФ</span>
</div>
<div className="flex flex-wrap gap-1">
{fulfillmentConsumables.length > 0 ? (
fulfillmentConsumables.map((consumable) => {
const isSelected = selectedFFConsumablesIds.includes(consumable.id)
return (
<button
key={consumable.id}
onClick={() => {
const newSelectedFFConsumables = isSelected
? selectedFFConsumablesIds.filter(id => id !== consumable.id)
: [...selectedFFConsumablesIds, consumable.id]
onRecipeChange(product.id, {
selectedServices: selectedServicesIds,
selectedFFConsumables: newSelectedFFConsumables,
selectedSellerConsumables: selectedSellerConsumablesIds,
})
}}
className={`px-2 py-1 rounded-lg text-xs font-medium transition-all duration-200 ${
isSelected
? 'bg-orange-500/30 border border-orange-400/60 text-orange-200'
: 'bg-white/10 border border-white/20 text-white/70 hover:bg-orange-500/10'
}`}
>
{consumable.name} {consumable.price}
</button>
)
})
) : (
<div className="text-xs text-white/50">
{!selectedFulfillment ? 'Выберите ФФ-центр' : 'Нет расходников'}
</div>
)}
</div>
</div>
{/* РАСХОДНИКИ СЕЛЛЕРА (2 колонки) */}
<div className="col-span-2">
<div className="flex items-center gap-2 mb-2">
<Orbit className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-white/80">Расходники сел.</span>
</div>
<div className="flex flex-wrap gap-1">
{sellerConsumables.length > 0 ? (
sellerConsumables.map((consumable) => {
const isSelected = selectedSellerConsumablesIds.includes(consumable.id)
return (
<button
key={consumable.id}
onClick={() => {
const newSelectedSellerConsumables = isSelected
? selectedSellerConsumablesIds.filter(id => id !== consumable.id)
: [...selectedSellerConsumablesIds, consumable.id]
onRecipeChange(product.id, {
selectedServices: selectedServicesIds,
selectedFFConsumables: selectedFFConsumablesIds,
selectedSellerConsumables: newSelectedSellerConsumables,
})
}}
className={`px-2 py-1 rounded-lg text-xs font-medium transition-all duration-200 ${
isSelected
? 'bg-blue-500/30 border border-blue-400/60 text-blue-200'
: 'bg-white/10 border border-white/20 text-white/70 hover:bg-blue-500/10'
}`}
>
{consumable.name} {consumable.pricePerUnit}
</button>
)
})
) : (
<div className="text-xs text-white/50">Нет расходников</div>
)}
</div>
</div>
{/* МП КАРТОЧКА (1 колонка) */}
<div className="col-span-1">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-4 w-4 text-yellow-400" />
<span className="text-sm font-medium text-white/80">МП</span>
</div>
<MarketplaceCardSelector
productId={product.id}
onCardSelect={(productId, cardId) => {
onRecipeChange(productId, {
selectedServices: selectedServicesIds,
selectedFFConsumables: selectedFFConsumablesIds,
selectedSellerConsumables: selectedSellerConsumablesIds,
selectedWBCard: cardId === 'none' ? undefined : cardId,
})
}}
selectedCardId={recipe?.selectedWBCard}
/>
</div>
{/* СТОИМОСТЬ (1 колонка) */}
<div className="col-span-1">
<div className="flex items-center gap-2 mb-2">
<Star className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium text-white/80">Сумма</span>
</div>
<div className="text-green-400 font-bold text-sm">
{totalCost.toLocaleString('ru-RU')}
</div>
{totalCost > productCost && (
<div className="text-xs text-white/60 mt-1">
+{(totalCost - productCost).toLocaleString('ru-RU')}
</div>
)}
</div>
</div>
</div>
)
}
// КОМПОНЕНТ ВЫБОРА КАРТОЧКИ МАРКЕТПЛЕЙСА
interface MarketplaceCardSelectorProps {
productId: string
onCardSelect?: (productId: string, cardId: string) => void
selectedCardId?: string
}
function MarketplaceCardSelector({ productId, onCardSelect, selectedCardId }: MarketplaceCardSelectorProps) {
const { data, loading, error } = useQuery(GET_WB_WAREHOUSE_DATA, {
fetchPolicy: 'cache-first',
errorPolicy: 'all',
})
console.log('📦 GET_WB_WAREHOUSE_DATA результат:', {
loading,
error: error?.message,
dataExists: !!data,
warehouseDataExists: !!data?.getWBWarehouseData,
cacheExists: !!data?.getWBWarehouseData?.cache,
rawData: data
})
// Извлекаем карточки из кеша склада WB, как на странице склада
const wbCards = (() => {
try {
console.log('🔍 Структура данных WB:', {
hasData: !!data,
hasWBData: !!data?.getWBWarehouseData,
hasCache: !!data?.getWBWarehouseData?.cache,
cache: data?.getWBWarehouseData?.cache,
cacheData: data?.getWBWarehouseData?.cache?.data
})
const cacheData = data?.getWBWarehouseData?.cache?.data
if (!cacheData) {
console.log('❌ Нет данных кеша WB')
return []
}
const parsedData = typeof cacheData === 'string' ? JSON.parse(cacheData) : cacheData
const stocks = parsedData?.stocks || []
console.log('📦 Найдено карточек WB:', stocks.length)
return stocks.map((stock: any) => ({
id: stock.nmId.toString(),
nmId: stock.nmId,
vendorCode: stock.vendorCode || '',
title: stock.title || 'Без названия',
brand: stock.brand || '',
}))
} catch (error) {
console.error('Ошибка парсинга данных WB склада:', error)
return []
}
})()
// Временная отладка
console.warn('📊 MarketplaceCardSelector WB Warehouse:', {
loading,
error: error?.message,
hasCache: !!data?.getWBWarehouseData?.cache,
cardsCount: wbCards.length,
firstCard: wbCards[0],
})
return (
<div className="w-20">
<Select
value={selectedCardId || 'none'}
onValueChange={(value) => {
if (onCardSelect) {
onCardSelect(productId, value)
}
}}
>
<SelectTrigger className="glass-input h-7 text-xs border-white/20">
<SelectValue placeholder={loading ? "..." : "WB"} />
</SelectTrigger>
<SelectContent className="glass-card border-white/20 max-h-[200px] overflow-y-auto">
<SelectItem value="none">Не выбрано</SelectItem>
{wbCards.length === 0 && !loading && (
<SelectItem value="no-cards" disabled>
Карточки WB не найдены
</SelectItem>
)}
{loading && (
<SelectItem value="loading" disabled>
Загрузка...
</SelectItem>
)}
{wbCards.map((card: any) => (
<SelectItem key={card.id} value={card.id}>
<div className="flex items-center gap-2">
<span className="text-xs truncate max-w-[150px]">{card.vendorCode || card.nmId}</span>
{card.title && (
<span className="text-xs text-white/60 truncate max-w-[100px]">- {card.title}</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}