Refactor: Replace wholesaler with supplier terminology and add fulfillment consumables logic
This commit is contained in:
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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 для расходников */}
|
||||
|
@ -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)}
|
||||
|
@ -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: {
|
||||
|
@ -36,7 +36,7 @@ interface Product {
|
||||
parameters: ProductParameter[];
|
||||
}
|
||||
|
||||
interface Wholesaler {
|
||||
interface Supplier {
|
||||
id: string;
|
||||
name: string;
|
||||
inn: string;
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
118
src/components/supplies/supplier-grid.tsx
Normal file
118
src/components/supplies/supplier-grid.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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 }[];
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user