Создан единый источник истины rules-complete.md v9.1 с полной интеграцией всех правил системы. Консолидированы правила создания предметов по ролям, уточнен статус брака (НЕ РЕАЛИЗОВАНО), обновлен механизм учета ПЛАН/ФАКТ с заменой брака на потери при пересчете. Добавлен экономический учет расходников фулфилмента для селлера через рецептуру. Удалены дублирующие файлы правил (CLAUDE.md, development-checklist.md, work-protocols.md, violation-prevention-protocol.md, self-validation.md, description.md). Интегрированы UI структуры создания поставок и концепция многоуровневых таблиц.
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -33,6 +33,10 @@ import {
|
||||
import {
|
||||
GET_MY_COUNTERPARTIES,
|
||||
GET_ORGANIZATION_PRODUCTS,
|
||||
GET_MY_SERVICES,
|
||||
GET_MY_SUPPLIES,
|
||||
GET_SELLER_SUPPLIES_ON_WAREHOUSE,
|
||||
GET_MY_WILDBERRIES_SUPPLIES,
|
||||
} from "@/graphql/queries";
|
||||
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
|
||||
import { OrganizationAvatar } from "@/components/market/organization-avatar";
|
||||
@ -102,6 +106,47 @@ interface LogisticsCompany {
|
||||
type: "EXPRESS" | "STANDARD" | "ECONOMY";
|
||||
}
|
||||
|
||||
// Новые интерфейсы для компонентов рецептуры
|
||||
interface FulfillmentService {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
interface FulfillmentConsumable {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface SellerConsumable {
|
||||
id: string;
|
||||
name: string;
|
||||
stock: number;
|
||||
unit?: string;
|
||||
supplierId: string;
|
||||
}
|
||||
|
||||
interface WBCard {
|
||||
id: string;
|
||||
title: string;
|
||||
nmID: string;
|
||||
vendorCode?: string;
|
||||
brand?: string;
|
||||
}
|
||||
|
||||
interface ProductRecipe {
|
||||
productId: string;
|
||||
selectedServices: string[];
|
||||
selectedFFConsumables: string[];
|
||||
selectedSellerConsumables: string[];
|
||||
selectedWBCard?: string;
|
||||
}
|
||||
|
||||
export function CreateSuppliersSupplyPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
@ -127,8 +172,9 @@ export function CreateSuppliersSupplyPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||||
|
||||
// Состояние количества товаров для карточек согласно rules2.md 13.3
|
||||
|
||||
// Состояния для компонентов рецептуры
|
||||
const [productRecipes, setProductRecipes] = useState<Record<string, ProductRecipe>>({});
|
||||
const [productQuantities, setProductQuantities] = useState<Record<string, number>>({});
|
||||
|
||||
// Загружаем партнеров-поставщиков согласно rules2.md 13.3
|
||||
@ -191,9 +237,36 @@ export function CreateSuppliersSupplyPage() {
|
||||
// Мутация создания поставки
|
||||
const [createSupplyOrder] = useMutation(CREATE_SUPPLY_ORDER);
|
||||
|
||||
// Запросы для компонентов рецептуры
|
||||
const { data: fulfillmentServicesData } = useQuery(GET_MY_SERVICES, {
|
||||
skip: !selectedFulfillment,
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
const { data: fulfillmentConsumablesData } = useQuery(GET_MY_SUPPLIES, {
|
||||
skip: !selectedFulfillment,
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
const { data: sellerConsumablesData } = useQuery(GET_SELLER_SUPPLIES_ON_WAREHOUSE, {
|
||||
skip: !user?.organization?.id,
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
const { data: wbCardsData } = useQuery(GET_MY_WILDBERRIES_SUPPLIES, {
|
||||
skip: !user?.organization?.id,
|
||||
errorPolicy: 'all'
|
||||
});
|
||||
|
||||
// Фильтруем только партнеров-поставщиков согласно rules2.md 13.3
|
||||
const allCounterparties = counterpartiesData?.myCounterparties || [];
|
||||
|
||||
// Извлекаем данные для компонентов рецептуры
|
||||
const fulfillmentServices: FulfillmentService[] = fulfillmentServicesData?.myServices || [];
|
||||
const fulfillmentConsumables: FulfillmentConsumable[] = fulfillmentConsumablesData?.mySupplies || [];
|
||||
const sellerConsumables: SellerConsumable[] = sellerConsumablesData?.sellerSuppliesOnWarehouse || [];
|
||||
const wbCards: WBCard[] = (wbCardsData?.myWildberriesSupplies || []).flatMap((supply: any) => supply.cards || []);
|
||||
|
||||
// Показываем только партнеров с типом WHOLESALE согласно rules2.md 13.3
|
||||
const wholesaleSuppliers = allCounterparties.filter((cp: any) => {
|
||||
try {
|
||||
@ -345,6 +418,106 @@ export function CreateSuppliersSupplyPage() {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Функции для работы с рецептурой
|
||||
const initializeProductRecipe = (productId: string) => {
|
||||
if (!productRecipes[productId]) {
|
||||
setProductRecipes(prev => ({
|
||||
...prev,
|
||||
[productId]: {
|
||||
productId,
|
||||
selectedServices: [],
|
||||
selectedFFConsumables: [],
|
||||
selectedSellerConsumables: [],
|
||||
selectedWBCard: undefined
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleService = (productId: string, serviceId: string) => {
|
||||
initializeProductRecipe(productId);
|
||||
setProductRecipes(prev => {
|
||||
const recipe = prev[productId];
|
||||
const isSelected = recipe.selectedServices.includes(serviceId);
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedServices: isSelected
|
||||
? recipe.selectedServices.filter(id => id !== serviceId)
|
||||
: [...recipe.selectedServices, serviceId]
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const toggleFFConsumable = (productId: string, consumableId: string) => {
|
||||
initializeProductRecipe(productId);
|
||||
setProductRecipes(prev => {
|
||||
const recipe = prev[productId];
|
||||
const isSelected = recipe.selectedFFConsumables.includes(consumableId);
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedFFConsumables: isSelected
|
||||
? recipe.selectedFFConsumables.filter(id => id !== consumableId)
|
||||
: [...recipe.selectedFFConsumables, consumableId]
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSellerConsumable = (productId: string, consumableId: string) => {
|
||||
initializeProductRecipe(productId);
|
||||
setProductRecipes(prev => {
|
||||
const recipe = prev[productId];
|
||||
const isSelected = recipe.selectedSellerConsumables.includes(consumableId);
|
||||
return {
|
||||
...prev,
|
||||
[productId]: {
|
||||
...recipe,
|
||||
selectedSellerConsumables: isSelected
|
||||
? recipe.selectedSellerConsumables.filter(id => id !== consumableId)
|
||||
: [...recipe.selectedSellerConsumables, consumableId]
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const setWBCard = (productId: string, cardId: string) => {
|
||||
initializeProductRecipe(productId);
|
||||
setProductRecipes(prev => ({
|
||||
...prev,
|
||||
[productId]: {
|
||||
...prev[productId],
|
||||
selectedWBCard: cardId
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Расчет стоимости компонентов рецептуры
|
||||
const calculateRecipeCost = (productId: string) => {
|
||||
const recipe = productRecipes[productId];
|
||||
if (!recipe) return { services: 0, consumables: 0, total: 0 };
|
||||
|
||||
const servicesTotal = recipe.selectedServices.reduce((sum, serviceId) => {
|
||||
const service = fulfillmentServices.find(s => s.id === serviceId);
|
||||
return sum + (service?.price || 0);
|
||||
}, 0);
|
||||
|
||||
const consumablesTotal = recipe.selectedFFConsumables.reduce((sum, consumableId) => {
|
||||
const consumable = fulfillmentConsumables.find(c => c.id === consumableId);
|
||||
return sum + (consumable?.price || 0);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
services: servicesTotal,
|
||||
consumables: consumablesTotal,
|
||||
total: servicesTotal + consumablesTotal
|
||||
};
|
||||
};
|
||||
|
||||
// Добавление товара в корзину из модального окна с дополнительными данными
|
||||
const addToCartFromModal = (
|
||||
product: GoodsProduct,
|
||||
@ -418,7 +591,7 @@ export function CreateSuppliersSupplyPage() {
|
||||
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 logisticsCost = selectedLogistics === "auto" ? 0 : (selectedLogisticsCompany?.estimatedCost || 0);
|
||||
const totalAmount = totalGoodsAmount + fulfillmentFee + logisticsCost;
|
||||
|
||||
// Валидация формы согласно rules2.md 9.7.6
|
||||
@ -475,14 +648,14 @@ export function CreateSuppliersSupplyPage() {
|
||||
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">
|
||||
<main className={`flex-1 ${getSidebarMargin()} overflow-hidden transition-all duration-300 p-4`}>
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
|
||||
{/* СТРУКТУРА ИЗ 3 БЛОКОВ согласно rules1.md 19.2.1 - блоки точно по уровню сайдбара */}
|
||||
<div className="flex-1 flex gap-2 min-h-0">
|
||||
<div className="flex-1 flex gap-4 min-h-0">
|
||||
|
||||
{/* ЛЕВЫЙ БЛОК: ПОСТАВЩИКИ И ТОВАРЫ */}
|
||||
<div className="flex-1 flex flex-col gap-2 min-h-0">
|
||||
<div className="flex-1 flex flex-col gap-4 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"
|
||||
@ -702,107 +875,279 @@ export function CreateSuppliersSupplyPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="space-y-4">
|
||||
{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"
|
||||
className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-4 hover:border-white/30 transition-all duration-200"
|
||||
>
|
||||
<div className="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="flex items-start gap-6 mb-4">
|
||||
|
||||
<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 className="flex items-start gap-4 flex-1">
|
||||
<div className="w-24 h-24 bg-white/5 rounded-lg overflow-hidden flex-shrink-0">
|
||||
{product.mainImage ? (
|
||||
<Image
|
||||
src={product.mainImage}
|
||||
alt={product.name}
|
||||
width={96}
|
||||
height={96}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Package className="h-8 w-8 text-white/40" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-semibold text-lg mb-1">{product.name}</h4>
|
||||
<p className="text-white/60 text-sm mb-2 font-mono">Артикул: {product.article}</p>
|
||||
{product.category && (
|
||||
<Badge className="bg-blue-500/20 text-blue-300 border border-blue-500/30 text-xs font-medium mb-2">
|
||||
{product.category.name}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-white font-bold text-xl">{product.price.toLocaleString('ru-RU')} ₽</span>
|
||||
{product.quantity !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
product.quantity > 0 ? 'bg-green-400' : 'bg-red-400'
|
||||
}`}></div>
|
||||
<p className={`text-xs font-medium ${
|
||||
<span className={`text-sm font-medium ${
|
||||
product.quantity > 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{product.quantity > 0
|
||||
? `Доступно: ${product.quantity}`
|
||||
: 'Нет в наличии'
|
||||
}
|
||||
</p>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ПРАВЫЙ БЛОК: Количество + общая сумма */}
|
||||
<div className="flex items-center gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<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)}
|
||||
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}
|
||||
>
|
||||
<ShoppingCart className="h-3 w-3 mr-1" />
|
||||
В корзину
|
||||
<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="0"
|
||||
/>
|
||||
|
||||
<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 || 0)}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{getProductQuantity(product.id) > 0 && (
|
||||
<div className="bg-gradient-to-r from-green-500/10 to-emerald-500/10 border border-green-500/20 rounded-lg px-4 py-2">
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{(product.price * getProductQuantity(product.id)).toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* БЛОК РЕЦЕПТУРЫ: 4 колонки с чекбоксами */}
|
||||
<div className="grid grid-cols-4 gap-4 pt-4 border-t border-white/10 mb-4">
|
||||
|
||||
{/* КОЛОНКА 1: Услуги фулфилмента */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-purple-400" />
|
||||
Услуги ФФ
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{fulfillmentServices.length > 0 ? fulfillmentServices.map(service => {
|
||||
const recipe = productRecipes[product.id];
|
||||
const isSelected = recipe?.selectedServices.includes(service.id) || false;
|
||||
return (
|
||||
<label key={service.id} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleService(product.id, service.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-purple-400 focus:ring-purple-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{service.name}</span>
|
||||
<span className="text-purple-400 text-xs font-medium">{service.price.toLocaleString('ru-RU')}₽</span>
|
||||
</label>
|
||||
);
|
||||
}) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
{selectedFulfillment ? 'Услуги загружаются...' : 'Выберите фулфилмент-центр'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 2: Расходники фулфилмента */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Box className="h-4 w-4 text-orange-400" />
|
||||
Расходники ФФ
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{fulfillmentConsumables.length > 0 ? fulfillmentConsumables.map(consumable => {
|
||||
const recipe = productRecipes[product.id];
|
||||
const isSelected = recipe?.selectedFFConsumables.includes(consumable.id) || false;
|
||||
return (
|
||||
<label key={consumable.id} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleFFConsumable(product.id, consumable.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-orange-400 focus:ring-orange-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{consumable.name}</span>
|
||||
<span className="text-orange-400 text-xs font-medium">{consumable.price.toLocaleString('ru-RU')}₽</span>
|
||||
</label>
|
||||
);
|
||||
}) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
{selectedFulfillment ? 'Расходники загружаются...' : 'Выберите фулфилмент-центр'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 3: Расходники селлера */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-blue-400" />
|
||||
Расходники селлера
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{sellerConsumables.length > 0 ? sellerConsumables.map(consumable => {
|
||||
const recipe = productRecipes[product.id];
|
||||
const isSelected = recipe?.selectedSellerConsumables.includes(consumable.id) || false;
|
||||
return (
|
||||
<label key={consumable.id} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSellerConsumable(product.id, consumable.id)}
|
||||
className="w-3 h-3 rounded bg-white/10 border-white/20 text-blue-400 focus:ring-blue-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{consumable.name}</span>
|
||||
<span className="text-blue-400 text-xs">Склад: {consumable.stock}</span>
|
||||
</label>
|
||||
);
|
||||
}) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
Расходники селлера загружаются...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* КОЛОНКА 4: Карточки Wildberries */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-white/80 font-medium text-sm flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4 text-pink-400" />
|
||||
Карточки WB
|
||||
</h5>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{wbCards.length > 0 ? wbCards.map(card => {
|
||||
const recipe = productRecipes[product.id];
|
||||
const isSelected = recipe?.selectedWBCard === card.id;
|
||||
return (
|
||||
<label key={card.id} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-white/5 p-1 rounded">
|
||||
<input
|
||||
type="radio"
|
||||
name={`wb-card-${product.id}`}
|
||||
checked={isSelected}
|
||||
onChange={() => setWBCard(product.id, card.id)}
|
||||
className="w-3 h-3 rounded-full bg-white/10 border-white/20 text-pink-400 focus:ring-pink-400/50 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-white/70 flex-1 truncate">{card.title}</span>
|
||||
<span className="text-pink-400 text-xs">{card.nmID}</span>
|
||||
</label>
|
||||
);
|
||||
}) : (
|
||||
<div className="text-white/50 text-xs p-2 bg-white/5 rounded border border-white/10">
|
||||
Карточки WB загружаются...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* НИЖНИЙ БЛОК: Итоговая стоимость рецептуры + кнопка добавления */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/10">
|
||||
<div className="flex items-center gap-6">
|
||||
{(() => {
|
||||
const quantity = getProductQuantity(product.id);
|
||||
const recipeCost = calculateRecipeCost(product.id);
|
||||
const productTotal = product.price * quantity;
|
||||
const totalRecipePrice = productTotal + recipeCost.total;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-sm text-white/70">
|
||||
Товар: <span className="text-white font-semibold">{productTotal.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
Услуги: <span className="text-purple-400 font-semibold">{recipeCost.services.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
<div className="text-sm text-white/70">
|
||||
Расходники: <span className="text-orange-400 font-semibold">{recipeCost.consumables.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{(() => {
|
||||
const quantity = getProductQuantity(product.id);
|
||||
const recipeCost = calculateRecipeCost(product.id);
|
||||
const productTotal = product.price * quantity;
|
||||
const totalRecipePrice = productTotal + recipeCost.total;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded-lg px-4 py-2">
|
||||
<span className="text-white/70 text-sm">Итого: </span>
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{totalRecipePrice.toLocaleString('ru-RU')} ₽
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => addToCart(product)}
|
||||
disabled={quantity === 0}
|
||||
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white border border-green-500/30 hover:border-green-400/50 transition-all duration-200"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||
Добавить рецептуру
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user