WIP: supplies and fulfillment updates

This commit is contained in:
Veronika Smirnova
2025-07-31 11:34:47 +03:00
parent b8ceba0913
commit f037c4888f
16 changed files with 1630 additions and 1119 deletions

1
package-lock.json generated
View File

@ -7,6 +7,7 @@
"": {
"name": "sferav",
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@apollo/client": "^3.13.8",
"@apollo/server": "^4.12.2",

View File

@ -429,7 +429,10 @@ enum ScheduleStatus {
enum SupplyOrderStatus {
PENDING
CONFIRMED
SUPPLIER_APPROVED
LOGISTICS_CONFIRMED
IN_TRANSIT
SHIPPED
DELIVERED
CANCELLED
}

View File

@ -40,7 +40,15 @@ interface SupplyOrder {
id: string;
partnerId: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
status:
| "PENDING"
| "CONFIRMED"
| "SUPPLIER_APPROVED"
| "LOGISTICS_CONFIRMED"
| "IN_TRANSIT"
| "SHIPPED"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;

View File

@ -37,7 +37,15 @@ interface SupplyOrderItem {
interface SupplyOrder {
id: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
status:
| "PENDING"
| "CONFIRMED"
| "SUPPLIER_APPROVED"
| "LOGISTICS_CONFIRMED"
| "IN_TRANSIT"
| "SHIPPED"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
@ -101,10 +109,22 @@ export function SuppliesConsumablesTab() {
label: "Подтверждена",
color: "bg-green-500/20 text-green-300 border-green-500/30",
},
SUPPLIER_APPROVED: {
label: "Одобрена поставщиком",
color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
},
LOGISTICS_CONFIRMED: {
label: "Подтверждена логистикой",
color: "bg-teal-500/20 text-teal-300 border-teal-500/30",
},
IN_TRANSIT: {
label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
SHIPPED: {
label: "Отправлена",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
},
DELIVERED: {
label: "Доставлена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
@ -154,29 +174,29 @@ export function SuppliesConsumablesTab() {
return (
<div className="space-y-6">
{/* Статистика заказов поставок */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-orange-500/20 rounded-lg">
<Box className="h-5 w-5 text-orange-400" />
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 mb-4">
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-orange-500/20 rounded-lg">
<Box className="h-4 w-4 text-orange-400" />
</div>
<div>
<p className="text-white/60 text-xs">Заказов поставок</p>
<p className="text-xl font-bold text-white">
<p className="text-base font-bold text-white">
{supplyOrders.length}
</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-green-500/20 rounded-lg">
<TrendingUp className="h-5 w-5 text-green-400" />
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-green-500/20 rounded-lg">
<TrendingUp className="h-4 w-4 text-green-400" />
</div>
<div>
<p className="text-white/60 text-xs">Общая сумма</p>
<p className="text-xl font-bold text-white">
<p className="text-base font-bold text-white">
{formatCurrency(
supplyOrders.reduce(
(sum, order) => sum + Number(order.totalAmount),
@ -188,14 +208,14 @@ export function SuppliesConsumablesTab() {
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-yellow-500/20 rounded-lg">
<Calendar className="h-5 w-5 text-yellow-400" />
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-yellow-500/20 rounded-lg">
<Calendar className="h-4 w-4 text-yellow-400" />
</div>
<div>
<p className="text-white/60 text-xs">В пути</p>
<p className="text-xl font-bold text-white">
<p className="text-base font-bold text-white">
{
supplyOrders.filter((order) => order.status === "IN_TRANSIT")
.length
@ -205,14 +225,14 @@ export function SuppliesConsumablesTab() {
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Calendar className="h-5 w-5 text-blue-400" />
<Card className="bg-white/10 backdrop-blur border-white/20 p-2">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-blue-500/20 rounded-lg">
<Calendar className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-xs">Доставлено</p>
<p className="text-xl font-bold text-white">
<p className="text-base font-bold text-white">
{
supplyOrders.filter((order) => order.status === "DELIVERED")
.length

View File

@ -495,7 +495,7 @@ export function CreateConsumablesSupplyPage() {
</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-3">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-2">
{supplierProducts.map(
(product: ConsumableProduct, index) => {
const selectedQuantity = getSelectedQuantity(
@ -504,14 +504,14 @@ export function CreateConsumablesSupplyPage() {
return (
<Card
key={product.id}
className={`relative bg-gradient-to-br from-white/10 via-white/5 to-white/10 backdrop-blur border border-white/20 p-3 rounded-xl overflow-hidden group hover:shadow-xl transition-all duration-300 ${
className={`relative bg-gradient-to-br from-white/10 via-white/5 to-white/10 backdrop-blur border border-white/20 p-2 rounded-xl overflow-hidden group hover:shadow-xl transition-all duration-300 ${
selectedQuantity > 0
? "ring-2 ring-green-400/50 bg-gradient-to-br from-green-500/20 via-green-400/10 to-green-500/20"
: "hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40"
}`}
style={{
animationDelay: `${index * 50}ms`,
minHeight: "200px",
minHeight: "180px",
width: "100%",
}}
>

View File

@ -20,19 +20,19 @@ export function AllSuppliesTab({
const isWholesale = user?.organization?.type === "WHOLESALE";
return (
<div className="h-full overflow-hidden space-y-4">
<div className="h-full overflow-hidden space-y-2">
{/* Секция товаров */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<h3 className="text-white font-semibold text-lg mb-3">Товары</h3>
<div className="h-64 overflow-hidden">
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<h3 className="text-white font-semibold text-base mb-2">Товары</h3>
<div className="h-48 overflow-hidden">
<FulfillmentGoodsTab />
</div>
</Card>
{/* Секция расходников */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<h3 className="text-white font-semibold text-lg mb-3">Расходники</h3>
<div className="h-64 overflow-hidden">
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
<h3 className="text-white font-semibold text-base mb-2">Расходники</h3>
<div className="h-48 overflow-hidden">
{isWholesale ? <RealSupplyOrdersTab /> : <SellerSupplyOrdersTab />}
</div>
</Card>

View File

@ -12,7 +12,6 @@ import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth";
import {
ChevronRight,
ChevronDown,
CheckCircle,
XCircle,
@ -93,15 +92,15 @@ const TableHeader = ({
onSort?: (field: string) => void;
}) => (
<div
className={`px-3 py-2 text-xs font-bold text-white flex items-center justify-between ${
sortable ? "cursor-pointer hover:bg-white/5" : ""
className={`px-3 py-2 text-xs font-semibold text-white/90 flex items-center justify-between transition-colors ${
sortable ? "cursor-pointer hover:bg-white/5 hover:text-white" : ""
}`}
onClick={() => sortable && onSort && onSort(field)}
>
<span>{children}</span>
<span className="select-none">{children}</span>
{sortable && (
<ArrowUpDown
className={`h-3 w-3 ml-1 ${
className={`h-3 w-3 ml-1 transition-colors ${
sortField === field ? "text-blue-400" : "text-white/40"
}`}
/>
@ -109,7 +108,7 @@ const TableHeader = ({
</div>
);
// Компонент для статистических карточек
// Современный компонент для статистических карточек
const StatsCard = ({
title,
value,
@ -127,38 +126,37 @@ const StatsCard = ({
iconBg?: string;
subtitle?: string;
}) => (
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className={`p-2 rounded-lg ${iconBg}`}>
<Icon className={`h-5 w-5 ${iconColor}`} />
</div>
<div>
<p className="text-white/60 text-sm">{title}</p>
<div className="flex items-center space-x-2">
<p className="text-white text-xl font-bold">{value}</p>
{change !== 0 && (
<div className="flex items-center space-x-1">
{change > 0 ? (
<TrendingUp className="h-3 w-3 text-green-400" />
) : (
<TrendingDown className="h-3 w-3 text-red-400" />
)}
<span
className={`text-xs font-medium ${
change > 0 ? "text-green-400" : "text-red-400"
}`}
>
{Math.abs(change)}%
</span>
</div>
)}
</div>
{subtitle && <p className="text-white/40 text-xs mt-1">{subtitle}</p>}
</div>
<div className="relative overflow-hidden rounded-lg bg-white/[0.06] backdrop-blur border border-white/[0.08] hover:border-white/15 transition-all duration-200">
<div className="p-2 flex items-center space-x-2">
<div className={`p-1.5 rounded-lg ${iconBg} flex-shrink-0`}>
<Icon className={`h-4 w-4 ${iconColor}`} />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white/70 text-[10px] font-medium truncate">
{title}
</h3>
<p className="text-sm font-bold text-white">{value}</p>
</div>
{change !== 0 && (
<div className="flex items-center space-x-1 flex-shrink-0">
{change > 0 ? (
<TrendingUp className="h-2.5 w-2.5 text-emerald-400" />
) : (
<TrendingDown className="h-2.5 w-2.5 text-red-400" />
)}
<span
className={`text-[10px] font-medium ${
change > 0 ? "text-emerald-400" : "text-red-400"
}`}
>
{Math.abs(change)}%
</span>
</div>
)}
</div>
</Card>
</div>
);
export function RealSupplyOrdersTab() {
@ -401,13 +399,25 @@ export function RealSupplyOrdersTab() {
label: "Одобрена",
className: "bg-green-500/20 text-green-300 border-green-500/30",
},
SUPPLIER_APPROVED: {
label: "Одобрена поставщиком",
className: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
},
LOGISTICS_CONFIRMED: {
label: "Подтверждена логистикой",
className: "bg-teal-500/20 text-teal-300 border-teal-500/30",
},
IN_TRANSIT: {
label: "В пути",
className: "bg-blue-500/20 text-blue-300 border-blue-500/30",
},
SHIPPED: {
label: "Отправлена",
className: "bg-orange-500/20 text-orange-300 border-orange-500/30",
},
DELIVERED: {
label: "Доставлена",
className: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
className: "bg-purple-500/20 text-purple-300 border-purple-500/30",
},
CANCELLED: {
label: "Отменена",
@ -559,9 +569,9 @@ export function RealSupplyOrdersTab() {
return (
<div className="h-full flex flex-col overflow-hidden">
{/* Статистические карточки - 30% экрана */}
<div className="flex-shrink-0 mb-4" style={{ maxHeight: "30vh" }}>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{/* Компактные статистические карточки */}
<div className="flex-shrink-0 mb-3">
<div className="grid grid-cols-6 gap-2">
<StatsCard
title="Всего заявок"
value={totalOrders}
@ -613,40 +623,54 @@ export function RealSupplyOrdersTab() {
</div>
</div>
{/* Основная таблица - 70% экрана */}
{/* Современная таблица заявок */}
<div className="flex-1 flex flex-col overflow-hidden">
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 flex flex-col overflow-hidden">
{/* Шапка таблицы с поиском */}
<div className="p-4 border-b border-white/10 flex-shrink-0">
<div className="bg-white/[0.06] backdrop-blur border border-white/[0.08] rounded-lg flex-1 flex flex-col overflow-hidden">
{/* Компактная шапка с поиском */}
<div className="p-3 border-b border-white/10 flex-shrink-0">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-white flex items-center space-x-2">
<Store className="h-4 w-4 text-blue-400" />
<span>Заявки на расходники</span>
</h2>
{/* Поиск */}
<div className="relative mx-2.5 flex-1 max-w-xs">
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-white/40" />
<Input
placeholder="Поиск по заявкам..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40"
/>
<div className="flex items-center space-x-2">
<div className="p-1.5 rounded-lg bg-blue-500/20">
<Store className="h-4 w-4 text-blue-400" />
</div>
<div>
<h2 className="text-base font-semibold text-white">
Заявки на расходники
</h2>
</div>
</div>
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs"
>
{filteredAndSortedOrders.length} заявок
</Badge>
<div className="flex items-center space-x-3">
{/* Компактный поиск */}
<div className="relative">
<div className="absolute left-2.5 top-1/2 transform -translate-y-1/2">
<Search className="h-3.5 w-3.5 text-white/40" />
</div>
<Input
placeholder="Поиск..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 pr-3 py-1.5 w-48 bg-white/5 border-white/20 rounded-lg text-sm text-white placeholder:text-white/50 focus:bg-white/10 focus:border-white/30 transition-all duration-200"
/>
</div>
<div className="px-2.5 py-1.5 rounded-lg bg-blue-500/20 border border-blue-500/30">
<span className="text-blue-300 font-medium text-xs">
{filteredAndSortedOrders.length}
</span>
</div>
</div>
</div>
</div>
{/* Заголовки таблицы */}
<div className="flex-shrink-0 bg-blue-500/20 border-b border-blue-500/40">
<div className="grid grid-cols-7 gap-0">
{/* Современные заголовки таблицы */}
<div className="flex-shrink-0 bg-gradient-to-r from-slate-800/50 to-slate-700/50 border-b border-white/10">
<div
className="grid gap-0"
style={{
gridTemplateColumns: "100px 2fr 120px 80px 100px 100px 300px",
}}
>
<TableHeader
field="id"
sortable
@ -705,26 +729,31 @@ export function RealSupplyOrdersTab() {
</div>
</div>
{/* Строка с итогами */}
<div className="flex-shrink-0 bg-blue-500/25 border-b border-blue-500/50">
<div className="grid grid-cols-7 gap-0">
<div className="px-3 py-2 text-xs font-bold text-blue-300">
{/* Компактная строка итогов */}
<div className="flex-shrink-0 bg-gradient-to-r from-emerald-500/10 to-blue-500/10 border-b border-emerald-500/20">
<div
className="grid gap-0"
style={{
gridTemplateColumns: "100px 2fr 120px 80px 100px 100px 300px",
}}
>
<div className="px-3 py-2 text-xs font-bold text-emerald-300">
ИТОГО ({totals.orders})
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
{totals.orders} заказчиков
</div>
<div className="px-3 py-2 text-xs font-bold text-white">-</div>
<div className="px-3 py-2 text-xs font-bold text-white/60"></div>
<div className="px-3 py-2 text-xs font-bold text-white">
{formatNumber(totals.items)} шт
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="px-3 py-2 text-xs font-bold text-emerald-400">
{formatCurrency(totals.amount)}
</div>
<div className="px-3 py-2 text-xs font-bold text-white">
<div className="px-3 py-2 text-xs font-bold text-amber-400">
{totals.pending} ожидают
</div>
<div className="px-3 py-2 text-xs font-bold text-white">-</div>
<div className="px-3 py-2 text-xs font-bold text-white/60"></div>
</div>
</div>
@ -760,65 +789,66 @@ export function RealSupplyOrdersTab() {
return (
<div
key={order.id}
className={`border-b ${colorScheme.border} ${colorScheme.hover} transition-colors border-l-8 ${colorScheme.borderLeft} ${colorScheme.bg} shadow-sm hover:shadow-md`}
className="group border-b border-white/5 hover:bg-white/[0.02] transition-all duration-200 hover:border-white/10"
>
{/* Основная строка заказа */}
<div className="grid grid-cols-7 gap-0">
{/* Компактная строка заказа */}
<div
className="grid gap-0 cursor-pointer hover:bg-white/[0.03] transition-colors"
style={{
gridTemplateColumns:
"100px 2fr 120px 80px 100px 100px 300px",
}}
onClick={() => toggleOrderExpansion(order.id)}
>
<div className="px-3 py-2.5 flex items-center space-x-2">
<span className="text-white/60 text-xs">
{filteredAndSortedOrders.length - index}
</span>
<button
onClick={() => toggleOrderExpansion(order.id)}
className="text-white/60 hover:text-white"
>
{isOrderExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
<span className="text-white font-medium text-xs">
{order.id.slice(-8)}
<span className="text-white/50 text-xs font-medium">
#{filteredAndSortedOrders.length - index}
</span>
<div className="px-1.5 py-0.5 rounded bg-white/5 border border-white/10">
<span className="text-white font-mono text-[10px]">
{order.id.slice(-8)}
</span>
</div>
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<Avatar className="w-6 h-6">
<Avatar className="w-6 h-6 ring-1 ring-white/10">
<AvatarFallback
className={`${getColorForOrder(
order.id
)} text-white text-xs`}
)} text-white text-xs font-semibold`}
>
{getInitials(organizationName)}
</AvatarFallback>
</Avatar>
<div>
<span className="text-white font-medium text-sm">
<span className="text-white font-medium text-xs">
{organizationName}
</span>
<p className="text-white/60 text-xs">
<p className="text-white/60 text-[10px]">
{order.organization.type}
</p>
</div>
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<Calendar className="h-4 w-4 text-white/40" />
<span className="text-white font-semibold text-sm">
<div className="px-3 py-2.5 flex items-center space-x-1.5">
<Calendar className="h-3.5 w-3.5 text-blue-400" />
<span className="text-white font-medium text-xs">
{formatDate(order.deliveryDate)}
</span>
</div>
<div className="px-3 py-2.5">
<span className="text-white font-semibold text-sm">
{order.totalItems} шт
</span>
<div className="px-1.5 py-0.5 rounded bg-slate-500/20">
<span className="text-white font-semibold text-xs">
{order.totalItems} шт
</span>
</div>
</div>
<div className="px-3 py-2.5 flex items-center space-x-2">
<DollarSign className="h-4 w-4 text-white/40" />
<span className="text-green-400 font-bold text-sm">
<div className="px-3 py-2.5 flex items-center space-x-1.5">
<DollarSign className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-emerald-400 font-bold text-xs">
{formatCurrency(order.totalAmount)}
</span>
</div>
@ -827,7 +857,7 @@ export function RealSupplyOrdersTab() {
{getStatusBadge(order.status)}
</div>
<div className="px-3 py-2.5">
<div className="px-1 py-2.5">
<div className="flex items-center space-x-1">
{order.status === "PENDING" && (
<>
@ -837,10 +867,10 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "CONFIRMED")
}
disabled={updating}
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-2 py-1 h-6"
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-1.5 py-0.5 h-5"
>
<CheckCircle className="h-3 w-3 mr-1" />
Одобрить
<CheckCircle className="h-2.5 w-2.5 mr-0.5" />
Одобр.
</Button>
<Button
size="sm"
@ -848,10 +878,10 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "CANCELLED")
}
disabled={updating}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-2 py-1 h-6"
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-1.5 py-0.5 h-5"
>
<XCircle className="h-3 w-3 mr-1" />
Отклонить
<XCircle className="h-2.5 w-2.5 mr-0.5" />
Откл.
</Button>
</>
)}
@ -862,10 +892,10 @@ export function RealSupplyOrdersTab() {
handleStatusUpdate(order.id, "IN_TRANSIT")
}
disabled={updating}
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30 text-xs px-2 py-1 h-6"
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30 text-xs px-1.5 py-0.5 h-5"
>
<Truck className="h-3 w-3 mr-1" />
Отправить
<Truck className="h-2.5 w-2.5 mr-0.5" />
Отпр.
</Button>
)}
{order.status === "CANCELLED" && (
@ -951,7 +981,7 @@ export function RealSupplyOrdersTab() {
})
)}
</div>
</Card>
</div>
</div>
</div>
);

View File

@ -42,7 +42,15 @@ interface SupplyOrderItem {
interface SupplyOrder {
id: string;
deliveryDate: string;
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
status:
| "PENDING"
| "CONFIRMED"
| "SUPPLIER_APPROVED"
| "LOGISTICS_CONFIRMED"
| "IN_TRANSIT"
| "SHIPPED"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
@ -106,10 +114,22 @@ export function SellerSupplyOrdersTab() {
label: "Одобрена",
color: "bg-green-500/20 text-green-300 border-green-500/30",
},
SUPPLIER_APPROVED: {
label: "Одобрена поставщиком",
color: "bg-emerald-500/20 text-emerald-300 border-emerald-500/30",
},
LOGISTICS_CONFIRMED: {
label: "Подтверждена логистикой",
color: "bg-teal-500/20 text-teal-300 border-teal-500/30",
},
IN_TRANSIT: {
label: "В пути",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
},
SHIPPED: {
label: "Отправлена",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
},
DELIVERED: {
label: "Доставлена",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",

View File

@ -1,59 +1,58 @@
"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 {
Plus,
Minus,
Eye,
Heart,
ShoppingCart
} from 'lucide-react'
import React from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Plus, Minus, Eye, Heart, ShoppingCart } from "lucide-react";
import { WholesalerProduct } from './types'
import { WholesalerProduct } from "./types";
interface ProductCardProps {
product: WholesalerProduct
selectedQuantity: number
onQuantityChange: (quantity: number) => void
formatCurrency: (amount: number) => string
product: WholesalerProduct;
selectedQuantity: number;
onQuantityChange: (quantity: number) => void;
formatCurrency: (amount: number) => string;
}
export function ProductCard({
product,
selectedQuantity,
onQuantityChange,
formatCurrency
export function ProductCard({
product,
selectedQuantity,
onQuantityChange,
formatCurrency,
}: ProductCardProps) {
const discountedPrice = product.discount
const discountedPrice = product.discount
? product.price * (1 - product.discount / 100)
: product.price
: product.price;
const handleQuantityChange = (newQuantity: number) => {
const clampedQuantity = Math.max(0, Math.min(product.quantity, newQuantity))
onQuantityChange(clampedQuantity)
}
const clampedQuantity = Math.max(
0,
Math.min(product.quantity, newQuantity)
);
onQuantityChange(clampedQuantity);
};
return (
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden group hover:bg-white/15 hover:border-white/30 transition-all duration-300 hover:scale-105 hover:shadow-2xl">
<div className="aspect-square relative bg-white/5 overflow-hidden">
<img
src={product.mainImage || '/api/placeholder/400/400'}
src={product.mainImage || "/api/placeholder/400/400"}
alt={product.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
{/* Количество в наличии */}
<div className="absolute top-2 right-2">
<Badge className={`${
product.quantity > 50
? 'bg-green-500/80'
: product.quantity > 10
? 'bg-yellow-500/80'
: 'bg-red-500/80'
} text-white border-0 backdrop-blur text-xs`}>
<Badge
className={`${
product.quantity > 50
? "bg-green-500/80"
: product.quantity > 10
? "bg-yellow-500/80"
: "bg-red-500/80"
} text-white border-0 backdrop-blur text-xs`}
>
{product.quantity}
</Badge>
</div>
@ -70,17 +69,25 @@ export function ProductCard({
{/* Overlay с кнопками */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<div className="flex space-x-2">
<Button size="sm" variant="secondary" className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30">
<Button
size="sm"
variant="secondary"
className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30"
>
<Eye className="h-4 w-4" />
</Button>
<Button size="sm" variant="secondary" className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30">
<Button
size="sm"
variant="secondary"
className="bg-white/20 backdrop-blur text-white border-white/30 hover:bg-white/30"
>
<Heart className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="p-3 space-y-3">
<div className="p-2 space-y-2">
{/* Заголовок и бренд */}
<div>
<div className="flex items-center justify-between mb-1">
@ -102,7 +109,7 @@ export function ProductCard({
)}
</div>
</div>
<h3 className="text-white font-semibold text-sm mb-1 line-clamp-2 leading-tight">
<h3 className="text-white font-semibold text-xs mb-1 line-clamp-2 leading-tight">
{product.name}
</h3>
</div>
@ -110,17 +117,19 @@ export function ProductCard({
{/* Основная характеристика */}
<div className="text-white/60 text-xs">
{product.color && <span className="text-white">{product.color}</span>}
{product.size && <span className="text-white ml-2">{product.size}</span>}
{product.size && (
<span className="text-white ml-2">{product.size}</span>
)}
</div>
{/* Цена */}
<div className="pt-2 border-t border-white/10">
<div className="pt-1.5 border-t border-white/10">
<div className="flex items-center space-x-2">
<div className="text-white font-bold text-lg">
<div className="text-white font-bold text-base">
{formatCurrency(discountedPrice)}
</div>
{product.discount && (
<div className="text-white/40 text-sm line-through">
<div className="text-white/40 text-xs line-through">
{formatCurrency(product.price)}
</div>
)}
@ -144,9 +153,9 @@ export function ProductCard({
pattern="[0-9]*"
value={selectedQuantity}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9]/g, '')
const numValue = parseInt(value) || 0
handleQuantityChange(numValue)
const value = e.target.value.replace(/[^0-9]/g, "");
const numValue = parseInt(value) || 0;
handleQuantityChange(numValue);
}}
onFocus={(e) => e.target.select()}
className="h-8 w-12 text-center bg-white/10 border border-white/20 text-white text-sm rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
@ -160,7 +169,7 @@ export function ProductCard({
>
<Plus className="h-3 w-3" />
</Button>
{selectedQuantity > 0 && (
<Badge className="bg-gradient-to-r from-purple-500 to-pink-500 text-white border-0 text-xs ml-auto">
<ShoppingCart className="h-3 w-3 mr-1" />
@ -179,5 +188,5 @@ export function ProductCard({
)}
</div>
</Card>
)
}
);
}

View File

@ -1,24 +1,24 @@
"use client"
"use client";
import React from 'react'
import { ProductCard } from './product-card'
import { Package } from 'lucide-react'
import { WholesalerProduct } from './types'
import React from "react";
import { ProductCard } from "./product-card";
import { Package } from "lucide-react";
import { WholesalerProduct } from "./types";
interface ProductGridProps {
products: WholesalerProduct[]
selectedProducts: Record<string, number>
onQuantityChange: (productId: string, quantity: number) => void
formatCurrency: (amount: number) => string
loading?: boolean
products: WholesalerProduct[];
selectedProducts: Record<string, number>;
onQuantityChange: (productId: string, quantity: number) => void;
formatCurrency: (amount: number) => string;
loading?: boolean;
}
export function ProductGrid({
products,
selectedProducts,
onQuantityChange,
export function ProductGrid({
products,
selectedProducts,
onQuantityChange,
formatCurrency,
loading = false
loading = false,
}: ProductGridProps) {
if (loading) {
return (
@ -28,7 +28,7 @@ export function ProductGrid({
<p className="text-white/60">Загружаем товары...</p>
</div>
</div>
)
);
}
if (products.length === 0) {
@ -37,23 +37,27 @@ export function ProductGrid({
<div className="text-center">
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
<p className="text-white/60">У этого поставщика нет товаров</p>
<p className="text-white/40 text-sm mt-2">Выберите другого поставщика</p>
<p className="text-white/40 text-sm mt-2">
Выберите другого поставщика
</p>
</div>
</div>
)
);
}
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
selectedQuantity={selectedProducts[product.id] || 0}
onQuantityChange={(quantity) => onQuantityChange(product.id, quantity)}
onQuantityChange={(quantity) =>
onQuantityChange(product.id, quantity)
}
formatCurrency={formatCurrency}
/>
))}
</div>
)
}
);
}

View File

@ -194,11 +194,11 @@ export function SupplierSelection({
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{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"
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-105"
onClick={() => setSelectedSupplier(supplier)}
>
<div className="space-y-4">

View File

@ -61,14 +61,14 @@ export function SuppliesDashboard() {
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main
className={`flex-1 ${getSidebarMargin()} px-2 py-2 overflow-hidden transition-all duration-300`}
className={`flex-1 ${getSidebarMargin()} px-1 py-1 overflow-hidden transition-all duration-300`}
>
<div className="h-full">
{/* Уведомляющий баннер */}
{hasPendingItems && (
<Alert className="mb-4 bg-blue-500/20 border-blue-400/30 text-blue-300 animate-pulse">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<Alert className="mb-2 bg-blue-500/20 border-blue-400/30 text-blue-300 animate-pulse py-2">
<AlertTriangle className="h-3 w-3" />
<AlertDescription className="text-xs">
У вас есть {pendingCount.total} элемент
{pendingCount.total > 1
? pendingCount.total < 5
@ -105,7 +105,7 @@ export function SuppliesDashboard() {
onValueChange={setActiveTab}
className="w-full h-full flex flex-col"
>
<div className="flex items-center justify-between mb-1 flex-wrap gap-2">
<div className="flex items-center justify-between mb-1 flex-wrap gap-1">
<TabsList
className={`grid grid-cols-3 bg-white/10 backdrop-blur border-white/20 w-fit text-sm ${
hasPendingItems ? "ring-2 ring-blue-400/50" : ""

View File

@ -32,21 +32,21 @@ export function StatsCard({
return (
<Card
className={cn(
"bg-white/10 backdrop-blur border-white/20 p-2 sm:p-3 hover:bg-white/15 transition-all duration-300",
"bg-white/10 backdrop-blur border-white/20 p-1.5 sm:p-2 hover:bg-white/15 transition-all duration-300",
className
)}
>
<div className="flex items-start justify-between">
<div className="flex items-center space-x-2 flex-1">
<div className={cn("p-1.5 sm:p-2 rounded-lg", iconBg)}>
<Icon className={cn("h-3 w-3 sm:h-4 sm:w-4", iconColor)} />
<div className={cn("p-1 sm:p-1.5 rounded-lg", iconBg)}>
<Icon className={cn("h-3 w-3", iconColor)} />
</div>
<div className="flex-1 min-w-0">
<p className="text-white/60 text-xs font-medium truncate">
{title}
</p>
<p
className="text-sm sm:text-lg font-bold text-white mt-0.5 truncate"
className="text-sm sm:text-base font-bold text-white mt-0.5 truncate"
title={value.toString()}
>
{value}

View File

@ -1,24 +1,39 @@
"use client"
"use client";
import React, { useState, useEffect } from 'react'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import React, { useState, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css"
import { Sidebar } from '@/components/dashboard/sidebar'
import { useSidebar } from '@/hooks/useSidebar'
import {
Search,
Plus,
Minus,
ShoppingCart,
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import {
Search,
Plus,
Minus,
ShoppingCart,
Calendar as CalendarIcon,
Phone,
User,
@ -29,690 +44,804 @@ import {
Check,
Eye,
ChevronLeft,
ChevronRight
} from 'lucide-react'
import { WildberriesService } from '@/services/wildberries-service'
import { useAuth } from '@/hooks/useAuth'
import { useQuery, useMutation } from '@apollo/client'
import { apolloClient } from '@/lib/apollo-client'
import { GET_MY_COUNTERPARTIES, GET_COUNTERPARTY_SERVICES, GET_COUNTERPARTY_SUPPLIES } from '@/graphql/queries'
import { CREATE_WILDBERRIES_SUPPLY } from '@/graphql/mutations'
import { toast } from 'sonner'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
import { SelectedCard, FulfillmentService, ConsumableService, WildberriesCard } from '@/types/supplies'
ChevronRight,
} from "lucide-react";
import { WildberriesService } from "@/services/wildberries-service";
import { useAuth } from "@/hooks/useAuth";
import { useQuery, useMutation } from "@apollo/client";
import { apolloClient } from "@/lib/apollo-client";
import {
GET_MY_COUNTERPARTIES,
GET_COUNTERPARTY_SERVICES,
GET_COUNTERPARTY_SUPPLIES,
} from "@/graphql/queries";
import { CREATE_WILDBERRIES_SUPPLY } from "@/graphql/mutations";
import { toast } from "sonner";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
import {
SelectedCard,
FulfillmentService,
ConsumableService,
WildberriesCard,
} from "@/types/supplies";
interface Organization {
id: string
name?: string
fullName?: string
type: string
id: string;
name?: string;
fullName?: string;
type: string;
}
interface WBProductCardsProps {
onBack: () => void
onComplete: (selectedCards: SelectedCard[]) => void
showSummary?: boolean
setShowSummary?: (show: boolean) => void
selectedCards?: SelectedCard[]
setSelectedCards?: (cards: SelectedCard[]) => void
onBack: () => void;
onComplete: (selectedCards: SelectedCard[]) => void;
showSummary?: boolean;
setShowSummary?: (show: boolean) => void;
selectedCards?: SelectedCard[];
setSelectedCards?: (cards: SelectedCard[]) => void;
}
export function WBProductCards({ onBack, onComplete, showSummary: externalShowSummary, setShowSummary: externalSetShowSummary, selectedCards: externalSelectedCards, setSelectedCards: externalSetSelectedCards }: WBProductCardsProps) {
const { user } = useAuth()
const { getSidebarMargin } = useSidebar()
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(false)
const [wbCards, setWbCards] = useState<WildberriesCard[]>([])
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([]) // Товары в корзине
export function WBProductCards({
onBack,
onComplete,
showSummary: externalShowSummary,
setShowSummary: externalSetShowSummary,
selectedCards: externalSelectedCards,
setSelectedCards: externalSetSelectedCards,
}: WBProductCardsProps) {
const { user } = useAuth();
const { getSidebarMargin } = useSidebar();
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [wbCards, setWbCards] = useState<WildberriesCard[]>([]);
const [selectedCards, setSelectedCards] = useState<SelectedCard[]>([]); // Товары в корзине
// Используем внешнее состояние если передано
const actualSelectedCards = externalSelectedCards !== undefined ? externalSelectedCards : selectedCards
const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards
const [preparingCards, setPreparingCards] = useState<SelectedCard[]>([]) // Товары, готовящиеся к добавлению
const [showSummary, setShowSummary] = useState(false)
const actualSelectedCards =
externalSelectedCards !== undefined ? externalSelectedCards : selectedCards;
const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards;
const [preparingCards, setPreparingCards] = useState<SelectedCard[]>([]); // Товары, готовящиеся к добавлению
const [showSummary, setShowSummary] = useState(false);
// Используем внешнее состояние если передано
const actualShowSummary = externalShowSummary !== undefined ? externalShowSummary : showSummary
const actualSetShowSummary = externalSetShowSummary || setShowSummary
const [globalDeliveryDate, setGlobalDeliveryDate] = useState<Date | undefined>(undefined)
const [fulfillmentServices, setFulfillmentServices] = useState<FulfillmentService[]>([])
const [organizationServices, setOrganizationServices] = useState<{[orgId: string]: Array<{id: string, name: string, description?: string, price: number}>}>({})
const [organizationSupplies, setOrganizationSupplies] = useState<{[orgId: string]: Array<{id: string, name: string, description?: string, price: number}>}>({})
const [selectedCardForDetails, setSelectedCardForDetails] = useState<WildberriesCard | null>(null)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const actualShowSummary =
externalShowSummary !== undefined ? externalShowSummary : showSummary;
const actualSetShowSummary = externalSetShowSummary || setShowSummary;
const [globalDeliveryDate, setGlobalDeliveryDate] = useState<
Date | undefined
>(undefined);
const [fulfillmentServices, setFulfillmentServices] = useState<
FulfillmentService[]
>([]);
const [organizationServices, setOrganizationServices] = useState<{
[orgId: string]: Array<{
id: string;
name: string;
description?: string;
price: number;
}>;
}>({});
const [organizationSupplies, setOrganizationSupplies] = useState<{
[orgId: string]: Array<{
id: string;
name: string;
description?: string;
price: number;
}>;
}>({});
const [selectedCardForDetails, setSelectedCardForDetails] =
useState<WildberriesCard | null>(null);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
// Моковые товары для демонстрации
const getMockCards = (): WildberriesCard[] => [
{
nmID: 123456789,
vendorCode: 'SKU001',
title: 'Смартфон Samsung Galaxy A54',
description: 'Современный смартфон с отличной камерой и долгим временем автономной работы',
brand: 'Samsung',
object: 'Смартфоны',
parent: 'Электроника',
countryProduction: 'Корея',
supplierVendorCode: 'SUPPLIER-001',
mediaFiles: ['/api/placeholder/400/400', '/api/placeholder/400/401', '/api/placeholder/400/402'],
vendorCode: "SKU001",
title: "Смартфон Samsung Galaxy A54",
description:
"Современный смартфон с отличной камерой и долгим временем автономной работы",
brand: "Samsung",
object: "Смартфоны",
parent: "Электроника",
countryProduction: "Корея",
supplierVendorCode: "SUPPLIER-001",
mediaFiles: [
"/api/placeholder/400/400",
"/api/placeholder/400/401",
"/api/placeholder/400/402",
],
sizes: [
{
chrtID: 123456,
techSize: '128GB',
wbSize: '128GB Черный',
techSize: "128GB",
wbSize: "128GB Черный",
price: 25990,
discountedPrice: 22990,
quantity: 15
}
]
quantity: 15,
},
],
},
{
nmID: 987654321,
vendorCode: 'SKU002',
title: 'Наушники Apple AirPods Pro',
description: 'Беспроводные наушники с активным шумоподавлением и пространственным звуком',
brand: 'Apple',
object: 'Наушники',
parent: 'Электроника',
countryProduction: 'Китай',
supplierVendorCode: 'SUPPLIER-002',
mediaFiles: ['/api/placeholder/400/403', '/api/placeholder/400/404'],
vendorCode: "SKU002",
title: "Наушники Apple AirPods Pro",
description:
"Беспроводные наушники с активным шумоподавлением и пространственным звуком",
brand: "Apple",
object: "Наушники",
parent: "Электроника",
countryProduction: "Китай",
supplierVendorCode: "SUPPLIER-002",
mediaFiles: ["/api/placeholder/400/403", "/api/placeholder/400/404"],
sizes: [
{
chrtID: 987654,
techSize: 'Standard',
wbSize: 'Белый',
techSize: "Standard",
wbSize: "Белый",
price: 24990,
discountedPrice: 19990,
quantity: 8
}
]
quantity: 8,
},
],
},
{
nmID: 555666777,
vendorCode: 'SKU003',
title: 'Кроссовки Nike Air Max 270',
description: 'Спортивные кроссовки с современным дизайном и комфортной посадкой',
brand: 'Nike',
object: 'Кроссовки',
parent: 'Обувь',
countryProduction: 'Вьетнам',
supplierVendorCode: 'SUPPLIER-003',
mediaFiles: ['/api/placeholder/400/405', '/api/placeholder/400/406', '/api/placeholder/400/407'],
vendorCode: "SKU003",
title: "Кроссовки Nike Air Max 270",
description:
"Спортивные кроссовки с современным дизайном и комфортной посадкой",
brand: "Nike",
object: "Кроссовки",
parent: "Обувь",
countryProduction: "Вьетнам",
supplierVendorCode: "SUPPLIER-003",
mediaFiles: [
"/api/placeholder/400/405",
"/api/placeholder/400/406",
"/api/placeholder/400/407",
],
sizes: [
{
chrtID: 555666,
techSize: '42',
wbSize: '42 EU',
techSize: "42",
wbSize: "42 EU",
price: 12990,
discountedPrice: 9990,
quantity: 25
quantity: 25,
},
{
chrtID: 555667,
techSize: '43',
wbSize: '43 EU',
techSize: "43",
wbSize: "43 EU",
price: 12990,
discountedPrice: 9990,
quantity: 20
}
]
quantity: 20,
},
],
},
{
nmID: 444333222,
vendorCode: 'SKU004',
title: 'Футболка Adidas Originals',
description: 'Классическая футболка из органического хлопка с логотипом бренда',
brand: 'Adidas',
object: 'Футболки',
parent: 'Одежда',
countryProduction: 'Бангладеш',
supplierVendorCode: 'SUPPLIER-004',
mediaFiles: ['/api/placeholder/400/408', '/api/placeholder/400/409'],
vendorCode: "SKU004",
title: "Футболка Adidas Originals",
description:
"Классическая футболка из органического хлопка с логотипом бренда",
brand: "Adidas",
object: "Футболки",
parent: "Одежда",
countryProduction: "Бангладеш",
supplierVendorCode: "SUPPLIER-004",
mediaFiles: ["/api/placeholder/400/408", "/api/placeholder/400/409"],
sizes: [
{
chrtID: 444333,
techSize: 'M',
wbSize: 'M',
techSize: "M",
wbSize: "M",
price: 2990,
discountedPrice: 2490,
quantity: 50
quantity: 50,
},
{
chrtID: 444334,
techSize: 'L',
wbSize: 'L',
techSize: "L",
wbSize: "L",
price: 2990,
discountedPrice: 2490,
quantity: 45
quantity: 45,
},
{
chrtID: 444335,
techSize: 'XL',
wbSize: 'XL',
techSize: "XL",
wbSize: "XL",
price: 2990,
discountedPrice: 2490,
quantity: 30
}
]
quantity: 30,
},
],
},
{
nmID: 111222333,
vendorCode: 'SKU005',
title: 'Рюкзак для ноутбука Xiaomi',
description: 'Стильный и функциональный рюкзак для ноутбука до 15.6 дюймов',
brand: 'Xiaomi',
object: 'Рюкзаки',
parent: 'Аксессуары',
countryProduction: 'Китай',
supplierVendorCode: 'SUPPLIER-005',
mediaFiles: ['/api/placeholder/400/410'],
vendorCode: "SKU005",
title: "Рюкзак для ноутбука Xiaomi",
description:
"Стильный и функциональный рюкзак для ноутбука до 15.6 дюймов",
brand: "Xiaomi",
object: "Рюкзаки",
parent: "Аксессуары",
countryProduction: "Китай",
supplierVendorCode: "SUPPLIER-005",
mediaFiles: ["/api/placeholder/400/410"],
sizes: [
{
chrtID: 111222,
techSize: '15.6"',
wbSize: 'Черный',
wbSize: "Черный",
price: 4990,
discountedPrice: 3990,
quantity: 35
}
]
quantity: 35,
},
],
},
{
nmID: 777888999,
vendorCode: 'SKU006',
title: 'Умные часы Apple Watch Series 9',
description: 'Новейшие умные часы с передовыми функциями здоровья и фитнеса',
brand: 'Apple',
object: 'Умные часы',
parent: 'Электроника',
countryProduction: 'Китай',
supplierVendorCode: 'SUPPLIER-006',
mediaFiles: ['/api/placeholder/400/411', '/api/placeholder/400/412', '/api/placeholder/400/413'],
vendorCode: "SKU006",
title: "Умные часы Apple Watch Series 9",
description:
"Новейшие умные часы с передовыми функциями здоровья и фитнеса",
brand: "Apple",
object: "Умные часы",
parent: "Электроника",
countryProduction: "Китай",
supplierVendorCode: "SUPPLIER-006",
mediaFiles: [
"/api/placeholder/400/411",
"/api/placeholder/400/412",
"/api/placeholder/400/413",
],
sizes: [
{
chrtID: 777888,
techSize: '41mm',
wbSize: '41mm GPS',
techSize: "41mm",
wbSize: "41mm GPS",
price: 39990,
discountedPrice: 35990,
quantity: 12
quantity: 12,
},
{
chrtID: 777889,
techSize: '45mm',
wbSize: '45mm GPS',
techSize: "45mm",
wbSize: "45mm GPS",
price: 42990,
discountedPrice: 38990,
quantity: 8
}
]
}
]
quantity: 8,
},
],
},
];
// Загружаем контрагентов-фулфилментов
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES)
const { data: counterpartiesData } = useQuery(GET_MY_COUNTERPARTIES);
// Автоматически загружаем услуги и расходники для уже выбранных организаций
useEffect(() => {
actualSelectedCards.forEach(sc => {
if (sc.selectedFulfillmentOrg && !organizationServices[sc.selectedFulfillmentOrg]) {
loadOrganizationServices(sc.selectedFulfillmentOrg)
actualSelectedCards.forEach((sc) => {
if (
sc.selectedFulfillmentOrg &&
!organizationServices[sc.selectedFulfillmentOrg]
) {
loadOrganizationServices(sc.selectedFulfillmentOrg);
}
if (sc.selectedConsumableOrg && !organizationSupplies[sc.selectedConsumableOrg]) {
loadOrganizationSupplies(sc.selectedConsumableOrg)
if (
sc.selectedConsumableOrg &&
!organizationSupplies[sc.selectedConsumableOrg]
) {
loadOrganizationSupplies(sc.selectedConsumableOrg);
}
})
}, [selectedCards])
});
}, [selectedCards]);
// Функция для загрузки услуг организации
const loadOrganizationServices = async (organizationId: string) => {
if (organizationServices[organizationId]) return // Уже загружены
if (organizationServices[organizationId]) return; // Уже загружены
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SERVICES,
variables: { organizationId }
})
variables: { organizationId },
});
if (response.data?.counterpartyServices) {
setOrganizationServices(prev => ({
setOrganizationServices((prev) => ({
...prev,
[organizationId]: response.data.counterpartyServices
}))
[organizationId]: response.data.counterpartyServices,
}));
}
} catch (error) {
console.error('Ошибка загрузки услуг организации:', error)
console.error("Ошибка загрузки услуг организации:", error);
}
}
};
// Функция для загрузки расходников организации
const loadOrganizationSupplies = async (organizationId: string) => {
if (organizationSupplies[organizationId]) return // Уже загружены
if (organizationSupplies[organizationId]) return; // Уже загружены
try {
const response = await apolloClient.query({
query: GET_COUNTERPARTY_SUPPLIES,
variables: { organizationId }
})
variables: { organizationId },
});
if (response.data?.counterpartySupplies) {
setOrganizationSupplies(prev => ({
setOrganizationSupplies((prev) => ({
...prev,
[organizationId]: response.data.counterpartySupplies
}))
[organizationId]: response.data.counterpartySupplies,
}));
}
} catch (error) {
console.error('Ошибка загрузки расходников организации:', error)
console.error("Ошибка загрузки расходников организации:", error);
}
}
};
// Мутация для создания поставки
const [createSupply, { loading: creatingSupply }] = useMutation(CREATE_WILDBERRIES_SUPPLY, {
onCompleted: (data) => {
if (data.createWildberriesSupply.success) {
toast.success(data.createWildberriesSupply.message)
onComplete(selectedCards)
} else {
toast.error(data.createWildberriesSupply.message)
}
},
onError: (error) => {
toast.error('Ошибка при создании поставки')
console.error('Error creating supply:', error)
const [createSupply, { loading: creatingSupply }] = useMutation(
CREATE_WILDBERRIES_SUPPLY,
{
onCompleted: (data) => {
if (data.createWildberriesSupply.success) {
toast.success(data.createWildberriesSupply.message);
onComplete(selectedCards);
} else {
toast.error(data.createWildberriesSupply.message);
}
},
onError: (error) => {
toast.error("Ошибка при создании поставки");
console.error("Error creating supply:", error);
},
}
})
);
// Моковые данные рынков
const markets = [
{ value: 'sadovod', label: 'Садовод' },
{ value: 'luzhniki', label: 'Лужники' },
{ value: 'tishinka', label: 'Тишинка' },
{ value: 'food-city', label: 'Фуд Сити' }
]
{ value: "sadovod", label: "Садовод" },
{ value: "luzhniki", label: "Лужники" },
{ value: "tishinka", label: "Тишинка" },
{ value: "food-city", label: "Фуд Сити" },
];
// Автоматически загружаем товары при открытии компонента
useEffect(() => {
const loadCards = async () => {
setLoading(true)
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
console.log('WB API Key found:', !!wbApiKey)
console.log('WB API Key active:', wbApiKey?.isActive)
console.log('WB API Key validationData:', wbApiKey?.validationData)
const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
console.log("WB API Key found:", !!wbApiKey);
console.log("WB API Key active:", wbApiKey?.isActive);
console.log("WB API Key validationData:", wbApiKey?.validationData);
if (wbApiKey?.isActive) {
// Попытка загрузить реальные данные из API Wildberries
const validationData = wbApiKey.validationData as Record<string, string>
const validationData = wbApiKey.validationData as Record<
string,
string
>;
// API ключ может храниться в разных местах
const apiToken = validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey // Прямое поле apiKey из базы
console.log('API Token extracted:', !!apiToken)
console.log('API Token length:', apiToken?.length)
const apiToken =
validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey; // Прямое поле apiKey из базы
console.log("API Token extracted:", !!apiToken);
console.log("API Token length:", apiToken?.length);
if (apiToken) {
console.log('Загружаем карточки из WB API...')
const cards = await WildberriesService.getAllCards(apiToken, 50)
setWbCards(cards)
console.log('Загружено карточек из WB API:', cards.length)
return
console.log("Загружаем карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 50);
setWbCards(cards);
console.log("Загружено карточек из WB API:", cards.length);
return;
}
}
// Если API ключ не настроен, оставляем пустое состояние
console.log('API ключ WB не настроен, показываем пустое состояние')
setWbCards([])
} catch (error) {
console.error('Ошибка загрузки карточек WB:', error)
// При ошибке API показываем пустое состояние
setWbCards([])
} finally {
setLoading(false)
}
}
loadCards()
}, [user])
// Если API ключ не настроен, оставляем пустое состояние
console.log("API ключ WB не настроен, показываем пустое состояние");
setWbCards([]);
} catch (error) {
console.error("Ошибка загрузки карточек WB:", error);
// При ошибке API показываем пустое состояние
setWbCards([]);
} finally {
setLoading(false);
}
};
loadCards();
}, [user]);
const loadAllCards = async () => {
setLoading(true)
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) {
// Попытка загрузить реальные данные из API Wildberries
const validationData = wbApiKey.validationData as Record<string, string>
const apiToken = validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey
const validationData = wbApiKey.validationData as Record<
string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) {
console.log('Загружаем все карточки из WB API...')
const cards = await WildberriesService.getAllCards(apiToken, 100)
setWbCards(cards)
console.log('Загружено карточек из WB API:', cards.length)
return
console.log("Загружаем все карточки из WB API...");
const cards = await WildberriesService.getAllCards(apiToken, 100);
setWbCards(cards);
console.log("Загружено карточек из WB API:", cards.length);
return;
}
}
// Если API ключ не настроен, загружаем моковые данные
console.log('API ключ WB не настроен, загружаем моковые данные')
const allCards = getMockCards()
setWbCards(allCards)
console.log('Загружены моковые товары:', allCards.length)
console.log("API ключ WB не настроен, загружаем моковые данные");
const allCards = getMockCards();
setWbCards(allCards);
console.log("Загружены моковые товары:", allCards.length);
} catch (error) {
console.error('Ошибка загрузки всех карточек WB:', error)
console.error("Ошибка загрузки всех карточек WB:", error);
// При ошибке загружаем моковые данные
const allCards = getMockCards()
setWbCards(allCards)
console.log('Загружены моковые товары (fallback):', allCards.length)
const allCards = getMockCards();
setWbCards(allCards);
console.log("Загружены моковые товары (fallback):", allCards.length);
} finally {
setLoading(false)
setLoading(false);
}
}
};
const searchCards = async () => {
if (!searchTerm.trim()) {
loadAllCards()
return
loadAllCards();
return;
}
setLoading(true)
setLoading(true);
try {
const wbApiKey = user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')
const wbApiKey = user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
);
if (wbApiKey?.isActive) {
// Попытка поиска в реальном API Wildberries
const validationData = wbApiKey.validationData as Record<string, string>
const apiToken = validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey
const validationData = wbApiKey.validationData as Record<
string,
string
>;
const apiToken =
validationData?.token ||
validationData?.apiKey ||
validationData?.key ||
(wbApiKey as { apiKey?: string }).apiKey;
if (apiToken) {
console.log('Поиск в WB API:', searchTerm)
const cards = await WildberriesService.searchCards(apiToken, searchTerm, 50)
setWbCards(cards)
console.log('Найдено карточек в WB API:', cards.length)
return
console.log("Поиск в WB API:", searchTerm);
const cards = await WildberriesService.searchCards(
apiToken,
searchTerm,
50
);
setWbCards(cards);
console.log("Найдено карточек в WB API:", cards.length);
return;
}
}
// Если API ключ не настроен, ищем в моковых данных
console.log('API ключ WB не настроен, поиск в моковых данных:', searchTerm)
const mockCards = getMockCards()
console.log(
"API ключ WB не настроен, поиск в моковых данных:",
searchTerm
);
const mockCards = getMockCards();
// Фильтруем товары по поисковому запросу
const filteredCards = mockCards.filter(card =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
)
const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
);
setWbCards(filteredCards)
console.log('Найдено моковых товаров:', filteredCards.length)
setWbCards(filteredCards);
console.log("Найдено моковых товаров:", filteredCards.length);
} catch (error) {
console.error('Ошибка поиска карточек WB:', error)
console.error("Ошибка поиска карточек WB:", error);
// При ошибке ищем в моковых данных
const mockCards = getMockCards()
const filteredCards = mockCards.filter(card =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
)
setWbCards(filteredCards)
console.log('Найдено моковых товаров (fallback):', filteredCards.length)
const mockCards = getMockCards();
const filteredCards = mockCards.filter(
(card) =>
card.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.brand.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.nmID.toString().includes(searchTerm.toLowerCase()) ||
card.object?.toLowerCase().includes(searchTerm.toLowerCase())
);
setWbCards(filteredCards);
console.log("Найдено моковых товаров (fallback):", filteredCards.length);
} finally {
setLoading(false)
setLoading(false);
}
}
};
const updateCardSelection = (card: WildberriesCard, field: keyof SelectedCard, value: string | number | string[]) => {
setPreparingCards(prev => {
const existing = prev.find(sc => sc.card.nmID === card.nmID)
if (field === 'selectedQuantity' && typeof value === 'number' && value === 0) {
return prev.filter(sc => sc.card.nmID !== card.nmID)
const updateCardSelection = (
card: WildberriesCard,
field: keyof SelectedCard,
value: string | number | string[]
) => {
setPreparingCards((prev) => {
const existing = prev.find((sc) => sc.card.nmID === card.nmID);
if (
field === "selectedQuantity" &&
typeof value === "number" &&
value === 0
) {
return prev.filter((sc) => sc.card.nmID !== card.nmID);
}
if (existing) {
const updatedCard = { ...existing, [field]: value }
const updatedCard = { ...existing, [field]: value };
// При изменении количества сбрасываем цену, чтобы пользователь ввел новую
if (field === 'selectedQuantity' && typeof value === 'number' && existing.customPrice > 0) {
updatedCard.customPrice = 0
if (
field === "selectedQuantity" &&
typeof value === "number" &&
existing.customPrice > 0
) {
updatedCard.customPrice = 0;
}
return prev.map(sc =>
return prev.map((sc) =>
sc.card.nmID === card.nmID ? updatedCard : sc
)
} else if (field === 'selectedQuantity' && typeof value === 'number' && value > 0) {
);
} else if (
field === "selectedQuantity" &&
typeof value === "number" &&
value > 0
) {
const newSelectedCard: SelectedCard = {
card,
selectedQuantity: value as number,
customPrice: 0,
selectedFulfillmentOrg: '',
selectedFulfillmentOrg: "",
selectedFulfillmentServices: [],
selectedConsumableOrg: '',
selectedConsumableOrg: "",
selectedConsumableServices: [],
deliveryDate: '',
selectedMarket: '',
selectedPlace: '',
sellerName: '',
sellerPhone: '',
selectedServices: []
}
return [...prev, newSelectedCard]
deliveryDate: "",
selectedMarket: "",
selectedPlace: "",
sellerName: "",
sellerPhone: "",
selectedServices: [],
};
return [...prev, newSelectedCard];
}
return prev
})
}
return prev;
});
};
// Функция для получения цены за единицу товара
const getSelectedUnitPrice = (card: WildberriesCard): number => {
const selected = preparingCards.find(sc => sc.card.nmID === card.nmID)
if (!selected || selected.selectedQuantity === 0) return 0
return selected.customPrice / selected.selectedQuantity
}
const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID);
if (!selected || selected.selectedQuantity === 0) return 0;
return selected.customPrice / selected.selectedQuantity;
};
// Функция для получения общей стоимости товара
const getSelectedTotalPrice = (card: WildberriesCard): number => {
const selected = preparingCards.find(sc => sc.card.nmID === card.nmID)
return selected ? selected.customPrice : 0
}
const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID);
return selected ? selected.customPrice : 0;
};
const getSelectedQuantity = (card: WildberriesCard): number => {
const selected = preparingCards.find(sc => sc.card.nmID === card.nmID)
return selected ? selected.selectedQuantity : 0
}
const selected = preparingCards.find((sc) => sc.card.nmID === card.nmID);
return selected ? selected.selectedQuantity : 0;
};
// Функция для добавления подготовленных товаров в корзину
const addToCart = () => {
const validCards = preparingCards.filter(card =>
card.selectedQuantity > 0 && card.customPrice > 0
)
const validCards = preparingCards.filter(
(card) => card.selectedQuantity > 0 && card.customPrice > 0
);
if (validCards.length === 0) {
toast.error('Выберите товары и укажите цены')
return
toast.error("Выберите товары и укажите цены");
return;
}
if (!globalDeliveryDate) {
toast.error('Выберите дату поставки')
return
toast.error("Выберите дату поставки");
return;
}
const newCards = [...actualSelectedCards]
validCards.forEach(prepCard => {
const newCards = [...actualSelectedCards];
validCards.forEach((prepCard) => {
const cardWithDate = {
...prepCard,
deliveryDate: globalDeliveryDate.toISOString().split('T')[0]
}
const existingIndex = newCards.findIndex(sc => sc.card.nmID === prepCard.card.nmID)
deliveryDate: globalDeliveryDate.toISOString().split("T")[0],
};
const existingIndex = newCards.findIndex(
(sc) => sc.card.nmID === prepCard.card.nmID
);
if (existingIndex >= 0) {
// Обновляем существующий товар
newCards[existingIndex] = cardWithDate
newCards[existingIndex] = cardWithDate;
} else {
// Добавляем новый товар
newCards.push(cardWithDate)
newCards.push(cardWithDate);
}
})
actualSetSelectedCards(newCards)
});
actualSetSelectedCards(newCards);
// Очищаем подготовленные товары
setPreparingCards([])
toast.success(`Добавлено ${validCards.length} товар(ов) в корзину`)
}
setPreparingCards([]);
toast.success(`Добавлено ${validCards.length} товар(ов) в корзину`);
};
// Функции подсчета для подготовленных товаров
const getPreparingTotalItems = () => {
return preparingCards.reduce((sum, card) => sum + card.selectedQuantity, 0)
}
return preparingCards.reduce((sum, card) => sum + card.selectedQuantity, 0);
};
const getPreparingTotalAmount = () => {
return preparingCards.reduce((sum, card) => sum + card.customPrice, 0)
}
return preparingCards.reduce((sum, card) => sum + card.customPrice, 0);
};
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);
};
// Функция для получения цены услуги по ID
const getServicePrice = (orgId: string, serviceId: string): number => {
const services = organizationServices[orgId]
if (!services) return 0
const service = services.find(s => s.id === serviceId)
return service ? service.price : 0
}
const services = organizationServices[orgId];
if (!services) return 0;
const service = services.find((s) => s.id === serviceId);
return service ? service.price : 0;
};
// Функция для получения цены расходника по ID
const getSupplyPrice = (orgId: string, supplyId: string): number => {
const supplies = organizationSupplies[orgId]
if (!supplies) return 0
const supply = supplies.find(s => s.id === supplyId)
return supply ? supply.price : 0
}
const supplies = organizationSupplies[orgId];
if (!supplies) return 0;
const supply = supplies.find((s) => s.id === supplyId);
return supply ? supply.price : 0;
};
// Функция для расчета стоимости услуг и расходников за 1 штуку
const calculateAdditionalCostPerUnit = (sc: SelectedCard): number => {
let servicesCost = 0
let suppliesCost = 0
let servicesCost = 0;
let suppliesCost = 0;
// Стоимость услуг фулфилмента
if (sc.selectedFulfillmentOrg && sc.selectedFulfillmentServices.length > 0) {
if (
sc.selectedFulfillmentOrg &&
sc.selectedFulfillmentServices.length > 0
) {
servicesCost = sc.selectedFulfillmentServices.reduce((sum, serviceId) => {
return sum + getServicePrice(sc.selectedFulfillmentOrg, serviceId)
}, 0)
return sum + getServicePrice(sc.selectedFulfillmentOrg, serviceId);
}, 0);
}
// Стоимость расходных материалов
if (sc.selectedConsumableOrg && sc.selectedConsumableServices.length > 0) {
suppliesCost = sc.selectedConsumableServices.reduce((sum, supplyId) => {
return sum + getSupplyPrice(sc.selectedConsumableOrg, supplyId)
}, 0)
return sum + getSupplyPrice(sc.selectedConsumableOrg, supplyId);
}, 0);
}
return servicesCost + suppliesCost
}
return servicesCost + suppliesCost;
};
const getTotalAmount = () => {
return actualSelectedCards.reduce((sum, sc) => {
const additionalCostPerUnit = calculateAdditionalCostPerUnit(sc)
const totalCostPerUnit = (sc.customPrice / sc.selectedQuantity) + additionalCostPerUnit
const totalCostForAllItems = totalCostPerUnit * sc.selectedQuantity
return sum + totalCostForAllItems
}, 0)
}
const additionalCostPerUnit = calculateAdditionalCostPerUnit(sc);
const totalCostPerUnit =
sc.customPrice / sc.selectedQuantity + additionalCostPerUnit;
const totalCostForAllItems = totalCostPerUnit * sc.selectedQuantity;
return sum + totalCostForAllItems;
}, 0);
};
const getTotalItems = () => {
return actualSelectedCards.reduce((sum, sc) => sum + sc.selectedQuantity, 0)
}
return actualSelectedCards.reduce(
(sum, sc) => sum + sc.selectedQuantity,
0
);
};
// Функция больше не нужна, так как услуги выбираются индивидуально
const handleCardClick = (card: WildberriesCard) => {
setSelectedCardForDetails(card)
setCurrentImageIndex(0)
}
setSelectedCardForDetails(card);
setCurrentImageIndex(0);
};
const closeDetailsModal = () => {
setSelectedCardForDetails(null)
setCurrentImageIndex(0)
}
setSelectedCardForDetails(null);
setCurrentImageIndex(0);
};
const nextImage = () => {
if (selectedCardForDetails) {
const images = WildberriesService.getCardImages(selectedCardForDetails)
const images = WildberriesService.getCardImages(selectedCardForDetails);
if (images.length > 1) {
setCurrentImageIndex((prev) => (prev + 1) % images.length)
setCurrentImageIndex((prev) => (prev + 1) % images.length);
}
}
}
};
const prevImage = () => {
if (selectedCardForDetails) {
const images = WildberriesService.getCardImages(selectedCardForDetails)
const images = WildberriesService.getCardImages(selectedCardForDetails);
if (images.length > 1) {
setCurrentImageIndex((prev) => (prev - 1 + images.length) % images.length)
setCurrentImageIndex(
(prev) => (prev - 1 + images.length) % images.length
);
}
}
}
};
const handleCreateSupply = async () => {
try {
const supplyInput = {
deliveryDate: selectedCards[0]?.deliveryDate || null,
cards: actualSelectedCards.map(sc => ({
cards: actualSelectedCards.map((sc) => ({
nmId: sc.card.nmID.toString(),
vendorCode: sc.card.vendorCode,
title: sc.card.title,
brand: sc.card.brand,
selectedQuantity: sc.selectedQuantity,
customPrice: sc.customPrice,
selectedFulfillmentOrg: sc.selectedFulfillmentOrg,
selectedFulfillmentServices: sc.selectedFulfillmentServices,
selectedConsumableOrg: sc.selectedConsumableOrg,
selectedConsumableServices: sc.selectedConsumableServices,
deliveryDate: sc.deliveryDate || null,
mediaFiles: sc.card.mediaFiles
}))
}
customPrice: sc.customPrice,
selectedFulfillmentOrg: sc.selectedFulfillmentOrg,
selectedFulfillmentServices: sc.selectedFulfillmentServices,
selectedConsumableOrg: sc.selectedConsumableOrg,
selectedConsumableServices: sc.selectedConsumableServices,
deliveryDate: sc.deliveryDate || null,
mediaFiles: sc.card.mediaFiles,
})),
};
await createSupply({ variables: { input: supplyInput } })
await createSupply({ variables: { input: supplyInput } });
} catch (error) {
console.error('Error creating supply:', error)
console.error("Error creating supply:", error);
}
}
};
if (actualShowSummary) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}>
<main
className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}
>
<div className="p-6 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={() => actualSetShowSummary(false)}
className="text-white/60 hover:text-white hover:bg-white/10"
@ -721,383 +850,587 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
Назад
</Button>
<div>
<h2 className="text-2xl font-bold text-white mb-1">Корзина</h2>
<p className="text-white/60">{actualSelectedCards.length} карточек товаров</p>
<h2 className="text-2xl font-bold text-white mb-1">
Корзина
</h2>
<p className="text-white/60">
{actualSelectedCards.length} карточек товаров
</p>
</div>
</div>
</div>
{/* Массовое назначение поставщиков */}
<Card className="bg-blue-500/10 backdrop-blur border-blue-500/20 p-4 mb-6">
<h3 className="text-white font-semibold mb-4">Быстрое назначение</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">Поставщик услуг для всех товаров:</label>
<Select onValueChange={(value) => {
if (value && value !== 'none') {
// Загружаем услуги для выбранной организации
loadOrganizationServices(value)
actualSelectedCards.forEach(sc => {
updateCardSelection(sc.card, 'selectedFulfillmentOrg', value)
// Сбрасываем выбранные услуги при смене организации
updateCardSelection(sc.card, 'selectedFulfillmentServices', [])
})
}
}}>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Не выбран</SelectItem>
{((counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT')).map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Поставщик расходников для всех:</label>
<Select onValueChange={(value) => {
if (value && value !== 'none') {
// Загружаем расходники для выбранной организации
loadOrganizationSupplies(value)
actualSelectedCards.forEach(sc => {
updateCardSelection(sc.card, 'selectedConsumableOrg', value)
// Сбрасываем выбранные расходники при смене организации
updateCardSelection(sc.card, 'selectedConsumableServices', [])
})
}
}}>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Не выбран</SelectItem>
{((counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT')).map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-white/60 text-sm mb-2 block">Дата поставки для всех:</label>
<Popover>
<PopoverTrigger asChild>
<button
className="w-full bg-white/5 border border-white/20 text-white hover:bg-white/10 justify-start text-left font-normal h-10 px-3 py-2 rounded-md flex items-center transition-colors"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{globalDeliveryDate ? format(globalDeliveryDate, "dd.MM.yyyy") : "Выберите дату"}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<DatePicker
selected={globalDeliveryDate}
onChange={(date: Date | null) => {
setGlobalDeliveryDate(date || undefined)
if (date) {
const dateString = date.toISOString().split('T')[0]
actualSelectedCards.forEach(sc => {
updateCardSelection(sc.card, 'deliveryDate', dateString)
})
{/* Массовое назначение поставщиков */}
<Card className="bg-blue-500/10 backdrop-blur border-blue-500/20 p-4 mb-6">
<h3 className="text-white font-semibold mb-4">
Быстрое назначение
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="text-white/60 text-sm mb-2 block">
Поставщик услуг для всех товаров:
</label>
<Select
onValueChange={(value) => {
if (value && value !== "none") {
// Загружаем услуги для выбранной организации
loadOrganizationServices(value);
actualSelectedCards.forEach((sc) => {
updateCardSelection(
sc.card,
"selectedFulfillmentOrg",
value
);
// Сбрасываем выбранные услуги при смене организации
updateCardSelection(
sc.card,
"selectedFulfillmentServices",
[]
);
});
}
}}
minDate={new Date()}
inline
locale="ru"
/>
</PopoverContent>
</Popover>
</div>
</div>
</Card>
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6">
<div className="xl:col-span-3 space-y-4">
{actualSelectedCards.map((sc) => {
const fulfillmentOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT')
const consumableOrgs = (counterpartiesData?.myCounterparties || []).filter((org: Organization) => org.type === 'FULFILLMENT')
return (
<Card key={sc.card.nmID} className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex space-x-4">
<img
src={WildberriesService.getCardImage(sc.card, 'c246x328') || '/api/placeholder/120/120'}
alt={sc.card.title}
className="w-20 h-20 rounded-lg object-cover"
/>
<div className="flex-1 space-y-4">
<div>
<h3 className="text-white font-medium">{sc.card.title}</h3>
<p className="text-white/60 text-sm">WB: {sc.card.nmID}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Количество и цена */}
<div className="space-y-3">
<div>
<label className="text-white/60 text-sm">Количество:</label>
<Input
type="number"
value={sc.selectedQuantity}
onChange={(e) => updateCardSelection(sc.card, 'selectedQuantity', parseInt(e.target.value) || 0)}
className="bg-white/5 border-white/20 text-white mt-1"
min="1"
/>
</div>
<div>
<label className="text-white/60 text-sm">Цена за единицу:</label>
<Input
type="number"
value={sc.customPrice === 0 ? '' : (sc.customPrice / sc.selectedQuantity).toFixed(2)}
onChange={(e) => {
const pricePerUnit = e.target.value === '' ? 0 : parseFloat(e.target.value) || 0
const totalPrice = pricePerUnit * sc.selectedQuantity
updateCardSelection(sc.card, 'customPrice', totalPrice)
}}
className="bg-white/5 border-white/20 text-white mt-1"
placeholder="Введите цену за 1 штуку"
/>
{/* Показываем расчет дополнительных расходов */}
{(() => {
const additionalCost = calculateAdditionalCostPerUnit(sc)
if (additionalCost > 0) {
return (
<div className="mt-2 p-2 bg-blue-500/20 border border-blue-500/30 rounded text-xs">
<div className="text-blue-300 font-medium">Дополнительные расходы за 1 шт:</div>
{sc.selectedFulfillmentServices.length > 0 && (
<div className="text-blue-200">
Услуги: {sc.selectedFulfillmentServices.map(serviceId => {
const price = getServicePrice(sc.selectedFulfillmentOrg, serviceId)
const services = organizationServices[sc.selectedFulfillmentOrg]
const service = services?.find(s => s.id === serviceId)
return service ? `${service.name} (${price}₽)` : ''
}).join(', ')}
</div>
)}
{sc.selectedConsumableServices.length > 0 && (
<div className="text-blue-200">
Расходники: {sc.selectedConsumableServices.map(supplyId => {
const price = getSupplyPrice(sc.selectedConsumableOrg, supplyId)
const supplies = organizationSupplies[sc.selectedConsumableOrg]
const supply = supplies?.find(s => s.id === supplyId)
return supply ? `${supply.name} (${price}₽)` : ''
}).join(', ')}
</div>
)}
<div className="text-blue-300 font-medium mt-1">
Итого доп. расходы: {formatCurrency(additionalCost)}
</div>
<div className="text-green-300 font-medium">
Полная стоимость за 1 шт: {formatCurrency((sc.customPrice / sc.selectedQuantity) + additionalCost)}
</div>
</div>
)
}
return null
})()}
</div>
</div>
{/* Услуги */}
<div className="space-y-3">
<div>
<label className="text-white/60 text-sm">Фулфилмент организация:</label>
<Select
value={sc.selectedFulfillmentOrg}
onValueChange={(value) => {
updateCardSelection(sc.card, 'selectedFulfillmentOrg', value)
updateCardSelection(sc.card, 'selectedFulfillmentServices', []) // Сбрасываем услуги
if (value) {
loadOrganizationServices(value) // Автоматически загружаем услуги
}
}}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white mt-1">
<SelectValue placeholder="Выберите фулфилмент" />
</SelectTrigger>
<SelectContent>
{fulfillmentOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{sc.selectedFulfillmentOrg && (
<div>
<label className="text-white/60 text-sm">Услуги фулфилмента:</label>
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto bg-white/5 border border-white/20 rounded p-2">
{organizationServices[sc.selectedFulfillmentOrg] ? (
organizationServices[sc.selectedFulfillmentOrg].length > 0 ? (
organizationServices[sc.selectedFulfillmentOrg].map((service) => {
const isSelected = sc.selectedFulfillmentServices.includes(service.id)
return (
<label key={service.id} className="flex items-center space-x-2 cursor-pointer hover:bg-white/5 p-1 rounded">
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
const newServices = e.target.checked
? [...sc.selectedFulfillmentServices, service.id]
: sc.selectedFulfillmentServices.filter(id => id !== service.id)
updateCardSelection(sc.card, 'selectedFulfillmentServices', newServices)
}}
className="rounded border-white/20 bg-white/10 text-purple-500 focus:ring-purple-500"
/>
<span className="text-white text-sm">
{service.name} - {service.price}
</span>
</label>
)
})
) : (
<div className="text-white/60 text-sm text-center py-2">
У данной организации нет услуг
</div>
)
) : (
<div className="text-white/60 text-sm text-center py-2">
Загрузка услуг...
</div>
)}
</div>
</div>
)}
<div>
<label className="text-white/60 text-sm">Поставщик расходников:</label>
<Select
value={sc.selectedConsumableOrg}
onValueChange={(value) => {
updateCardSelection(sc.card, 'selectedConsumableOrg', value)
updateCardSelection(sc.card, 'selectedConsumableServices', []) // Сбрасываем услуги
if (value) {
loadOrganizationSupplies(value) // Автоматически загружаем расходники
}
}}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white mt-1">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
{consumableOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{sc.selectedConsumableOrg && (
<div>
<label className="text-white/60 text-sm">Расходные материалы:</label>
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto bg-white/5 border border-white/20 rounded p-2">
{organizationSupplies[sc.selectedConsumableOrg] ? (
organizationSupplies[sc.selectedConsumableOrg].length > 0 ? (
organizationSupplies[sc.selectedConsumableOrg].map((supply) => {
const isSelected = sc.selectedConsumableServices.includes(supply.id)
return (
<label key={supply.id} className="flex items-center space-x-2 cursor-pointer hover:bg-white/5 p-1 rounded">
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
const newSupplies = e.target.checked
? [...sc.selectedConsumableServices, supply.id]
: sc.selectedConsumableServices.filter(id => id !== supply.id)
updateCardSelection(sc.card, 'selectedConsumableServices', newSupplies)
}}
className="rounded border-white/20 bg-white/10 text-green-500 focus:ring-green-500"
/>
<span className="text-white text-sm">
{supply.name} - {supply.price}
</span>
</label>
)
})
) : (
<div className="text-white/60 text-sm text-center py-2">
У данной организации нет расходников
</div>
)
) : (
<div className="text-white/60 text-sm text-center py-2">
Загрузка расходников...
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="text-right">
<span className="text-white font-bold text-lg">
{formatCurrency(sc.customPrice)}
</span>
{sc.selectedQuantity > 0 && sc.customPrice > 0 && (
<p className="text-white/60 text-sm">
~{formatCurrency(sc.customPrice / sc.selectedQuantity)} за шт.
</p>
)}
</div>
</div>
</div>
</Card>
)
})}
</div>
<div className="xl:col-span-1">
<Card className="bg-white/10 backdrop-blur border-white/20 p-4 sticky top-4">
<h3 className="text-white font-semibold text-lg mb-4">Итого</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-white/60">Товаров:</span>
<span className="text-white">{getTotalItems()}</span>
>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Не выбран</SelectItem>
{(counterpartiesData?.myCounterparties || [])
.filter(
(org: Organization) => org.type === "FULFILLMENT"
)
.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-between text-sm">
<span className="text-white/60">Карточек:</span>
<span className="text-white">{actualSelectedCards.length}</span>
<div>
<label className="text-white/60 text-sm mb-2 block">
Поставщик расходников для всех:
</label>
<Select
onValueChange={(value) => {
if (value && value !== "none") {
// Загружаем расходники для выбранной организации
loadOrganizationSupplies(value);
actualSelectedCards.forEach((sc) => {
updateCardSelection(
sc.card,
"selectedConsumableOrg",
value
);
// Сбрасываем выбранные расходники при смене организации
updateCardSelection(
sc.card,
"selectedConsumableServices",
[]
);
});
}
}}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Не выбран</SelectItem>
{(counterpartiesData?.myCounterparties || [])
.filter(
(org: Organization) => org.type === "FULFILLMENT"
)
.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="border-t border-white/20 pt-3 flex justify-between">
<span className="text-white font-semibold">Общая сумма:</span>
<span className="text-white font-bold text-lg">{formatCurrency(getTotalAmount())}</span>
<div>
<label className="text-white/60 text-sm mb-2 block">
Дата поставки для всех:
</label>
<Popover>
<PopoverTrigger asChild>
<button className="w-full bg-white/5 border border-white/20 text-white hover:bg-white/10 justify-start text-left font-normal h-10 px-3 py-2 rounded-md flex items-center transition-colors">
<CalendarIcon className="mr-2 h-4 w-4" />
{globalDeliveryDate
? format(globalDeliveryDate, "dd.MM.yyyy")
: "Выберите дату"}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<DatePicker
selected={globalDeliveryDate}
onChange={(date: Date | null) => {
setGlobalDeliveryDate(date || undefined);
if (date) {
const dateString = date.toISOString().split("T")[0];
actualSelectedCards.forEach((sc) => {
updateCardSelection(
sc.card,
"deliveryDate",
dateString
);
});
}
}}
minDate={new Date()}
inline
locale="ru"
/>
</PopoverContent>
</Popover>
</div>
<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={creatingSupply}
>
<Check className="h-4 w-4 mr-2" />
{creatingSupply ? 'Создание...' : 'Создать поставку'}
</Button>
</div>
</Card>
</div>
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6">
<div className="xl:col-span-3 space-y-4">
{actualSelectedCards.map((sc) => {
const fulfillmentOrgs = (
counterpartiesData?.myCounterparties || []
).filter((org: Organization) => org.type === "FULFILLMENT");
const consumableOrgs = (
counterpartiesData?.myCounterparties || []
).filter((org: Organization) => org.type === "FULFILLMENT");
return (
<Card
key={sc.card.nmID}
className="bg-white/10 backdrop-blur border-white/20 p-4"
>
<div className="flex space-x-4">
<img
src={
WildberriesService.getCardImage(
sc.card,
"c246x328"
) || "/api/placeholder/120/120"
}
alt={sc.card.title}
className="w-20 h-20 rounded-lg object-cover"
/>
<div className="flex-1 space-y-4">
<div>
<h3 className="text-white font-medium">
{sc.card.title}
</h3>
<p className="text-white/60 text-sm">
WB: {sc.card.nmID}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Количество и цена */}
<div className="space-y-3">
<div>
<label className="text-white/60 text-sm">
Количество:
</label>
<Input
type="number"
value={sc.selectedQuantity}
onChange={(e) =>
updateCardSelection(
sc.card,
"selectedQuantity",
parseInt(e.target.value) || 0
)
}
className="bg-white/5 border-white/20 text-white mt-1"
min="1"
/>
</div>
<div>
<label className="text-white/60 text-sm">
Цена за единицу:
</label>
<Input
type="number"
value={
sc.customPrice === 0
? ""
: (
sc.customPrice / sc.selectedQuantity
).toFixed(2)
}
onChange={(e) => {
const pricePerUnit =
e.target.value === ""
? 0
: parseFloat(e.target.value) || 0;
const totalPrice =
pricePerUnit * sc.selectedQuantity;
updateCardSelection(
sc.card,
"customPrice",
totalPrice
);
}}
className="bg-white/5 border-white/20 text-white mt-1"
placeholder="Введите цену за 1 штуку"
/>
{/* Показываем расчет дополнительных расходов */}
{(() => {
const additionalCost =
calculateAdditionalCostPerUnit(sc);
if (additionalCost > 0) {
return (
<div className="mt-2 p-2 bg-blue-500/20 border border-blue-500/30 rounded text-xs">
<div className="text-blue-300 font-medium">
Дополнительные расходы за 1 шт:
</div>
{sc.selectedFulfillmentServices.length >
0 && (
<div className="text-blue-200">
Услуги:{" "}
{sc.selectedFulfillmentServices
.map((serviceId) => {
const price = getServicePrice(
sc.selectedFulfillmentOrg,
serviceId
);
const services =
organizationServices[
sc.selectedFulfillmentOrg
];
const service = services?.find(
(s) => s.id === serviceId
);
return service
? `${service.name} (${price}₽)`
: "";
})
.join(", ")}
</div>
)}
{sc.selectedConsumableServices.length >
0 && (
<div className="text-blue-200">
Расходники:{" "}
{sc.selectedConsumableServices
.map((supplyId) => {
const price = getSupplyPrice(
sc.selectedConsumableOrg,
supplyId
);
const supplies =
organizationSupplies[
sc.selectedConsumableOrg
];
const supply = supplies?.find(
(s) => s.id === supplyId
);
return supply
? `${supply.name} (${price}₽)`
: "";
})
.join(", ")}
</div>
)}
<div className="text-blue-300 font-medium mt-1">
Итого доп. расходы:{" "}
{formatCurrency(additionalCost)}
</div>
<div className="text-green-300 font-medium">
Полная стоимость за 1 шт:{" "}
{formatCurrency(
sc.customPrice /
sc.selectedQuantity +
additionalCost
)}
</div>
</div>
);
}
return null;
})()}
</div>
</div>
{/* Услуги */}
<div className="space-y-3">
<div>
<label className="text-white/60 text-sm">
Фулфилмент организация:
</label>
<Select
value={sc.selectedFulfillmentOrg}
onValueChange={(value) => {
updateCardSelection(
sc.card,
"selectedFulfillmentOrg",
value
);
updateCardSelection(
sc.card,
"selectedFulfillmentServices",
[]
); // Сбрасываем услуги
if (value) {
loadOrganizationServices(value); // Автоматически загружаем услуги
}
}}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white mt-1">
<SelectValue placeholder="Выберите фулфилмент" />
</SelectTrigger>
<SelectContent>
{fulfillmentOrgs.map(
(org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
{sc.selectedFulfillmentOrg && (
<div>
<label className="text-white/60 text-sm">
Услуги фулфилмента:
</label>
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto bg-white/5 border border-white/20 rounded p-2">
{organizationServices[
sc.selectedFulfillmentOrg
] ? (
organizationServices[
sc.selectedFulfillmentOrg
].length > 0 ? (
organizationServices[
sc.selectedFulfillmentOrg
].map((service) => {
const isSelected =
sc.selectedFulfillmentServices.includes(
service.id
);
return (
<label
key={service.id}
className="flex items-center space-x-2 cursor-pointer hover:bg-white/5 p-1 rounded"
>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
const newServices = e.target
.checked
? [
...sc.selectedFulfillmentServices,
service.id,
]
: sc.selectedFulfillmentServices.filter(
(id) =>
id !== service.id
);
updateCardSelection(
sc.card,
"selectedFulfillmentServices",
newServices
);
}}
className="rounded border-white/20 bg-white/10 text-purple-500 focus:ring-purple-500"
/>
<span className="text-white text-sm">
{service.name} - {service.price}{" "}
</span>
</label>
);
})
) : (
<div className="text-white/60 text-sm text-center py-2">
У данной организации нет услуг
</div>
)
) : (
<div className="text-white/60 text-sm text-center py-2">
Загрузка услуг...
</div>
)}
</div>
</div>
)}
<div>
<label className="text-white/60 text-sm">
Поставщик расходников:
</label>
<Select
value={sc.selectedConsumableOrg}
onValueChange={(value) => {
updateCardSelection(
sc.card,
"selectedConsumableOrg",
value
);
updateCardSelection(
sc.card,
"selectedConsumableServices",
[]
); // Сбрасываем услуги
if (value) {
loadOrganizationSupplies(value); // Автоматически загружаем расходники
}
}}
>
<SelectTrigger className="bg-white/5 border-white/20 text-white mt-1">
<SelectValue placeholder="Выберите поставщика" />
</SelectTrigger>
<SelectContent>
{consumableOrgs.map((org: Organization) => (
<SelectItem key={org.id} value={org.id}>
{org.name || org.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{sc.selectedConsumableOrg && (
<div>
<label className="text-white/60 text-sm">
Расходные материалы:
</label>
<div className="mt-2 space-y-2 max-h-32 overflow-y-auto bg-white/5 border border-white/20 rounded p-2">
{organizationSupplies[
sc.selectedConsumableOrg
] ? (
organizationSupplies[
sc.selectedConsumableOrg
].length > 0 ? (
organizationSupplies[
sc.selectedConsumableOrg
].map((supply) => {
const isSelected =
sc.selectedConsumableServices.includes(
supply.id
);
return (
<label
key={supply.id}
className="flex items-center space-x-2 cursor-pointer hover:bg-white/5 p-1 rounded"
>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
const newSupplies = e.target
.checked
? [
...sc.selectedConsumableServices,
supply.id,
]
: sc.selectedConsumableServices.filter(
(id) => id !== supply.id
);
updateCardSelection(
sc.card,
"selectedConsumableServices",
newSupplies
);
}}
className="rounded border-white/20 bg-white/10 text-green-500 focus:ring-green-500"
/>
<span className="text-white text-sm">
{supply.name} - {supply.price}
</span>
</label>
);
})
) : (
<div className="text-white/60 text-sm text-center py-2">
У данной организации нет расходников
</div>
)
) : (
<div className="text-white/60 text-sm text-center py-2">
Загрузка расходников...
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="text-right">
<span className="text-white font-bold text-lg">
{formatCurrency(sc.customPrice)}
</span>
{sc.selectedQuantity > 0 && sc.customPrice > 0 && (
<p className="text-white/60 text-sm">
~
{formatCurrency(
sc.customPrice / sc.selectedQuantity
)}{" "}
за шт.
</p>
)}
</div>
</div>
</div>
</Card>
);
})}
</div>
<div className="xl:col-span-1">
<Card className="bg-white/10 backdrop-blur border-white/20 p-4 sticky top-4">
<h3 className="text-white font-semibold text-lg mb-4">
Итого
</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-white/60">Товаров:</span>
<span className="text-white">{getTotalItems()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-white/60">Карточек:</span>
<span className="text-white">
{actualSelectedCards.length}
</span>
</div>
<div className="border-t border-white/20 pt-3 flex justify-between">
<span className="text-white font-semibold">
Общая сумма:
</span>
<span className="text-white font-bold text-lg">
{formatCurrency(getTotalAmount())}
</span>
</div>
<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={creatingSupply}
>
<Check className="h-4 w-4 mr-2" />
{creatingSupply ? "Создание..." : "Создать поставку"}
</Button>
</div>
</Card>
</div>
</div>
</div>
</main>
</div>
)
);
}
return (
<div className="space-y-4">
{/* Поиск */}
{/* Поиск товаров и выбор даты поставки */}
<Card className="bg-white/10 backdrop-blur border-white/20 p-3">
@ -1109,17 +1442,15 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white/5 border-white/20 text-white placeholder-white/50 h-9"
onKeyPress={(e) => e.key === 'Enter' && searchCards()}
onKeyPress={(e) => e.key === "Enter" && searchCards()}
/>
</div>
{/* Выбор даты поставки */}
<div className="w-44">
<Popover>
<PopoverTrigger asChild>
<button
className="w-full justify-start text-left font-normal bg-white/5 border border-white/20 text-white hover:bg-white/10 h-9 text-xs px-3 py-2 rounded-md flex items-center transition-colors"
>
<button className="w-full justify-start text-left font-normal bg-white/5 border border-white/20 text-white hover:bg-white/10 h-9 text-xs px-3 py-2 rounded-md flex items-center transition-colors">
<CalendarIcon className="mr-1 h-3 w-3" />
{globalDeliveryDate ? (
format(globalDeliveryDate, "dd.MM.yy", { locale: ru })
@ -1128,20 +1459,22 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<DatePicker
selected={globalDeliveryDate}
onChange={(date: Date | null) => setGlobalDeliveryDate(date || undefined)}
minDate={new Date()}
inline
locale="ru"
/>
</PopoverContent>
<PopoverContent className="w-auto p-0" align="end">
<DatePicker
selected={globalDeliveryDate}
onChange={(date: Date | null) =>
setGlobalDeliveryDate(date || undefined)
}
minDate={new Date()}
inline
locale="ru"
/>
</PopoverContent>
</Popover>
</div>
{/* Кнопка поиска */}
<Button
<Button
onClick={searchCards}
disabled={loading || !searchTerm.trim()}
variant="glass"
@ -1157,7 +1490,10 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{loading && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{[...Array(12)].map((_, i) => (
<Card key={i} className="bg-white/10 backdrop-blur border-white/20 p-3 animate-pulse">
<Card
key={i}
className="bg-white/10 backdrop-blur border-white/20 p-3 animate-pulse"
>
<div className="space-y-3">
<div className="bg-white/20 rounded-lg aspect-square w-full"></div>
<div className="space-y-2">
@ -1176,24 +1512,34 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{!loading && wbCards.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{wbCards.map((card) => {
const selectedQuantity = getSelectedQuantity(card)
const isSelected = selectedQuantity > 0
const selectedCard = actualSelectedCards.find(sc => sc.card.nmID === card.nmID)
const selectedQuantity = getSelectedQuantity(card);
const isSelected = selectedQuantity > 0;
const selectedCard = actualSelectedCards.find(
(sc) => sc.card.nmID === card.nmID
);
return (
<Card key={card.nmID} className={`bg-white/10 backdrop-blur border-white/20 transition-all hover:scale-105 hover:shadow-2xl group ${isSelected ? 'ring-2 ring-purple-500/50 bg-purple-500/10' : ''} relative overflow-hidden`}>
<div className="p-2 space-y-2">
<Card
key={card.nmID}
className={`bg-white/10 backdrop-blur border-white/20 transition-all hover:scale-105 hover:shadow-2xl group ${
isSelected ? "ring-2 ring-purple-500/50 bg-purple-500/10" : ""
} relative overflow-hidden`}
>
<div className="p-1.5 space-y-1.5">
{/* Изображение и основная информация */}
<div className="space-y-2">
<div className="relative">
<div className="aspect-square relative bg-white/5 overflow-hidden rounded-lg">
<img
src={WildberriesService.getCardImage(card, 'c516x688') || '/api/placeholder/300/300'}
src={
WildberriesService.getCardImage(card, "c516x688") ||
"/api/placeholder/300/300"
}
alt={card.title}
className="w-full h-full object-cover cursor-pointer group-hover:scale-110 transition-transform duration-500"
onClick={() => handleCardClick(card)}
/>
{/* Индикатор товара WB */}
<div className="absolute top-2 right-2">
<Badge className="bg-blue-500/90 text-white border-0 backdrop-blur text-xs font-medium px-2 py-1">
@ -1225,7 +1571,7 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
</div>
</div>
</div>
<div className="space-y-2">
{/* Заголовок и бренд */}
<div>
@ -1233,9 +1579,14 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
<Badge className="bg-blue-500/20 text-blue-300 border-blue-500/30 text-xs font-medium">
{card.brand}
</Badge>
<span className="text-white/40 text-xs">{card.nmID}</span>
<span className="text-white/40 text-xs">
{card.nmID}
</span>
</div>
<h3 className="text-white font-semibold text-sm mb-1 line-clamp-2 leading-tight cursor-pointer hover:text-purple-300 transition-colors" onClick={() => handleCardClick(card)}>
<h3
className="text-white font-semibold text-sm mb-1 line-clamp-2 leading-tight cursor-pointer hover:text-purple-300 transition-colors"
onClick={() => handleCardClick(card)}
>
{card.title}
</h3>
</div>
@ -1243,7 +1594,9 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{/* Информация о товаре */}
<div className="pt-2 border-t border-white/10">
<div className="text-center">
<span className="text-white/60 text-xs">Добавьте в поставку для настройки</span>
<span className="text-white/60 text-xs">
Добавьте в поставку для настройки
</span>
</div>
</div>
</div>
@ -1257,8 +1610,12 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
variant="ghost"
size="sm"
onClick={() => {
const newQuantity = Math.max(0, selectedQuantity - 1)
updateCardSelection(card, 'selectedQuantity', newQuantity)
const newQuantity = Math.max(0, selectedQuantity - 1);
updateCardSelection(
card,
"selectedQuantity",
newQuantity
);
}}
disabled={selectedQuantity <= 0}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20 disabled:opacity-50 flex-shrink-0"
@ -1271,9 +1628,13 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
pattern="[0-9]*"
value={selectedQuantity}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9]/g, '')
const numValue = Math.max(0, parseInt(value) || 0)
updateCardSelection(card, 'selectedQuantity', numValue)
const value = e.target.value.replace(/[^0-9]/g, "");
const numValue = Math.max(0, parseInt(value) || 0);
updateCardSelection(
card,
"selectedQuantity",
numValue
);
}}
onFocus={(e) => e.target.select()}
className="flex-1 h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-purple-500 focus:border-transparent [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-w-0"
@ -1283,8 +1644,12 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
variant="ghost"
size="sm"
onClick={() => {
const newQuantity = selectedQuantity + 1
updateCardSelection(card, 'selectedQuantity', newQuantity)
const newQuantity = selectedQuantity + 1;
updateCardSelection(
card,
"selectedQuantity",
newQuantity
);
}}
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10 border border-white/20 flex-shrink-0"
>
@ -1297,11 +1662,13 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
<input
type="text"
inputMode="decimal"
value={getSelectedTotalPrice(card) || ''}
value={getSelectedTotalPrice(card) || ""}
onChange={(e) => {
const value = e.target.value.replace(/[^0-9.,]/g, '').replace(',', '.')
const totalPrice = parseFloat(value) || 0
updateCardSelection(card, 'customPrice', totalPrice)
const value = e.target.value
.replace(/[^0-9.,]/g, "")
.replace(",", ".");
const totalPrice = parseFloat(value) || 0;
updateCardSelection(card, "customPrice", totalPrice);
}}
onFocus={(e) => e.target.select()}
className="w-full h-6 text-center bg-white/10 border border-white/20 text-white text-xs rounded focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent"
@ -1310,18 +1677,19 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
)}
{/* Результат - очень компактно */}
{selectedQuantity > 0 && getSelectedTotalPrice(card) > 0 && (
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded p-1.5">
<div className="text-center space-y-0.5">
<div className="text-green-300 text-xs font-medium">
{formatCurrency(getSelectedTotalPrice(card))}
</div>
<div className="text-green-200 text-[10px]">
~{formatCurrency(getSelectedUnitPrice(card))}/шт
{selectedQuantity > 0 &&
getSelectedTotalPrice(card) > 0 && (
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 rounded p-1.5">
<div className="text-center space-y-0.5">
<div className="text-green-300 text-xs font-medium">
{formatCurrency(getSelectedTotalPrice(card))}
</div>
<div className="text-green-200 text-[10px]">
~{formatCurrency(getSelectedUnitPrice(card))}/шт
</div>
</div>
</div>
</div>
)}
)}
{/* Индикатор подготовки к добавлению */}
{selectedQuantity > 0 && (
@ -1335,7 +1703,7 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
</div>
</div>
</Card>
)
);
})}
</div>
)}
@ -1343,12 +1711,13 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{/* Плавающая кнопка "В корзину" для подготовленных товаров */}
{preparingCards.length > 0 && getPreparingTotalItems() > 0 && (
<div className="fixed bottom-6 right-6 z-50">
<Button
<Button
onClick={addToCart}
className="bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white shadow-lg h-12 px-6"
>
<ShoppingCart className="h-5 w-5 mr-2" />
В корзину ({getPreparingTotalItems()}) {formatCurrency(getPreparingTotalAmount())}
<ShoppingCart className="h-5 w-5 mr-2" />В корзину (
{getPreparingTotalItems()}) {" "}
{formatCurrency(getPreparingTotalAmount())}
</Button>
</div>
)}
@ -1357,13 +1726,18 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center max-w-md mx-auto">
<Package className="h-12 w-12 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">Нет товаров</h3>
{user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? (
<h3 className="text-lg font-semibold text-white mb-2">
Нет товаров
</h3>
{user?.organization?.apiKeys?.find(
(key) => key.marketplace === "WILDBERRIES"
)?.isActive ? (
<>
<p className="text-white/60 mb-4 text-sm">
Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки
Введите запрос в поле поиска, чтобы найти товары в вашем
каталоге Wildberries, или загрузите все доступные карточки
</p>
<Button
<Button
onClick={loadAllCards}
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
>
@ -1374,12 +1748,13 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
) : (
<>
<p className="text-white/60 mb-3 text-sm">
Для работы с реальными карточками необходимо настроить API ключ Wildberries
Для работы с реальными карточками необходимо настроить API
ключ Wildberries
</p>
<p className="text-white/40 text-xs mb-4">
Показаны демонстрационные товары для тестирования
</p>
<Button
<Button
onClick={loadAllCards}
className="bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white"
>
@ -1393,23 +1768,33 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
)}
{/* Модальное окно с детальной информацией о товаре */}
<Dialog open={!!selectedCardForDetails} onOpenChange={(open) => !open && closeDetailsModal()}>
<Dialog
open={!!selectedCardForDetails}
onOpenChange={(open) => !open && closeDetailsModal()}
>
<DialogContent className="glass-card border-white/10 max-w-2xl">
<DialogHeader>
<DialogTitle className="text-white">Информация о товаре</DialogTitle>
<DialogTitle className="text-white">
Информация о товаре
</DialogTitle>
</DialogHeader>
{selectedCardForDetails && (
<div className="space-y-4">
{/* Изображение */}
<div className="relative">
<img
src={WildberriesService.getCardImages(selectedCardForDetails)[currentImageIndex] || '/api/placeholder/400/400'}
src={
WildberriesService.getCardImages(selectedCardForDetails)[
currentImageIndex
] || "/api/placeholder/400/400"
}
alt={selectedCardForDetails.title}
className="w-full aspect-video rounded-lg object-cover"
/>
{/* Навигация по изображениям */}
{WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
{WildberriesService.getCardImages(selectedCardForDetails)
.length > 1 && (
<>
<button
onClick={prevImage}
@ -1423,9 +1808,13 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
>
<ChevronRight className="h-4 w-4" />
</button>
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 bg-black/70 px-3 py-1 rounded-full text-white text-sm">
{currentImageIndex + 1} из {WildberriesService.getCardImages(selectedCardForDetails).length}
{currentImageIndex + 1} из{" "}
{
WildberriesService.getCardImages(selectedCardForDetails)
.length
}
</div>
</>
)}
@ -1434,18 +1823,26 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{/* Основная информация */}
<div className="space-y-3">
<div>
<h3 className="text-white font-semibold text-lg">{selectedCardForDetails.title}</h3>
<p className="text-white/60 text-sm">Артикул WB: {selectedCardForDetails.nmID}</p>
<h3 className="text-white font-semibold text-lg">
{selectedCardForDetails.title}
</h3>
<p className="text-white/60 text-sm">
Артикул WB: {selectedCardForDetails.nmID}
</p>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between">
<span className="text-white/60">Бренд:</span>
<span className="text-white font-medium">{selectedCardForDetails.brand}</span>
<span className="text-white font-medium">
{selectedCardForDetails.brand}
</span>
</div>
<div className="flex justify-between">
<span className="text-white/60">Категория:</span>
<span className="text-white font-medium">{selectedCardForDetails.object}</span>
<span className="text-white font-medium">
{selectedCardForDetails.object}
</span>
</div>
</div>
@ -1460,19 +1857,24 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
</div>
{/* Миниатюры изображений */}
{WildberriesService.getCardImages(selectedCardForDetails).length > 1 && (
{WildberriesService.getCardImages(selectedCardForDetails).length >
1 && (
<div className="flex space-x-2 overflow-x-auto pb-2">
{WildberriesService.getCardImages(selectedCardForDetails).map((image, index) => (
<img
key={index}
src={image}
alt={`${selectedCardForDetails.title} ${index + 1}`}
className={`w-16 h-16 rounded-lg object-cover cursor-pointer flex-shrink-0 transition-all ${
index === currentImageIndex ? 'ring-2 ring-purple-500' : 'opacity-60 hover:opacity-100'
}`}
onClick={() => setCurrentImageIndex(index)}
/>
))}
{WildberriesService.getCardImages(selectedCardForDetails).map(
(image, index) => (
<img
key={index}
src={image}
alt={`${selectedCardForDetails.title} ${index + 1}`}
className={`w-16 h-16 rounded-lg object-cover cursor-pointer flex-shrink-0 transition-all ${
index === currentImageIndex
? "ring-2 ring-purple-500"
: "opacity-60 hover:opacity-100"
}`}
onClick={() => setCurrentImageIndex(index)}
/>
)
)}
</div>
)}
</div>
@ -1480,5 +1882,5 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
</DialogContent>
</Dialog>
</div>
)
}
);
}

View File

@ -936,7 +936,15 @@ export const resolvers = {
where: {
organizationId: currentUser.organization.id, // Создали мы
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
status: { in: ["CONFIRMED", "IN_TRANSIT"] }, // Подтверждено или в пути
status: {
in: [
"CONFIRMED",
"SUPPLIER_APPROVED",
"LOGISTICS_CONFIRMED",
"IN_TRANSIT",
"SHIPPED",
],
}, // Активные статусы
},
});
@ -945,7 +953,7 @@ export const resolvers = {
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
status: "IN_TRANSIT", // В пути - нужно подтвердить получение
status: { in: ["IN_TRANSIT", "SHIPPED"] }, // В пути или отправлено - нужно подтвердить получение
},
});
@ -5216,7 +5224,10 @@ export const resolvers = {
status:
| "PENDING"
| "CONFIRMED"
| "SUPPLIER_APPROVED"
| "LOGISTICS_CONFIRMED"
| "IN_TRANSIT"
| "SHIPPED"
| "DELIVERED"
| "CANCELLED";
},

View File

@ -594,7 +594,10 @@ export const typeDefs = gql`
enum SupplyOrderStatus {
PENDING
CONFIRMED
SUPPLIER_APPROVED
LOGISTICS_CONFIRMED
IN_TRANSIT
SHIPPED
DELIVERED
CANCELLED
}