Унификация дизайна корзины и обновление правил
- Убран текст "(с рецептурой)" из названий товаров в корзине - Добавлен раздел 9.2.6 в rules-complete.md с единым стандартом корзины - Определены обязательные размеры, структура и функциональность - Запрещено отображение технических суффиксов в UI корзины 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -86,7 +86,7 @@ export function CreateConsumablesSupplyPage() {
|
||||
const { data: productsData, loading: productsLoading } = useQuery(GET_ORGANIZATION_PRODUCTS, {
|
||||
skip: !selectedSupplier,
|
||||
variables: {
|
||||
organizationId: selectedSupplier.id,
|
||||
organizationId: selectedSupplier?.id || '',
|
||||
search: productSearchQuery || null,
|
||||
category: null,
|
||||
type: 'CONSUMABLE', // Фильтруем только расходники согласно rules2.md
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
AlertCircle,
|
||||
Settings,
|
||||
DollarSign,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@ -25,18 +26,16 @@ import { toast } from 'sonner'
|
||||
import { OrganizationAvatar } from '@/components/market/organization-avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DatePicker } from '@/components/ui/date-picker'
|
||||
import { Input } from '@/components/ui/input'
|
||||
// ВРЕМЕННО ОТКЛЮЧЕНО: импорты для верхней панели - до исправления Apollo ошибки
|
||||
// import { DatePicker } from '@/components/ui/date-picker'
|
||||
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { CREATE_SUPPLY_ORDER } from '@/graphql/mutations'
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_ORGANIZATION_PRODUCTS,
|
||||
GET_MY_SERVICES,
|
||||
GET_MY_SUPPLIES,
|
||||
GET_SELLER_SUPPLIES_ON_WAREHOUSE,
|
||||
GET_MY_WILDBERRIES_SUPPLIES,
|
||||
GET_COUNTERPARTY_SERVICES,
|
||||
GET_COUNTERPARTY_SUPPLIES,
|
||||
GET_AVAILABLE_SUPPLIES_FOR_RECIPE,
|
||||
} from '@/graphql/queries'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useSidebar } from '@/hooks/useSidebar'
|
||||
@ -119,16 +118,16 @@ interface FulfillmentConsumable {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
stock: number
|
||||
quantity: number
|
||||
unit?: string
|
||||
}
|
||||
|
||||
interface SellerConsumable {
|
||||
id: string
|
||||
name: string
|
||||
stock: number
|
||||
pricePerUnit: number
|
||||
warehouseStock: number
|
||||
unit?: string
|
||||
supplierId: string
|
||||
}
|
||||
|
||||
interface WBCard {
|
||||
@ -149,7 +148,7 @@ interface ProductRecipe {
|
||||
|
||||
export function CreateSuppliersSupplyPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const { user: _user } = useAuth()
|
||||
const { getSidebarMargin } = useSidebar()
|
||||
|
||||
// Основные состояния
|
||||
@ -182,7 +181,6 @@ export function CreateSuppliersSupplyPage() {
|
||||
(GoodsProduct & { selectedQuantity: number; supplierId: string; supplierName: string })[]
|
||||
>([])
|
||||
|
||||
|
||||
// Загружаем партнеров-поставщиков согласно rules2.md 13.3
|
||||
const {
|
||||
data: counterpartiesData,
|
||||
@ -248,34 +246,37 @@ export function CreateSuppliersSupplyPage() {
|
||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER)
|
||||
|
||||
// Запросы для компонентов рецептуры
|
||||
const { data: fulfillmentServicesData } = useQuery(GET_MY_SERVICES, {
|
||||
const { data: fulfillmentServicesData } = useQuery(GET_COUNTERPARTY_SERVICES, {
|
||||
variables: { organizationId: selectedFulfillment || '' },
|
||||
skip: !selectedFulfillment,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
|
||||
const { data: fulfillmentConsumablesData } = useQuery(GET_MY_SUPPLIES, {
|
||||
const { data: fulfillmentConsumablesData } = useQuery(GET_COUNTERPARTY_SUPPLIES, {
|
||||
variables: { organizationId: selectedFulfillment || '' },
|
||||
skip: !selectedFulfillment,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
|
||||
const { data: sellerConsumablesData } = useQuery(GET_SELLER_SUPPLIES_ON_WAREHOUSE, {
|
||||
skip: !user?.organization?.id,
|
||||
const { data: sellerConsumablesData } = useQuery(GET_AVAILABLE_SUPPLIES_FOR_RECIPE, {
|
||||
skip: !selectedFulfillment,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
|
||||
const { data: wbCardsData } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, {
|
||||
skip: !user?.organization?.id,
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
// TODO: Нужен запрос для получения карточек товаров селлера
|
||||
// const { data: wbCardsData } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, {
|
||||
// skip: !user?.organization?.id,
|
||||
// errorPolicy: 'all',
|
||||
// })
|
||||
|
||||
// Фильтруем только партнеров-поставщиков согласно rules2.md 13.3
|
||||
const allCounterparties = counterpartiesData?.myCounterparties || []
|
||||
|
||||
// Извлекаем данные для компонентов рецептуры
|
||||
const fulfillmentServices: FulfillmentService[] = fulfillmentServicesData?.myServices || []
|
||||
const fulfillmentConsumables: FulfillmentConsumable[] = fulfillmentConsumablesData?.mySupplies || []
|
||||
const sellerConsumables: SellerConsumable[] = sellerConsumablesData?.sellerSuppliesOnWarehouse || []
|
||||
const wbCards: WBCard[] = (wbCardsData?.myWildberriesSupplies || []).flatMap((supply: any) => supply.cards || [])
|
||||
const fulfillmentServices: FulfillmentService[] = fulfillmentServicesData?.counterpartyServices || []
|
||||
const fulfillmentConsumables: FulfillmentConsumable[] = fulfillmentConsumablesData?.counterpartySupplies || []
|
||||
const sellerConsumables: SellerConsumable[] = sellerConsumablesData?.getAvailableSuppliesForRecipe || []
|
||||
const _wbCards: WBCard[] = [] // Временно отключено
|
||||
|
||||
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
|
||||
const wholesaleSuppliers = allCounterparties.filter((cp: any) => {
|
||||
@ -343,7 +344,7 @@ export function CreateSuppliersSupplyPage() {
|
||||
// Функции для работы с рынками согласно rules-complete.md v10.0
|
||||
const getMarketLabel = (market?: string) => {
|
||||
const marketLabels = {
|
||||
'sadovod': 'Садовод',
|
||||
sadovod: 'Садовод',
|
||||
'tyak-moscow': 'ТЯК Москва',
|
||||
'opt-market': 'ОПТ Маркет',
|
||||
}
|
||||
@ -352,18 +353,15 @@ export function CreateSuppliersSupplyPage() {
|
||||
|
||||
const getMarketBadgeStyle = (market?: string) => {
|
||||
const styles = {
|
||||
'sadovod': 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
sadovod: 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
'opt-market': 'bg-purple-500/20 text-purple-300 border-purple-500/30',
|
||||
}
|
||||
return styles[market as keyof typeof styles] || 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
|
||||
const logisticsCompanies: LogisticsCompany[] = [
|
||||
{ id: 'express', name: 'Экспресс доставка', estimatedCost: 2500, deliveryDays: 1, type: 'EXPRESS' },
|
||||
{ id: 'standard', name: 'Стандартная доставка', estimatedCost: 1200, deliveryDays: 3, type: 'STANDARD' },
|
||||
{ id: 'economy', name: 'Экономичная доставка', estimatedCost: 800, deliveryDays: 7, type: 'ECONOMY' },
|
||||
]
|
||||
// Получаем логистические компании из партнеров
|
||||
const logisticsCompanies = allCounterparties?.filter((partner) => partner.type === 'LOGIST') || []
|
||||
|
||||
// Моковые фулфилмент-центры согласно rules2.md 9.7.2
|
||||
const fulfillmentCenters = [
|
||||
@ -432,6 +430,8 @@ export function CreateSuppliersSupplyPage() {
|
||||
} else {
|
||||
// Добавляем новый товар
|
||||
setSelectedGoods((prev) => [...prev, newGoodsItem])
|
||||
// Инициализируем рецептуру для нового товара
|
||||
initializeProductRecipe(product.id)
|
||||
toast.success(`Товар "${product.name}" добавлен в корзину`)
|
||||
}
|
||||
|
||||
@ -508,7 +508,7 @@ export function CreateSuppliersSupplyPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const setWBCard = (productId: string, cardId: string) => {
|
||||
const _setWBCard = (productId: string, cardId: string) => {
|
||||
initializeProductRecipe(productId)
|
||||
setProductRecipes((prev) => ({
|
||||
...prev,
|
||||
@ -606,24 +606,70 @@ export function CreateSuppliersSupplyPage() {
|
||||
// Удаление из корзины
|
||||
const removeFromCart = (productId: string) => {
|
||||
setSelectedGoods((prev) => prev.filter((item) => item.id !== productId))
|
||||
// Удаляем рецептуру товара
|
||||
setProductRecipes((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[productId]
|
||||
return updated
|
||||
})
|
||||
toast.success('Товар удален из корзины')
|
||||
}
|
||||
|
||||
// Расчеты согласно rules2.md 9.7.6
|
||||
const totalGoodsAmount = selectedGoods.reduce((sum, item) => sum + item.price * item.selectedQuantity, 0)
|
||||
// Функция расчета полной стоимости товара с рецептурой
|
||||
const getProductTotalWithRecipe = (productId: string, quantity: number) => {
|
||||
const product = allSelectedProducts.find(p => p.id === productId)
|
||||
if (!product) return 0
|
||||
|
||||
const baseTotal = product.price * quantity
|
||||
const recipe = productRecipes[productId]
|
||||
|
||||
if (!recipe) return baseTotal
|
||||
|
||||
// Услуги ФФ
|
||||
const servicesCost = (recipe.selectedServices || []).reduce((sum, serviceId) => {
|
||||
const service = fulfillmentServices.find(s => s.id === serviceId)
|
||||
return sum + (service ? service.price * quantity : 0)
|
||||
}, 0)
|
||||
|
||||
// Расходники ФФ
|
||||
const ffConsumablesCost = (recipe.selectedFFConsumables || []).reduce((sum, consumableId) => {
|
||||
const consumable = fulfillmentConsumables.find(c => c.id === consumableId)
|
||||
// Используем такую же логику как в карточке - только price
|
||||
return sum + (consumable ? consumable.price * quantity : 0)
|
||||
}, 0)
|
||||
|
||||
// Расходники селлера
|
||||
const sellerConsumablesCost = (recipe.selectedSellerConsumables || []).reduce((sum, consumableId) => {
|
||||
const consumable = sellerConsumables.find(c => c.id === consumableId)
|
||||
return sum + (consumable ? (consumable.pricePerUnit || 0) * quantity : 0)
|
||||
}, 0)
|
||||
|
||||
return baseTotal + servicesCost + ffConsumablesCost + sellerConsumablesCost
|
||||
}
|
||||
|
||||
// Расчеты для корзины - используем функцию расчета
|
||||
const totalGoodsAmount = selectedGoods.reduce((sum, item) => {
|
||||
return sum + getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
||||
}, 0)
|
||||
|
||||
const totalQuantity = selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0)
|
||||
const fulfillmentFee = totalGoodsAmount * 0.08 // 8% комиссия фулфилмента
|
||||
const selectedLogisticsCompany = logisticsCompanies.find((lc) => lc.id === selectedLogistics)
|
||||
const logisticsCost = selectedLogistics === 'auto' ? 0 : selectedLogisticsCompany?.estimatedCost || 0
|
||||
const totalAmount = totalGoodsAmount + fulfillmentFee + logisticsCost
|
||||
const totalAmount = totalGoodsAmount
|
||||
|
||||
// Валидация формы согласно rules2.md 9.7.6
|
||||
const isFormValid = selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment
|
||||
// Проверяем обязательность услуг фулфилмента согласно rules-complete.md
|
||||
const hasRequiredServices = selectedGoods.every((item) => productRecipes[item.id]?.selectedServices?.length > 0)
|
||||
|
||||
const isFormValid =
|
||||
selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment && hasRequiredServices // Обязательно: каждый товар должен иметь услуги
|
||||
|
||||
// Создание поставки
|
||||
const handleCreateSupply = async () => {
|
||||
if (!isFormValid) {
|
||||
toast.error('Заполните все обязательные поля')
|
||||
if (!hasRequiredServices) {
|
||||
toast.error('Каждый товар должен иметь минимум 1 услугу фулфилмента')
|
||||
} else {
|
||||
toast.error('Заполните все обязательные поля')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -636,11 +682,14 @@ export function CreateSuppliersSupplyPage() {
|
||||
items: selectedGoods.map((item) => ({
|
||||
productId: item.id,
|
||||
quantity: item.selectedQuantity,
|
||||
price: item.price,
|
||||
completeness: item.completeness,
|
||||
recipe: item.recipe,
|
||||
specialRequirements: item.specialRequirements,
|
||||
parameters: item.parameters,
|
||||
recipe: productRecipes[item.id]
|
||||
? {
|
||||
services: productRecipes[item.id].selectedServices,
|
||||
fulfillmentConsumables: productRecipes[item.id].selectedFFConsumables,
|
||||
sellerConsumables: productRecipes[item.id].selectedSellerConsumables,
|
||||
marketplaceCardId: productRecipes[item.id].selectedWBCard,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
deliveryDate,
|
||||
logisticsCompany: selectedLogistics === 'auto' ? null : selectedLogistics,
|
||||
@ -675,10 +724,10 @@ export function CreateSuppliersSupplyPage() {
|
||||
<div className="flex-1 flex gap-4 min-h-0">
|
||||
{/* ЛЕВЫЙ БЛОК: ПОСТАВЩИКИ, КАРТОЧКИ ТОВАРОВ И ДЕТАЛЬНЫЙ КАТАЛОГ */}
|
||||
<div className="flex-1 flex flex-col gap-4 min-h-0">
|
||||
{/* БЛОК 1: ПОСТАВЩИКИ - обязательный блок согласно rules1.md 19.2.1 */}
|
||||
{/* БЛОК 1: ПОСТАВЩИКИ - обязательный блок согласно rules-complete.md 9.2.1 */}
|
||||
<div
|
||||
className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0 flex flex-col"
|
||||
style={{ minHeight: '120px', maxHeight: suppliers.length > 4 ? '200px' : 'auto' }}
|
||||
style={{ height: '180px' }}
|
||||
>
|
||||
<div className="p-4 flex-shrink-0">
|
||||
{/* Навигация и заголовок в одном блоке */}
|
||||
@ -699,19 +748,19 @@ export function CreateSuppliersSupplyPage() {
|
||||
</div>
|
||||
<div className="w-64">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/90 h-4 w-4 drop-shadow-sm z-10" />
|
||||
<Input
|
||||
placeholder="Поиск поставщиков..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-white/5 border-white/10 text-white placeholder:text-white/50 pl-10 h-9 text-sm transition-all duration-200 focus:border-white/20"
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-white/60 pl-10 h-9 text-sm rounded-full transition-all duration-200 focus:border-white/30 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопка поиска в маркете */}
|
||||
{allCounterparties.length === 0 && (
|
||||
{!isLoading && allCounterparties.length === 0 && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -725,15 +774,15 @@ export function CreateSuppliersSupplyPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Список поставщиков согласно visual-design-rules.md */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{/* Контейнер скролла поставщиков согласно rules-complete.md 9.2.1 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex items-center justify-center h-44">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60"></div>
|
||||
<span className="ml-3 text-white/70">Загрузка поставщиков...</span>
|
||||
</div>
|
||||
) : suppliers.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex items-center justify-center h-44">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-12 h-12 mx-auto bg-white/5 rounded-full flex items-center justify-center">
|
||||
<Building2 className="h-6 w-6 text-white/40" />
|
||||
@ -753,45 +802,53 @@ export function CreateSuppliersSupplyPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`gap-2 overflow-y-auto ${
|
||||
suppliers.length <= 2
|
||||
? 'flex flex-wrap'
|
||||
: suppliers.length <= 4
|
||||
? 'grid grid-cols-2'
|
||||
: 'grid grid-cols-1 md:grid-cols-2 max-h-32'
|
||||
}`}
|
||||
>
|
||||
{suppliers.map((supplier: GoodsSupplier) => (
|
||||
<div
|
||||
key={supplier.id}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
className={`p-3 rounded-lg cursor-pointer group transition-all duration-200 ${
|
||||
selectedSupplier?.id === supplier.id
|
||||
? 'bg-white/15 border border-white/40 shadow-lg'
|
||||
: 'bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
<OrganizationAvatar organization={supplier} size="sm" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
|
||||
{supplier.name || supplier.fullName}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<p className="text-white/60 text-xs font-mono">ИНН: {supplier.inn}</p>
|
||||
<div className="h-44 overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${
|
||||
suppliers.length <= 4
|
||||
? 'flex items-start gap-3 px-4'
|
||||
: 'flex gap-3 overflow-x-auto px-4 pb-2 scrollbar-hide'
|
||||
}`}
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
}}
|
||||
>
|
||||
{suppliers.map((supplier: GoodsSupplier) => (
|
||||
<div
|
||||
key={supplier.id}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
className={`flex-shrink-0 p-3 rounded-lg cursor-pointer group transition-all duration-200
|
||||
w-[184px] md:w-[200px] lg:w-[216px] h-[92px]
|
||||
${
|
||||
selectedSupplier?.id === supplier.id
|
||||
? 'bg-green-500/20 border border-green-400/60 shadow-lg ring-1 ring-green-400/30'
|
||||
: 'bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2 h-full">
|
||||
<div className="flex-shrink-0">
|
||||
<OrganizationAvatar organization={supplier} size="sm" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
|
||||
{supplier.name || supplier.fullName}
|
||||
</h4>
|
||||
<p className="text-white/60 text-xs font-mono mt-1">ИНН: {supplier.inn}</p>
|
||||
{supplier.market && (
|
||||
<Badge className={`text-xs font-medium border ${getMarketBadgeStyle(supplier.market)}`}>
|
||||
{getMarketLabel(supplier.market)}
|
||||
</Badge>
|
||||
<div className="mt-1">
|
||||
<Badge
|
||||
className={`text-xs font-medium border ${getMarketBadgeStyle(supplier.market)}`}
|
||||
>
|
||||
{getMarketLabel(supplier.market)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -801,52 +858,56 @@ export function CreateSuppliersSupplyPage() {
|
||||
{/* БЛОК 2: КАРТОЧКИ ТОВАРОВ */}
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0">
|
||||
<div className="flex gap-3 overflow-x-auto p-4" style={{ scrollbarWidth: 'thin' }}>
|
||||
{selectedSupplier && products.length > 0 && products.map((product: GoodsProduct) => {
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="relative flex-shrink-0 bg-white/5 rounded-lg overflow-hidden border cursor-pointer transition-all duration-300 group w-20 h-28 border-white/10 hover:border-white/30"
|
||||
onClick={() => {
|
||||
// Добавляем товар в детальный каталог (блок 3)
|
||||
if (!allSelectedProducts.find((p) => p.id === product.id)) {
|
||||
setAllSelectedProducts((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...product,
|
||||
selectedQuantity: 1,
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик',
|
||||
},
|
||||
])
|
||||
}
|
||||
}}
|
||||
>
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={80}
|
||||
height={112}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-6 w-6 text-white/40" />
|
||||
{selectedSupplier &&
|
||||
products.length > 0 &&
|
||||
products.map((product: GoodsProduct) => {
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="relative flex-shrink-0 bg-white/5 rounded-lg overflow-hidden border cursor-pointer transition-all duration-300 group w-20 h-28 border-white/10 hover:border-white/30"
|
||||
onClick={() => {
|
||||
// Добавляем товар в детальный каталог (блок 3)
|
||||
if (!allSelectedProducts.find((p) => p.id === product.id)) {
|
||||
setAllSelectedProducts((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...product,
|
||||
selectedQuantity: 0,
|
||||
supplierId: selectedSupplier.id,
|
||||
supplierName: selectedSupplier.name || selectedSupplier.fullName || 'Поставщик',
|
||||
},
|
||||
])
|
||||
// Инициализируем рецептуру для нового товара
|
||||
initializeProductRecipe(product.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={80}
|
||||
height={112}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-6 w-6 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* БЛОК 3: ТОВАРЫ ПОСТАВЩИКА - детальный каталог согласно rules-complete.md 9.2.3 */}
|
||||
{/* БЛОК 3: КАТАЛОГ ТОВАРОВ согласно rules-complete.md 9.2.3 */}
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 min-h-0 flex flex-col">
|
||||
{/* ВРЕМЕННО ОТКЛЮЧЕНО: Верхняя панель согласно правилам 9.2.3.1 - до исправления Apollo ошибки
|
||||
{/* Верхняя панель каталога товаров согласно правилам 9.2.3.1 */}
|
||||
{!counterpartiesLoading && (
|
||||
<div className="flex items-center gap-4 p-4 bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl mb-4">
|
||||
<DatePicker
|
||||
placeholder="Дата поставки"
|
||||
<DatePicker
|
||||
placeholder="Дата поставки"
|
||||
value={deliveryDate}
|
||||
onChange={setDeliveryDate}
|
||||
className="min-w-[140px]"
|
||||
@ -873,16 +934,16 @@ export function CreateSuppliersSupplyPage() {
|
||||
</Select>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/40" />
|
||||
<Input
|
||||
placeholder="Поиск товаров..."
|
||||
<Input
|
||||
placeholder="Поиск товаров..."
|
||||
value={productSearchQuery}
|
||||
onChange={(e) => setProductSearchQuery(e.target.value)}
|
||||
className="pl-10 glass-input"
|
||||
className="pl-10 glass-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{counterpartiesLoading && (
|
||||
<div className="flex items-center justify-center p-4 bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl mb-4">
|
||||
<div className="text-white/60 text-sm">Загрузка партнеров...</div>
|
||||
@ -891,27 +952,9 @@ export function CreateSuppliersSupplyPage() {
|
||||
|
||||
{counterpartiesError && (
|
||||
<div className="flex items-center justify-center p-4 bg-red-500/10 backdrop-blur-xl border border-red-500/20 rounded-2xl mb-4">
|
||||
<div className="text-red-300 text-sm">
|
||||
Ошибка загрузки партнеров: {counterpartiesError.message}
|
||||
</div>
|
||||
<div className="text-red-300 text-sm">Ошибка загрузки партнеров: {counterpartiesError.message}</div>
|
||||
</div>
|
||||
)}
|
||||
*/}
|
||||
|
||||
{/* Заголовок каталога */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-400/10 rounded-lg border border-blue-400/20">
|
||||
<Package className="h-6 w-6 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
Детальный каталог ({allSelectedProducts.length} товаров)
|
||||
</h3>
|
||||
<p className="text-white/60 text-sm mt-1">Товары для детального управления поставкой</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{allSelectedProducts.length === 0 ? (
|
||||
@ -921,361 +964,361 @@ export function CreateSuppliersSupplyPage() {
|
||||
<Package className="h-12 w-12 text-blue-400/50" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xl font-medium text-white mb-2">Детальный каталог пуст</h4>
|
||||
<p className="text-white/60 max-w-sm mx-auto">
|
||||
Добавьте товары
|
||||
</p>
|
||||
<h4 className="text-xl font-medium text-white mb-2">Каталог товаров пуст</h4>
|
||||
<p className="text-white/60 max-w-sm mx-auto">Выберите поставщика для просмотра товаров</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{allSelectedProducts.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-4 hover:border-white/30 transition-all duration-200"
|
||||
>
|
||||
{/* ОСНОВНОЙ БЛОК: Информация о товаре + количество + сумма */}
|
||||
<div className="flex items-start gap-6 mb-4">
|
||||
{/* ЛЕВЫЙ БЛОК: Изображение + основная информация */}
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div className="w-24 h-24 bg-white/5 rounded-lg overflow-hidden flex-shrink-0">
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={96}
|
||||
height={96}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h4 className="text-white font-semibold text-lg">{product.name}</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAllSelectedProducts((prev) => prev.filter((p) => p.id !== product.id))
|
||||
}}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 p-1 h-auto"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm mb-2 font-mono">Артикул: {product.article}</p>
|
||||
<p className="text-white/50 text-xs mb-2">От: {product.supplierName}</p>
|
||||
{product.category && (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border border-blue-500/30 text-xs font-medium mb-2">
|
||||
{product.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-white font-bold text-xl">
|
||||
{product.price.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
{product.quantity !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'
|
||||
}`}
|
||||
></div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
product.quantity > 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}
|
||||
>
|
||||
{product.quantity > 0 ? `Доступно: ${product.quantity}` : 'Нет в наличии'}
|
||||
</span>
|
||||
{allSelectedProducts.map((product) => {
|
||||
// Расчет стоимостей для каждого блока рецептуры
|
||||
const recipe = productRecipes[product.id]
|
||||
const selectedServicesIds = recipe?.selectedServices || []
|
||||
const selectedFFConsumablesIds = recipe?.selectedFFConsumables || []
|
||||
const selectedSellerConsumablesIds = recipe?.selectedSellerConsumables || []
|
||||
|
||||
// Стоимость услуг ФФ
|
||||
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 totalWithRecipe =
|
||||
product.price * product.selectedQuantity + servicesCost + ffConsumablesCost + sellerConsumablesCost
|
||||
|
||||
// Debug: сравниваем с функцией расчета корзины
|
||||
const cartTotal = getProductTotalWithRecipe(product.id, product.selectedQuantity)
|
||||
if (Math.abs(totalWithRecipe - cartTotal) > 0.01) {
|
||||
console.log(`РАЗНИЦА для ${product.name}:`, {
|
||||
карточка: totalWithRecipe,
|
||||
корзина: cartTotal,
|
||||
базовая_цена: product.price * product.selectedQuantity,
|
||||
услуги: servicesCost,
|
||||
расходники_ФФ: ffConsumablesCost,
|
||||
расходники_селлера: sellerConsumablesCost,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="glass-card border-white/10 hover:border-white/20 transition-all duration-300 group relative"
|
||||
style={{ height: '140px' }}
|
||||
>
|
||||
{/* Элегантный крестик удаления - согласно visual-design-rules.md */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setAllSelectedProducts((prev) => prev.filter((p) => p.id !== product.id))
|
||||
// Очищаем рецептуру
|
||||
setProductRecipes((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[product.id]
|
||||
return updated
|
||||
})
|
||||
}}
|
||||
className="absolute top-3 right-3 z-10 w-7 h-7 flex items-center justify-center rounded-full bg-white/5 text-white/40 hover:bg-red-500/20 hover:text-red-400 transition-all duration-200 opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Plus className="h-4 w-4 rotate-45" />
|
||||
</button>
|
||||
|
||||
{/* 7 модулей согласно rules-complete.md 9.2.3.2 + visual-design-rules.md */}
|
||||
<div className="flex h-full">
|
||||
{/* 1. ИЗОБРАЖЕНИЕ (80px фиксированная ширина) */}
|
||||
<div className="w-20 flex-shrink-0 p-3">
|
||||
<div className="w-full h-full bg-white/5 rounded-lg overflow-hidden">
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={80}
|
||||
height={112}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-5 w-5 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ПРАВЫЙ БЛОК: Количество + общая сумма */}
|
||||
<div className="flex items-center gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (product.selectedQuantity > 1) {
|
||||
setAllSelectedProducts((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === product.id ? { ...p, selectedQuantity: p.selectedQuantity - 1 } : p,
|
||||
),
|
||||
{/* 2. ОБЩАЯ ИНФОРМАЦИЯ (flex-1) - Правильная типографика согласно 2.2 */}
|
||||
<div className="flex-1 p-3 flex flex-col justify-center">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-white font-semibold text-sm truncate">{product.name}</h4>
|
||||
<div className="text-white font-bold text-lg">
|
||||
{product.price.toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
{product.category && (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border-0 text-xs font-medium px-2 py-1">
|
||||
{product.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
<p className="text-white/60 text-xs truncate">От: {product.supplierName}</p>
|
||||
<p className="font-mono text-xs text-white/60 truncate">Артикул: {product.article}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. КОЛИЧЕСТВО/СУММА/ОСТАТОК (flex-1) */}
|
||||
<div className="flex-1 p-3 flex flex-col justify-center">
|
||||
<div className="space-y-3">
|
||||
{product.quantity !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'}`}
|
||||
></div>
|
||||
<span className={`text-xs ${product.quantity > 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{product.quantity > 0 ? `${product.quantity} шт` : 'Нет в наличии'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<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)
|
||||
setAllSelectedProducts((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === product.id ? { ...p, selectedQuantity: newQuantity } : p,
|
||||
),
|
||||
)
|
||||
|
||||
// Автоматическое добавление/удаление из корзины
|
||||
if (newQuantity > 0) {
|
||||
// Добавляем в корзину
|
||||
const existingItem = selectedGoods.find(item => item.id === product.id)
|
||||
if (!existingItem) {
|
||||
// Добавляем новый товар
|
||||
setSelectedGoods(prev => [...prev, {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
sku: product.article,
|
||||
price: product.price,
|
||||
category: product.category?.name || '',
|
||||
selectedQuantity: newQuantity,
|
||||
unit: product.unit || 'шт',
|
||||
supplierId: selectedSupplier?.id || '',
|
||||
supplierName: selectedSupplier?.name || selectedSupplier?.fullName || 'Поставщик',
|
||||
}])
|
||||
// Инициализируем рецептуру
|
||||
initializeProductRecipe(product.id)
|
||||
} else {
|
||||
// Обновляем количество
|
||||
setSelectedGoods(prev =>
|
||||
prev.map(item =>
|
||||
item.id === product.id
|
||||
? { ...item, selectedQuantity: newQuantity }
|
||||
: item,
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Удаляем из корзины при количестве 0
|
||||
setSelectedGoods(prev => prev.filter(item => item.id !== product.id))
|
||||
}
|
||||
}}
|
||||
className="glass-input w-16 h-8 text-sm text-center text-white placeholder:text-white/50"
|
||||
placeholder="0"
|
||||
/>
|
||||
<span className="text-white/60 text-sm">шт</span>
|
||||
</div>
|
||||
|
||||
<div className="text-green-400 font-semibold text-sm">
|
||||
{(product.price * product.selectedQuantity).toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. УСЛУГИ ФФ (flex-1) - Правильные цвета согласно 1.2 */}
|
||||
<div className="flex-1 p-3 flex flex-col">
|
||||
<div className="text-center mb-2">
|
||||
{servicesCost > 0 && (
|
||||
<div className="text-purple-400 font-semibold text-sm mb-1">
|
||||
{servicesCost.toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
)}
|
||||
<h6 className="text-purple-400 text-xs font-medium uppercase tracking-wider">🛠️ Услуги ФФ</h6>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||
{fulfillmentServices.length > 0 ? (
|
||||
fulfillmentServices.map((service) => {
|
||||
const isSelected = selectedServicesIds.includes(service.id)
|
||||
return (
|
||||
<label
|
||||
key={service.id}
|
||||
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
|
||||
isSelected
|
||||
? 'bg-purple-500/20 border-purple-500/30 text-purple-300 border'
|
||||
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleService(product.id, service.id)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="truncate font-medium">{service.name}</div>
|
||||
<div className="text-xs opacity-80 mt-1">
|
||||
{service.price.toLocaleString('ru-RU')}₽
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}}
|
||||
className="h-8 w-8 p-0 bg-white/5 border-white/20 hover:bg-white/10 hover:border-white/30 text-white"
|
||||
disabled={product.selectedQuantity <= 1}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">
|
||||
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
max={product.quantity}
|
||||
value={product.selectedQuantity}
|
||||
onChange={(e) => {
|
||||
const newQuantity = parseInt(e.target.value) || 1
|
||||
setAllSelectedProducts((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === product.id ? { ...p, selectedQuantity: newQuantity } : p,
|
||||
),
|
||||
)
|
||||
}}
|
||||
className="h-8 w-20 text-center bg-white/5 border-white/20 text-white placeholder:text-white/40 focus:border-white/40"
|
||||
placeholder="1"
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (product.selectedQuantity < (product.quantity || 0)) {
|
||||
setAllSelectedProducts((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === product.id ? { ...p, selectedQuantity: p.selectedQuantity + 1 } : p,
|
||||
),
|
||||
{/* 5. РАСХОДНИКИ ФФ (flex-1) */}
|
||||
<div className="flex-1 p-3 flex flex-col">
|
||||
<div className="text-center mb-2">
|
||||
{ffConsumablesCost > 0 && (
|
||||
<div className="text-orange-400 font-semibold text-sm mb-1">
|
||||
{ffConsumablesCost.toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
)}
|
||||
<h6 className="text-orange-400 text-xs font-medium uppercase tracking-wider">📦 Расходники ФФ</h6>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||
{fulfillmentConsumables.length > 0 ? (
|
||||
fulfillmentConsumables.map((consumable) => {
|
||||
const isSelected = selectedFFConsumablesIds.includes(consumable.id)
|
||||
return (
|
||||
<label
|
||||
key={consumable.id}
|
||||
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
|
||||
isSelected
|
||||
? 'bg-orange-500/20 border-orange-500/30 text-orange-300 border'
|
||||
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFFConsumable(product.id, consumable.id)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="truncate font-medium">{consumable.name}</div>
|
||||
<div className="text-xs opacity-80 mt-1">
|
||||
{consumable.price.toLocaleString('ru-RU')}₽
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}}
|
||||
className="h-8 w-8 p-0 bg-white/5 border-white/20 hover:bg-white/10 hover:border-white/30 text-white"
|
||||
disabled={
|
||||
product.quantity === 0 || product.selectedQuantity >= (product.quantity || 0)
|
||||
}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">
|
||||
{selectedFulfillment ? 'Загрузка...' : 'Выберите ФФ'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/20 rounded-lg px-4 py-2">
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{(product.price * product.selectedQuantity).toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
|
||||
{/* 6. РАСХОДНИКИ СЕЛЛЕРА (flex-1) */}
|
||||
<div className="flex-1 p-3 flex flex-col">
|
||||
<div className="text-center mb-2">
|
||||
{sellerConsumablesCost > 0 && (
|
||||
<div className="text-blue-400 font-semibold text-sm mb-1">
|
||||
{sellerConsumablesCost.toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
)}
|
||||
<h6 className="text-blue-400 text-xs font-medium uppercase tracking-wider">🏪 Расходники сел.</h6>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-1" style={{ maxHeight: '75px' }}>
|
||||
{sellerConsumables.length > 0 ? (
|
||||
sellerConsumables.map((consumable) => {
|
||||
const isSelected = selectedSellerConsumablesIds.includes(consumable.id)
|
||||
return (
|
||||
<label
|
||||
key={consumable.id}
|
||||
className={`block w-full px-2 py-1.5 rounded-md text-xs cursor-pointer transition-all duration-200 hover:scale-105 ${
|
||||
isSelected
|
||||
? 'bg-blue-500/20 border-blue-500/30 text-blue-300 border'
|
||||
: 'bg-white/5 text-white/70 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSellerConsumable(product.id, consumable.id)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<div className="truncate font-medium">{consumable.name}</div>
|
||||
<div className="text-xs opacity-80 mt-1">
|
||||
{consumable.pricePerUnit} ₽/{consumable.unit || 'шт'}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/60 text-xs p-2 text-center bg-white/5 rounded-md">Загрузка...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 7. МП + ИТОГО (flex-1) */}
|
||||
<div className="flex-1 p-3 flex flex-col justify-between">
|
||||
<div className="text-center">
|
||||
<div className="text-green-400 font-bold text-lg mb-3">
|
||||
Итого: {totalWithRecipe.toLocaleString('ru-RU')} ₽
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<Select
|
||||
value={recipe?.selectedWBCard || 'none'}
|
||||
onValueChange={(value) => {
|
||||
if (value !== 'none') {
|
||||
_setWBCard(product.id, value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="glass-input h-9 text-sm text-white">
|
||||
<SelectValue placeholder="Не выбрано" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
Не выбрано
|
||||
</SelectItem>
|
||||
{/* TODO: Загружать из БД */}
|
||||
<SelectItem value="card1">Карточка 1</SelectItem>
|
||||
<SelectItem value="card2">Карточка 2</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* БЛОК РЕЦЕПТУРЫ: 4 колонки с чекбоксами */}
|
||||
<div className="grid grid-cols-4 gap-4 pt-4 border-t border-white/10 mb-4">
|
||||
{/* КОЛОНКА 1: Услуги фулфилмента */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-purple-400" />
|
||||
Услуги ФФ
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{fulfillmentServices.length > 0 ? (
|
||||
fulfillmentServices.map((service) => {
|
||||
const recipe = productRecipes[product.id]
|
||||
const isSelected = recipe?.selectedServices.includes(service.id) || false
|
||||
return (
|
||||
<label
|
||||
key={service.id}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleService(product.id, service.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-purple-400 focus:ring-purple-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{service.name}</span>
|
||||
<span className="text-purple-400 text-xs font-medium">
|
||||
{service.price.toLocaleString('ru-RU')}₽
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
{selectedFulfillment ? 'Услуги загружаются...' : 'Выберите фулфилмент-центр'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 2: Расходники фулфилмента */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Box className="h-4 w-4 text-orange-400" />
|
||||
Расходники ФФ
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{fulfillmentConsumables.length > 0 ? (
|
||||
fulfillmentConsumables.map((consumable) => {
|
||||
const recipe = productRecipes[product.id]
|
||||
const isSelected = recipe?.selectedFFConsumables.includes(consumable.id) || false
|
||||
return (
|
||||
<label
|
||||
key={consumable.id}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFFConsumable(product.id, consumable.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-orange-400 focus:ring-orange-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{consumable.name}</span>
|
||||
<span className="text-orange-400 text-xs font-medium">
|
||||
{consumable.price.toLocaleString('ru-RU')}₽
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
{selectedFulfillment ? 'Расходники загружаются...' : 'Выберите фулфилмент-центр'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 3: Расходники селлера */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-blue-400" />
|
||||
Расходники селлера
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{sellerConsumables.length > 0 ? (
|
||||
sellerConsumables.map((consumable) => {
|
||||
const recipe = productRecipes[product.id]
|
||||
const isSelected =
|
||||
recipe?.selectedSellerConsumables.includes(consumable.id) || false
|
||||
return (
|
||||
<label
|
||||
key={consumable.id}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSellerConsumable(product.id, consumable.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-blue-400 focus:ring-blue-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{consumable.name}</span>
|
||||
<span className="text-blue-400 text-xs">Склад: {consumable.stock}</span>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
Расходники селлера загружаются...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 4: Карточки Wildberries */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4 text-pink-400" />
|
||||
Карточки WB
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{wbCards.length > 0 ? (
|
||||
wbCards.map((card) => {
|
||||
const recipe = productRecipes[product.id]
|
||||
const isSelected = recipe?.selectedWBCard === card.id
|
||||
return (
|
||||
<label
|
||||
key={card.id}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`wb-card-${product.id}`}
|
||||
checked={isSelected}
|
||||
onChange={() => setWBCard(product.id, card.id)}
|
||||
className="w-3 h-3 rounded-full bg-white/10 border-white/20 text-pink-400 focus:ring-pink-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{card.title}</span>
|
||||
<span className="text-pink-400 text-xs">{card.nmID}</span>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
Карточки WB загружаются...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* НИЖНИЙ БЛОК: Итоговая стоимость рецептуры + кнопка добавления */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-6">
|
||||
{(() => {
|
||||
const quantity = getProductQuantity(product.id)
|
||||
const recipeCost = calculateRecipeCost(product.id)
|
||||
const productTotal = product.price * quantity
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-sm text-white/70">
|
||||
Товар:{' '}
|
||||
<span className="text-white font-semibold">
|
||||
{productTotal.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
Услуги:{' '}
|
||||
<span className="text-purple-400 font-semibold">
|
||||
{recipeCost.services.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
Расходники:{' '}
|
||||
<span className="text-orange-400 font-semibold">
|
||||
{recipeCost.consumables.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{(() => {
|
||||
const quantity = getProductQuantity(product.id)
|
||||
const recipeCost = calculateRecipeCost(product.id)
|
||||
const productTotal = product.price * quantity
|
||||
const totalRecipePrice = productTotal + recipeCost.total
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded-lg px-4 py-2">
|
||||
<span className="text-white/70 text-sm">Итого: </span>
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{totalRecipePrice.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => addToCart(product)}
|
||||
disabled={quantity === 0}
|
||||
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white border border-green-500/30 hover:border-green-400/50 transition-all duration-200"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Добавить рецептуру
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1283,401 +1326,118 @@ export function CreateSuppliersSupplyPage() {
|
||||
</div>
|
||||
|
||||
{/* БЛОК 4: КОРЗИНА И НАСТРОЙКИ - правый блок согласно rules-complete.md 9.2 */}
|
||||
<div className="w-96 flex-shrink-0 flex flex-col min-h-0">
|
||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 flex flex-col min-h-0">
|
||||
{/* ЗАГОЛОВОК И СТАТИСТИКА */}
|
||||
<div className="p-4 border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-purple-400/10 rounded-lg border border-purple-400/20">
|
||||
<ShoppingCart className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Корзина и настройки поставки</h3>
|
||||
<p className="text-white/60 text-xs mt-1">Управление заказом и параметрами доставки</p>
|
||||
<div className="w-72 flex-shrink-0">
|
||||
<div className="bg-white/10 backdrop-blur border-white/20 p-3 sticky top-0 rounded-2xl">
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center text-sm">
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Корзина ({selectedGoods.length} шт)
|
||||
</h3>
|
||||
|
||||
{selectedGoods.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-4 w-fit mx-auto mb-3">
|
||||
<ShoppingCart className="h-8 w-8 text-purple-300" />
|
||||
</div>
|
||||
<p className="text-white/60 text-sm font-medium mb-2">Корзина пуста</p>
|
||||
<p className="text-white/40 text-xs mb-3">Добавьте товары из каталога для создания поставки</p>
|
||||
</div>
|
||||
|
||||
{/* СТАТИСТИКА ПОСТАВКИ */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-white/60">Поставщиков</p>
|
||||
<p className="text-lg font-semibold text-white">{selectedSupplier ? 1 : 0}</p>
|
||||
</div>
|
||||
<Building2 className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-white/60">Товаров</p>
|
||||
<p className="text-lg font-semibold text-white">{selectedGoods.length}</p>
|
||||
</div>
|
||||
<Package className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-white/60">Количество</p>
|
||||
<p className="text-lg font-semibold text-white">{totalQuantity} шт</p>
|
||||
</div>
|
||||
<Box className="h-4 w-4 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-white/60">Сумма</p>
|
||||
<p className="text-lg font-semibold text-white">{totalAmount.toLocaleString('ru-RU')} ₽</p>
|
||||
</div>
|
||||
<DollarSign className="h-4 w-4 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* НАСТРОЙКИ ПОСТАВКИ */}
|
||||
<div className="p-4 border-b border-white/10 flex-shrink-0">
|
||||
<h4 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-blue-400" />
|
||||
Настройки поставки
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Выбор фулфилмент-центра */}
|
||||
<div>
|
||||
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
|
||||
<Building2 className="h-3 w-3 text-green-400" />
|
||||
Фулфилмент-центр *
|
||||
</label>
|
||||
<select
|
||||
value={selectedFulfillment}
|
||||
onChange={(e) => setSelectedFulfillment(e.target.value)}
|
||||
className="w-full bg-white/5 border-white/10 text-white h-8 text-sm rounded-lg hover:border-white/30 focus:border-green-400/50 transition-all duration-200"
|
||||
>
|
||||
<option value="" className="bg-gray-800 text-white">
|
||||
Выберите фулфилмент-центр
|
||||
</option>
|
||||
{fulfillmentCenters.map((center) => (
|
||||
<option key={center.id} value={center.id} className="bg-gray-800 text-white">
|
||||
{center.name} - {center.address}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Дата поставки */}
|
||||
<div>
|
||||
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3 text-blue-400" />
|
||||
Желаемая дата поставки *
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-3 w-3 z-10" />
|
||||
<Input
|
||||
type="date"
|
||||
value={deliveryDate}
|
||||
onChange={(e) => setDeliveryDate(e.target.value)}
|
||||
min={minDateString}
|
||||
max={maxDateString}
|
||||
className="bg-white/5 border-white/10 text-white pl-9 h-8 text-sm hover:border-white/30 focus:border-blue-400/50 transition-all duration-200"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Выбор логистики */}
|
||||
<div>
|
||||
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
|
||||
<Truck className="h-3 w-3 text-orange-400" />
|
||||
Логистическая компания
|
||||
</label>
|
||||
<select
|
||||
value={selectedLogistics}
|
||||
onChange={(e) => setSelectedLogistics(e.target.value)}
|
||||
className="w-full bg-white/5 border-white/10 text-white h-8 text-sm rounded-lg hover:border-white/30 focus:border-orange-400/50 transition-all duration-200"
|
||||
>
|
||||
<option value="auto" className="bg-gray-800 text-white">
|
||||
Автоматический выбор
|
||||
</option>
|
||||
{logisticsCompanies.map((company) => (
|
||||
<option key={company.id} value={company.id} className="bg-gray-800 text-white">
|
||||
{company.name} (~{company.estimatedCost} ₽, {company.deliveryDays} дн.)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ТОВАРЫ В КОРЗИНЕ */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{selectedGoods.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-16 h-16 mx-auto bg-purple-400/5 rounded-full flex items-center justify-center">
|
||||
<ShoppingCart className="h-8 w-8 text-purple-400/50" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-base font-medium text-white mb-2">Корзина пуста</h4>
|
||||
<p className="text-white/60 text-sm">Добавьте товары из каталога поставщика</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{selectedGoods.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="glass-card hover:border-white/20 transition-all duration-200 group"
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-semibold text-base truncate group-hover:text-white transition-colors">
|
||||
{item.name}
|
||||
</h4>
|
||||
<p className="text-white/60 text-sm font-mono mt-1">Артикул: {item.sku}</p>
|
||||
{item.category && (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border border-blue-500/30 text-xs font-medium mt-2">
|
||||
{item.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 border border-transparent hover:border-red-500/30 p-2 transition-all duration-200"
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/70 text-xs font-medium">Количество:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newQuantity = Math.max(1, item.selectedQuantity - 1)
|
||||
addToCart(
|
||||
{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
sku: item.sku,
|
||||
price: item.price,
|
||||
category: { name: item.category || '' },
|
||||
images: [],
|
||||
organization: { id: item.supplierId, name: item.supplierName },
|
||||
unit: item.unit,
|
||||
} as GoodsProduct,
|
||||
newQuantity,
|
||||
)
|
||||
}}
|
||||
className="h-7 w-7 p-0 border border-white/20 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={item.selectedQuantity}
|
||||
onChange={(e) => {
|
||||
const newQuantity = parseInt(e.target.value) || 1
|
||||
addToCart(
|
||||
{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
sku: item.sku,
|
||||
price: item.price,
|
||||
category: { name: item.category || '' },
|
||||
images: [],
|
||||
organization: { id: item.supplierId, name: item.supplierName },
|
||||
unit: item.unit,
|
||||
} as GoodsProduct,
|
||||
newQuantity,
|
||||
)
|
||||
}}
|
||||
className="glass-input text-white w-16 h-7 text-center text-xs font-medium"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newQuantity = item.selectedQuantity + 1
|
||||
addToCart(
|
||||
{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
sku: item.sku,
|
||||
price: item.price,
|
||||
category: { name: item.category || '' },
|
||||
images: [],
|
||||
organization: { id: item.supplierId, name: item.supplierName },
|
||||
unit: item.unit,
|
||||
} as GoodsProduct,
|
||||
newQuantity,
|
||||
)
|
||||
}}
|
||||
className="h-7 w-7 p-0 border border-white/20 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/70 text-xs font-medium">Цена за {item.unit || 'шт'}:</span>
|
||||
<span className="text-white text-xs font-semibold">
|
||||
{item.price.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-2 bg-white/5 rounded-lg border border-white/10">
|
||||
<span className="text-white/80 text-sm font-medium">Сумма:</span>
|
||||
<span className="text-green-400 text-base font-bold">
|
||||
{(item.price * item.selectedQuantity).toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Дополнительная информация */}
|
||||
{(item.completeness || item.recipe || item.specialRequirements || item.parameters) && (
|
||||
<div className="mt-2 pt-2 border-t border-white/10 space-y-1">
|
||||
{item.completeness && (
|
||||
<div className="flex items-start gap-2">
|
||||
<FileText className="h-3 w-3 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="text-blue-300 text-xs font-medium">Комплектность: </span>
|
||||
<span className="text-white/80 text-xs">{item.completeness}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.recipe && (
|
||||
<div className="flex items-start gap-2">
|
||||
<Settings className="h-3 w-3 text-purple-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="text-purple-300 text-xs font-medium">Рецептура: </span>
|
||||
<span className="text-white/80 text-xs">{item.recipe}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.specialRequirements && (
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-3 w-3 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="text-yellow-300 text-xs font-medium">Требования: </span>
|
||||
<span className="text-white/80 text-xs">{item.specialRequirements}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.parameters && item.parameters.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Settings className="h-3 w-3 text-green-400" />
|
||||
<span className="text-green-300 text-xs font-medium">Параметры:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.parameters.map((param, idx) => (
|
||||
<Badge
|
||||
key={idx}
|
||||
className="bg-green-500/10 text-green-300 border border-green-500/20 text-xs"
|
||||
>
|
||||
{param.name}: {param.value}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 mb-4">
|
||||
{selectedGoods.map((item) => {
|
||||
// Используем единую функцию расчета
|
||||
const itemTotalPrice = getProductTotalWithRecipe(item.id, item.selectedQuantity)
|
||||
const basePrice = item.price
|
||||
const priceWithRecipe = itemTotalPrice / item.selectedQuantity
|
||||
|
||||
return (
|
||||
<div key={item.id} className="flex items-center justify-between bg-white/5 rounded-lg p-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white text-sm font-medium truncate">{item.name}</h4>
|
||||
<p className="text-white/60 text-xs">
|
||||
{priceWithRecipe.toLocaleString('ru-RU')} ₽ × {item.selectedQuantity}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-400 font-bold text-sm">
|
||||
{itemTotalPrice.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 p-1 h-6 w-6"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ИТОГИ И КНОПКА СОЗДАНИЯ */}
|
||||
<div className="p-4 border-t border-white/10 space-y-4 flex-shrink-0">
|
||||
{/* Детальные итоги */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/70 text-xs font-medium">Товаров:</span>
|
||||
<span className="text-white text-xs font-semibold">{totalQuantity} шт</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/70 text-xs font-medium">Стоимость товаров:</span>
|
||||
<span className="text-white text-xs font-semibold">
|
||||
{totalGoodsAmount.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-purple-300 text-xs font-medium">Фулфилмент (8%):</span>
|
||||
<span className="text-purple-300 text-xs font-semibold">
|
||||
{fulfillmentFee.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-orange-300 text-xs font-medium">Логистика:</span>
|
||||
<span className="text-orange-300 text-xs font-semibold">
|
||||
{selectedLogistics === 'auto' ? '~' : ''}
|
||||
{logisticsCost.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/20 rounded-lg">
|
||||
<span className="text-white text-sm font-semibold">Итого к оплате:</span>
|
||||
<span className="text-green-400 text-lg font-bold">{totalAmount.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка создания поставки */}
|
||||
<Button
|
||||
onClick={handleCreateSupply}
|
||||
disabled={!isFormValid || isCreatingSupply}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white font-semibold py-3 text-sm border border-green-500/30 hover:border-green-400/50 transition-all duration-300 disabled:opacity-50"
|
||||
>
|
||||
{isCreatingSupply ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>Создание поставки...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>Продолжить оформление</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
{selectedGoods.length > 0 && (
|
||||
<>
|
||||
<div className="border-t border-white/10 pt-3 mb-3">
|
||||
{deliveryDate && (
|
||||
<div className="mb-2">
|
||||
<p className="text-white/60 text-xs">Дата поставки:</p>
|
||||
<p className="text-white text-xs font-medium">
|
||||
{new Date(deliveryDate).toLocaleDateString('ru-RU')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFulfillment && (
|
||||
<div className="mb-2">
|
||||
<p className="text-white/60 text-xs">Фулфилмент-центр:</p>
|
||||
<p className="text-white text-xs font-medium">
|
||||
{allCounterparties?.find(c => c.id === selectedFulfillment)?.name ||
|
||||
allCounterparties?.find(c => c.id === selectedFulfillment)?.fullName ||
|
||||
'Выбранный центр'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Сообщения об ошибках валидации */}
|
||||
{!isFormValid && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-2 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-3 w-3 text-red-400 flex-shrink-0" />
|
||||
<p className="text-red-300 text-xs font-medium">
|
||||
{!selectedSupplier
|
||||
? 'Выберите поставщика'
|
||||
: selectedSupplier && selectedGoods.length === 0
|
||||
? 'Добавьте товары в корзину'
|
||||
: selectedSupplier && selectedGoods.length > 0 && !deliveryDate
|
||||
? 'Укажите дату поставки'
|
||||
: selectedSupplier &&
|
||||
selectedGoods.length > 0 &&
|
||||
deliveryDate &&
|
||||
!selectedFulfillment
|
||||
? 'Выберите фулфилмент-центр'
|
||||
: ''}
|
||||
</p>
|
||||
<div className="mb-3">
|
||||
<p className="text-white/60 text-xs mb-1">Логистическая компания:</p>
|
||||
<select
|
||||
value={selectedLogistics}
|
||||
onChange={(e) => setSelectedLogistics(e.target.value)}
|
||||
className="w-full bg-white/5 border-white/10 text-white h-7 text-xs rounded hover:border-white/30 focus:border-purple-400/50 transition-all duration-200"
|
||||
>
|
||||
<option value="auto" className="bg-gray-800 text-white">
|
||||
Выбрать
|
||||
</option>
|
||||
{logisticsCompanies.length > 0 ? (
|
||||
logisticsCompanies.map((logisticsPartner) => (
|
||||
<option key={logisticsPartner.id} value={logisticsPartner.id} className="bg-gray-800 text-white">
|
||||
{logisticsPartner.name || logisticsPartner.fullName}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="" disabled className="bg-gray-800 text-white">
|
||||
Нет доступных логистических партнеров
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3 pt-2 border-t border-white/10">
|
||||
<span className="text-white font-semibold text-sm">Итого:</span>
|
||||
<span className="text-green-400 font-bold text-lg">{totalAmount.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleCreateSupply}
|
||||
disabled={!isFormValid || isCreatingSupply}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50 h-8 text-sm"
|
||||
>
|
||||
{isCreatingSupply ? 'Создание...' : 'Создать поставку'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,7 +54,7 @@ interface WBProductCardsProps {
|
||||
}
|
||||
|
||||
export function WBProductCards({
|
||||
_onBack, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
_onBack,
|
||||
onComplete,
|
||||
showSummary: externalShowSummary,
|
||||
setShowSummary: externalSetShowSummary,
|
||||
|
@ -678,6 +678,34 @@ export const CREATE_SUPPLY_ORDER = gql`
|
||||
quantity
|
||||
price
|
||||
totalPrice
|
||||
recipe {
|
||||
services {
|
||||
id
|
||||
name
|
||||
description
|
||||
price
|
||||
}
|
||||
fulfillmentConsumables {
|
||||
id
|
||||
name
|
||||
description
|
||||
pricePerUnit
|
||||
unit
|
||||
imageUrl
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
sellerConsumables {
|
||||
id
|
||||
name
|
||||
description
|
||||
price
|
||||
unit
|
||||
}
|
||||
marketplaceCardId
|
||||
}
|
||||
product {
|
||||
id
|
||||
name
|
||||
|
@ -3797,7 +3797,16 @@ export const resolvers = {
|
||||
deliveryDate: string
|
||||
fulfillmentCenterId?: string // ID фулфилмент-центра для доставки
|
||||
logisticsPartnerId?: string // ID логистической компании
|
||||
items: Array<{ productId: string; quantity: number }>
|
||||
items: Array<{
|
||||
productId: string
|
||||
quantity: number
|
||||
recipe?: {
|
||||
services: string[]
|
||||
fulfillmentConsumables: string[]
|
||||
sellerConsumables: string[]
|
||||
marketplaceCardId?: string
|
||||
}
|
||||
}>
|
||||
notes?: string // Дополнительные заметки к заказу
|
||||
consumableType?: string // Классификация расходников
|
||||
}
|
||||
@ -3941,6 +3950,11 @@ export const resolvers = {
|
||||
quantity: item.quantity,
|
||||
price: product.price,
|
||||
totalPrice: new Prisma.Decimal(itemTotal),
|
||||
// Передача данных рецептуры в Prisma модель
|
||||
services: item.recipe?.services || [],
|
||||
fulfillmentConsumables: item.recipe?.fulfillmentConsumables || [],
|
||||
sellerConsumables: item.recipe?.sellerConsumables || [],
|
||||
marketplaceCardId: item.recipe?.marketplaceCardId,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -609,6 +609,7 @@ export const typeDefs = gql`
|
||||
quantity: Int!
|
||||
price: Float!
|
||||
totalPrice: Float!
|
||||
recipe: ProductRecipe
|
||||
}
|
||||
|
||||
enum SupplyOrderStatus {
|
||||
@ -635,6 +636,7 @@ export const typeDefs = gql`
|
||||
input SupplyOrderItemInput {
|
||||
productId: ID!
|
||||
quantity: Int!
|
||||
recipe: ProductRecipeInput
|
||||
}
|
||||
|
||||
type PendingSuppliesCount {
|
||||
@ -655,6 +657,21 @@ export const typeDefs = gql`
|
||||
status: String! # Текущий статус заказа
|
||||
}
|
||||
|
||||
# Типы для рецептуры продуктов
|
||||
type ProductRecipe {
|
||||
services: [Service!]!
|
||||
fulfillmentConsumables: [Supply!]!
|
||||
sellerConsumables: [Supply!]!
|
||||
marketplaceCardId: String
|
||||
}
|
||||
|
||||
input ProductRecipeInput {
|
||||
services: [ID!]!
|
||||
fulfillmentConsumables: [ID!]!
|
||||
sellerConsumables: [ID!]!
|
||||
marketplaceCardId: String
|
||||
}
|
||||
|
||||
type SupplyOrderResponse {
|
||||
success: Boolean!
|
||||
message: String!
|
||||
|
Reference in New Issue
Block a user