WIP: supplies and fulfillment updates
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "sferav",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@apollo/server": "^4.12.2",
|
||||
|
@ -429,7 +429,10 @@ enum ScheduleStatus {
|
||||
enum SupplyOrderStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
SUPPLIER_APPROVED
|
||||
LOGISTICS_CONFIRMED
|
||||
IN_TRANSIT
|
||||
SHIPPED
|
||||
DELIVERED
|
||||
CANCELLED
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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%",
|
||||
}}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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" : ""
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
},
|
||||
|
@ -594,7 +594,10 @@ export const typeDefs = gql`
|
||||
enum SupplyOrderStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
SUPPLIER_APPROVED
|
||||
LOGISTICS_CONFIRMED
|
||||
IN_TRANSIT
|
||||
SHIPPED
|
||||
DELIVERED
|
||||
CANCELLED
|
||||
}
|
||||
|
Reference in New Issue
Block a user