diff --git a/package-lock.json b/package-lock.json index 2546135..f4d5fe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "sferav", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@apollo/client": "^3.13.8", "@apollo/server": "^4.12.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2a03fdc..b8318ad 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -429,7 +429,10 @@ enum ScheduleStatus { enum SupplyOrderStatus { PENDING CONFIRMED + SUPPLIER_APPROVED + LOGISTICS_CONFIRMED IN_TRANSIT + SHIPPED DELIVERED CANCELLED } diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx index 6a923da..f38e45d 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/fulfillment-consumables-orders-tab.tsx @@ -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; diff --git a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx index f77e949..fcac76e 100644 --- a/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx +++ b/src/components/supplies/consumables-supplies/consumables-supplies-tab.tsx @@ -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 (
{/* Статистика заказов поставок */} -
- -
-
- +
+ +
+
+

Заказов поставок

-

+

{supplyOrders.length}

- -
-
- + +
+
+

Общая сумма

-

+

{formatCurrency( supplyOrders.reduce( (sum, order) => sum + Number(order.totalAmount), @@ -188,14 +208,14 @@ export function SuppliesConsumablesTab() {

- -
-
- + +
+
+

В пути

-

+

{ supplyOrders.filter((order) => order.status === "IN_TRANSIT") .length @@ -205,14 +225,14 @@ export function SuppliesConsumablesTab() {

- -
-
- + +
+
+

Доставлено

-

+

{ supplyOrders.filter((order) => order.status === "DELIVERED") .length diff --git a/src/components/supplies/create-consumables-supply-page.tsx b/src/components/supplies/create-consumables-supply-page.tsx index e37ec7f..b1c4bef 100644 --- a/src/components/supplies/create-consumables-supply-page.tsx +++ b/src/components/supplies/create-consumables-supply-page.tsx @@ -495,7 +495,7 @@ export function CreateConsumablesSupplyPage() {

) : ( -
+
{supplierProducts.map( (product: ConsumableProduct, index) => { const selectedQuantity = getSelectedQuantity( @@ -504,14 +504,14 @@ export function CreateConsumablesSupplyPage() { return ( 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%", }} > diff --git a/src/components/supplies/fulfillment-supplies/all-supplies-tab.tsx b/src/components/supplies/fulfillment-supplies/all-supplies-tab.tsx index 1b8dca1..72ac5e9 100644 --- a/src/components/supplies/fulfillment-supplies/all-supplies-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/all-supplies-tab.tsx @@ -20,19 +20,19 @@ export function AllSuppliesTab({ const isWholesale = user?.organization?.type === "WHOLESALE"; return ( -
+
{/* Секция товаров */} - -

Товары

-
+ +

Товары

+
{/* Секция расходников */} - -

Расходники

-
+ +

Расходники

+
{isWholesale ? : }
diff --git a/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx b/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx index 232f8a8..43b79a6 100644 --- a/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/real-supply-orders-tab.tsx @@ -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; }) => (
sortable && onSort && onSort(field)} > - {children} + {children} {sortable && ( @@ -109,7 +108,7 @@ const TableHeader = ({
); -// Компонент для статистических карточек +// Современный компонент для статистических карточек const StatsCard = ({ title, value, @@ -127,38 +126,37 @@ const StatsCard = ({ iconBg?: string; subtitle?: string; }) => ( - -
-
-
- -
-
-

{title}

-
-

{value}

- {change !== 0 && ( -
- {change > 0 ? ( - - ) : ( - - )} - 0 ? "text-green-400" : "text-red-400" - }`} - > - {Math.abs(change)}% - -
- )} -
- {subtitle &&

{subtitle}

} -
+
+
+
+
+ +
+

+ {title} +

+

{value}

+
+ + {change !== 0 && ( +
+ {change > 0 ? ( + + ) : ( + + )} + 0 ? "text-emerald-400" : "text-red-400" + }`} + > + {Math.abs(change)}% + +
+ )}
- +
); 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 (
- {/* Статистические карточки - 30% экрана */} -
-
+ {/* Компактные статистические карточки */} +
+
- {/* Основная таблица - 70% экрана */} + {/* Современная таблица заявок */}
- - {/* Шапка таблицы с поиском */} -
+
+ {/* Компактная шапка с поиском */} +
-

- - Заявки на расходники -

- - {/* Поиск */} -
- - setSearchTerm(e.target.value)} - className="pl-8 h-8 text-sm glass-input text-white placeholder:text-white/40" - /> +
+
+ +
+
+

+ Заявки на расходники +

+
- - {filteredAndSortedOrders.length} заявок - +
+ {/* Компактный поиск */} +
+
+ +
+ 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" + /> +
+ +
+ + {filteredAndSortedOrders.length} + +
+
- {/* Заголовки таблицы */} -
-
+ {/* Современные заголовки таблицы */} +
+
- {/* Строка с итогами */} -
-
-
+ {/* Компактная строка итогов */} +
+
+
ИТОГО ({totals.orders})
{totals.orders} заказчиков
-
-
+
{formatNumber(totals.items)} шт
-
+
{formatCurrency(totals.amount)}
-
+
{totals.pending} ожидают
-
-
+
@@ -760,65 +789,66 @@ export function RealSupplyOrdersTab() { return (
- {/* Основная строка заказа */} -
+ {/* Компактная строка заказа */} +
toggleOrderExpansion(order.id)} + >
- - {filteredAndSortedOrders.length - index} - - - - {order.id.slice(-8)} + + #{filteredAndSortedOrders.length - index} +
+ + {order.id.slice(-8)} + +
- + {getInitials(organizationName)}
- + {organizationName} -

+

{order.organization.type}

-
- - +
+ + {formatDate(order.deliveryDate)}
- - {order.totalItems} шт - +
+ + {order.totalItems} шт + +
-
- - +
+ + {formatCurrency(order.totalAmount)}
@@ -827,7 +857,7 @@ export function RealSupplyOrdersTab() { {getStatusBadge(order.status)}
-
+
{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" > - - Одобрить + + Одобр. )} @@ -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" > - - Отправить + + Отпр. )} {order.status === "CANCELLED" && ( @@ -951,7 +981,7 @@ export function RealSupplyOrdersTab() { }) )}
- +
); diff --git a/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx b/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx index 91fd18b..0a5beb7 100644 --- a/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/seller-supply-orders-tab.tsx @@ -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", diff --git a/src/components/supplies/product-card.tsx b/src/components/supplies/product-card.tsx index fd1072d..2510760 100644 --- a/src/components/supplies/product-card.tsx +++ b/src/components/supplies/product-card.tsx @@ -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 (
{product.name} - + {/* Количество в наличии */}
- 50 - ? 'bg-green-500/80' - : product.quantity > 10 - ? 'bg-yellow-500/80' - : 'bg-red-500/80' - } text-white border-0 backdrop-blur text-xs`}> + 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}
@@ -70,17 +69,25 @@ export function ProductCard({ {/* Overlay с кнопками */}
- -
- -
+ +
{/* Заголовок и бренд */}
@@ -102,7 +109,7 @@ export function ProductCard({ )}
-

+

{product.name}

@@ -110,17 +117,19 @@ export function ProductCard({ {/* Основная характеристика */}
{product.color && {product.color}} - {product.size && {product.size}} + {product.size && ( + {product.size} + )}
{/* Цена */} -
+
-
+
{formatCurrency(discountedPrice)}
{product.discount && ( -
+
{formatCurrency(product.price)}
)} @@ -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({ > - + {selectedQuantity > 0 && ( @@ -179,5 +188,5 @@ export function ProductCard({ )}
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/supplies/product-grid.tsx b/src/components/supplies/product-grid.tsx index 5b5520a..d5d0d5c 100644 --- a/src/components/supplies/product-grid.tsx +++ b/src/components/supplies/product-grid.tsx @@ -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 - onQuantityChange: (productId: string, quantity: number) => void - formatCurrency: (amount: number) => string - loading?: boolean + products: WholesalerProduct[]; + selectedProducts: Record; + 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({

Загружаем товары...

- ) + ); } if (products.length === 0) { @@ -37,23 +37,27 @@ export function ProductGrid({

У этого поставщика нет товаров

-

Выберите другого поставщика

+

+ Выберите другого поставщика +

- ) + ); } return ( -
+
{products.map((product) => ( onQuantityChange(product.id, quantity)} + onQuantityChange={(quantity) => + onQuantityChange(product.id, quantity) + } formatCurrency={formatCurrency} /> ))}
- ) -} \ No newline at end of file + ); +} diff --git a/src/components/supplies/supplier-selection.tsx b/src/components/supplies/supplier-selection.tsx index 7c22de4..d9ac94a 100644 --- a/src/components/supplies/supplier-selection.tsx +++ b/src/components/supplies/supplier-selection.tsx @@ -194,11 +194,11 @@ export function SupplierSelection({
-
+
{mockSuppliers.map((supplier) => ( setSelectedSupplier(supplier)} >
diff --git a/src/components/supplies/supplies-dashboard.tsx b/src/components/supplies/supplies-dashboard.tsx index 0ffd622..b6c779f 100644 --- a/src/components/supplies/supplies-dashboard.tsx +++ b/src/components/supplies/supplies-dashboard.tsx @@ -61,14 +61,14 @@ export function SuppliesDashboard() {
{/* Уведомляющий баннер */} {hasPendingItems && ( - - - + + + У вас есть {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" > -
+
-
- +
+

{title}

{value} diff --git a/src/components/supplies/wb-product-cards.tsx b/src/components/supplies/wb-product-cards.tsx index 8596658..8cb40f8 100644 --- a/src/components/supplies/wb-product-cards.tsx +++ b/src/components/supplies/wb-product-cards.tsx @@ -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([]) - const [selectedCards, setSelectedCards] = useState([]) // Товары в корзине - +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([]); + const [selectedCards, setSelectedCards] = useState([]); // Товары в корзине + // Используем внешнее состояние если передано - const actualSelectedCards = externalSelectedCards !== undefined ? externalSelectedCards : selectedCards - const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards - const [preparingCards, setPreparingCards] = useState([]) // Товары, готовящиеся к добавлению - const [showSummary, setShowSummary] = useState(false) - + const actualSelectedCards = + externalSelectedCards !== undefined ? externalSelectedCards : selectedCards; + const actualSetSelectedCards = externalSetSelectedCards || setSelectedCards; + const [preparingCards, setPreparingCards] = useState([]); // Товары, готовящиеся к добавлению + const [showSummary, setShowSummary] = useState(false); + // Используем внешнее состояние если передано - const actualShowSummary = externalShowSummary !== undefined ? externalShowSummary : showSummary - const actualSetShowSummary = externalSetShowSummary || setShowSummary - const [globalDeliveryDate, setGlobalDeliveryDate] = useState(undefined) - const [fulfillmentServices, setFulfillmentServices] = useState([]) - 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(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(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 - + 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 - 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 - 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 (

-
+
-
-

Корзина

-

{actualSelectedCards.length} карточек товаров

+

+ Корзина +

+

+ {actualSelectedCards.length} карточек товаров +

- {/* Массовое назначение поставщиков */} - -

Быстрое назначение

-
-
- - -
- -
- - -
- -
- - - - - - - { - setGlobalDeliveryDate(date || undefined) - if (date) { - const dateString = date.toISOString().split('T')[0] - actualSelectedCards.forEach(sc => { - updateCardSelection(sc.card, 'deliveryDate', dateString) - }) + {/* Массовое назначение поставщиков */} + +

+ Быстрое назначение +

+
+
+ + updateCardSelection(sc.card, 'selectedQuantity', parseInt(e.target.value) || 0)} - className="bg-white/5 border-white/20 text-white mt-1" - min="1" - /> -
-
- - { - 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 ( -
-
Дополнительные расходы за 1 шт:
- {sc.selectedFulfillmentServices.length > 0 && ( -
- Услуги: {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(', ')} -
- )} - {sc.selectedConsumableServices.length > 0 && ( -
- Расходники: {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(', ')} -
- )} -
- Итого доп. расходы: {formatCurrency(additionalCost)} -
-
- Полная стоимость за 1 шт: {formatCurrency((sc.customPrice / sc.selectedQuantity) + additionalCost)} -
-
- ) - } - return null - })()} -
-
- - {/* Услуги */} -
-
- - -
- - {sc.selectedFulfillmentOrg && ( -
- -
- {organizationServices[sc.selectedFulfillmentOrg] ? ( - organizationServices[sc.selectedFulfillmentOrg].length > 0 ? ( - organizationServices[sc.selectedFulfillmentOrg].map((service) => { - const isSelected = sc.selectedFulfillmentServices.includes(service.id) - return ( - - ) - }) - ) : ( -
- У данной организации нет услуг -
- ) - ) : ( -
- Загрузка услуг... -
- )} -
-
- )} - -
- - -
- - {sc.selectedConsumableOrg && ( -
- -
- {organizationSupplies[sc.selectedConsumableOrg] ? ( - organizationSupplies[sc.selectedConsumableOrg].length > 0 ? ( - organizationSupplies[sc.selectedConsumableOrg].map((supply) => { - const isSelected = sc.selectedConsumableServices.includes(supply.id) - return ( - - ) - }) - ) : ( -
- У данной организации нет расходников -
- ) - ) : ( -
- Загрузка расходников... -
- )} -
-
- )} -
-
- -
- - {formatCurrency(sc.customPrice)} - - {sc.selectedQuantity > 0 && sc.customPrice > 0 && ( -

- ~{formatCurrency(sc.customPrice / sc.selectedQuantity)} за шт. -

- )} -
-
-
- - ) - })} -
- -
- -

Итого

-
-
- Товаров: - {getTotalItems()} + > + + + + + Не выбран + {(counterpartiesData?.myCounterparties || []) + .filter( + (org: Organization) => org.type === "FULFILLMENT" + ) + .map((org: Organization) => ( + + {org.name} + + ))} + +
-
- Карточек: - {actualSelectedCards.length} + +
+ +
-
- Общая сумма: - {formatCurrency(getTotalAmount())} + +
+ + + + + + + { + 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" + /> + +
-
-
+ +
+
+ {actualSelectedCards.map((sc) => { + const fulfillmentOrgs = ( + counterpartiesData?.myCounterparties || [] + ).filter((org: Organization) => org.type === "FULFILLMENT"); + const consumableOrgs = ( + counterpartiesData?.myCounterparties || [] + ).filter((org: Organization) => org.type === "FULFILLMENT"); + + return ( + +
+ {sc.card.title} +
+
+

+ {sc.card.title} +

+

+ WB: {sc.card.nmID} +

+
+ +
+ {/* Количество и цена */} +
+
+ + + updateCardSelection( + sc.card, + "selectedQuantity", + parseInt(e.target.value) || 0 + ) + } + className="bg-white/5 border-white/20 text-white mt-1" + min="1" + /> +
+
+ + { + 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 ( +
+
+ Дополнительные расходы за 1 шт: +
+ {sc.selectedFulfillmentServices.length > + 0 && ( +
+ Услуги:{" "} + {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(", ")} +
+ )} + {sc.selectedConsumableServices.length > + 0 && ( +
+ Расходники:{" "} + {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(", ")} +
+ )} +
+ Итого доп. расходы:{" "} + {formatCurrency(additionalCost)} +
+
+ Полная стоимость за 1 шт:{" "} + {formatCurrency( + sc.customPrice / + sc.selectedQuantity + + additionalCost + )} +
+
+ ); + } + return null; + })()} +
+
+ + {/* Услуги */} +
+
+ + +
+ + {sc.selectedFulfillmentOrg && ( +
+ +
+ {organizationServices[ + sc.selectedFulfillmentOrg + ] ? ( + organizationServices[ + sc.selectedFulfillmentOrg + ].length > 0 ? ( + organizationServices[ + sc.selectedFulfillmentOrg + ].map((service) => { + const isSelected = + sc.selectedFulfillmentServices.includes( + service.id + ); + return ( + + ); + }) + ) : ( +
+ У данной организации нет услуг +
+ ) + ) : ( +
+ Загрузка услуг... +
+ )} +
+
+ )} + +
+ + +
+ + {sc.selectedConsumableOrg && ( +
+ +
+ {organizationSupplies[ + sc.selectedConsumableOrg + ] ? ( + organizationSupplies[ + sc.selectedConsumableOrg + ].length > 0 ? ( + organizationSupplies[ + sc.selectedConsumableOrg + ].map((supply) => { + const isSelected = + sc.selectedConsumableServices.includes( + supply.id + ); + return ( + + ); + }) + ) : ( +
+ У данной организации нет расходников +
+ ) + ) : ( +
+ Загрузка расходников... +
+ )} +
+
+ )} +
+
+ +
+ + {formatCurrency(sc.customPrice)} + + {sc.selectedQuantity > 0 && sc.customPrice > 0 && ( +

+ ~ + {formatCurrency( + sc.customPrice / sc.selectedQuantity + )}{" "} + за шт. +

+ )} +
+
+
+
+ ); + })} +
+ +
+ +

+ Итого +

+
+
+ Товаров: + {getTotalItems()} +
+
+ Карточек: + + {actualSelectedCards.length} + +
+
+ + Общая сумма: + + + {formatCurrency(getTotalAmount())} + +
+ +
+
+
- ) + ); } return (
- {/* Поиск */} {/* Поиск товаров и выбор даты поставки */} @@ -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()} />
- + {/* Выбор даты поставки */}
- - - setGlobalDeliveryDate(date || undefined)} - minDate={new Date()} - inline - locale="ru" - /> - + + + setGlobalDeliveryDate(date || undefined) + } + minDate={new Date()} + inline + locale="ru" + /> +
- + {/* Кнопка поиска */} -
)} @@ -1357,13 +1726,18 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
-

Нет товаров

- {user?.organization?.apiKeys?.find(key => key.marketplace === 'WILDBERRIES')?.isActive ? ( +

+ Нет товаров +

+ {user?.organization?.apiKeys?.find( + (key) => key.marketplace === "WILDBERRIES" + )?.isActive ? ( <>

- Введите запрос в поле поиска, чтобы найти товары в вашем каталоге Wildberries, или загрузите все доступные карточки + Введите запрос в поле поиска, чтобы найти товары в вашем + каталоге Wildberries, или загрузите все доступные карточки

- - +
- {currentImageIndex + 1} из {WildberriesService.getCardImages(selectedCardForDetails).length} + {currentImageIndex + 1} из{" "} + { + WildberriesService.getCardImages(selectedCardForDetails) + .length + }
)} @@ -1434,18 +1823,26 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu {/* Основная информация */}
-

{selectedCardForDetails.title}

-

Артикул WB: {selectedCardForDetails.nmID}

+

+ {selectedCardForDetails.title} +

+

+ Артикул WB: {selectedCardForDetails.nmID} +

- +
Бренд: - {selectedCardForDetails.brand} + + {selectedCardForDetails.brand} +
Категория: - {selectedCardForDetails.object} + + {selectedCardForDetails.object} +
@@ -1460,19 +1857,24 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
{/* Миниатюры изображений */} - {WildberriesService.getCardImages(selectedCardForDetails).length > 1 && ( + {WildberriesService.getCardImages(selectedCardForDetails).length > + 1 && (
- {WildberriesService.getCardImages(selectedCardForDetails).map((image, index) => ( - {`${selectedCardForDetails.title} setCurrentImageIndex(index)} - /> - ))} + {WildberriesService.getCardImages(selectedCardForDetails).map( + (image, index) => ( + {`${selectedCardForDetails.title} setCurrentImageIndex(index)} + /> + ) + )}
)}
@@ -1480,5 +1882,5 @@ export function WBProductCards({ onBack, onComplete, showSummary: externalShowSu
- ) -} \ No newline at end of file + ); +} diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index 4811824..0bcf126 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -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"; }, diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 4c0ace8..9eb0079 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -594,7 +594,10 @@ export const typeDefs = gql` enum SupplyOrderStatus { PENDING CONFIRMED + SUPPLIER_APPROVED + LOGISTICS_CONFIRMED IN_TRANSIT + SHIPPED DELIVERED CANCELLED }