Объединены файлы правил системы в единую базу знаний v3.0 с устранением противоречий и дублирования. Создан rules-unified.md на основе rules.md, rules1.md и rules2.md с добавлением всех уникальных разделов. Обновлена терминология системы с соответствием реальной схеме БД (ТОВАР→PRODUCT, РАСХОДНИКИ→CONSUMABLE). Архивированы старые файлы правил в папку archive. Обновлены ссылки в CLAUDE.md и development-checklist.md на новый единый источник истины.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Veronika Smirnova
2025-08-05 00:19:17 +03:00
parent 17ffd6c9ed
commit ee72a9488b
21 changed files with 9147 additions and 174 deletions

View File

@ -0,0 +1,1210 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@apollo/client";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useAuth } from "@/hooks/useAuth";
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,
Building2,
MapPin,
Phone,
Mail,
Star,
Search,
Package,
Plus,
Minus,
ShoppingCart,
Calendar,
Truck,
Box,
FileText,
AlertCircle,
Settings,
DollarSign,
} from "lucide-react";
import {
GET_MY_COUNTERPARTIES,
GET_ORGANIZATION_PRODUCTS,
} from "@/graphql/queries";
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
import { OrganizationAvatar } from "@/components/market/organization-avatar";
import { AddGoodsModal } from "./add-goods-modal";
import { toast } from "sonner";
import Image from "next/image";
// Интерфейсы согласно rules2.md 9.7
interface GoodsSupplier {
id: string;
inn: string;
name?: string;
fullName?: string;
type: "FULFILLMENT" | "SELLER" | "LOGIST" | "WHOLESALE";
address?: string;
phones?: Array<{ value: string }>;
emails?: Array<{ value: string }>;
users?: Array<{ id: string; avatar?: string; managerName?: string }>;
createdAt: string;
rating?: number;
}
interface GoodsProduct {
id: string;
name: string;
description?: string;
price: number;
category?: { name: string };
images: string[];
mainImage?: string;
article: string; // Артикул поставщика
organization: {
id: string;
name: string;
};
quantity?: number;
unit?: string;
weight?: number;
dimensions?: {
length: number;
width: number;
height: number;
};
}
interface SelectedGoodsItem {
id: string;
name: string;
sku: string;
price: number;
selectedQuantity: number;
unit?: string;
category?: string;
supplierId: string;
supplierName: string;
completeness?: string; // Комплектность согласно rules2.md 9.7.2
recipe?: string; // Рецептура/состав
specialRequirements?: string; // Особые требования
parameters?: Array<{ name: string; value: string }>; // Параметры товара
}
interface LogisticsCompany {
id: string;
name: string;
estimatedCost: number;
deliveryDays: number;
type: "EXPRESS" | "STANDARD" | "ECONOMY";
}
export function CreateSuppliersSupplyPage() {
const router = useRouter();
const { user } = useAuth();
const { getSidebarMargin } = useSidebar();
// Основные состояния
const [selectedSupplier, setSelectedSupplier] = useState<GoodsSupplier | null>(null);
const [selectedGoods, setSelectedGoods] = useState<SelectedGoodsItem[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [productSearchQuery, setProductSearchQuery] = useState("");
// Обязательные поля согласно rules2.md 9.7.8
const [deliveryDate, setDeliveryDate] = useState("");
// Выбор логистики согласно rules2.md 9.7.7
const [selectedLogistics, setSelectedLogistics] = useState<string>("auto"); // "auto" или ID компании
// Выбор фулфилмента согласно rules2.md 9.7.2
const [selectedFulfillment, setSelectedFulfillment] = useState<string>("");
// Модальное окно для детального добавления товара
const [selectedProductForModal, setSelectedProductForModal] = useState<GoodsProduct | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
// Состояние количества товаров для карточек согласно rules2.md 13.3
const [productQuantities, setProductQuantities] = useState<Record<string, number>>({});
// Загружаем партнеров-поставщиков согласно rules2.md 13.3
const { data: counterpartiesData, loading: counterpartiesLoading, error: counterpartiesError } = useQuery(
GET_MY_COUNTERPARTIES,
{
errorPolicy: 'all', // Показываем все ошибки, но не прерываем работу
onError: (error) => {
try {
console.log("🚨 GET_MY_COUNTERPARTIES ERROR:", {
errorMessage: error?.message || "Unknown error",
hasGraphQLErrors: !!error?.graphQLErrors?.length,
hasNetworkError: !!error?.networkError,
});
} catch (logError) {
console.log("❌ Error in counterparties error handler:", logError);
}
},
}
);
// Загружаем каталог товаров согласно rules2.md 13.3
// Товары поставщика загружаются из Product таблицы where organizationId = поставщик.id
const { data: productsData, loading: productsLoading, error: productsError } = useQuery(
GET_ORGANIZATION_PRODUCTS,
{
variables: {
organizationId: selectedSupplier?.id || "", // Избегаем undefined для обязательного параметра
search: productSearchQuery, // Используем поисковый запрос для фильтрации
category: "", // Пока без фильтра по категории
type: "PRODUCT", // КРИТИЧЕСКИ ВАЖНО: показываем только PRODUCT, не CONSUMABLE согласно development-checklist.md
},
skip: !selectedSupplier || !selectedSupplier.id, // Более строгая проверка
fetchPolicy: 'network-only', // Обходим кеш для получения актуальных данных
errorPolicy: 'all', // Показываем все ошибки, но не прерываем работу
onError: (error) => {
try {
console.log("🚨 GET_ORGANIZATION_PRODUCTS ERROR:", {
errorMessage: error?.message || "Unknown error",
hasGraphQLErrors: !!error?.graphQLErrors?.length,
hasNetworkError: !!error?.networkError,
variables: {
organizationId: selectedSupplier?.id || "not_selected",
search: productSearchQuery || "empty",
category: "",
type: "PRODUCT",
},
selectedSupplier: selectedSupplier ? {
id: selectedSupplier.id,
name: selectedSupplier.name || selectedSupplier.fullName || "Unknown",
} : "not_selected",
});
} catch (logError) {
console.log("❌ Error in error handler:", logError);
}
},
}
);
// Мутация создания поставки
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
// Фильтруем только партнеров-поставщиков согласно rules2.md 13.3
const allCounterparties = counterpartiesData?.myCounterparties || [];
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
const wholesaleSuppliers = allCounterparties.filter((cp: any) => {
try {
return cp && cp.type === "WHOLESALE";
} catch (error) {
console.log("❌ Error filtering wholesale suppliers:", error);
return false;
}
});
const suppliers = wholesaleSuppliers.filter((cp: GoodsSupplier) => {
try {
if (!cp) return false;
const searchLower = searchQuery.toLowerCase();
return (
cp.name?.toLowerCase().includes(searchLower) ||
cp.fullName?.toLowerCase().includes(searchLower) ||
cp.inn?.includes(searchQuery) ||
cp.phones?.some(phone => phone.value?.includes(searchQuery))
);
} catch (error) {
console.log("❌ Error filtering suppliers by search:", error);
return false;
}
});
const isLoading = counterpartiesLoading;
// Получаем товары выбранного поставщика согласно rules2.md 13.3
// Теперь фильтрация происходит на сервере через GraphQL запрос
const products = (productsData?.organizationProducts || []).filter((product: any) => {
try {
return product && product.id && product.name;
} catch (error) {
console.log("❌ Error filtering products:", error);
return false;
}
});
// Отладочные логи согласно development-checklist.md
console.log("🛒 CREATE_SUPPLIERS_SUPPLY DEBUG:", {
selectedSupplier: selectedSupplier ? {
id: selectedSupplier.id,
name: selectedSupplier.name,
type: selectedSupplier.type,
} : null,
counterpartiesStatus: {
loading: counterpartiesLoading,
error: counterpartiesError?.message,
dataCount: counterpartiesData?.myCounterparties?.length || 0,
},
productsStatus: {
loading: productsLoading,
error: productsError?.message,
dataCount: products.length,
hasData: !!productsData?.organizationProducts,
productSample: products.slice(0, 3).map(p => ({ id: p.id, name: p.name, article: p.article })),
},
});
// Моковые логистические компании согласно rules2.md 9.7.7
const logisticsCompanies: LogisticsCompany[] = [
{ id: "express", name: "Экспресс доставка", estimatedCost: 2500, deliveryDays: 1, type: "EXPRESS" },
{ id: "standard", name: "Стандартная доставка", estimatedCost: 1200, deliveryDays: 3, type: "STANDARD" },
{ id: "economy", name: "Экономичная доставка", estimatedCost: 800, deliveryDays: 7, type: "ECONOMY" },
];
// Моковые фулфилмент-центры согласно rules2.md 9.7.2
const fulfillmentCenters = [
{ id: "ff1", name: "СФ Центр Москва", address: "г. Москва, ул. Складская 10" },
{ id: "ff2", name: "СФ Центр СПб", address: "г. Санкт-Петербург, пр. Логистический 5" },
{ id: "ff3", name: "СФ Центр Екатеринбург", address: "г. Екатеринбург, ул. Промышленная 15" },
];
// Функции для работы с количеством товаров в карточках согласно rules2.md 13.3
const getProductQuantity = (productId: string): number => {
return productQuantities[productId] || 0;
};
const setProductQuantity = (productId: string, quantity: number): void => {
setProductQuantities(prev => ({
...prev,
[productId]: Math.max(0, quantity)
}));
};
const updateProductQuantity = (productId: string, delta: number): void => {
const currentQuantity = getProductQuantity(productId);
const newQuantity = currentQuantity + delta;
setProductQuantity(productId, newQuantity);
};
// Добавление товара в корзину из карточки с заданным количеством
const addToCart = (product: GoodsProduct) => {
const quantity = getProductQuantity(product.id);
if (quantity <= 0) {
toast.error("Укажите количество товара");
return;
}
// Проверка остатков согласно rules2.md 9.7.9
if (product.quantity !== undefined && quantity > product.quantity) {
toast.error(`Недостаточно товара на складе. Доступно: ${product.quantity} ${product.unit || 'шт'}`);
return;
}
if (!selectedSupplier) {
toast.error("Не выбран поставщик");
return;
}
const newGoodsItem: SelectedGoodsItem = {
id: product.id,
name: product.name,
sku: product.article,
price: product.price,
selectedQuantity: quantity,
unit: product.unit,
category: product.category?.name,
supplierId: selectedSupplier.id,
supplierName: selectedSupplier.name || selectedSupplier.fullName || "Неизвестный поставщик",
};
// Проверяем, есть ли уже такой товар в корзине
const existingItemIndex = selectedGoods.findIndex(item => item.id === product.id);
if (existingItemIndex >= 0) {
// Обновляем количество существующего товара
const updatedGoods = [...selectedGoods];
updatedGoods[existingItemIndex] = {
...updatedGoods[existingItemIndex],
selectedQuantity: quantity
};
setSelectedGoods(updatedGoods);
toast.success(`Количество товара "${product.name}" обновлено в корзине`);
} else {
// Добавляем новый товар
setSelectedGoods(prev => [...prev, newGoodsItem]);
toast.success(`Товар "${product.name}" добавлен в корзину`);
}
// Сбрасываем количество в карточке
setProductQuantity(product.id, 0);
};
// Открытие модального окна для детального добавления
const openAddModal = (product: GoodsProduct) => {
setSelectedProductForModal(product);
setIsModalOpen(true);
};
// Добавление товара в корзину из модального окна с дополнительными данными
const addToCartFromModal = (
product: GoodsProduct,
quantity: number,
additionalData?: {
completeness?: string;
recipe?: string;
specialRequirements?: string;
parameters?: Array<{ name: string; value: string }>;
customPrice?: number;
}
) => {
// Проверка остатков согласно rules2.md 9.7.9
if (product.quantity !== undefined && quantity > product.quantity) {
toast.error(`Недостаточно товара на складе. Доступно: ${product.quantity} ${product.unit || 'шт'}`);
return;
}
const existingItem = selectedGoods.find(item => item.id === product.id);
const finalPrice = additionalData?.customPrice || product.price;
if (existingItem) {
// Обновляем существующий товар
setSelectedGoods(prev =>
prev.map(item =>
item.id === product.id
? {
...item,
selectedQuantity: quantity,
price: finalPrice,
completeness: additionalData?.completeness,
recipe: additionalData?.recipe,
specialRequirements: additionalData?.specialRequirements,
parameters: additionalData?.parameters,
}
: item
)
);
} else {
// Добавляем новый товар
const newItem: SelectedGoodsItem = {
id: product.id,
name: product.name,
sku: product.article,
price: finalPrice,
selectedQuantity: quantity,
unit: product.unit,
category: product.category?.name,
supplierId: selectedSupplier!.id,
supplierName: selectedSupplier!.name || selectedSupplier!.fullName || '',
completeness: additionalData?.completeness,
recipe: additionalData?.recipe,
specialRequirements: additionalData?.specialRequirements,
parameters: additionalData?.parameters,
};
setSelectedGoods(prev => [...prev, newItem]);
}
toast.success("Товар добавлен в корзину");
};
// Удаление из корзины
const removeFromCart = (productId: string) => {
setSelectedGoods(prev => prev.filter(item => item.id !== productId));
toast.success("Товар удален из корзины");
};
// Расчеты согласно rules2.md 9.7.6
const totalGoodsAmount = selectedGoods.reduce((sum, item) => sum + (item.price * item.selectedQuantity), 0);
const totalQuantity = selectedGoods.reduce((sum, item) => sum + item.selectedQuantity, 0);
const fulfillmentFee = totalGoodsAmount * 0.08; // 8% комиссия фулфилмента
const selectedLogisticsCompany = logisticsCompanies.find(lc => lc.id === selectedLogistics);
const logisticsCost = selectedLogistics === "auto" ? 1000 : (selectedLogisticsCompany?.estimatedCost || 0);
const totalAmount = totalGoodsAmount + fulfillmentFee + logisticsCost;
// Валидация формы согласно rules2.md 9.7.6
const isFormValid = selectedSupplier && selectedGoods.length > 0 && deliveryDate && selectedFulfillment;
// Создание поставки
const handleCreateSupply = async () => {
if (!isFormValid) {
toast.error("Заполните все обязательные поля");
return;
}
setIsCreatingSupply(true);
try {
await createSupplyOrder({
variables: {
supplierId: selectedSupplier!.id,
fulfillmentCenterId: selectedFulfillment,
items: selectedGoods.map(item => ({
productId: item.id,
quantity: item.selectedQuantity,
price: item.price,
completeness: item.completeness,
recipe: item.recipe,
specialRequirements: item.specialRequirements,
parameters: item.parameters,
})),
deliveryDate,
logisticsCompany: selectedLogistics === "auto" ? null : selectedLogistics,
type: "ТОВАР",
creationMethod: "suppliers",
},
});
toast.success("Поставка успешно создана");
router.push("/supplies?tab=goods&subTab=suppliers");
} catch (error) {
console.error("Ошибка создания поставки:", error);
toast.error("Ошибка при создании поставки");
} finally {
setIsCreatingSupply(false);
}
};
// Получение минимальной и максимальной даты согласно rules2.md 9.7.8
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const maxDate = new Date();
maxDate.setDate(maxDate.getDate() + 90);
const minDateString = tomorrow.toISOString().split('T')[0];
const maxDateString = maxDate.toISOString().split('T')[0];
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-hidden transition-all duration-300`}>
<div className="h-full flex flex-col">
{/* СТРУКТУРА ИЗ 3 БЛОКОВ согласно rules1.md 19.2.1 - блоки точно по уровню сайдбара */}
<div className="flex-1 flex gap-2 min-h-0">
{/* ЛЕВЫЙ БЛОК: ПОСТАВЩИКИ И ТОВАРЫ */}
<div className="flex-1 flex flex-col gap-2 min-h-0">
{/* БЛОК 1: ПОСТАВЩИКИ - обязательный блок согласно rules1.md 19.2.1 */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-shrink-0 flex flex-col"
style={{ minHeight: '120px', maxHeight: suppliers.length > 4 ? '200px' : 'auto' }}>
<div className="p-4 flex-shrink-0">
{/* Навигация и заголовок в одном блоке */}
<div className="flex items-center justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/supplies?tab=goods&subTab=suppliers")}
className="glass-secondary hover:text-white/90 gap-2 transition-all duration-200 -ml-2"
>
<ArrowLeft className="h-3 w-3" />
Назад
</Button>
<div className="h-4 w-px bg-white/20"></div>
<div className="p-2 bg-green-400/10 rounded-lg border border-green-400/20">
<Building2 className="h-4 w-4 text-green-400" />
</div>
<div>
<h2 className="text-base font-semibold text-white">Поставщики товаров</h2>
<Badge className="bg-purple-500/20 text-purple-300 border border-purple-500/30 text-xs font-medium mt-0.5">
Создание поставки
</Badge>
</div>
</div>
<div className="flex-1 max-w-sm">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
<Input
placeholder="Поиск..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white/5 border-white/10 text-white placeholder:text-white/50 pl-10 h-9 text-sm transition-all duration-200 focus:border-white/20"
/>
</div>
{allCounterparties.length === 0 && (
<Button
variant="outline"
size="sm"
onClick={() => router.push("/market")}
className="glass-secondary hover:text-white/90 transition-all duration-200 mt-2 w-full"
>
<Building2 className="h-3 w-3 mr-2" />
Найти поставщиков в маркете
</Button>
)}
</div>
</div>
{/* Список поставщиков согласно visual-design-rules.md */}
<div className="flex-1 min-h-0">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60"></div>
<span className="ml-3 text-white/70">Загрузка поставщиков...</span>
</div>
) : suppliers.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-3">
<div className="w-12 h-12 mx-auto bg-white/5 rounded-full flex items-center justify-center">
<Building2 className="h-6 w-6 text-white/40" />
</div>
<div>
<h3 className="text-base font-medium text-white mb-1">Поставщики товаров не найдены</h3>
<p className="text-white/60 text-xs max-w-md mx-auto">
{allCounterparties.length === 0
? "У вас нет партнеров. Найдите поставщиков в маркете или добавьте их через раздел 'Партнеры'"
: wholesaleSuppliers.length === 0
? `Найдено ${allCounterparties.length} партнеров, но среди них нет поставщиков (тип WHOLESALE)`
: searchQuery && suppliers.length === 0
? "Поставщики-партнеры не найдены по вашему запросу"
: `Найдено ${suppliers.length} поставщиков-партнеров`
}
</p>
</div>
</div>
</div>
) : (
<div className={`gap-2 overflow-y-auto ${
suppliers.length <= 2
? 'flex flex-wrap'
: suppliers.length <= 4
? 'grid grid-cols-2'
: 'grid grid-cols-1 md:grid-cols-2 max-h-32'
}`}>
{suppliers.map((supplier: GoodsSupplier) => (
<div
key={supplier.id}
onClick={() => setSelectedSupplier(supplier)}
className={`p-3 rounded-lg cursor-pointer group transition-all duration-200 ${
selectedSupplier?.id === supplier.id
? "bg-white/15 border border-white/40 shadow-lg"
: "bg-white/5 border border-white/10 hover:border-white/20 hover:bg-white/10"
}`}
>
<div className="flex items-start gap-2">
<div className="flex-shrink-0">
<OrganizationAvatar organization={supplier} size="sm" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="text-white font-medium text-sm truncate group-hover:text-white transition-colors">
{supplier.name || supplier.fullName}
</h4>
{supplier.rating && (
<div className="flex items-center gap-1 bg-yellow-400/10 px-2 py-0.5 rounded-full">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-yellow-300 text-xs font-medium">{supplier.rating}</span>
</div>
)}
</div>
<div className="flex items-center gap-2 mb-1">
<p className="text-white/60 text-xs font-mono">ИНН: {supplier.inn}</p>
<Badge className={`text-xs font-medium ${
supplier.type === "WHOLESALE"
? "bg-green-500/20 text-green-300 border border-green-500/30"
: "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30"
}`}>
{supplier.type === "WHOLESALE" ? "Поставщик" : supplier.type}
</Badge>
</div>
{supplier.address && (
<p className="text-white/50 text-xs line-clamp-1">{supplier.address}</p>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* БЛОК 2: ТОВАРЫ - зависимый блок согласно rules1.md 19.2.1 */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 min-h-0 flex flex-col">
<div className="p-6 border-b border-white/10 flex-shrink-0">
<div className="flex items-center justify-between gap-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-400/10 rounded-lg border border-blue-400/20">
<Package className="h-6 w-6 text-blue-400" />
</div>
<div>
<h3 className="text-xl font-semibold text-white">
{selectedSupplier ? `Товары ${selectedSupplier.name || selectedSupplier.fullName}` : "Каталог товаров"}
</h3>
{selectedSupplier && (
<p className="text-white/60 text-sm mt-1">
Выберите товары для добавления в корзину
</p>
)}
</div>
</div>
{selectedSupplier && (
<div className="flex-1 max-w-sm">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
<Input
placeholder="Поиск товаров..."
value={productSearchQuery}
onChange={(e) => setProductSearchQuery(e.target.value)}
className="glass-input text-white placeholder:text-white/50 pl-10 h-10 transition-all duration-200 focus-visible:ring-ring/50"
/>
</div>
</div>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
{!selectedSupplier ? (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-4">
<div className="w-24 h-24 mx-auto bg-blue-400/5 rounded-full flex items-center justify-center">
<Building2 className="h-12 w-12 text-blue-400/50" />
</div>
<div>
<h4 className="text-xl font-medium text-white mb-2">Выберите поставщика</h4>
<p className="text-white/60 max-w-sm mx-auto">
Для просмотра каталога товаров сначала выберите поставщика из списка выше
</p>
</div>
</div>
</div>
) : productsError ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto bg-red-500/10 rounded-full flex items-center justify-center mb-4">
<AlertCircle className="h-8 w-8 text-red-400" />
</div>
<h4 className="text-xl font-medium text-white mb-2">Ошибка загрузки товаров</h4>
<p className="text-red-400 text-sm">
{productsError.message || "Произошла ошибка при загрузке каталога"}
</p>
</div>
) : productsLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
<span className="ml-3 text-white/70">Загрузка товаров...</span>
</div>
) : products.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto bg-white/5 rounded-full flex items-center justify-center mb-4">
<Package className="h-8 w-8 text-white/40" />
</div>
<h4 className="text-xl font-medium text-white mb-2">
{productSearchQuery ? "Товары не найдены" : "Каталог пуст"}
</h4>
<p className="text-white/60">
{productSearchQuery
? "Попробуйте изменить поисковый запрос"
: "У выбранного поставщика нет товаров"
}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product: GoodsProduct) => (
<div
key={product.id}
className="glass-card hover:border-white/20 hover:bg-white/10 hover:scale-105 hover:shadow-xl hover:shadow-blue-500/20 transition-all duration-200 cursor-pointer group"
>
<div className="p-5 space-y-4">
{product.mainImage && (
<div className="w-full h-40 rounded-lg overflow-hidden bg-white/5">
<Image
src={product.mainImage}
alt={product.name}
width={280}
height={160}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
</div>
)}
<div className="space-y-3">
<div>
<h4 className="text-white font-semibold text-base line-clamp-2 group-hover:text-white transition-colors">
{product.name}
</h4>
<p className="text-white/60 text-sm font-mono mt-1">Артикул: {product.article}</p>
</div>
{product.category && (
<Badge className="bg-blue-500/20 text-blue-300 border border-blue-500/30 text-xs font-medium">
{product.category.name}
</Badge>
)}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-white font-bold text-lg">
{product.price.toLocaleString('ru-RU')}
</p>
<p className="text-white/60 text-xs">за единицу</p>
</div>
{product.quantity !== undefined && (
<div className="flex items-center gap-1">
<div className={`w-2 h-2 rounded-full ${
product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'
}`}></div>
<p className={`text-xs font-medium ${
product.quantity > 0 ? 'text-green-400' : 'text-red-400'
}`}>
{product.quantity > 0
? `Доступно: ${product.quantity}`
: 'Нет в наличии'
}
</p>
</div>
)}
</div>
{/* Поле количества с кнопками +/- согласно rules2.md 13.3 */}
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => updateProductQuantity(product.id, -1)}
className="h-8 w-8 p-0 bg-white/5 border-white/20 hover:bg-white/10 hover:border-white/30 text-white"
disabled={!getProductQuantity(product.id) || getProductQuantity(product.id) <= 0}
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="number"
min="0"
max={product.quantity}
value={getProductQuantity(product.id) || ""}
onChange={(e) => setProductQuantity(product.id, parseInt(e.target.value) || 0)}
className="h-8 w-20 text-center bg-white/5 border-white/20 text-white placeholder:text-white/40 focus:border-white/40"
placeholder="00000"
/>
<Button
size="sm"
variant="outline"
onClick={() => updateProductQuantity(product.id, 1)}
className="h-8 w-8 p-0 bg-white/5 border-white/20 hover:bg-white/10 hover:border-white/30 text-white"
disabled={product.quantity === 0 || getProductQuantity(product.id) >= product.quantity}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<Button
size="sm"
onClick={() => addToCart(product)}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white border border-green-500/30 hover:border-green-400/50 transition-all duration-200 h-8 text-xs"
disabled={product.quantity === 0 || !getProductQuantity(product.id)}
>
<ShoppingCart className="h-3 w-3 mr-1" />
В корзину
</Button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* БЛОК 3: КОРЗИНА - правый блок согласно rules1.md 19.2.1 */}
<div className="w-96 flex-shrink-0 flex flex-col min-h-0">
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl flex-1 flex flex-col min-h-0">
{/* ЗАГОЛОВОК И СТАТИСТИКА */}
<div className="p-4 border-b border-white/10 flex-shrink-0">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-purple-400/10 rounded-lg border border-purple-400/20">
<ShoppingCart className="h-5 w-5 text-purple-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-white">Корзина и настройки поставки</h3>
<p className="text-white/60 text-xs mt-1">
Управление заказом и параметрами доставки
</p>
</div>
</div>
{/* СТАТИСТИКА ПОСТАВКИ */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-white/60">Поставщиков</p>
<p className="text-lg font-semibold text-white">{selectedSupplier ? 1 : 0}</p>
</div>
<Building2 className="h-4 w-4 text-green-400" />
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-white/60">Товаров</p>
<p className="text-lg font-semibold text-white">{selectedGoods.length}</p>
</div>
<Package className="h-4 w-4 text-blue-400" />
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-white/60">Количество</p>
<p className="text-lg font-semibold text-white">{totalQuantity} шт</p>
</div>
<Box className="h-4 w-4 text-orange-400" />
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-lg p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-white/60">Сумма</p>
<p className="text-lg font-semibold text-white">{totalAmount.toLocaleString('ru-RU')} </p>
</div>
<DollarSign className="h-4 w-4 text-purple-400" />
</div>
</div>
</div>
</div>
{/* НАСТРОЙКИ ПОСТАВКИ */}
<div className="p-4 border-b border-white/10 flex-shrink-0">
<h4 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<Settings className="h-4 w-4 text-blue-400" />
Настройки поставки
</h4>
<div className="space-y-3">
{/* Выбор фулфилмент-центра */}
<div>
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
<Building2 className="h-3 w-3 text-green-400" />
Фулфилмент-центр *
</label>
<select
value={selectedFulfillment}
onChange={(e) => setSelectedFulfillment(e.target.value)}
className="w-full bg-white/5 border-white/10 text-white h-8 text-sm rounded-lg hover:border-white/30 focus:border-green-400/50 transition-all duration-200"
>
<option value="" className="bg-gray-800 text-white">Выберите фулфилмент-центр</option>
{fulfillmentCenters.map((center) => (
<option key={center.id} value={center.id} className="bg-gray-800 text-white">
{center.name} - {center.address}
</option>
))}
</select>
</div>
{/* Дата поставки */}
<div>
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
<Calendar className="h-3 w-3 text-blue-400" />
Желаемая дата поставки *
</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-3 w-3 z-10" />
<Input
type="date"
value={deliveryDate}
onChange={(e) => setDeliveryDate(e.target.value)}
min={minDateString}
max={maxDateString}
className="bg-white/5 border-white/10 text-white pl-9 h-8 text-sm hover:border-white/30 focus:border-blue-400/50 transition-all duration-200"
required
/>
</div>
</div>
{/* Выбор логистики */}
<div>
<label className="text-white/70 text-xs font-medium mb-2 flex items-center gap-2">
<Truck className="h-3 w-3 text-orange-400" />
Логистическая компания
</label>
<select
value={selectedLogistics}
onChange={(e) => setSelectedLogistics(e.target.value)}
className="w-full bg-white/5 border-white/10 text-white h-8 text-sm rounded-lg hover:border-white/30 focus:border-orange-400/50 transition-all duration-200"
>
<option value="auto" className="bg-gray-800 text-white">Автоматический выбор</option>
{logisticsCompanies.map((company) => (
<option key={company.id} value={company.id} className="bg-gray-800 text-white">
{company.name} (~{company.estimatedCost} , {company.deliveryDays} дн.)
</option>
))}
</select>
</div>
</div>
</div>
{/* ТОВАРЫ В КОРЗИНЕ */}
<div className="flex-1 overflow-y-auto p-4">
{selectedGoods.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center space-y-3">
<div className="w-16 h-16 mx-auto bg-purple-400/5 rounded-full flex items-center justify-center">
<ShoppingCart className="h-8 w-8 text-purple-400/50" />
</div>
<div>
<h4 className="text-base font-medium text-white mb-2">Корзина пуста</h4>
<p className="text-white/60 text-sm">
Добавьте товары из каталога поставщика
</p>
</div>
</div>
</div>
) : (
<div className="space-y-4">
{selectedGoods.map((item) => (
<div key={item.id} className="glass-card hover:border-white/20 transition-all duration-200 group">
<div className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<h4 className="text-white font-semibold text-base truncate group-hover:text-white transition-colors">
{item.name}
</h4>
<p className="text-white/60 text-sm font-mono mt-1">Артикул: {item.sku}</p>
{item.category && (
<Badge className="bg-blue-500/20 text-blue-300 border border-blue-500/30 text-xs font-medium mt-2">
{item.category}
</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeFromCart(item.id)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 border border-transparent hover:border-red-500/30 p-2 transition-all duration-200"
>
<Minus className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-white/70 text-xs font-medium">Количество:</span>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
const newQuantity = Math.max(1, item.selectedQuantity - 1);
addToCart(
{
id: item.id,
name: item.name,
sku: item.sku,
price: item.price,
category: { name: item.category || '' },
images: [],
organization: { id: item.supplierId, name: item.supplierName },
unit: item.unit,
} as GoodsProduct,
newQuantity
);
}}
className="h-7 w-7 p-0 border border-white/20 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
>
<Minus className="h-3 w-3" />
</Button>
<Input
type="number"
min="1"
value={item.selectedQuantity}
onChange={(e) => {
const newQuantity = parseInt(e.target.value) || 1;
addToCart(
{
id: item.id,
name: item.name,
sku: item.sku,
price: item.price,
category: { name: item.category || '' },
images: [],
organization: { id: item.supplierId, name: item.supplierName },
unit: item.unit,
} as GoodsProduct,
newQuantity
);
}}
className="glass-input text-white w-16 h-7 text-center text-xs font-medium"
/>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newQuantity = item.selectedQuantity + 1;
addToCart(
{
id: item.id,
name: item.name,
sku: item.sku,
price: item.price,
category: { name: item.category || '' },
images: [],
organization: { id: item.supplierId, name: item.supplierName },
unit: item.unit,
} as GoodsProduct,
newQuantity
);
}}
className="h-7 w-7 p-0 border border-white/20 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
>
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-white/70 text-xs font-medium">Цена за {item.unit || 'шт'}:</span>
<span className="text-white text-xs font-semibold">{item.price.toLocaleString('ru-RU')} </span>
</div>
<div className="flex items-center justify-between p-2 bg-white/5 rounded-lg border border-white/10">
<span className="text-white/80 text-sm font-medium">Сумма:</span>
<span className="text-green-400 text-base font-bold">
{(item.price * item.selectedQuantity).toLocaleString('ru-RU')}
</span>
</div>
{/* Дополнительная информация */}
{(item.completeness || item.recipe || item.specialRequirements || item.parameters) && (
<div className="mt-2 pt-2 border-t border-white/10 space-y-1">
{item.completeness && (
<div className="flex items-start gap-2">
<FileText className="h-3 w-3 text-blue-400 flex-shrink-0 mt-0.5" />
<div>
<span className="text-blue-300 text-xs font-medium">Комплектность: </span>
<span className="text-white/80 text-xs">{item.completeness}</span>
</div>
</div>
)}
{item.recipe && (
<div className="flex items-start gap-2">
<Settings className="h-3 w-3 text-purple-400 flex-shrink-0 mt-0.5" />
<div>
<span className="text-purple-300 text-xs font-medium">Рецептура: </span>
<span className="text-white/80 text-xs">{item.recipe}</span>
</div>
</div>
)}
{item.specialRequirements && (
<div className="flex items-start gap-2">
<AlertCircle className="h-3 w-3 text-yellow-400 flex-shrink-0 mt-0.5" />
<div>
<span className="text-yellow-300 text-xs font-medium">Требования: </span>
<span className="text-white/80 text-xs">{item.specialRequirements}</span>
</div>
</div>
)}
{item.parameters && item.parameters.length > 0 && (
<div className="space-y-1">
<div className="flex items-center gap-1">
<Settings className="h-3 w-3 text-green-400" />
<span className="text-green-300 text-xs font-medium">Параметры:</span>
</div>
<div className="flex flex-wrap gap-1">
{item.parameters.map((param, idx) => (
<Badge key={idx} className="bg-green-500/10 text-green-300 border border-green-500/20 text-xs">
{param.name}: {param.value}
</Badge>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* ИТОГИ И КНОПКА СОЗДАНИЯ */}
<div className="p-4 border-t border-white/10 space-y-4 flex-shrink-0">
{/* Детальные итоги */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-white/70 text-xs font-medium">Товаров:</span>
<span className="text-white text-xs font-semibold">{totalQuantity} шт</span>
</div>
<div className="flex justify-between items-center">
<span className="text-white/70 text-xs font-medium">Стоимость товаров:</span>
<span className="text-white text-xs font-semibold">{totalGoodsAmount.toLocaleString('ru-RU')} </span>
</div>
<div className="flex justify-between items-center">
<span className="text-purple-300 text-xs font-medium">Фулфилмент (8%):</span>
<span className="text-purple-300 text-xs font-semibold">{fulfillmentFee.toLocaleString('ru-RU')} </span>
</div>
<div className="flex justify-between items-center">
<span className="text-orange-300 text-xs font-medium">Логистика:</span>
<span className="text-orange-300 text-xs font-semibold">
{selectedLogistics === "auto" ? "~" : ""}{logisticsCost.toLocaleString('ru-RU')}
</span>
</div>
<div className="flex justify-between items-center p-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/20 rounded-lg">
<span className="text-white text-sm font-semibold">Итого к оплате:</span>
<span className="text-green-400 text-lg font-bold">{totalAmount.toLocaleString('ru-RU')} </span>
</div>
</div>
{/* Кнопка создания поставки */}
<Button
onClick={handleCreateSupply}
disabled={!isFormValid || isCreatingSupply}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white font-semibold py-3 text-sm border border-green-500/30 hover:border-green-400/50 transition-all duration-300 disabled:opacity-50"
>
{isCreatingSupply ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Создание поставки...</span>
</div>
) : (
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<span>Продолжить оформление</span>
</div>
)}
</Button>
{/* Сообщения об ошибках валидации */}
{!isFormValid && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-2 mt-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-3 w-3 text-red-400 flex-shrink-0" />
<p className="text-red-300 text-xs font-medium">
{!selectedSupplier && "Выберите поставщика"}
{selectedSupplier && selectedGoods.length === 0 && "Добавьте товары в корзину"}
{selectedSupplier && selectedGoods.length > 0 && !deliveryDate && "Укажите дату поставки"}
{selectedSupplier && selectedGoods.length > 0 && deliveryDate && !selectedFulfillment && "Выберите фулфилмент-центр"}
</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</main>
{/* Модальное окно для детального добавления товара */}
<AddGoodsModal
product={selectedProductForModal}
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedProductForModal(null);
}}
onAdd={addToCartFromModal}
/>
</div>
);
}