Files
sfera-new/src/components/supplies/create-supply-page.tsx
Veronika Smirnova 10af6f08cc Обновления системы после анализа и оптимизации архитектуры
- Обновлена схема Prisma с новыми полями и связями
- Актуализированы правила системы в rules-complete.md
- Оптимизированы GraphQL типы, запросы и мутации
- Улучшены компоненты интерфейса и валидация данных
- Исправлены критические ESLint ошибки: удалены неиспользуемые импорты и переменные
- Добавлены тестовые файлы для проверки функционала

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-06 23:44:49 +03:00

381 lines
18 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.

'use client'
import { useQuery } from '@apollo/client'
import { ArrowLeft, Package, CalendarIcon, Building } from 'lucide-react'
import { useRouter } from 'next/navigation'
import React, { useState, useMemo, useCallback } from 'react'
import { Sidebar } from '@/components/dashboard/sidebar'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { GET_MY_COUNTERPARTIES, GET_ORGANIZATION_LOGISTICS } from '@/graphql/queries'
import { useSidebar } from '@/hooks/useSidebar'
import { apolloClient } from '@/lib/apollo-client'
import { DirectSupplyCreation } from './direct-supply-creation'
// Компонент создания поставки товаров с новым интерфейсом
interface Organization {
id: string
name?: string
fullName?: string
type: string
}
const CreateSupplyPage = React.memo(() => {
const router = useRouter()
const { getSidebarMargin } = useSidebar()
const [canCreateSupply, setCanCreateSupply] = useState(false)
const [isCreatingSupply, setIsCreatingSupply] = useState(false)
// Состояния для полей формы
const [deliveryDate, setDeliveryDate] = useState<string>('')
const [selectedFulfillment, setSelectedFulfillment] = useState<string>('')
const [goodsVolume, setGoodsVolume] = useState<number>(0)
const [cargoPlaces, setCargoPlaces] = useState<number>(0)
const [goodsPrice, setGoodsPrice] = useState<number>(0)
const [fulfillmentServicesPrice, setFulfillmentServicesPrice] = useState<number>(0)
const [logisticsPrice, setLogisticsPrice] = useState<number>(0)
const [selectedServicesCost, setSelectedServicesCost] = useState<number>(0)
const [selectedConsumablesCost, setSelectedConsumablesCost] = useState<number>(0)
const [hasItemsInSupply, setHasItemsInSupply] = useState<boolean>(false)
// Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
// Фильтруем только фулфилмент организации
const fulfillmentOrgs = useMemo(() =>
(counterpartiesData?.myCounterparties || []).filter(
(org: Organization) => org.type === 'FULFILLMENT',
), [counterpartiesData?.myCounterparties],
)
const formatCurrency = useCallback((amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
}).format(amount)
}, [])
// Функция для обновления цены товаров из поставки
const handleItemsUpdate = useCallback((totalItemsPrice: number) => {
setGoodsPrice(totalItemsPrice)
}, [])
// Функция для обновления статуса наличия товаров
const handleItemsCountChange = useCallback((hasItems: boolean) => {
setHasItemsInSupply(hasItems)
}, [])
// Функция для обновления объема товаров из поставки
const handleVolumeUpdate = useCallback((totalVolume: number) => {
setGoodsVolume(totalVolume)
// После обновления объема пересчитываем логистику (если есть поставщик)
// calculateLogisticsPrice будет вызван из handleSuppliersUpdate
}, [])
// Функция для обновления информации о поставщиках (для расчета логистики)
const handleSuppliersUpdate = (suppliersData: unknown[]) => {
// Находим рынок из выбранного поставщика
const selectedSupplier = suppliersData.find((supplier: unknown) => (supplier as { selected?: boolean }).selected)
const supplierMarket = (selectedSupplier as { market?: string })?.market
console.warn('Обновление поставщиков:', {
selectedSupplier,
supplierMarket,
volume: goodsVolume,
})
// Пересчитываем логистику с учетом рынка поставщика
calculateLogisticsPrice(goodsVolume, supplierMarket)
}
// Функция для расчета логистики по рынку поставщика и объему
const calculateLogisticsPrice = async (volume: number, supplierMarket?: string) => {
// Логистика рассчитывается ТОЛЬКО если есть:
// 1. Выбранный фулфилмент
// 2. Объем товаров > 0
// 3. Рынок поставщика (откуда везти)
if (!selectedFulfillment || !volume || volume <= 0 || !supplierMarket) {
setLogisticsPrice(0)
return
}
try {
console.warn(`Расчет логистики: ${supplierMarket}${selectedFulfillment}, объем: ${volume.toFixed(4)} м³`)
// Получаем логистику выбранного фулфилмента из БД
const { data: logisticsData } = await apolloClient.query({
query: GET_ORGANIZATION_LOGISTICS,
variables: { organizationId: selectedFulfillment },
fetchPolicy: 'network-only',
})
const logistics = logisticsData?.organizationLogistics || []
console.warn(`Логистика фулфилмента ${selectedFulfillment}:`, logistics)
// Ищем логистику для данного рынка
const logisticsRoute = logistics.find(
(route: { fromLocation: string; toLocation: string; pricePerCubicMeter: number }) =>
route.fromLocation.toLowerCase().includes(supplierMarket.toLowerCase()) ||
supplierMarket.toLowerCase().includes(route.fromLocation.toLowerCase()),
)
if (!logisticsRoute) {
console.warn(`Логистика для рынка "${supplierMarket}" не найдена`)
setLogisticsPrice(0)
return
}
// Выбираем цену в зависимости от объема
const pricePerM3 = volume <= 1 ? logisticsRoute.priceUnder1m3 : logisticsRoute.priceOver1m3
const calculatedPrice = volume * pricePerM3
console.warn(`Найдена логистика: ${logisticsRoute.fromLocation}${logisticsRoute.toLocation}`)
console.warn(
`Цена: ${pricePerM3}₽/м³ (${
volume <= 1 ? 'до 1м³' : 'больше 1м³'
}) × ${volume.toFixed(4)}м³ = ${calculatedPrice.toFixed(2)}`,
)
setLogisticsPrice(calculatedPrice)
} catch (error) {
console.error('Error calculating logistics price:', error)
setLogisticsPrice(0)
}
}
const getTotalSum = useMemo(() => {
return goodsPrice + selectedServicesCost + selectedConsumablesCost + logisticsPrice
}, [goodsPrice, selectedServicesCost, selectedConsumablesCost, logisticsPrice])
const handleSupplyComplete = useCallback(() => {
router.push('/supplies')
}, [router])
const handleCreateSupplyClick = useCallback(() => {
setIsCreatingSupply(true)
}, [])
const handleCanCreateSupplyChange = useCallback((canCreate: boolean) => {
setCanCreateSupply(canCreate)
}, [])
// Пересчитываем логистику при изменении фулфилмента (если есть поставщик)
React.useEffect(() => {
// Логистика пересчитается автоматически через handleSuppliersUpdate
// когда будет выбран поставщик с рынком
}, [selectedFulfillment, goodsVolume])
const handleSupplyCompleted = useCallback(() => {
setIsCreatingSupply(false)
handleSupplyComplete()
}, [handleSupplyComplete])
// Главная страница с табами в новом стиле интерфейса
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300 p-4`}>
<div className="min-h-full w-full flex flex-col gap-4">
{/* Заголовок */}
<div className="flex items-center justify-between flex-shrink-0">
<div>
<h1 className="text-xl font-bold text-white mb-1">Создание поставки товаров</h1>
<p className="text-white/60 text-sm">Выберите карточки товаров Wildberries для создания поставки</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => router.push('/supplies')}
className="text-white/60 hover:text-white hover:bg-white/10 text-sm"
>
<ArrowLeft className="h-4 w-4 mr-1" />
Назад
</Button>
</div>
{/* Основной контент - карточки Wildberries */}
<div className="flex-1 flex gap-4 min-h-0">
{/* Левая колонка - карточки товаров */}
<div className="flex-1 min-h-0">
<DirectSupplyCreation
onComplete={handleSupplyCompleted}
onCreateSupply={handleCreateSupplyClick}
canCreateSupply={canCreateSupply}
isCreatingSupply={isCreatingSupply}
onCanCreateSupplyChange={handleCanCreateSupplyChange}
selectedFulfillmentId={selectedFulfillment}
onServicesCostChange={setSelectedServicesCost}
onItemsPriceChange={handleItemsUpdate}
onItemsCountChange={handleItemsCountChange}
onConsumablesCostChange={setSelectedConsumablesCost}
onVolumeChange={handleVolumeUpdate}
onSuppliersChange={handleSuppliersUpdate}
/>
</div>
{/* Правая колонка - Форма поставки */}
<div className="w-80 flex-shrink-0">
<Card className="bg-white/10 backdrop-blur-xl border-white/20 p-4 sticky top-0">
<h3 className="text-white font-semibold mb-4 flex items-center text-sm">
<Package className="h-4 w-4 mr-2" />
Параметры поставки
</h3>
{/* Первая строка */}
<div className="space-y-3 mb-4">
{/* 1. Модуль выбора даты */}
<div>
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<CalendarIcon className="h-3 w-3" />
Дата
</Label>
<input
type="date"
value={deliveryDate}
onChange={(e) => setDeliveryDate(e.target.value)}
className="w-full h-8 rounded-lg border-0 bg-white/20 backdrop-blur px-3 py-1 text-white placeholder:text-white/50 focus:bg-white/30 focus:outline-none focus:ring-1 focus:ring-white/20 text-xs font-medium"
min={new Date().toISOString().split('T')[0]}
/>
</div>
{/* 2. Модуль выбора фулфилмента */}
<div>
<Label className="text-white/80 text-xs mb-1 block flex items-center gap-1">
<Building className="h-3 w-3" />
Фулфилмент
</Label>
<Select
value={selectedFulfillment}
onValueChange={(value) => {
console.warn('Выбран фулфилмент:', value)
setSelectedFulfillment(value)
}}
>
<SelectTrigger className="w-full h-8 py-0 px-3 bg-white/20 border-0 text-white focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs">
<SelectValue placeholder="ФУЛФИЛМЕНТ ИВАНОВО" />
</SelectTrigger>
<SelectContent>
{fulfillmentOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 3. Объём товаров (автоматически) */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Объём товаров</Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
<span className="text-white/80 text-xs">
{goodsVolume > 0 ? `${goodsVolume.toFixed(2)} м³` : 'Рассчитывается автоматически'}
</span>
</div>
</div>
{/* 4. Грузовые места */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Грузовые места</Label>
<Input
type="number"
value={cargoPlaces || ''}
onChange={(e) => setCargoPlaces(parseInt(e.target.value) || 0)}
placeholder="шт"
className="h-8 bg-white/20 border-0 text-white placeholder:text-white/50 focus:bg-white/30 focus:ring-1 focus:ring-white/20 text-xs"
/>
</div>
</div>
{/* Вторая группа - цены */}
<div className="space-y-3 mb-4">
{/* 5. Цена товаров (автоматически) */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Цена товаров</Label>
<div className="h-8 bg-white/10 border border-white/20 rounded-lg flex items-center px-3">
<span className="text-white/80 text-xs font-medium">
{goodsPrice > 0 ? formatCurrency(goodsPrice) : 'Рассчитывается автоматически'}
</span>
</div>
</div>
{/* 6. Цена услуг фулфилмента (автоматически) */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Цена услуг фулфилмента</Label>
<div className="h-8 bg-green-500/20 border border-green-400/30 rounded-lg flex items-center px-3">
<span className="text-green-400 text-xs font-medium">
{selectedServicesCost > 0 ? formatCurrency(selectedServicesCost) : 'Выберите услуги'}
</span>
</div>
</div>
{/* 7. Цена расходников фулфилмента (автоматически) */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Цена расходников фулфилмента</Label>
<div className="h-8 bg-orange-500/20 border border-orange-400/30 rounded-lg flex items-center px-3">
<span className="text-orange-400 text-xs font-medium">
{selectedConsumablesCost > 0 ? formatCurrency(selectedConsumablesCost) : 'Выберите расходники'}
</span>
</div>
</div>
{/* 8. Цена логистики (автоматически) */}
<div>
<Label className="text-white/80 text-xs mb-1 block">Логистика до фулфилмента</Label>
<div className="h-8 bg-blue-500/20 border border-blue-400/30 rounded-lg flex items-center px-3">
<span className="text-blue-400 text-xs font-medium">
{logisticsPrice > 0 ? formatCurrency(logisticsPrice) : 'Выберите поставщика'}
</span>
</div>
</div>
</div>
{/* 9. Итоговая сумма */}
<div className="border-t border-white/20 pt-4 mb-4">
<Label className="text-white/80 text-xs mb-2 block">Итого</Label>
<div className="h-10 bg-gradient-to-r from-green-500/20 to-blue-500/20 rounded-lg flex items-center justify-center border border-green-400/30">
<span className="text-white font-bold text-lg">{formatCurrency(getTotalSum)}</span>
</div>
</div>
{/* 10. Кнопка создания поставки */}
<Button
onClick={handleCreateSupplyClick}
disabled={
!canCreateSupply || isCreatingSupply || !deliveryDate || !selectedFulfillment || !hasItemsInSupply
}
className={`w-full h-12 text-sm font-medium transition-all duration-200 ${
canCreateSupply && deliveryDate && selectedFulfillment && hasItemsInSupply && !isCreatingSupply
? 'bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white'
: 'bg-gray-500/20 text-gray-400 cursor-not-allowed'
}`}
>
{isCreatingSupply ? (
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border border-white/30 border-t-white"></div>
<span>Создание...</span>
</div>
) : (
'Создать поставку'
)}
</Button>
</Card>
</div>
</div>
</div>
</main>
</div>
)
})
CreateSupplyPage.displayName = 'CreateSupplyPage'
export { CreateSupplyPage }