From 158411cc98a50cc1a930c8295c302d653c34906c Mon Sep 17 00:00:00 2001 From: Bivekich Date: Wed, 23 Jul 2025 15:16:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8:=20react?= =?UTF-8?q?-datepicker=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=20WBProductCards.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D1=8B=20=D0=BF=D0=BE=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B4=D0=B0=D1=80=D1=8F.=20=D0=9E=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=B4=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 37 + package.json | 1 + src/components/admin/ui-kit-section.tsx | 11 + src/components/admin/ui-kit/supplies-demo.tsx | 634 ++++++++++ src/components/supplies/cart-summary.tsx | 207 ++++ .../supplies/create-supply-page.tsx | 1057 ++--------------- src/components/supplies/floating-cart.tsx | 38 + src/components/supplies/product-card.tsx | 183 +++ src/components/supplies/product-grid.tsx | 59 + src/components/supplies/tabs-header.tsx | 85 ++ src/components/supplies/types.ts | 50 + src/components/supplies/wb-product-cards.tsx | 283 +++-- src/components/supplies/wholesaler-card.tsx | 84 ++ src/components/supplies/wholesaler-grid.tsx | 112 ++ .../supplies/wholesaler-products-page.tsx | 134 +++ 15 files changed, 1896 insertions(+), 1079 deletions(-) create mode 100644 src/components/admin/ui-kit/supplies-demo.tsx create mode 100644 src/components/supplies/cart-summary.tsx create mode 100644 src/components/supplies/floating-cart.tsx create mode 100644 src/components/supplies/product-card.tsx create mode 100644 src/components/supplies/product-grid.tsx create mode 100644 src/components/supplies/tabs-header.tsx create mode 100644 src/components/supplies/types.ts create mode 100644 src/components/supplies/wholesaler-card.tsx create mode 100644 src/components/supplies/wholesaler-grid.tsx create mode 100644 src/components/supplies/wholesaler-products-page.tsx diff --git a/package-lock.json b/package-lock.json index 2567347..91bc00f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "next-themes": "^0.4.6", "prisma": "^6.12.0", "react": "19.1.0", + "react-datepicker": "^8.4.0", "react-day-picker": "^9.8.0", "react-dom": "19.1.0", "react-imask": "^7.6.1", @@ -1813,6 +1814,21 @@ "@floating-ui/utils": "^0.2.10" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.13", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.13.tgz", + "integrity": "sha512-Qmj6t9TjgWAvbygNEu1hj4dbHI9CY0ziCMIJrmYoDIn9TUAH5lRmiIeZmRd4c6QEZkzdoH7jNnoNyoY1AIESiA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.4", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", @@ -9855,6 +9871,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.4.0.tgz", + "integrity": "sha512-6nPDnj8vektWCIOy9ArS3avus9Ndsyz5XgFCJ7nBxXASSpBdSL6lG9jzNNmViPOAOPh6T5oJyGaXuMirBLECag==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/react-day-picker": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.0.tgz", @@ -10834,6 +10865,12 @@ "node": ">=0.10" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", diff --git a/package.json b/package.json index b16f320..adc998e 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "next-themes": "^0.4.6", "prisma": "^6.12.0", "react": "19.1.0", + "react-datepicker": "^8.4.0", "react-day-picker": "^9.8.0", "react-dom": "19.1.0", "react-imask": "^7.6.1", diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx index b16beb5..35e3c54 100644 --- a/src/components/admin/ui-kit-section.tsx +++ b/src/components/admin/ui-kit-section.tsx @@ -17,6 +17,7 @@ import { InteractiveDemo } from "./ui-kit/interactive-demo"; import { BusinessDemo } from "./ui-kit/business-demo"; import { TimesheetDemo } from "./ui-kit/timesheet-demo"; import { FulfillmentWarehouseDemo } from "./ui-kit/fulfillment-warehouse-demo"; +import { SuppliesDemo } from "./ui-kit/supplies-demo"; export function UIKitSection() { return ( @@ -126,6 +127,12 @@ export function UIKitSection() { > Склад фулфилмент + + Поставки + @@ -191,6 +198,10 @@ export function UIKitSection() { + + + + ); diff --git a/src/components/admin/ui-kit/supplies-demo.tsx b/src/components/admin/ui-kit/supplies-demo.tsx new file mode 100644 index 0000000..ce31326 --- /dev/null +++ b/src/components/admin/ui-kit/supplies-demo.tsx @@ -0,0 +1,634 @@ +"use client"; + +import React, { useState } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + ShoppingCart, + Plus, + Minus, + Eye, + Heart, + Package, + Building2, + Calendar, + Users, + Search, + Star, + Truck +} from "lucide-react"; + +export function SuppliesDemo() { + const [selectedQuantity, setSelectedQuantity] = useState(1); + const [cartItems, setCartItems] = useState(3); + const [cartTotal, setCartTotal] = useState(15750); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('ru-RU', { + style: 'currency', + currency: 'RUB', + minimumFractionDigits: 0 + }).format(amount); + }; + + const mockProduct = { + id: "1", + name: "Футболка мужская базовая хлопок 100%", + brand: "BASIC", + price: 1299, + discount: 15, + quantity: 47, + color: "Черный", + size: "L", + mainImage: "/api/placeholder/300/300", + isNew: true, + isBestseller: false + }; + + const mockWholesaler = { + id: "w1", + name: "ТекстильПром ООО", + rating: 4.8, + reviewsCount: 1247, + location: "Москва", + specialization: "Текстиль и одежда", + verified: true + }; + + const discountedPrice = mockProduct.discount + ? mockProduct.price * (1 - mockProduct.discount / 100) + : mockProduct.price; + + return ( +
+
+

Поставки - Компоненты

+

+ Демонстрация компонентов системы управления поставками +

+
+ + + + Карточки товаров + Карточки оптовиков + Плавающая корзина + Типы поставок + + + +
+

Карточки товаров

+

+ Интерактивные карточки товаров с возможностью управления количеством +

+ +
+ {/* Обычная карточка товара */} + +
+
+ +
+ + {/* Количество в наличии */} +
+ 50 + ? 'bg-green-500/80' + : mockProduct.quantity > 10 + ? 'bg-yellow-500/80' + : 'bg-red-500/80' + } text-white border-0 backdrop-blur text-xs`}> + {mockProduct.quantity} + +
+ + {/* Скидка */} + {mockProduct.discount && ( +
+ + -{mockProduct.discount}% + +
+ )} + + {/* Overlay с кнопками */} +
+
+ + +
+
+
+ +
+ {/* Заголовок и бренд */} +
+
+ {mockProduct.brand && ( + + {mockProduct.brand} + + )} +
+ {mockProduct.isNew && ( + + NEW + + )} + {mockProduct.isBestseller && ( + + HIT + + )} +
+
+

+ {mockProduct.name} +

+
+ + {/* Основная характеристика */} +
+ {mockProduct.color && {mockProduct.color}} + {mockProduct.size && {mockProduct.size}} +
+ + {/* Цена */} +
+
+
+ {formatCurrency(discountedPrice)} +
+ {mockProduct.discount && ( +
+ {formatCurrency(mockProduct.price)} +
+ )} +
+
+ + {/* Управление количеством */} +
+ + { + const value = e.target.value.replace(/[^0-9]/g, ''); + const numValue = parseInt(value) || 0; + setSelectedQuantity(Math.min(mockProduct.quantity, numValue)); + }} + className="h-8 w-12 text-center bg-white/10 border-white/20 text-white text-sm" + /> + + + {selectedQuantity > 0 && ( + + + {selectedQuantity} + + )} +
+ + {/* Сумма для выбранного товара */} + {selectedQuantity > 0 && ( +
+
+ {formatCurrency(discountedPrice * selectedQuantity)} +
+
+ )} +
+
+ + {/* Компактная карточка товара */} + +
+
+ +
+
+

Товар компактно

+

Краткое описание

+
+ {formatCurrency(1599)} + + В наличии + +
+
+
+
+ + {/* Карточка товара со статусом */} + +
+
+ + Ожидается + + Арт: TB-001 +
+

Товар с уведомлением

+
+

Поступление: 15 марта

+

Количество: ~200 шт

+
+
+ {formatCurrency(899)} + +
+
+
+
+
+
+ + +
+

Карточки оптовиков

+

+ Информационные карточки поставщиков и оптовиков +

+ +
+ {/* Главная карточка оптовика */} + +
+
+
+
+ +
+
+

+ {mockWholesaler.name} +

+

+ {mockWholesaler.specialization} +

+
+
+ {mockWholesaler.verified && ( + + Верифицирован + + )} +
+ +
+
+ Рейтинг: +
+
+ {Array.from({ length: 5 }, (_, i) => ( + + ))} +
+ + {mockWholesaler.rating} + +
+
+ +
+ Отзывы: + + {mockWholesaler.reviewsCount.toLocaleString('ru-RU')} + +
+ +
+ Местоположение: + {mockWholesaler.location} +
+
+ +
+ + +
+
+
+ + {/* Компактная карточка поставщика */} + +
+
+ +
+
+

Логистический партнер

+

Быстрая доставка

+
+
+ {Array.from({ length: 5 }, (_, i) => ( + + ))} +
+ (4.9) +
+
+
+
+ + {/* Карточка нового поставщика */} + +
+
+ + Новый партнер + + +
+

ТехноТрейд ООО

+

+ Электроника и техника +

+
+ Заявка на партнерство + +
+
+
+
+
+
+ + +
+

Плавающая корзина

+

+ Плавающий элемент для быстрого доступа к корзине +

+ +
+ {/* Демонстрация плавающей корзины */} +
+

+ Область контента страницы +
+ Плавающая корзина появляется в правом нижнем углу +

+ + {/* Демо плавающей корзины */} +
+ +
+
+ + {/* Различные варианты плавающих кнопок */} +
+ +

Стандартная корзина

+ +
+ + +

Компактная

+ +
+ + +

С индикатором

+
+ + + {cartItems} + +
+
+
+ + {/* Кнопки управления демо */} +
+ + + +
+
+
+
+ + +
+

Типы поставок

+

+ Различные варианты выбора типа поставки +

+ +
+ {/* Вариант 1: Карточки */} + +
+
+ +
+
+

Карточки

+

+ Создание поставки через выбор товаров по карточкам +

+
+ + Доступно + +
+
+ + {/* Вариант 2: Оптовик */} + +
+
+ +
+
+

Оптовик

+

+ Создание поставки через выбор товаров у оптовиков +

+
+ + Доступно + +
+
+ + {/* Дополнительные типы */} + +
+
+ +
+
+

Импорт

+

+ Массовый импорт товаров из файла +

+
+ + Скоро + +
+
+ + +
+
+ +
+
+

Регулярные

+

+ Автоматические поставки по расписанию +

+
+ + В разработке + +
+
+
+ + {/* Статусы поставок */} +
+

Статусы поставок

+
+
+ + Подготовка + +

Сбор товаров

+
+
+ + В пути + +

Доставка

+
+
+ + Доставлено + +

Успешно

+
+
+ + Ошибка + +

Требует внимания

+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/supplies/cart-summary.tsx b/src/components/supplies/cart-summary.tsx new file mode 100644 index 0000000..cfe6700 --- /dev/null +++ b/src/components/supplies/cart-summary.tsx @@ -0,0 +1,207 @@ +"use client" + +import React from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { + ShoppingCart, + Building2, + Plus, + Minus, + Eye +} from 'lucide-react' +import { SelectedProduct } from './types' + +interface CartSummaryProps { + selectedProducts: SelectedProduct[] + onQuantityChange: (productId: string, wholesalerId: string, quantity: number) => void + onRemoveProduct: (productId: string, wholesalerId: string) => void + onCreateSupply: () => void + onToggleVisibility: () => void + formatCurrency: (amount: number) => string + visible: boolean +} + +export function CartSummary({ + selectedProducts, + onQuantityChange, + onRemoveProduct, + onCreateSupply, + onToggleVisibility, + formatCurrency, + visible +}: CartSummaryProps) { + if (!visible || selectedProducts.length === 0) { + return null + } + + // Группируем товары по оптовикам + const groupedProducts = selectedProducts.reduce((acc, product) => { + if (!acc[product.wholesalerId]) { + acc[product.wholesalerId] = { + wholesaler: product.wholesalerName, + products: [] + } + } + acc[product.wholesalerId].products.push(product) + return acc + }, {} as Record) + + const getTotalAmount = () => { + return selectedProducts.reduce((sum, product) => { + const discountedPrice = product.discount + ? product.price * (1 - product.discount / 100) + : product.price + return sum + (discountedPrice * product.selectedQuantity) + }, 0) + } + + const getTotalItems = () => { + return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0) + } + + return ( + +
+
+
+
+ +
+
+

Корзина

+

+ {selectedProducts.length} товаров от {Object.keys(groupedProducts).length} поставщиков +

+
+
+ +
+ + {/* Группировка по оптовикам */} + {Object.entries(groupedProducts).map(([wholesalerId, group]) => ( +
+
+ + {group.wholesaler} + + {group.products.length} товар(ов) + +
+ +
+ {group.products.map((product) => { + const discountedPrice = product.discount + ? product.price * (1 - product.discount / 100) + : product.price + const totalPrice = discountedPrice * product.selectedQuantity + + return ( +
+ {product.name} +
+

{product.name}

+

{product.article}

+
+
+ + {product.selectedQuantity} + +
+
+
{formatCurrency(totalPrice)}
+ {product.discount && ( +
+ {formatCurrency(product.price * product.selectedQuantity)} +
+ )} +
+
+
+ +
+ ) + })} +
+
+ ))} + + {/* Итого */} +
+
+ + Итого: {getTotalItems()} товаров + + + {formatCurrency(getTotalAmount())} + +
+
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/create-supply-page.tsx b/src/components/supplies/create-supply-page.tsx index bd6973f..3ec2d8a 100644 --- a/src/components/supplies/create-supply-page.tsx +++ b/src/components/supplies/create-supply-page.tsx @@ -2,255 +2,23 @@ import React, { useState, useEffect } from 'react' import { useQuery } from '@apollo/client' -import { Card } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' -import { Input } from '@/components/ui/input' import { Sidebar } from '@/components/dashboard/sidebar' import { useSidebar } from '@/hooks/useSidebar' import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from '@/graphql/queries' -import { - ArrowLeft, - ShoppingCart, - Users, - Building2, - MapPin, - Phone, - Mail, - Star, - Plus, - Minus, - Info, - Package, - Zap, - Heart, - Eye, - ShoppingBag, - Search -} from 'lucide-react' import { useRouter } from 'next/navigation' -import Image from 'next/image' import { WBProductCards } from './wb-product-cards' - -interface WholesalerForCreation { - id: string - inn: string - name: string - fullName: string - address: string - phone?: string - email?: string - rating: number - productCount: number - avatar?: string - specialization: string[] -} - -interface WholesalerProduct { - id: string - name: string - article: string - description: string - price: number - quantity: number - category: string - brand?: string - color?: string - size?: string - weight?: number - dimensions?: string - material?: string - images: string[] - mainImage?: string - discount?: number - isNew?: boolean - isBestseller?: boolean -} - -interface SelectedProduct extends WholesalerProduct { - selectedQuantity: number - wholesalerId: string - wholesalerName: string -} - -interface WildberriesCard { - nmID: number - vendorCode: string - title: string - description: string - brand: string - mediaFiles: string[] - sizes: Array<{ - chrtID: number - techSize: string - wbSize: string - price: number - discountedPrice: number - quantity: number - }> -} - -interface SelectedCard { - card: WildberriesCard - selectedQuantity: number - selectedMarket: string - selectedPlace: string - sellerName: string - sellerPhone: string - deliveryDate: string - selectedServices: string[] -} - -// Моковые данные оптовиков -const mockWholesalers: WholesalerForCreation[] = [ - { - id: '1', - inn: '7707083893', - name: 'ОПТ-Электроника', - fullName: 'ООО "ОПТ-Электроника"', - address: 'г. Москва, ул. Садовая, д. 15', - phone: '+7 (495) 123-45-67', - email: 'opt@electronics.ru', - rating: 4.8, - productCount: 1250, - specialization: ['Электроника', 'Бытовая техника'] - }, - { - id: '2', - inn: '7707083894', - name: 'ТекстильМастер', - fullName: 'ООО "ТекстильМастер"', - address: 'г. Иваново, пр. Ленина, д. 42', - phone: '+7 (4932) 55-66-77', - email: 'sales@textilmaster.ru', - rating: 4.6, - productCount: 850, - specialization: ['Текстиль', 'Одежда', 'Домашний текстиль'] - }, - { - id: '3', - inn: '7707083895', - name: 'МетизКомплект', - fullName: 'ООО "МетизКомплект"', - address: 'г. Тула, ул. Металлургов, д. 8', - phone: '+7 (4872) 33-44-55', - email: 'info@metiz.ru', - rating: 4.9, - productCount: 2100, - specialization: ['Крепеж', 'Метизы', 'Инструменты'] - } -] - -// Улучшенные моковые данные товаров -const mockProducts: WholesalerProduct[] = [ - { - id: '1', - name: 'iPhone 15 Pro Max', - article: 'APL-15PM-256', - description: 'Флагманский смартфон Apple с титановым корпусом, камерой 48 МП и чипом A17 Pro', - price: 124900, - quantity: 45, - category: 'Смартфоны', - brand: 'Apple', - color: 'Натуральный титан', - size: '6.7"', - weight: 221, - dimensions: '159.9 x 76.7 x 8.25 мм', - material: 'Титан, стекло', - images: ['https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/iphone-15-pro-max-naturaltitanium-select?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1692845705224'], - mainImage: 'https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/iphone-15-pro-max-naturaltitanium-select?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1692845705224', - isNew: true, - isBestseller: true - }, - { - id: '2', - name: 'Sony WH-1000XM5', - article: 'SNY-WH1000XM5', - description: 'Беспроводные наушники премиум-класса с лучшим в отрасли шумоподавлением', - price: 34900, - quantity: 128, - category: 'Наушники', - brand: 'Sony', - color: 'Черный', - weight: 250, - material: 'Пластик, эко-кожа', - images: ['https://www.sony.ru/image/5d02da5df552836db894cead8a68f5f3?fmt=pjpeg&wid=330&bgcolor=FFFFFF&bgc=FFFFFF'], - mainImage: 'https://www.sony.ru/image/5d02da5df552836db894cead8a68f5f3?fmt=pjpeg&wid=330&bgcolor=FFFFFF&bgc=FFFFFF', - discount: 15, - isBestseller: true - }, - { - id: '3', - name: 'iPad Pro 12.9" M2', - article: 'APL-IPADPRO-M2', - description: 'Самый мощный iPad с чипом M2, дисплеем Liquid Retina XDR и поддержкой Apple Pencil', - price: 109900, - quantity: 32, - category: 'Планшеты', - brand: 'Apple', - color: 'Серый космос', - size: '12.9"', - weight: 682, - dimensions: '280.6 x 214.9 x 6.4 мм', - material: 'Алюминий', - images: ['https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/ipad-pro-12-select-wifi-spacegray-202210?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1664411207213'], - mainImage: 'https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/ipad-pro-12-select-wifi-spacegray-202210?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1664411207213', - isNew: true - }, - { - id: '4', - name: 'MacBook Pro 16" M3 Max', - article: 'APL-MBP16-M3MAX', - description: 'Профессиональный ноутбук с чипом M3 Max, 36 ГБ памяти и дисплеем Liquid Retina XDR', - price: 329900, - quantity: 18, - category: 'Ноутбуки', - brand: 'Apple', - color: 'Серый космос', - size: '16"', - weight: 2160, - dimensions: '355.7 x 248.1 x 16.8 мм', - material: 'Алюминий', - images: ['https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/mbp16-spacegray-select-202310?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1697230830200'], - mainImage: 'https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/mbp16-spacegray-select-202310?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1697230830200', - isNew: true, - isBestseller: true - }, - { - id: '5', - name: 'Apple Watch Ultra 2', - article: 'APL-AWU2-49', - description: 'Самые прочные и функциональные умные часы Apple для экстремальных приключений', - price: 89900, - quantity: 67, - category: 'Умные часы', - brand: 'Apple', - color: 'Натуральный титан', - size: '49 мм', - weight: 61, - dimensions: '49 x 44 x 14.4 мм', - material: 'Титан', - images: ['https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/watch-ultra2-select-202309?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1693967875133'], - mainImage: 'https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/watch-ultra2-select-202309?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1693967875133', - isNew: true - }, - { - id: '6', - name: 'Magic Keyboard для iPad Pro', - article: 'APL-MK-IPADPRO', - description: 'Клавиатура с трекпадом и подсветкой клавиш для iPad Pro 12.9"', - price: 36900, - quantity: 89, - category: 'Аксессуары', - brand: 'Apple', - color: 'Черный', - weight: 710, - dimensions: '280.9 x 214.3 x 25 мм', - material: 'Алюминий, пластик', - images: ['https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/MJQJ3?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1639066901000'], - mainImage: 'https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/MJQJ3?wid=470&hei=556&fmt=jpeg&qlt=99&.v=1639066901000' - } -] +import { SelectedCard as WBSelectedCard } from '@/types/supplies' +import { TabsHeader } from './tabs-header' +import { WholesalerGrid } from './wholesaler-grid' +import { CartSummary } from './cart-summary' +import { FloatingCart } from './floating-cart' +import { WholesalerProductsPage } from './wholesaler-products-page' +import { + WholesalerForCreation, + WholesalerProduct, + SelectedProduct, + CounterpartyWholesaler +} from './types' export function CreateSupplyPage() { const router = useRouter() @@ -258,7 +26,7 @@ export function CreateSupplyPage() { const [activeTab, setActiveTab] = useState<'cards' | 'wholesaler'>('cards') const [selectedWholesaler, setSelectedWholesaler] = useState(null) const [selectedProducts, setSelectedProducts] = useState([]) - const [selectedCards, setSelectedCards] = useState([]) + const [selectedCards, setSelectedCards] = useState([]) const [showSummary, setShowSummary] = useState(false) const [searchQuery, setSearchQuery] = useState('') @@ -272,17 +40,11 @@ export function CreateSupplyPage() { }) // Фильтруем только оптовиков - const wholesalers = (counterpartiesData?.myCounterparties || []).filter((org: { type: string }) => org.type === 'WHOLESALE') - - // Фильтруем оптовиков по поисковому запросу - const filteredWholesalers = wholesalers.filter((wholesaler: { name?: string; fullName?: string; inn?: string }) => - wholesaler.name?.toLowerCase().includes(searchQuery.toLowerCase()) || - wholesaler.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) || - wholesaler.inn?.toLowerCase().includes(searchQuery.toLowerCase()) - ) + const wholesalers: CounterpartyWholesaler[] = (counterpartiesData?.myCounterparties || []) + .filter((org: { type: string }) => org.type === 'WHOLESALE') // Фильтруем товары по выбранному оптовику - const wholesalerProducts = selectedWholesaler + const wholesalerProducts: WholesalerProduct[] = selectedWholesaler ? (productsData?.allProducts || []).filter((product: { organization: { id: string } }) => product.organization.id === selectedWholesaler.id ) @@ -303,17 +65,8 @@ export function CreateSupplyPage() { }).format(amount) } - const renderStars = (rating: number) => { - return Array.from({ length: 5 }, (_, i) => ( - - )) - } - const updateProductQuantity = (productId: string, quantity: number) => { - const product = wholesalerProducts.find((p: { id: string }) => p.id === productId) + const product = wholesalerProducts.find((p) => p.id === productId) if (!product || !selectedWholesaler) return setSelectedProducts(prev => { @@ -340,12 +93,6 @@ export function CreateSupplyPage() { }) } - const getSelectedQuantity = (productId: string): number => { - if (!selectedWholesaler) return 0 - const selected = selectedProducts.find(p => p.id === productId && p.wholesalerId === selectedWholesaler.id) - return selected ? selected.selectedQuantity : 0 - } - const getTotalAmount = () => { return selectedProducts.reduce((sum, product) => { const discountedPrice = product.discount @@ -359,20 +106,17 @@ export function CreateSupplyPage() { return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0) } - const handleCardsComplete = (cards: SelectedCard[]) => { + const handleCardsComplete = (cards: WBSelectedCard[]) => { setSelectedCards(cards) console.log('Карточки товаров выбраны:', cards) - // TODO: Здесь будет создание поставки с данными карточек router.push('/supplies') } const handleCreateSupply = () => { if (activeTab === 'cards') { console.log('Создание поставки с карточками Wildberries') - // TODO: Здесь будет создание поставки с данными карточек } else { console.log('Создание поставки с товарами:', selectedProducts) - // TODO: Здесь будет реальное создание поставки } router.push('/supplies') } @@ -380,366 +124,45 @@ export function CreateSupplyPage() { const handleGoBack = () => { if (selectedWholesaler) { setSelectedWholesaler(null) - // НЕ очищаем корзину! setSelectedProducts([]) setShowSummary(false) } else { router.push('/supplies') } } - // Рендер товаров оптовика - if (selectedWholesaler && activeTab === 'wholesaler') { - return ( -
- -
-
-
-
- -
-

Товары оптовика

-

{selectedWholesaler.name} • {wholesalerProducts.length} товаров

-
-
-
- -
-
- - {showSummary && selectedProducts.length > 0 && ( - -
-
-

- - Корзина ({selectedProducts.length}) -

- -
- - {/* Текущий оптовик */} -
-
- - {selectedWholesaler?.name} - - {selectedProducts.filter(p => p.wholesalerId === selectedWholesaler?.id).length} товар(ов) - -
- -
- {selectedProducts.filter(p => p.wholesalerId === selectedWholesaler?.id).map((product) => { - const discountedPrice = product.discount - ? product.price * (1 - product.discount / 100) - : product.price - const totalPrice = discountedPrice * product.selectedQuantity - - return ( -
- {product.name} -
-

{product.name}

-

{product.article}

-
- × {product.selectedQuantity} -
-
{formatCurrency(totalPrice)}
- {product.discount && ( -
- {formatCurrency(product.price * product.selectedQuantity)} -
- )} -
-
-
-
- ) - })} -
-
- - {/* Другие оптовики в корзине */} - {Object.entries( - selectedProducts.filter(p => p.wholesalerId !== selectedWholesaler?.id).reduce((acc, product) => { - if (!acc[product.wholesalerId]) { - acc[product.wholesalerId] = { - wholesaler: product.wholesalerName, - products: [] - } - } - acc[product.wholesalerId].products.push(product) - return acc - }, {} as Record) - ).map(([wholesalerId, group]) => ( -
-
- - {group.wholesaler} - - {group.products.length} товар(ов) - -
- -
- {group.products.map((product) => { - const discountedPrice = product.discount - ? product.price * (1 - product.discount / 100) - : product.price - const totalPrice = discountedPrice * product.selectedQuantity - - return ( -
- {product.name} -
-

{product.name}

-

{product.article}

-
- × {product.selectedQuantity} -
-
{formatCurrency(totalPrice)}
- {product.discount && ( -
- {formatCurrency(product.price * product.selectedQuantity)} -
- )} -
-
-
-
- ) - })} -
-
- ))} - - {/* Итого */} -
-
- - Итого: {getTotalItems()} товаров - - - {formatCurrency(getTotalAmount())} - -
- -
-
-
- )} - - {productsLoading ? ( -
-
-
-

Загружаем товары...

-
-
- ) : wholesalerProducts.length === 0 ? ( -
-
- -

У этого оптовика нет товаров

-

Выберите другого оптовика

-
-
- ) : ( -
- {wholesalerProducts.map((product: { - id: string; - name: string; - article: string; - description?: string; - price: number; - quantity: number; - category?: { name: string }; - brand?: string; - color?: string; - size?: string; - mainImage?: string; - images?: string[] - }) => { - const selectedQuantity = getSelectedQuantity(product.id) - const discountedPrice = product.price // Убираем discount так как его нет в схеме - - return ( - -
- {product.name} - - {/* Количество в наличии */} -
- 50 ? 'bg-green-500/80' : product.quantity > 10 ? 'bg-yellow-500/80' : 'bg-red-500/80'} text-white border-0 backdrop-blur text-xs`}> - {product.quantity} - -
- - {/* Убираем discount badge так как поля нет в схеме */} - - {/* Overlay с кнопками */} -
-
- - -
-
-
- -
- {/* Заголовок и бренд */} -
-
- {product.brand && ( - - {product.brand} - - )} - {/* Убираем isNew и isBestseller так как этих полей нет в схеме */} -
-

- {product.name} -

-
- - {/* Основная характеристика */} -
- {product.color && {product.color}} - {product.size && {product.size}} -
- - {/* Цена */} -
-
-
- {formatCurrency(discountedPrice)} -
- {/* Убираем отображение оригинальной цены так как discount нет */} -
-
- - {/* Управление количеством */} -
- - { - const value = e.target.value.replace(/[^0-9]/g, '') - const numValue = Math.max(0, Math.min(product.quantity, parseInt(value) || 0)) - updateProductQuantity(product.id, numValue) - }} - onFocus={(e) => e.target.select()} - className="h-8 w-12 text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - /> - - - {selectedQuantity > 0 && ( - - - {selectedQuantity} - - )} -
- - {/* Сумма для выбранного товара */} - {selectedQuantity > 0 && ( -
-
- {formatCurrency(discountedPrice * selectedQuantity)} -
-
- )} -
-
- ) - })} -
- )} - - {/* Floating корзина */} - {selectedProducts.length > 0 && ( -
- -
- )} -
-
-
+ const handleRemoveProduct = (productId: string, wholesalerId: string) => { + setSelectedProducts(prev => + prev.filter(p => !(p.id === productId && p.wholesalerId === wholesalerId)) ) } + const handleCartQuantityChange = (productId: string, wholesalerId: string, quantity: number) => { + setSelectedProducts(prev => + prev.map(p => + p.id === productId && p.wholesalerId === wholesalerId + ? { ...p, selectedQuantity: quantity } + : p + ) + ) + } + // Рендер страницы товаров оптовика + if (selectedWholesaler && activeTab === 'wholesaler') { + return ( + + ) + } // Главная страница с табами return ( @@ -747,360 +170,68 @@ export function CreateSupplyPage() {
- {/* Верхняя строка: Назад + Заголовок + Табы */} -
-
- -

Создание поставки

-
- -
-
- - -
-
-
+ router.push('/supplies')} + cartInfo={ + activeTab === 'cards' && selectedCards.length > 0 + ? { + itemCount: selectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0), + totalAmount: 0, + formatCurrency + } + : activeTab === 'wholesaler' && selectedProducts.length > 0 + ? { + itemCount: selectedProducts.length, + totalAmount: getTotalAmount(), + formatCurrency + } + : undefined + } + onCartClick={() => setShowSummary(true)} + /> {/* Контент карточек */} {activeTab === 'cards' && ( router.push('/supplies')} onComplete={handleCardsComplete} + showSummary={showSummary} + setShowSummary={setShowSummary} + selectedCards={selectedCards} + setSelectedCards={setSelectedCards} /> )} {/* Контент оптовиков */} {activeTab === 'wholesaler' && (
- {/* Поиск */} -
-
- - setSearchQuery(e.target.value)} - className="pl-10 glass-input text-white placeholder:text-white/40 h-10" - /> -
-
+ setShowSummary(false)} + formatCurrency={formatCurrency} + visible={showSummary && selectedProducts.length > 0} + /> - {/* Корзина */} - {showSummary && selectedProducts.length > 0 && ( - -
-
-
-
- -
-
-

Корзина

-

{selectedProducts.length} товаров от {Object.keys(selectedProducts.reduce((acc, p) => ({ ...acc, [p.wholesalerId]: true }), {})).length} поставщиков

-
-
- -
- - {/* Группировка по оптовикам */} - {Object.entries( - selectedProducts.reduce((acc, product) => { - if (!acc[product.wholesalerId]) { - acc[product.wholesalerId] = { - wholesaler: product.wholesalerName, - products: [] - } - } - acc[product.wholesalerId].products.push(product) - return acc - }, {} as Record) - ).map(([wholesalerId, group]) => ( -
-
- - {group.wholesaler} - - {group.products.length} товар(ов) - -
- -
- {group.products.map((product) => { - const discountedPrice = product.discount - ? product.price * (1 - product.discount / 100) - : product.price - const totalPrice = discountedPrice * product.selectedQuantity - - return ( -
- {product.name} -
-

{product.name}

-

{product.article}

-
-
- - {product.selectedQuantity} - -
-
-
{formatCurrency(totalPrice)}
- {product.discount && ( -
- {formatCurrency(product.price * product.selectedQuantity)} -
- )} -
-
-
- -
- ) - })} -
-
- ))} - - {/* Итого */} -
-
- - Итого: {getTotalItems()} товаров - - - {formatCurrency(getTotalAmount())} - -
-
- - -
-
-
-
- )} + - {counterpartiesLoading ? ( -
-
-
-

Загружаем оптовиков...

-
-
- ) : filteredWholesalers.length === 0 ? ( -
- -

- {searchQuery ? 'Оптовики не найдены' : 'У вас нет контрагентов-оптовиков'} -

-

- {searchQuery ? 'Попробуйте изменить условия поиска' : 'Добавьте оптовиков в разделе "Партнеры"'} -

-
- ) : ( -
- {filteredWholesalers.map((wholesaler: { - id: string; - name?: string; - fullName?: string; - inn?: string; - address?: string; - phones?: { value: string }[]; - emails?: { value: string }[] - }) => ( - { - // Адаптируем данные под существующий интерфейс - const adaptedWholesaler = { - id: wholesaler.id, - inn: wholesaler.inn || '', - name: wholesaler.name || 'Неизвестная организация', - fullName: wholesaler.fullName || wholesaler.name || 'Неизвестная организация', - address: wholesaler.address || 'Адрес не указан', - phone: wholesaler.phones?.[0]?.value, - email: wholesaler.emails?.[0]?.value, - rating: 4.5, // Временное значение - productCount: 0, // Временное значение - specialization: ['Оптовая торговля'] // Временное значение - } - setSelectedWholesaler(adaptedWholesaler) - }} - > -
-
-
- -
-
-

- {wholesaler.name || 'Неизвестная организация'} -

-

- {wholesaler.fullName || wholesaler.name} -

- {wholesaler.inn && ( -

- ИНН: {wholesaler.inn} -

- )} -
-
- -
- {wholesaler.address && ( -
- - {wholesaler.address} -
- )} - - {wholesaler.phones?.[0]?.value && ( -
- - {wholesaler.phones[0].value} -
- )} - - {wholesaler.emails?.[0]?.value && ( -
- - {wholesaler.emails[0].value} -
- )} -
- -
- - Контрагент - -
- -
-

ИНН: {wholesaler.inn}

-
-
-
- ))} -
- )} - - {/* Floating корзина */} - {selectedProducts.length > 0 && !showSummary && ( -
- -
- )} + visible={selectedProducts.length > 0 && !showSummary} + />
)}
diff --git a/src/components/supplies/floating-cart.tsx b/src/components/supplies/floating-cart.tsx new file mode 100644 index 0000000..917ac41 --- /dev/null +++ b/src/components/supplies/floating-cart.tsx @@ -0,0 +1,38 @@ +"use client" + +import React from 'react' +import { Button } from '@/components/ui/button' +import { ShoppingCart } from 'lucide-react' + +interface FloatingCartProps { + itemCount: number + totalAmount: number + formatCurrency: (amount: number) => string + onClick: () => void + visible: boolean +} + +export function FloatingCart({ + itemCount, + totalAmount, + formatCurrency, + onClick, + visible +}: FloatingCartProps) { + if (!visible || itemCount === 0) { + return null + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/product-card.tsx b/src/components/supplies/product-card.tsx new file mode 100644 index 0000000..fd1072d --- /dev/null +++ b/src/components/supplies/product-card.tsx @@ -0,0 +1,183 @@ +"use client" + +import React from 'react' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { + Plus, + Minus, + Eye, + Heart, + ShoppingCart +} from 'lucide-react' + +import { WholesalerProduct } from './types' + +interface ProductCardProps { + product: WholesalerProduct + selectedQuantity: number + onQuantityChange: (quantity: number) => void + formatCurrency: (amount: number) => string +} + +export function ProductCard({ + product, + selectedQuantity, + onQuantityChange, + formatCurrency +}: ProductCardProps) { + const discountedPrice = product.discount + ? product.price * (1 - product.discount / 100) + : product.price + + const handleQuantityChange = (newQuantity: number) => { + const clampedQuantity = Math.max(0, Math.min(product.quantity, newQuantity)) + onQuantityChange(clampedQuantity) + } + + return ( + +
+ {product.name} + + {/* Количество в наличии */} +
+ 50 + ? 'bg-green-500/80' + : product.quantity > 10 + ? 'bg-yellow-500/80' + : 'bg-red-500/80' + } text-white border-0 backdrop-blur text-xs`}> + {product.quantity} + +
+ + {/* Скидка */} + {product.discount && ( +
+ + -{product.discount}% + +
+ )} + + {/* Overlay с кнопками */} +
+
+ + +
+
+
+ +
+ {/* Заголовок и бренд */} +
+
+ {product.brand && ( + + {product.brand} + + )} +
+ {product.isNew && ( + + NEW + + )} + {product.isBestseller && ( + + HIT + + )} +
+
+

+ {product.name} +

+
+ + {/* Основная характеристика */} +
+ {product.color && {product.color}} + {product.size && {product.size}} +
+ + {/* Цена */} +
+
+
+ {formatCurrency(discountedPrice)} +
+ {product.discount && ( +
+ {formatCurrency(product.price)} +
+ )} +
+
+ + {/* Управление количеством */} +
+ + { + const value = e.target.value.replace(/[^0-9]/g, '') + const numValue = parseInt(value) || 0 + handleQuantityChange(numValue) + }} + onFocus={(e) => e.target.select()} + className="h-8 w-12 text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + + + {selectedQuantity > 0 && ( + + + {selectedQuantity} + + )} +
+ + {/* Сумма для выбранного товара */} + {selectedQuantity > 0 && ( +
+
+ {formatCurrency(discountedPrice * selectedQuantity)} +
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/product-grid.tsx b/src/components/supplies/product-grid.tsx new file mode 100644 index 0000000..c0fc6e2 --- /dev/null +++ b/src/components/supplies/product-grid.tsx @@ -0,0 +1,59 @@ +"use client" + +import React from 'react' +import { ProductCard } from './product-card' +import { Package } from 'lucide-react' +import { WholesalerProduct } from './types' + +interface ProductGridProps { + products: WholesalerProduct[] + selectedProducts: Record + onQuantityChange: (productId: string, quantity: number) => void + formatCurrency: (amount: number) => string + loading?: boolean +} + +export function ProductGrid({ + products, + selectedProducts, + onQuantityChange, + formatCurrency, + loading = false +}: ProductGridProps) { + if (loading) { + return ( +
+
+
+

Загружаем товары...

+
+
+ ) + } + + if (products.length === 0) { + return ( +
+
+ +

У этого оптовика нет товаров

+

Выберите другого оптовика

+
+
+ ) + } + + return ( +
+ {products.map((product) => ( + onQuantityChange(product.id, quantity)} + formatCurrency={formatCurrency} + /> + ))} +
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/tabs-header.tsx b/src/components/supplies/tabs-header.tsx new file mode 100644 index 0000000..eaad07d --- /dev/null +++ b/src/components/supplies/tabs-header.tsx @@ -0,0 +1,85 @@ +"use client" + +import React from 'react' +import { Button } from '@/components/ui/button' +import { + ArrowLeft, + ShoppingCart, + Users +} from 'lucide-react' + +interface TabsHeaderProps { + activeTab: 'cards' | 'wholesaler' + onTabChange: (tab: 'cards' | 'wholesaler') => void + onBack: () => void + cartInfo?: { + itemCount: number + totalAmount: number + formatCurrency: (amount: number) => string + } + onCartClick?: () => void +} + +export function TabsHeader({ + activeTab, + onTabChange, + onBack, + cartInfo, + onCartClick +}: TabsHeaderProps) { + return ( +
+
+ +

Создание поставки

+ + {/* Кнопка корзины */} + {cartInfo && cartInfo.itemCount > 0 && onCartClick && ( + + )} +
+ +
+
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/types.ts b/src/components/supplies/types.ts new file mode 100644 index 0000000..c862f3f --- /dev/null +++ b/src/components/supplies/types.ts @@ -0,0 +1,50 @@ +export interface WholesalerForCreation { + id: string + inn: string + name: string + fullName: string + address: string + phone?: string + email?: string + rating: number + productCount: number + avatar?: string + specialization: string[] +} + +export interface WholesalerProduct { + id: string + name: string + article: string + description: string + price: number + quantity: number + category: string + brand?: string + color?: string + size?: string + weight?: number + dimensions?: string + material?: string + images: string[] + mainImage?: string + discount?: number + isNew?: boolean + isBestseller?: boolean +} + +export interface SelectedProduct extends WholesalerProduct { + selectedQuantity: number + wholesalerId: string + wholesalerName: string +} + +export interface CounterpartyWholesaler { + id: string + inn?: string + name?: string + fullName?: string + address?: string + phones?: { value: string }[] + emails?: { value: string }[] +} \ No newline at end of file diff --git a/src/components/supplies/wb-product-cards.tsx b/src/components/supplies/wb-product-cards.tsx index 3bf2bcb..8596658 100644 --- a/src/components/supplies/wb-product-cards.tsx +++ b/src/components/supplies/wb-product-cards.tsx @@ -8,8 +8,10 @@ import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Calendar as CalendarComponent } from '@/components/ui/calendar' + import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import DatePicker from "react-datepicker" +import "react-datepicker/dist/react-datepicker.css" import { Sidebar } from '@/components/dashboard/sidebar' import { useSidebar } from '@/hooks/useSidebar' import { @@ -17,7 +19,7 @@ import { Plus, Minus, ShoppingCart, - Calendar, + Calendar as CalendarIcon, Phone, User, MapPin, @@ -42,6 +44,8 @@ import { SelectedCard, FulfillmentService, ConsumableService, WildberriesCard } + + interface Organization { id: string name?: string @@ -52,17 +56,29 @@ interface Organization { interface WBProductCardsProps { onBack: () => void onComplete: (selectedCards: SelectedCard[]) => void + showSummary?: boolean + setShowSummary?: (show: boolean) => void + selectedCards?: SelectedCard[] + setSelectedCards?: (cards: SelectedCard[]) => void } -export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { +export function WBProductCards({ onBack, onComplete, showSummary: externalShowSummary, setShowSummary: externalSetShowSummary, selectedCards: externalSelectedCards, setSelectedCards: externalSetSelectedCards }: WBProductCardsProps) { const { user } = useAuth() const { getSidebarMargin } = useSidebar() const [searchTerm, setSearchTerm] = useState('') const [loading, setLoading] = useState(false) const [wbCards, setWbCards] = useState([]) const [selectedCards, setSelectedCards] = useState([]) // Товары в корзине + + // Используем внешнее состояние если передано + const actualSelectedCards = externalSelectedCards !== undefined ? externalSelectedCards : selectedCards + const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards const [preparingCards, setPreparingCards] = useState([]) // Товары, готовящиеся к добавлению const [showSummary, setShowSummary] = useState(false) + + // Используем внешнее состояние если передано + const actualShowSummary = externalShowSummary !== undefined ? externalShowSummary : showSummary + const actualSetShowSummary = externalSetShowSummary || setShowSummary const [globalDeliveryDate, setGlobalDeliveryDate] = useState(undefined) const [fulfillmentServices, setFulfillmentServices] = useState([]) const [organizationServices, setOrganizationServices] = useState<{[orgId: string]: Array<{id: string, name: string, description?: string, price: number}>}>({}) @@ -243,7 +259,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { // Автоматически загружаем услуги и расходники для уже выбранных организаций useEffect(() => { - selectedCards.forEach(sc => { + actualSelectedCards.forEach(sc => { if (sc.selectedFulfillmentOrg && !organizationServices[sc.selectedFulfillmentOrg]) { loadOrganizationServices(sc.selectedFulfillmentOrg) } @@ -541,24 +557,22 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { return } - setSelectedCards(prev => { - const newCards = [...prev] - validCards.forEach(prepCard => { - const cardWithDate = { - ...prepCard, - deliveryDate: globalDeliveryDate.toISOString().split('T')[0] - } - const existingIndex = newCards.findIndex(sc => sc.card.nmID === prepCard.card.nmID) - if (existingIndex >= 0) { - // Обновляем существующий товар - newCards[existingIndex] = cardWithDate - } else { - // Добавляем новый товар - newCards.push(cardWithDate) - } - }) - return newCards + const newCards = [...actualSelectedCards] + validCards.forEach(prepCard => { + const cardWithDate = { + ...prepCard, + deliveryDate: globalDeliveryDate.toISOString().split('T')[0] + } + const existingIndex = newCards.findIndex(sc => sc.card.nmID === prepCard.card.nmID) + if (existingIndex >= 0) { + // Обновляем существующий товар + newCards[existingIndex] = cardWithDate + } else { + // Добавляем новый товар + newCards.push(cardWithDate) + } }) + actualSetSelectedCards(newCards) // Очищаем подготовленные товары setPreparingCards([]) @@ -621,7 +635,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { } const getTotalAmount = () => { - return selectedCards.reduce((sum, sc) => { + return actualSelectedCards.reduce((sum, sc) => { const additionalCostPerUnit = calculateAdditionalCostPerUnit(sc) const totalCostPerUnit = (sc.customPrice / sc.selectedQuantity) + additionalCostPerUnit const totalCostForAllItems = totalCostPerUnit * sc.selectedQuantity @@ -630,7 +644,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { } const getTotalItems = () => { - return selectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0) + return actualSelectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0) } // Функция больше не нужна, так как услуги выбираются индивидуально @@ -667,7 +681,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { try { const supplyInput = { deliveryDate: selectedCards[0]?.deliveryDate || null, - cards: selectedCards.map(sc => ({ + cards: actualSelectedCards.map(sc => ({ nmId: sc.card.nmID.toString(), vendorCode: sc.card.vendorCode, title: sc.card.title, @@ -689,7 +703,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { } } - if (showSummary) { + if (actualShowSummary) { return (
@@ -700,30 +714,115 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
-

Сводка заказа

-

Проверьте данные перед созданием поставки

+

Корзина

+

{actualSelectedCards.length} карточек товаров

- -
-
- {selectedCards.map((sc) => { + {/* Массовое назначение поставщиков */} + +

Быстрое назначение

+
+
+ + +
+ +
+ + +
+ +
+ + + + + + + { + setGlobalDeliveryDate(date || undefined) + if (date) { + const dateString = date.toISOString().split('T')[0] + actualSelectedCards.forEach(sc => { + updateCardSelection(sc.card, 'deliveryDate', dateString) + }) + } + }} + minDate={new Date()} + inline + locale="ru" + /> + + +
+
+
+ +
+
+ {actualSelectedCards.map((sc) => { const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT') const consumableOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT') @@ -962,21 +1061,21 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { })}
-
- +
+

Итого

-
+
Товаров: {getTotalItems()}
-
+
Карточек: - {selectedCards.length} + {actualSelectedCards.length}
Общая сумма: - {formatCurrency(getTotalAmount())} + {formatCurrency(getTotalAmount())}
- )} -
{/* Поиск */} {/* Поиск товаров и выбор даты поставки */} @@ -1030,62 +1117,26 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) {
- + - -
-

Дата поставки

-

Выберите дату для всех товаров

-
- date < new Date()} - initialFocus - locale={ru} - className="glass-card border-0 p-3" - classNames={{ - months: "flex flex-col space-y-3", - month: "space-y-3", - caption: "flex justify-center pt-2 relative items-center", - caption_label: "text-sm font-medium text-white", - nav: "space-x-1 flex items-center", - nav_button: "h-7 w-7 glass-secondary text-white hover:bg-white/20 hover:text-white rounded-md border-white/20", - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: "text-white/70 rounded-md w-9 font-normal text-xs", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-xs p-0 relative focus-within:relative focus-within:z-20", - day: "h-9 w-9 p-0 font-normal text-white hover:bg-white/15 hover:text-white rounded-md transition-colors", - day_selected: "glass-button text-white hover:bg-gradient-to-r hover:from-purple-500 hover:to-pink-500", - day_today: "bg-white/15 text-white font-semibold border border-white/30", - day_outside: "text-white/30 opacity-50", - day_disabled: "text-white/20 opacity-30", - }} - /> - {globalDeliveryDate && ( -
- - - {format(globalDeliveryDate, "dd MMMM yyyy", { locale: ru })} - -
- )} -
+ + setGlobalDeliveryDate(date || undefined)} + minDate={new Date()} + inline + locale="ru" + /> +
@@ -1127,7 +1178,7 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { {wbCards.map((card) => { const selectedQuantity = getSelectedQuantity(card) const isSelected = selectedQuantity > 0 - const selectedCard = selectedCards.find(sc => sc.card.nmID === card.nmID) + const selectedCard = actualSelectedCards.find(sc => sc.card.nmID === card.nmID) return ( @@ -1210,9 +1261,9 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { updateCardSelection(card, 'selectedQuantity', newQuantity) }} disabled={selectedQuantity <= 0} - className="h-7 w-7 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20 disabled:opacity-50" + className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20 disabled:opacity-50 flex-shrink-0" > - + e.target.select()} - className="flex-1 h-7 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - placeholder="Кол-во" + className="flex-1 h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-w-0" + placeholder="0" />
@@ -1253,8 +1304,8 @@ export function WBProductCards({ onBack, onComplete }: WBProductCardsProps) { updateCardSelection(card, 'customPrice', totalPrice) }} onFocus={(e) => e.target.select()} - className="w-full h-7 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent" - placeholder={`Общая цена за ${selectedQuantity} шт`} + className="w-full h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent" + placeholder={`Цена за ${selectedQuantity} шт`} /> )} diff --git a/src/components/supplies/wholesaler-card.tsx b/src/components/supplies/wholesaler-card.tsx new file mode 100644 index 0000000..d79aa26 --- /dev/null +++ b/src/components/supplies/wholesaler-card.tsx @@ -0,0 +1,84 @@ +"use client" + +import React from 'react' +import { Card } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { + Building2, + MapPin, + Phone, + Mail +} from 'lucide-react' +import { WholesalerForCreation } from './types' + +interface WholesalerCardProps { + wholesaler: WholesalerForCreation + onClick: () => void +} + +export function WholesalerCard({ wholesaler, onClick }: WholesalerCardProps) { + return ( + +
+
+
+ +
+
+

+ {wholesaler.name} +

+

+ {wholesaler.fullName} +

+

+ ИНН: {wholesaler.inn} +

+
+
+ +
+
+ + {wholesaler.address} +
+ + {wholesaler.phone && ( +
+ + {wholesaler.phone} +
+ )} + + {wholesaler.email && ( +
+ + {wholesaler.email} +
+ )} +
+ +
+ {wholesaler.specialization.map((spec, index) => ( + + {spec} + + ))} +
+ +
+
+

Товаров: {wholesaler.productCount}

+

Рейтинг: {wholesaler.rating}/5

+
+ + Контрагент + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/wholesaler-grid.tsx b/src/components/supplies/wholesaler-grid.tsx new file mode 100644 index 0000000..9d5a44d --- /dev/null +++ b/src/components/supplies/wholesaler-grid.tsx @@ -0,0 +1,112 @@ +"use client" + +import React from 'react' +import { WholesalerCard } from './wholesaler-card' +import { Input } from '@/components/ui/input' +import { Users, Search } from 'lucide-react' +import { WholesalerForCreation, CounterpartyWholesaler } from './types' + +interface WholesalerGridProps { + wholesalers: CounterpartyWholesaler[] + onWholesalerSelect: (wholesaler: WholesalerForCreation) => void + searchQuery: string + onSearchChange: (query: string) => void + loading?: boolean +} + +export function WholesalerGrid({ + wholesalers, + onWholesalerSelect, + searchQuery, + onSearchChange, + loading = false +}: WholesalerGridProps) { + // Фильтруем оптовиков по поисковому запросу + const filteredWholesalers = wholesalers.filter((wholesaler) => + wholesaler.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + wholesaler.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) || + wholesaler.inn?.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + const handleWholesalerClick = (wholesaler: CounterpartyWholesaler) => { + // Адаптируем данные под существующий интерфейс + const adaptedWholesaler: WholesalerForCreation = { + id: wholesaler.id, + inn: wholesaler.inn || '', + name: wholesaler.name || 'Неизвестная организация', + fullName: wholesaler.fullName || wholesaler.name || 'Неизвестная организация', + address: wholesaler.address || 'Адрес не указан', + phone: wholesaler.phones?.[0]?.value, + email: wholesaler.emails?.[0]?.value, + rating: 4.5, // Временное значение + productCount: 0, // Временное значение + specialization: ['Оптовая торговля'] // Временное значение + } + onWholesalerSelect(adaptedWholesaler) + } + + if (loading) { + return ( +
+
+
+

Загружаем оптовиков...

+
+
+ ) + } + + return ( +
+ {/* Поиск */} +
+
+ + onSearchChange(e.target.value)} + className="pl-10 glass-input text-white placeholder:text-white/40 h-10" + /> +
+
+ + {filteredWholesalers.length === 0 ? ( +
+ +

+ {searchQuery ? 'Оптовики не найдены' : 'У вас нет контрагентов-оптовиков'} +

+

+ {searchQuery ? 'Попробуйте изменить условия поиска' : 'Добавьте оптовиков в разделе "Партнеры"'} +

+
+ ) : ( +
+ {filteredWholesalers.map((wholesaler) => { + const adaptedWholesaler: WholesalerForCreation = { + id: wholesaler.id, + inn: wholesaler.inn || '', + name: wholesaler.name || 'Неизвестная организация', + fullName: wholesaler.fullName || wholesaler.name || 'Неизвестная организация', + address: wholesaler.address || 'Адрес не указан', + phone: wholesaler.phones?.[0]?.value, + email: wholesaler.emails?.[0]?.value, + rating: 4.5, + productCount: 0, + specialization: ['Оптовая торговля'] + } + + return ( + handleWholesalerClick(wholesaler)} + /> + ) + })} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/components/supplies/wholesaler-products-page.tsx b/src/components/supplies/wholesaler-products-page.tsx new file mode 100644 index 0000000..c1e61ce --- /dev/null +++ b/src/components/supplies/wholesaler-products-page.tsx @@ -0,0 +1,134 @@ +"use client" + +import React from 'react' +import { Button } from '@/components/ui/button' +import { ProductGrid } from './product-grid' +import { CartSummary } from './cart-summary' +import { FloatingCart } from './floating-cart' +import { Sidebar } from '@/components/dashboard/sidebar' +import { useSidebar } from '@/hooks/useSidebar' +import { ArrowLeft, Info } from 'lucide-react' +import { WholesalerForCreation, WholesalerProduct, SelectedProduct } from './types' + +interface WholesalerProductsPageProps { + selectedWholesaler: WholesalerForCreation + products: WholesalerProduct[] + selectedProducts: SelectedProduct[] + onQuantityChange: (productId: string, quantity: number) => void + onBack: () => void + onCreateSupply: () => void + formatCurrency: (amount: number) => string + showSummary: boolean + setShowSummary: (show: boolean) => void + loading: boolean +} + +export function WholesalerProductsPage({ + selectedWholesaler, + products, + selectedProducts, + onQuantityChange, + onBack, + onCreateSupply, + formatCurrency, + showSummary, + setShowSummary, + loading +}: WholesalerProductsPageProps) { + const { getSidebarMargin } = useSidebar() + + const getSelectedQuantity = (productId: string): number => { + const selected = selectedProducts.find(p => p.id === productId && p.wholesalerId === selectedWholesaler.id) + return selected ? selected.selectedQuantity : 0 + } + + const selectedProductsMap = products.reduce((acc, product) => { + acc[product.id] = getSelectedQuantity(product.id) + return acc + }, {} as Record) + + const getTotalAmount = () => { + return selectedProducts.reduce((sum, product) => { + const discountedPrice = product.discount + ? product.price * (1 - product.discount / 100) + : product.price + return sum + (discountedPrice * product.selectedQuantity) + }, 0) + } + + const getTotalItems = () => { + return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0) + } + + const handleRemoveProduct = (productId: string, wholesalerId: string) => { + onQuantityChange(productId, 0) + } + + const handleCartQuantityChange = (productId: string, wholesalerId: string, quantity: number) => { + onQuantityChange(productId, quantity) + } + + return ( +
+ +
+
+
+
+ +
+

Товары оптовика

+

{selectedWholesaler.name} • {products.length} товаров

+
+
+
+ +
+
+ + setShowSummary(false)} + formatCurrency={formatCurrency} + visible={showSummary && selectedProducts.length > 0} + /> + + + + setShowSummary(!showSummary)} + visible={selectedProducts.length > 0 && !showSummary} + /> +
+
+
+ ) +} \ No newline at end of file