Refactor: Replace wholesaler with supplier terminology and add fulfillment consumables logic

This commit is contained in:
Veronika Smirnova
2025-07-30 17:03:31 +03:00
parent e351752b09
commit 3e7ea13026
31 changed files with 3343 additions and 1538 deletions

View File

@ -1,26 +1,24 @@
"use client"
"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'
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
selectedProducts: SelectedProduct[];
onQuantityChange: (
productId: string,
supplierId: string,
quantity: number
) => void;
onRemoveProduct: (productId: string, supplierId: string) => void;
onCreateSupply: () => void;
onToggleVisibility: () => void;
formatCurrency: (amount: number) => string;
visible: boolean;
}
export function CartSummary({
@ -30,36 +28,39 @@ export function CartSummary({
onCreateSupply,
onToggleVisibility,
formatCurrency,
visible
visible,
}: CartSummaryProps) {
if (!visible || selectedProducts.length === 0) {
return null
return null;
}
// Группируем товары по поставщикам
const groupedProducts = selectedProducts.reduce((acc, product) => {
if (!acc[product.wholesalerId]) {
acc[product.wholesalerId] = {
wholesaler: product.wholesalerName,
products: []
}
if (!acc[product.supplierId]) {
acc[product.supplierId] = {
supplier: product.supplierName,
products: [],
};
}
acc[product.wholesalerId].products.push(product)
return acc
}, {} as Record<string, { wholesaler: string; products: SelectedProduct[] }>)
acc[product.supplierId].products.push(product);
return acc;
}, {} as Record<string, { supplier: string; products: SelectedProduct[] }>);
const getTotalAmount = () => {
return selectedProducts.reduce((sum, product) => {
const discountedPrice = product.discount
const discountedPrice = product.discount
? product.price * (1 - product.discount / 100)
: product.price
return sum + (discountedPrice * product.selectedQuantity)
}, 0)
}
: product.price;
return sum + discountedPrice * product.selectedQuantity;
}, 0);
};
const getTotalItems = () => {
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
}
return selectedProducts.reduce(
(sum, product) => sum + product.selectedQuantity,
0
);
};
return (
<Card className="bg-gradient-to-br from-purple-500/10 to-pink-500/10 backdrop-blur-xl border border-purple-500/20 mb-6 shadow-2xl">
@ -72,7 +73,8 @@ export function CartSummary({
<div>
<h3 className="text-white font-bold text-lg">Корзина</h3>
<p className="text-purple-200 text-xs">
{selectedProducts.length} товаров от {Object.keys(groupedProducts).length} поставщиков
{selectedProducts.length} товаров от{" "}
{Object.keys(groupedProducts).length} поставщиков
</p>
</div>
</div>
@ -85,74 +87,99 @@ export function CartSummary({
<Eye className="h-4 w-4" />
</Button>
</div>
{/* Группировка по поставщикам */}
{Object.entries(groupedProducts).map(([wholesalerId, group]) => (
<div key={wholesalerId} className="mb-4 last:mb-0">
{Object.entries(groupedProducts).map(([supplierId, group]) => (
<div key={supplierId} className="mb-4 last:mb-0">
<div className="flex items-center mb-2 pb-1 border-b border-white/10">
<Building2 className="h-4 w-4 text-blue-400 mr-2" />
<span className="text-white font-medium">{group.wholesaler}</span>
<span className="text-white font-medium">{group.supplier}</span>
<Badge className="ml-2 bg-blue-500/20 text-blue-300 border-blue-500/30 text-xs">
{group.products.length} товар(ов)
</Badge>
</div>
<div className="space-y-2">
{group.products.map((product) => {
const discountedPrice = product.discount
const discountedPrice = product.discount
? product.price * (1 - product.discount / 100)
: product.price
const totalPrice = discountedPrice * product.selectedQuantity
: product.price;
const totalPrice = discountedPrice * product.selectedQuantity;
return (
<div key={`${product.wholesalerId}-${product.id}`} className="flex items-center space-x-3 bg-white/5 rounded-lg p-3">
<div
key={`${product.supplierId}-${product.id}`}
className="flex items-center space-x-3 bg-white/5 rounded-lg p-3"
>
<img
src={product.mainImage || '/api/placeholder/50/50'}
src={product.mainImage || "/api/placeholder/50/50"}
alt={product.name}
className="w-12 h-12 rounded-lg object-cover"
/>
<div className="flex-1 min-w-0">
<h4 className="text-white font-medium text-xs mb-1 truncate">{product.name}</h4>
<p className="text-white/60 text-xs mb-1">{product.article}</p>
<h4 className="text-white font-medium text-xs mb-1 truncate">
{product.name}
</h4>
<p className="text-white/60 text-xs mb-1">
{product.article}
</p>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => {
const newQuantity = Math.max(0, product.selectedQuantity - 1)
const newQuantity = Math.max(
0,
product.selectedQuantity - 1
);
if (newQuantity === 0) {
onRemoveProduct(product.id, product.wholesalerId)
onRemoveProduct(product.id, product.supplierId);
} else {
onQuantityChange(product.id, product.wholesalerId, newQuantity)
onQuantityChange(
product.id,
product.supplierId,
newQuantity
);
}
}}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Minus className="h-3 w-3" />
</Button>
<span className="text-white text-xs w-6 text-center">{product.selectedQuantity}</span>
<span className="text-white text-xs w-6 text-center">
{product.selectedQuantity}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
onQuantityChange(
product.id,
product.wholesalerId,
Math.min(product.quantity, product.selectedQuantity + 1)
)
product.id,
product.wholesalerId,
Math.min(
product.quantity,
product.selectedQuantity + 1
)
);
}}
disabled={product.selectedQuantity >= product.quantity}
disabled={
product.selectedQuantity >= product.quantity
}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="text-right">
<div className="text-white font-semibold text-xs">{formatCurrency(totalPrice)}</div>
<div className="text-white font-semibold text-xs">
{formatCurrency(totalPrice)}
</div>
{product.discount && (
<div className="text-white/40 text-xs line-through">
{formatCurrency(product.price * product.selectedQuantity)}
{formatCurrency(
product.price * product.selectedQuantity
)}
</div>
)}
</div>
@ -161,18 +188,20 @@ export function CartSummary({
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveProduct(product.id, product.wholesalerId)}
onClick={() =>
onRemoveProduct(product.id, product.supplierId)
}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
</Button>
</div>
)
);
})}
</div>
</div>
))}
{/* Итого */}
<div className="border-t border-white/20 pt-3 mt-4">
<div className="flex justify-between items-center">
@ -184,7 +213,7 @@ export function CartSummary({
</span>
</div>
<div className="flex space-x-2 mt-3">
<Button
<Button
variant="outline"
className="flex-1 border-purple-300/30 text-white hover:bg-white/10"
onClick={onToggleVisibility}
@ -192,7 +221,7 @@ export function CartSummary({
<Plus className="h-4 w-4 mr-2" />
Добавить еще
</Button>
<Button
<Button
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
onClick={onCreateSupply}
>
@ -203,5 +232,5 @@ export function CartSummary({
</div>
</div>
</Card>
)
}
);
}

View File

@ -1343,13 +1343,13 @@ export function DirectSupplyCreation({
Цена
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Услуги фф
Услуги фулфилмента
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Поставщик
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Расходники фф
Расходники фулфилмента
</div>
<div className="text-white/80 text-[9px] font-medium text-center">
Расходники
@ -1654,7 +1654,7 @@ export function DirectSupplyCreation({
</div>
</div>
{/* Блок 7: Расходники фф */}
{/* Блок 7: Расходники фулфилмента */}
<div className="bg-white/10 rounded-lg p-2 flex flex-col justify-center h-20">
<div className="space-y-1 max-h-16 overflow-y-auto">
{/* DEBUG для расходников */}

View File

@ -21,7 +21,7 @@ import {
Tags,
} from "lucide-react";
// Типы данных для расходников ФФ
// Типы данных для расходников фулфилмента
interface ConsumableParameter {
id: string;
name: string;
@ -79,7 +79,7 @@ interface FulfillmentConsumableSupply {
status: "planned" | "in-transit" | "delivered" | "completed";
}
// Моковые данные для расходников ФФ
// Моковые данные для расходников фулфилмента
const mockFulfillmentConsumables: FulfillmentConsumableSupply[] = [
{
id: "ffc1",
@ -116,7 +116,7 @@ const mockFulfillmentConsumables: FulfillmentConsumableSupply[] = [
id: "ffcons1",
name: "Коробки для ФФ 40x30x15",
sku: "BOX-FF-403015",
category: "Расходники ФФ",
category: "Расходники фулфилмента",
type: "packaging",
plannedQty: 2000,
actualQty: 1980,
@ -269,10 +269,10 @@ export function FulfillmentSuppliesTab() {
return (
<div className="space-y-6">
{/* Статистика расходников ФФ */}
{/* Статистика расходников фулфилмента */}
<StatsGrid>
<StatsCard
title="Расходники ФФ"
title="Расходники фулфилмента"
value={mockFulfillmentConsumables.length}
icon={Package2}
iconColor="text-orange-400"
@ -282,7 +282,7 @@ export function FulfillmentSuppliesTab() {
/>
<StatsCard
title="Сумма расходников ФФ"
title="Сумма расходников фулфилмента"
value={formatCurrency(
mockFulfillmentConsumables.reduce(
(sum, supply) => sum + supply.grandTotal,
@ -324,7 +324,7 @@ export function FulfillmentSuppliesTab() {
/>
</StatsGrid>
{/* Таблица поставок расходников ФФ */}
{/* Таблица поставок расходников фулфилмента */}
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
@ -359,7 +359,7 @@ export function FulfillmentSuppliesTab() {
return (
<React.Fragment key={supply.id}>
{/* Основная строка поставки расходников ФФ */}
{/* Основная строка поставки расходников фулфилмента */}
<tr
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-orange-500/10 cursor-pointer"
onClick={() => toggleSupplyExpansion(supply.id)}

View File

@ -171,6 +171,7 @@ export function RealSupplyOrdersTab() {
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
fetchPolicy: "cache-and-network",
notifyOnNetworkStatusChange: true,
pollInterval: 30000, // 🔔 Опрашиваем каждые 30 секунд для получения новых заказов
});
// Мутация для обновления статуса заказа
@ -189,15 +190,22 @@ export function RealSupplyOrdersTab() {
toast.error("Ошибка при обновлении статуса заказа");
},
update: (cache, { data }) => {
if (data?.updateSupplyOrderStatus?.success && data?.updateSupplyOrderStatus?.order) {
console.log(`✅ Обновляем кэш для заказа ${data.updateSupplyOrderStatus.order.id} на статус ${data.updateSupplyOrderStatus.order.status}`);
if (
data?.updateSupplyOrderStatus?.success &&
data?.updateSupplyOrderStatus?.order
) {
console.log(
`✅ Обновляем кэш для заказа ${data.updateSupplyOrderStatus.order.id} на статус ${data.updateSupplyOrderStatus.order.status}`
);
// Точечно обновляем кэш для конкретного заказа
cache.modify({
id: cache.identify(data.updateSupplyOrderStatus.order),
fields: {
status() {
console.log(`📝 Обновляем поле status для заказа ${data.updateSupplyOrderStatus.order.id}`);
console.log(
`📝 Обновляем поле status для заказа ${data.updateSupplyOrderStatus.order.id}`
);
return data.updateSupplyOrderStatus.order.status;
},
},
@ -205,9 +213,13 @@ export function RealSupplyOrdersTab() {
// Также обновляем данные в запросе GET_SUPPLY_ORDERS если нужно
try {
const existingData = cache.readQuery({ query: GET_SUPPLY_ORDERS }) as any;
const existingData = cache.readQuery({
query: GET_SUPPLY_ORDERS,
}) as any;
if (existingData?.supplyOrders) {
console.log(`📋 Обновляем список заказов в кэше, всего заказов: ${existingData.supplyOrders.length}`);
console.log(
`📋 Обновляем список заказов в кэше, всего заказов: ${existingData.supplyOrders.length}`
);
cache.writeQuery({
query: GET_SUPPLY_ORDERS,
data: {
@ -215,7 +227,10 @@ export function RealSupplyOrdersTab() {
supplyOrders: existingData.supplyOrders.map((order: any) => {
if (order.id === data.updateSupplyOrderStatus.order.id) {
console.log(`🎯 Найден и обновлен заказ ${order.id}`);
return { ...order, status: data.updateSupplyOrderStatus.order.status };
return {
...order,
status: data.updateSupplyOrderStatus.order.status,
};
}
return order;
}),
@ -243,11 +258,16 @@ export function RealSupplyOrdersTab() {
// Отладочное логирование для проверки дублирующихся ID
React.useEffect(() => {
if (incomingSupplyOrders.length > 0) {
const ids = incomingSupplyOrders.map(order => order.id);
const ids = incomingSupplyOrders.map((order) => order.id);
const uniqueIds = new Set(ids);
if (ids.length !== uniqueIds.size) {
console.warn(`⚠️ Обнаружены дублирующиеся ID заказов! Всего: ${ids.length}, уникальных: ${uniqueIds.size}`);
console.warn('Дублирующиеся ID:', ids.filter((id, index) => ids.indexOf(id) !== index));
console.warn(
`⚠️ Обнаружены дублирующиеся ID заказов! Всего: ${ids.length}, уникальных: ${uniqueIds.size}`
);
console.warn(
"Дублирующиеся ID:",
ids.filter((id, index) => ids.indexOf(id) !== index)
);
} else {
console.log(`Все ID заказов уникальны: ${ids.length} заказов`);
}
@ -276,7 +296,7 @@ export function RealSupplyOrdersTab() {
const handleStatusUpdate = async (orderId: string, status: string) => {
console.log(`🔄 Обновляем статус заказа ${orderId} на ${status}`);
try {
await updateSupplyOrderStatus({
variables: {

View File

@ -36,7 +36,7 @@ interface Product {
parameters: ProductParameter[];
}
interface Wholesaler {
interface Supplier {
id: string;
name: string;
inn: string;

View File

@ -1,24 +1,19 @@
"use client"
"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'
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 { SupplierForCreation } from "./types";
interface WholesalerCardProps {
wholesaler: WholesalerForCreation
onClick: () => void
interface SupplierCardProps {
supplier: SupplierForCreation;
onClick: () => void;
}
export function WholesalerCard({ wholesaler, onClick }: WholesalerCardProps) {
export function SupplierCard({ supplier, onClick }: SupplierCardProps) {
return (
<Card
<Card
className="bg-white/10 backdrop-blur border-white/20 p-4 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-[1.02]"
onClick={onClick}
>
@ -29,41 +24,46 @@ export function WholesalerCard({ wholesaler, onClick }: WholesalerCardProps) {
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-sm mb-1 truncate">
{wholesaler.name}
{supplier.name}
</h3>
<p className="text-white/60 text-xs mb-1 truncate">
{wholesaler.fullName}
</p>
<p className="text-white/40 text-xs">
ИНН: {wholesaler.inn}
{supplier.fullName}
</p>
<p className="text-white/40 text-xs">ИНН: {supplier.inn}</p>
</div>
</div>
<div className="space-y-1">
<div className="flex items-center space-x-1">
<MapPin className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">{wholesaler.address}</span>
<span className="text-white/80 text-xs truncate">
{supplier.address}
</span>
</div>
{wholesaler.phone && (
{supplier.phone && (
<div className="flex items-center space-x-1">
<Phone className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs">{wholesaler.phone}</span>
<span className="text-white/80 text-xs">{supplier.phone}</span>
</div>
)}
{wholesaler.email && (
{supplier.email && (
<div className="flex items-center space-x-1">
<Mail className="h-3 w-3 text-gray-400" />
<span className="text-white/80 text-xs truncate">{wholesaler.email}</span>
<span className="text-white/80 text-xs truncate">
{supplier.email}
</span>
</div>
)}
</div>
<div className="flex flex-wrap gap-1">
{wholesaler.specialization.map((spec, index) => (
<Badge key={index} className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs">
{supplier.specialization.map((spec, index) => (
<Badge
key={index}
className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs"
>
{spec}
</Badge>
))}
@ -71,8 +71,12 @@ export function WholesalerCard({ wholesaler, onClick }: WholesalerCardProps) {
<div className="pt-2 border-t border-white/10 flex items-center justify-between">
<div>
<p className="text-white/60 text-xs">Товаров: {wholesaler.productCount}</p>
<p className="text-white/60 text-xs">Рейтинг: {wholesaler.rating}/5</p>
<p className="text-white/60 text-xs">
Товаров: {supplier.productCount}
</p>
<p className="text-white/60 text-xs">
Рейтинг: {supplier.rating}/5
</p>
</div>
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 text-xs">
Контрагент
@ -80,5 +84,5 @@ export function WholesalerCard({ wholesaler, onClick }: WholesalerCardProps) {
</div>
</div>
</Card>
)
}
);
}

View File

@ -0,0 +1,118 @@
"use client";
import React from "react";
import { SupplierCard } from "./supplier-card";
import { Input } from "@/components/ui/input";
import { Users, Search } from "lucide-react";
import { SupplierForCreation, CounterpartySupplier } from "./types";
interface SupplierGridProps {
suppliers: CounterpartySupplier[];
onSupplierSelect: (supplier: SupplierForCreation) => void;
searchQuery: string;
onSearchChange: (query: string) => void;
loading?: boolean;
}
export function SupplierGrid({
suppliers,
onSupplierSelect,
searchQuery,
onSearchChange,
loading = false,
}: SupplierGridProps) {
// Фильтруем поставщиков по поисковому запросу
const filteredSuppliers = suppliers.filter(
(supplier) =>
supplier.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
supplier.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
supplier.inn?.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleSupplierClick = (supplier: CounterpartySupplier) => {
// Адаптируем данные под существующий интерфейс
const adaptedSupplier: SupplierForCreation = {
id: supplier.id,
inn: supplier.inn || "",
name: supplier.name || "Неизвестная организация",
fullName: supplier.fullName || supplier.name || "Неизвестная организация",
address: supplier.address || "Адрес не указан",
phone: supplier.phones?.[0]?.value,
email: supplier.emails?.[0]?.value,
rating: 4.5, // Временное значение
productCount: 0, // Временное значение
specialization: ["Оптовая торговля"], // Временное значение
};
onSupplierSelect(adaptedSupplier);
};
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent mx-auto mb-4"></div>
<p className="text-white/60">Загружаем поставщиков...</p>
</div>
</div>
);
}
return (
<div>
{/* Поиск */}
<div className="mb-4">
<div className="relative max-w-md">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск поставщиков..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
{filteredSuppliers.length === 0 ? (
<div className="text-center p-8">
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchQuery
? "Поставщики не найдены"
: "У вас нет контрагентов-поставщиков"}
</p>
<p className="text-white/40 text-sm mt-2">
{searchQuery
? "Попробуйте изменить условия поиска"
: 'Добавьте поставщиков в разделе "Партнеры"'}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredSuppliers.map((supplier) => {
const adaptedSupplier: SupplierForCreation = {
id: supplier.id,
inn: supplier.inn || "",
name: supplier.name || "Неизвестная организация",
fullName:
supplier.fullName || supplier.name || "Неизвестная организация",
address: supplier.address || "Адрес не указан",
phone: supplier.phones?.[0]?.value,
email: supplier.emails?.[0]?.value,
rating: 4.5,
productCount: 0,
specialization: ["Оптовая торговля"],
};
return (
<SupplierCard
key={supplier.id}
supplier={adaptedSupplier}
onClick={() => handleSupplierClick(supplier)}
/>
);
})}
</div>
)}
</div>
);
}

View File

@ -1,30 +1,30 @@
"use client"
"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'
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 { SupplierForCreation, SupplierProduct, 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
interface SupplierProductsPageProps {
selectedSupplier: SupplierForCreation;
products: SupplierProduct[];
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,
export function SupplierProductsPage({
selectedSupplier,
products,
selectedProducts,
onQuantityChange,
@ -33,50 +33,61 @@ export function WholesalerProductsPage({
formatCurrency,
showSummary,
setShowSummary,
loading
}: WholesalerProductsPageProps) {
const { getSidebarMargin } = useSidebar()
loading,
}: SupplierProductsPageProps) {
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 selected = selectedProducts.find(
(p) => p.id === productId && p.supplierId === selectedSupplier.id
);
return selected ? selected.selectedQuantity : 0;
};
const selectedProductsMap = products.reduce((acc, product) => {
acc[product.id] = getSelectedQuantity(product.id)
return acc
}, {} as Record<string, number>)
acc[product.id] = getSelectedQuantity(product.id);
return acc;
}, {} as Record<string, number>);
const getTotalAmount = () => {
return selectedProducts.reduce((sum, product) => {
const discountedPrice = product.discount
const discountedPrice = product.discount
? product.price * (1 - product.discount / 100)
: product.price
return sum + (discountedPrice * product.selectedQuantity)
}, 0)
}
: product.price;
return sum + discountedPrice * product.selectedQuantity;
}, 0);
};
const getTotalItems = () => {
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
}
return selectedProducts.reduce(
(sum, product) => sum + product.selectedQuantity,
0
);
};
const handleRemoveProduct = (productId: string, wholesalerId: string) => {
onQuantityChange(productId, 0)
}
const handleRemoveProduct = (productId: string, supplierId: string) => {
onQuantityChange(productId, 0);
};
const handleCartQuantityChange = (productId: string, wholesalerId: string, quantity: number) => {
onQuantityChange(productId, quantity)
}
const handleCartQuantityChange = (
productId: string,
supplierId: string,
quantity: number
) => {
onQuantityChange(productId, quantity);
};
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}>
<main
className={`flex-1 ${getSidebarMargin()} px-6 py-4 overflow-hidden transition-all duration-300`}
>
<div className="p-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -85,13 +96,17 @@ export function WholesalerProductsPage({
Назад
</Button>
<div>
<h1 className="text-3xl font-bold text-white mb-2">Товары поставщика</h1>
<p className="text-white/60">{selectedWholesaler.name} {products.length} товаров</p>
<h1 className="text-3xl font-bold text-white mb-2">
Товары поставщика
</h1>
<p className="text-white/60">
{selectedSupplier.name} {products.length} товаров
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={() => setShowSummary(!showSummary)}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -130,5 +145,5 @@ export function WholesalerProductsPage({
</div>
</main>
</div>
)
}
);
}

View File

@ -1,230 +1,241 @@
"use client"
"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 {
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 {
ArrowLeft,
Package,
Plus,
Minus,
ShoppingCart,
Eye,
Info
} from 'lucide-react'
import Image from 'next/image'
Info,
} from "lucide-react";
import Image from "next/image";
interface Wholesaler {
id: string
inn: string
name: string
fullName: string
address: string
phone?: string
email?: string
rating: number
productCount: number
avatar?: string
specialization: string[]
interface Supplier {
id: string;
inn: string;
name: string;
fullName: string;
address: string;
phone?: string;
email?: string;
rating: number;
productCount: number;
avatar?: string;
specialization: string[];
}
interface Product {
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
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;
}
interface SelectedProduct extends Product {
selectedQuantity: number
selectedQuantity: number;
}
interface WholesalerProductsProps {
wholesaler: Wholesaler
onBack: () => void
onClose: () => void
onSupplyCreated: () => void
interface SupplierProductsProps {
supplier: Supplier;
onBack: () => void;
onClose: () => void;
onSupplyCreated: () => void;
}
// Моковые данные товаров
const mockProducts: Product[] = [
{
id: '1',
name: 'Смартфон Samsung Galaxy A54',
article: 'SGX-A54-128',
id: "1",
name: "Смартфон Samsung Galaxy A54",
article: "SGX-A54-128",
description: 'Смартфон с экраном 6.4", камерой 50 МП, 128 ГБ памяти',
price: 28900,
quantity: 150,
category: 'Смартфоны',
brand: 'Samsung',
color: 'Черный',
category: "Смартфоны",
brand: "Samsung",
color: "Черный",
size: '6.4"',
weight: 202,
dimensions: '158.2 x 76.7 x 8.2 мм',
material: 'Алюминий, стекло',
images: ['/api/placeholder/300/300?text=Samsung+A54'],
mainImage: '/api/placeholder/300/300?text=Samsung+A54'
dimensions: "158.2 x 76.7 x 8.2 мм",
material: "Алюминий, стекло",
images: ["/api/placeholder/300/300?text=Samsung+A54"],
mainImage: "/api/placeholder/300/300?text=Samsung+A54",
},
{
id: '2',
name: 'Наушники Sony WH-1000XM4',
article: 'SNY-WH1000XM4',
description: 'Беспроводные наушники с шумоподавлением',
id: "2",
name: "Наушники Sony WH-1000XM4",
article: "SNY-WH1000XM4",
description: "Беспроводные наушники с шумоподавлением",
price: 24900,
quantity: 85,
category: 'Наушники',
brand: 'Sony',
color: 'Черный',
category: "Наушники",
brand: "Sony",
color: "Черный",
weight: 254,
material: 'Пластик, кожа',
images: ['/api/placeholder/300/300?text=Sony+WH1000XM4'],
mainImage: '/api/placeholder/300/300?text=Sony+WH1000XM4'
material: "Пластик, кожа",
images: ["/api/placeholder/300/300?text=Sony+WH1000XM4"],
mainImage: "/api/placeholder/300/300?text=Sony+WH1000XM4",
},
{
id: '3',
id: "3",
name: 'Планшет iPad Air 10.9"',
article: 'APL-IPADAIR-64',
description: 'Планшет Apple iPad Air с чипом M1, 64 ГБ',
article: "APL-IPADAIR-64",
description: "Планшет Apple iPad Air с чипом M1, 64 ГБ",
price: 54900,
quantity: 45,
category: 'Планшеты',
brand: 'Apple',
color: 'Серый космос',
category: "Планшеты",
brand: "Apple",
color: "Серый космос",
size: '10.9"',
weight: 461,
dimensions: '247.6 x 178.5 x 6.1 мм',
material: 'Алюминий',
images: ['/api/placeholder/300/300?text=iPad+Air'],
mainImage: '/api/placeholder/300/300?text=iPad+Air'
dimensions: "247.6 x 178.5 x 6.1 мм",
material: "Алюминий",
images: ["/api/placeholder/300/300?text=iPad+Air"],
mainImage: "/api/placeholder/300/300?text=iPad+Air",
},
{
id: '4',
name: 'Ноутбук Lenovo ThinkPad E15',
article: 'LNV-TE15-I5',
id: "4",
name: "Ноутбук Lenovo ThinkPad E15",
article: "LNV-TE15-I5",
description: 'Ноутбук 15.6" Intel Core i5, 8 ГБ ОЗУ, 256 ГБ SSD',
price: 45900,
quantity: 25,
category: 'Ноутбуки',
brand: 'Lenovo',
color: 'Черный',
category: "Ноутбуки",
brand: "Lenovo",
color: "Черный",
size: '15.6"',
weight: 1700,
dimensions: '365 x 240 x 19.9 мм',
material: 'Пластик',
images: ['/api/placeholder/300/300?text=ThinkPad+E15'],
mainImage: '/api/placeholder/300/300?text=ThinkPad+E15'
dimensions: "365 x 240 x 19.9 мм",
material: "Пластик",
images: ["/api/placeholder/300/300?text=ThinkPad+E15"],
mainImage: "/api/placeholder/300/300?text=ThinkPad+E15",
},
{
id: '5',
name: 'Умные часы Apple Watch SE',
article: 'APL-AWSE-40',
description: 'Умные часы Apple Watch SE 40 мм',
id: "5",
name: "Умные часы Apple Watch SE",
article: "APL-AWSE-40",
description: "Умные часы Apple Watch SE 40 мм",
price: 21900,
quantity: 120,
category: 'Умные часы',
brand: 'Apple',
color: 'Белый',
size: '40 мм',
category: "Умные часы",
brand: "Apple",
color: "Белый",
size: "40 мм",
weight: 30,
dimensions: '40 x 34 x 10.7 мм',
material: 'Алюминий',
images: ['/api/placeholder/300/300?text=Apple+Watch+SE'],
mainImage: '/api/placeholder/300/300?text=Apple+Watch+SE'
dimensions: "40 x 34 x 10.7 мм",
material: "Алюминий",
images: ["/api/placeholder/300/300?text=Apple+Watch+SE"],
mainImage: "/api/placeholder/300/300?text=Apple+Watch+SE",
},
{
id: '6',
name: 'Клавиатура Logitech MX Keys',
article: 'LGT-MXKEYS',
description: 'Беспроводная клавиатура для продуктивной работы',
id: "6",
name: "Клавиатура Logitech MX Keys",
article: "LGT-MXKEYS",
description: "Беспроводная клавиатура для продуктивной работы",
price: 8900,
quantity: 75,
category: 'Клавиатуры',
brand: 'Logitech',
color: 'Графит',
category: "Клавиатуры",
brand: "Logitech",
color: "Графит",
weight: 810,
dimensions: '430.2 x 20.5 x 131.6 мм',
material: 'Пластик, металл',
images: ['/api/placeholder/300/300?text=MX+Keys'],
mainImage: '/api/placeholder/300/300?text=MX+Keys'
}
]
dimensions: "430.2 x 20.5 x 131.6 мм",
material: "Пластик, металл",
images: ["/api/placeholder/300/300?text=MX+Keys"],
mainImage: "/api/placeholder/300/300?text=MX+Keys",
},
];
export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreated }: WholesalerProductsProps) {
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>([])
const [showSummary, setShowSummary] = useState(false)
export function SupplierProducts({
supplier,
onBack,
onClose,
onSupplyCreated,
}: SupplierProductsProps) {
const [selectedProducts, setSelectedProducts] = useState<SelectedProduct[]>(
[]
);
const [showSummary, setShowSummary] = useState(false);
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(amount)
}
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
}).format(amount);
};
const updateProductQuantity = (productId: string, quantity: number) => {
const product = mockProducts.find(p => p.id === productId)
if (!product) return
const product = mockProducts.find((p) => p.id === productId);
if (!product) return;
setSelectedProducts((prev) => {
const existing = prev.find((p) => p.id === productId);
setSelectedProducts(prev => {
const existing = prev.find(p => p.id === productId)
if (quantity === 0) {
// Удаляем продукт если количество 0
return prev.filter(p => p.id !== productId)
return prev.filter((p) => p.id !== productId);
}
if (existing) {
// Обновляем количество существующего продукта
return prev.map(p =>
return prev.map((p) =>
p.id === productId ? { ...p, selectedQuantity: quantity } : p
)
);
} else {
// Добавляем новый продукт
return [...prev, { ...product, selectedQuantity: quantity }]
return [...prev, { ...product, selectedQuantity: quantity }];
}
})
}
});
};
const getSelectedQuantity = (productId: string): number => {
const selected = selectedProducts.find(p => p.id === productId)
return selected ? selected.selectedQuantity : 0
}
const selected = selectedProducts.find((p) => p.id === productId);
return selected ? selected.selectedQuantity : 0;
};
const getTotalAmount = () => {
return selectedProducts.reduce((sum, product) =>
sum + (product.price * product.selectedQuantity), 0
)
}
return selectedProducts.reduce(
(sum, product) => sum + product.price * product.selectedQuantity,
0
);
};
const getTotalItems = () => {
return selectedProducts.reduce((sum, product) => sum + product.selectedQuantity, 0)
}
return selectedProducts.reduce(
(sum, product) => sum + product.selectedQuantity,
0
);
};
const handleCreateSupply = () => {
console.log('Создание поставки с товарами:', selectedProducts)
console.log("Создание поставки с товарами:", selectedProducts);
// TODO: Здесь будет реальное создание поставки
onSupplyCreated()
}
onSupplyCreated();
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -233,13 +244,17 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
Назад
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Товары поставщика</h2>
<p className="text-white/60">{wholesaler.name} {mockProducts.length} товаров</p>
<h2 className="text-2xl font-bold text-white mb-1">
Товары поставщика
</h2>
<p className="text-white/60">
{supplier.name} {mockProducts.length} товаров
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={() => setShowSummary(!showSummary)}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -247,8 +262,8 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
<Info className="h-4 w-4 mr-2" />
Резюме ({selectedProducts.length})
</Button>
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -260,13 +275,20 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
{showSummary && selectedProducts.length > 0 && (
<Card className="bg-purple-500/10 backdrop-blur border-purple-500/30 p-6">
<h3 className="text-white font-semibold text-lg mb-4">Резюме заказа</h3>
<h3 className="text-white font-semibold text-lg mb-4">
Резюме заказа
</h3>
<div className="space-y-3">
{selectedProducts.map((product) => (
<div key={product.id} className="flex justify-between items-center">
<div
key={product.id}
className="flex justify-between items-center"
>
<div>
<span className="text-white">{product.name}</span>
<span className="text-white/60 text-sm ml-2">× {product.selectedQuantity}</span>
<span className="text-white/60 text-sm ml-2">
× {product.selectedQuantity}
</span>
</div>
<span className="text-white font-medium">
{formatCurrency(product.price * product.selectedQuantity)}
@ -281,7 +303,7 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
{formatCurrency(getTotalAmount())}
</span>
</div>
<Button
<Button
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white"
onClick={handleCreateSupply}
disabled={selectedProducts.length === 0}
@ -295,13 +317,16 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockProducts.map((product) => {
const selectedQuantity = getSelectedQuantity(product.id)
const selectedQuantity = getSelectedQuantity(product.id);
return (
<Card key={product.id} className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
<Card
key={product.id}
className="bg-white/10 backdrop-blur border-white/20 overflow-hidden"
>
<div className="aspect-square relative bg-white/5">
<Image
src={product.mainImage || '/api/placeholder/300/300'}
src={product.mainImage || "/api/placeholder/300/300"}
alt={product.name}
fill
className="object-cover"
@ -312,7 +337,7 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
</Badge>
</div>
</div>
<div className="p-4 space-y-3">
<div>
<h3 className="text-white font-semibold mb-1 line-clamp-2">
@ -350,7 +375,8 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
)}
{product.weight && (
<div className="text-white/60 text-xs">
Вес: <span className="text-white">{product.weight} г</span>
Вес:{" "}
<span className="text-white">{product.weight} г</span>
</div>
)}
</div>
@ -368,7 +394,12 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
<Button
variant="ghost"
size="sm"
onClick={() => updateProductQuantity(product.id, Math.max(0, selectedQuantity - 1))}
onClick={() =>
updateProductQuantity(
product.id,
Math.max(0, selectedQuantity - 1)
)
}
disabled={selectedQuantity === 0}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
@ -378,8 +409,14 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
type="number"
value={selectedQuantity}
onChange={(e) => {
const value = Math.max(0, Math.min(product.quantity, parseInt(e.target.value) || 0))
updateProductQuantity(product.id, value)
const value = Math.max(
0,
Math.min(
product.quantity,
parseInt(e.target.value) || 0
)
);
updateProductQuantity(product.id, value);
}}
className="h-8 w-16 text-center bg-white/10 border-white/20 text-white"
min={0}
@ -388,7 +425,12 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
<Button
variant="ghost"
size="sm"
onClick={() => updateProductQuantity(product.id, Math.min(product.quantity, selectedQuantity + 1))}
onClick={() =>
updateProductQuantity(
product.id,
Math.min(product.quantity, selectedQuantity + 1)
)
}
disabled={selectedQuantity >= product.quantity}
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
>
@ -405,21 +447,22 @@ export function WholesalerProducts({ wholesaler, onBack, onClose, onSupplyCreate
)}
</div>
</Card>
)
);
})}
</div>
{selectedProducts.length > 0 && (
<div className="fixed bottom-6 right-6">
<Button
<Button
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-lg"
onClick={() => setShowSummary(!showSummary)}
>
<ShoppingCart className="h-4 w-4 mr-2" />
Корзина ({selectedProducts.length}) {formatCurrency(getTotalAmount())}
Корзина ({selectedProducts.length}) {" "}
{formatCurrency(getTotalAmount())}
</Button>
</div>
)}
</div>
)
}
);
}

View File

@ -1,136 +1,144 @@
"use client"
"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 {
import React, { useState } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
ArrowLeft,
Building2,
MapPin,
Phone,
Mail,
Package,
Star
} from 'lucide-react'
// import { WholesalerProducts } from './wholesaler-products'
Star,
} from "lucide-react";
// import { SupplierProducts } from './supplier-products'
interface Wholesaler {
id: string
inn: string
name: string
fullName: string
address: string
phone?: string
email?: string
rating: number
productCount: number
avatar?: string
specialization: string[]
interface Supplier {
id: string;
inn: string;
name: string;
fullName: string;
address: string;
phone?: string;
email?: string;
rating: number;
productCount: number;
avatar?: string;
specialization: string[];
}
interface WholesalerSelectionProps {
onBack: () => void
onClose: () => void
onSupplyCreated: () => void
interface SupplierSelectionProps {
onBack: () => void;
onClose: () => void;
onSupplyCreated: () => void;
}
// Моковые данные поставщиков
const mockWholesalers: Wholesaler[] = [
const mockSuppliers: Supplier[] = [
{
id: '1',
inn: '7707083893',
name: 'ОПТ-Электроника',
id: "1",
inn: "7707083893",
name: "ОПТ-Электроника",
fullName: 'ООО "ОПТ-Электроника"',
address: 'г. Москва, ул. Садовая, д. 15',
phone: '+7 (495) 123-45-67',
email: 'opt@electronics.ru',
address: "г. Москва, ул. Садовая, д. 15",
phone: "+7 (495) 123-45-67",
email: "opt@electronics.ru",
rating: 4.8,
productCount: 1250,
specialization: ['Электроника', 'Бытовая техника']
specialization: ["Электроника", "Бытовая техника"],
},
{
id: '2',
inn: '7707083894',
name: 'ТекстильМастер',
id: "2",
inn: "7707083894",
name: "ТекстильМастер",
fullName: 'ООО "ТекстильМастер"',
address: 'г. Иваново, пр. Ленина, д. 42',
phone: '+7 (4932) 55-66-77',
email: 'sales@textilmaster.ru',
address: "г. Иваново, пр. Ленина, д. 42",
phone: "+7 (4932) 55-66-77",
email: "sales@textilmaster.ru",
rating: 4.6,
productCount: 850,
specialization: ['Текстиль', 'Одежда', 'Домашний текстиль']
specialization: ["Текстиль", "Одежда", "Домашний текстиль"],
},
{
id: '3',
inn: '7707083895',
name: 'МетизКомплект',
id: "3",
inn: "7707083895",
name: "МетизКомплект",
fullName: 'ООО "МетизКомплект"',
address: 'г. Тула, ул. Металлургов, д. 8',
phone: '+7 (4872) 33-44-55',
email: 'info@metiz.ru',
address: "г. Тула, ул. Металлургов, д. 8",
phone: "+7 (4872) 33-44-55",
email: "info@metiz.ru",
rating: 4.9,
productCount: 2100,
specialization: ['Крепеж', 'Метизы', 'Инструменты']
specialization: ["Крепеж", "Метизы", "Инструменты"],
},
{
id: '4',
inn: '7707083896',
name: 'ПродОпт',
id: "4",
inn: "7707083896",
name: "ПродОпт",
fullName: 'ООО "ПродОпт"',
address: 'г. Краснодар, ул. Красная, д. 123',
phone: '+7 (861) 777-88-99',
email: 'order@prodopt.ru',
address: "г. Краснодар, ул. Красная, д. 123",
phone: "+7 (861) 777-88-99",
email: "order@prodopt.ru",
rating: 4.7,
productCount: 560,
specialization: ['Продукты питания', 'Напитки']
specialization: ["Продукты питания", "Напитки"],
},
{
id: '5',
inn: '7707083897',
name: 'СтройМатериалы+',
id: "5",
inn: "7707083897",
name: "СтройМатериалы+",
fullName: 'ООО "СтройМатериалы+"',
address: 'г. Воронеж, пр. Революции, д. 67',
phone: '+7 (473) 222-33-44',
email: 'stroim@materials.ru',
address: "г. Воронеж, пр. Революции, д. 67",
phone: "+7 (473) 222-33-44",
email: "stroim@materials.ru",
rating: 4.5,
productCount: 1800,
specialization: ['Стройматериалы', 'Сантехника']
specialization: ["Стройматериалы", "Сантехника"],
},
{
id: '6',
inn: '7707083898',
name: 'КосметикОпт',
id: "6",
inn: "7707083898",
name: "КосметикОпт",
fullName: 'ООО "КосметикОпт"',
address: 'г. Санкт-Петербург, Невский пр., д. 45',
phone: '+7 (812) 111-22-33',
email: 'beauty@cosmeticopt.ru',
address: "г. Санкт-Петербург, Невский пр., д. 45",
phone: "+7 (812) 111-22-33",
email: "beauty@cosmeticopt.ru",
rating: 4.4,
productCount: 920,
specialization: ['Косметика', 'Парфюмерия', 'Уход']
}
]
specialization: ["Косметика", "Парфюмерия", "Уход"],
},
];
export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: WholesalerSelectionProps) {
const [selectedWholesaler, setSelectedWholesaler] = useState<Wholesaler | null>(null)
export function SupplierSelection({
onBack,
onClose,
onSupplyCreated,
}: SupplierSelectionProps) {
const [selectedSupplier, setSelectedSupplier] = useState<Supplier | null>(
null
);
if (selectedWholesaler) {
if (selectedSupplier) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedWholesaler(null)}
onClick={() => setSelectedSupplier(null)}
className="text-white/60 hover:text-white hover:bg-white/10"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Назад
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Товары поставщика</h2>
<p className="text-white/60">{selectedWholesaler.name}</p>
<h2 className="text-2xl font-bold text-white mb-1">
Товары поставщика
</h2>
<p className="text-white/60">{selectedSupplier.name}</p>
</div>
</div>
</div>
@ -138,24 +146,28 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
<p className="text-white/60">Компонент товаров в разработке...</p>
</div>
</div>
)
);
}
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`h-4 w-4 ${i < Math.floor(rating) ? 'text-yellow-400 fill-current' : 'text-gray-400'}`}
<Star
key={i}
className={`h-4 w-4 ${
i < Math.floor(rating)
? "text-yellow-400 fill-current"
: "text-gray-400"
}`}
/>
))
}
));
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -164,12 +176,16 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
Назад
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Выбор поставщика</h2>
<p className="text-white/60">Выберите поставщика для создания поставки</p>
<h2 className="text-2xl font-bold text-white mb-1">
Выбор поставщика
</h2>
<p className="text-white/60">
Выберите поставщика для создания поставки
</p>
</div>
</div>
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -179,11 +195,11 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mockWholesalers.map((wholesaler) => (
<Card
key={wholesaler.id}
{mockSuppliers.map((supplier) => (
<Card
key={supplier.id}
className="bg-white/10 backdrop-blur border-white/20 p-6 cursor-pointer transition-all hover:bg-white/15 hover:border-white/30 hover:scale-105"
onClick={() => setSelectedWholesaler(wholesaler)}
onClick={() => setSelectedSupplier(supplier)}
>
<div className="space-y-4">
{/* Заголовок карточки */}
@ -193,14 +209,16 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold text-lg mb-1 truncate">
{wholesaler.name}
{supplier.name}
</h3>
<p className="text-white/60 text-xs mb-2 truncate">
{wholesaler.fullName}
{supplier.fullName}
</p>
<div className="flex items-center space-x-1 mb-2">
{renderStars(wholesaler.rating)}
<span className="text-white/60 text-sm ml-2">{wholesaler.rating}</span>
{renderStars(supplier.rating)}
<span className="text-white/60 text-sm ml-2">
{supplier.rating}
</span>
</div>
</div>
</div>
@ -209,26 +227,34 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
<div className="space-y-2">
<div className="flex items-center space-x-2">
<MapPin className="h-4 w-4 text-gray-400" />
<span className="text-white/80 text-sm truncate">{wholesaler.address}</span>
<span className="text-white/80 text-sm truncate">
{supplier.address}
</span>
</div>
{wholesaler.phone && (
{supplier.phone && (
<div className="flex items-center space-x-2">
<Phone className="h-4 w-4 text-gray-400" />
<span className="text-white/80 text-sm">{wholesaler.phone}</span>
<span className="text-white/80 text-sm">
{supplier.phone}
</span>
</div>
)}
{wholesaler.email && (
{supplier.email && (
<div className="flex items-center space-x-2">
<Mail className="h-4 w-4 text-gray-400" />
<span className="text-white/80 text-sm truncate">{wholesaler.email}</span>
<span className="text-white/80 text-sm truncate">
{supplier.email}
</span>
</div>
)}
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-gray-400" />
<span className="text-white/80 text-sm">{wholesaler.productCount} товаров</span>
<span className="text-white/80 text-sm">
{supplier.productCount} товаров
</span>
</div>
</div>
@ -236,8 +262,8 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
<div className="space-y-2">
<p className="text-white/60 text-xs">Специализация:</p>
<div className="flex flex-wrap gap-1">
{wholesaler.specialization.map((spec, index) => (
<Badge
{supplier.specialization.map((spec, index) => (
<Badge
key={index}
className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs"
>
@ -249,12 +275,12 @@ export function WholesalerSelection({ onBack, onClose, onSupplyCreated }: Wholes
{/* ИНН */}
<div className="pt-2 border-t border-white/10">
<p className="text-white/60 text-xs">ИНН: {wholesaler.inn}</p>
<p className="text-white/60 text-xs">ИНН: {supplier.inn}</p>
</div>
</div>
</Card>
))}
</div>
</div>
)
}
);
}

View File

@ -1,44 +1,39 @@
"use client"
"use client";
import React from 'react'
import { Button } from '@/components/ui/button'
import {
ArrowLeft,
ShoppingCart,
Users,
Check
} from 'lucide-react'
import React from "react";
import { Button } from "@/components/ui/button";
import { ArrowLeft, ShoppingCart, Users, Check } from "lucide-react";
interface TabsHeaderProps {
activeTab: 'cards' | 'wholesaler'
onTabChange: (tab: 'cards' | 'wholesaler') => void
onBack: () => void
activeTab: "cards" | "wholesaler";
onTabChange: (tab: "cards" | "wholesaler") => void;
onBack: () => void;
cartInfo?: {
itemCount: number
totalAmount: number
formatCurrency: (amount: number) => string
}
onCartClick?: () => void
onCreateSupply?: () => void
canCreateSupply?: boolean
isCreatingSupply?: boolean
itemCount: number;
totalAmount: number;
formatCurrency: (amount: number) => string;
};
onCartClick?: () => void;
onCreateSupply?: () => void;
canCreateSupply?: boolean;
isCreatingSupply?: boolean;
}
export function TabsHeader({
activeTab,
onTabChange,
onBack,
export function TabsHeader({
activeTab,
onTabChange,
onBack,
cartInfo,
onCartClick,
onCreateSupply,
canCreateSupply = false,
isCreatingSupply = false
isCreatingSupply = false,
}: TabsHeaderProps) {
return (
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
<Button
variant="ghost"
size="sm"
onClick={onBack}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -47,58 +42,59 @@ export function TabsHeader({
Назад
</Button>
<h1 className="text-2xl font-bold text-white">Создание поставки</h1>
{/* Кнопка корзины */}
{cartInfo && cartInfo.itemCount > 0 && onCartClick && (
<Button
<Button
onClick={onCartClick}
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white"
>
<ShoppingCart className="h-4 w-4 mr-2" />
Корзина ({cartInfo.itemCount})
{activeTab === 'wholesaler' && `${cartInfo.formatCurrency(cartInfo.totalAmount)}`}
{activeTab === "supplier" &&
`${cartInfo.formatCurrency(cartInfo.totalAmount)}`}
</Button>
)}
{/* Кнопка создания поставки для таба карточек */}
{activeTab === 'cards' && onCreateSupply && (
<Button
{activeTab === "cards" && onCreateSupply && (
<Button
onClick={onCreateSupply}
disabled={!canCreateSupply || isCreatingSupply}
className="bg-white/20 hover:bg-white/30 text-white border-0"
>
<Check className="h-4 w-4 mr-2" />
{isCreatingSupply ? 'Создание...' : 'Создать поставку'}
{isCreatingSupply ? "Создание..." : "Создать поставку"}
</Button>
)}
</div>
<div>
<div className="grid grid-cols-2 bg-white/10 backdrop-blur border border-white/20 rounded-lg p-1">
<button
onClick={() => onTabChange('cards')}
onClick={() => onTabChange("cards")}
className={`px-4 py-2 text-sm rounded transition-all ${
activeTab === 'cards'
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
activeTab === "cards"
? "bg-white/20 text-white"
: "text-white/60 hover:text-white hover:bg-white/10"
}`}
>
<ShoppingCart className="h-4 w-4 mr-1 inline" />
Карточки
</button>
<button
onClick={() => onTabChange('wholesaler')}
onClick={() => onTabChange("wholesaler")}
className={`px-4 py-2 text-sm rounded transition-all ${
activeTab === 'wholesaler'
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
activeTab === "wholesaler"
? "bg-white/20 text-white"
: "text-white/60 hover:text-white hover:bg-white/10"
}`}
>
<Users className="h-4 w-4 mr-1 inline" />
Поставщики
Поставщики
</button>
</div>
</div>
</div>
)
}
);
}

View File

@ -1,50 +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 SupplierForCreation {
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 SupplierProduct {
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 SelectedProduct extends SupplierProduct {
selectedQuantity: number;
supplierId: string;
supplierName: string;
}
export interface CounterpartyWholesaler {
id: string
inn?: string
name?: string
fullName?: string
address?: string
phones?: { value: string }[]
emails?: { value: string }[]
}
export interface CounterpartySupplier {
id: string;
inn?: string;
name?: string;
fullName?: string;
address?: string;
phones?: { value: string }[];
emails?: { value: string }[];
}

View File

@ -1,112 +0,0 @@
"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 (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent mx-auto mb-4"></div>
<p className="text-white/60">Загружаем поставщиков...</p>
</div>
</div>
)
}
return (
<div>
{/* Поиск */}
<div className="mb-4">
<div className="relative max-w-md">
<Search className="absolute left-3 top-3 h-4 w-4 text-white/40" />
<Input
placeholder="Поиск поставщиков..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 glass-input text-white placeholder:text-white/40 h-10"
/>
</div>
</div>
{filteredWholesalers.length === 0 ? (
<div className="text-center p-8">
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">
{searchQuery ? 'Поставщики не найдены' : 'У вас нет контрагентов-поставщиков'}
</p>
<p className="text-white/40 text-sm mt-2">
{searchQuery ? 'Попробуйте изменить условия поиска' : 'Добавьте поставщиков в разделе "Партнеры"'}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{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 (
<WholesalerCard
key={wholesaler.id}
wholesaler={adaptedWholesaler}
onClick={() => handleWholesalerClick(wholesaler)}
/>
)
})}
</div>
)}
</div>
)
}