From 1e22f6fef997e2260627d819efd371eec8717128 Mon Sep 17 00:00:00 2001 From: Bivekich Date: Thu, 24 Jul 2025 16:13:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20PvzReturnsTab:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=20API=20Wildberries=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D1=8F=D0=B2=D0=BE=D0=BA=20=D0=BD=D0=B0=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=82,=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B8=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=83=D1=81=D0=B0=D0=BC.=20=D0=A3=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=20=D1=81=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=D0=BC=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BE=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=D1=85.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87=D0=B8=D0=B5=20?= =?UTF-8?q?API=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BE=D0=BA=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D0=BA=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fulfillment-supplies/pvz-returns-tab.tsx | 535 +++++--- .../fulfillment-supplies/pvz-returns-tab.tsx | 1133 +++++++---------- src/services/wildberries-service.ts | 107 ++ 3 files changed, 910 insertions(+), 865 deletions(-) diff --git a/src/components/fulfillment-supplies/fulfillment-supplies/pvz-returns-tab.tsx b/src/components/fulfillment-supplies/fulfillment-supplies/pvz-returns-tab.tsx index 5a47fd9..58056b6 100644 --- a/src/components/fulfillment-supplies/fulfillment-supplies/pvz-returns-tab.tsx +++ b/src/components/fulfillment-supplies/fulfillment-supplies/pvz-returns-tab.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -13,51 +13,130 @@ import { AlertCircle, Eye, MapPin, + RefreshCw, + ExternalLink, + MessageCircle, } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; +import { WildberriesService, type WBClaim, type WBClaimsResponse } from "@/services/wildberries-service"; +import { toast } from "sonner"; -// Мок данные для возвратов с ПВЗ -const mockPvzReturns = [ - { - id: "1", - productName: "Смартфон Samsung Galaxy S23", - sku: "SAM-S23-128-BLK", - pvzAddress: "ул. Ленина, 15, ПВЗ №1234", - returnDate: "2024-01-13", - status: "collected", - quantity: 3, - reason: "Брак товара", - estimatedValue: 150000, - seller: "TechWorld", - }, - { - id: "2", - productName: "Кроссовки Nike Air Max", - sku: "NIKE-AM-42-WHT", - pvzAddress: "пр. Мира, 88, ПВЗ №5678", - returnDate: "2024-01-12", - status: "pending", - quantity: 2, - reason: "Не подошел размер", - estimatedValue: 24000, - seller: "SportsGear", - }, - { - id: "3", - productName: "Планшет iPad Air", - sku: "IPAD-AIR-256-GRY", - pvzAddress: "ул. Советская, 42, ПВЗ №9012", - returnDate: "2024-01-11", - status: "processed", - quantity: 1, - reason: "Передумал покупать", - estimatedValue: 85000, - seller: "AppleStore", - }, -]; +// Интерфейс для обработанных данных возврата +interface ProcessedClaim { + id: string; + productName: string; + nmId: number; + returnDate: string; + status: string; + reason: string; + price: number; + userComment: string; + wbComment: string; + photos: string[]; + videoPaths: string[]; + actions: string[]; + orderDate: string; + lastUpdate: string; +} export function PvzReturnsTab() { + const { user } = useAuth(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const [archiveFilter, setArchiveFilter] = useState(false); + const [claims, setClaims] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [total, setTotal] = useState(0); + + // Загрузка заявок + const loadClaims = async (showToast = false) => { + const isInitialLoad = !refreshing; + if (isInitialLoad) setLoading(true); + else setRefreshing(true); + + try { + const wbApiKey = user?.organization?.apiKeys?.find( + (key) => key.marketplace === "WILDBERRIES" && key.isActive + ); + + if (!wbApiKey) { + if (showToast) { + toast.error("API ключ Wildberries не настроен"); + } + return; + } + + const apiToken = wbApiKey.apiKey; + + console.log("WB Claims: Loading claims with archive =", archiveFilter); + + const response = await WildberriesService.getClaims(apiToken, { + isArchive: archiveFilter, + limit: 100, + offset: 0, + }); + + const processedClaims = response.claims.map(processClaim); + setClaims(processedClaims); + setTotal(response.total); + + console.log(`WB Claims: Loaded ${processedClaims.length} claims`); + + if (showToast) { + toast.success(`Загружено заявок: ${processedClaims.length}`); + } + } catch (error) { + console.error("Error loading claims:", error); + if (showToast) { + toast.error("Ошибка загрузки заявок на возврат"); + } + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + // Обработка данных из API в удобный формат + const processClaim = (claim: WBClaim): ProcessedClaim => { + const getStatusLabel = (status: number, statusEx: number) => { + // Мапинг статусов на основе документации API + switch (status) { + case 1: + return "На рассмотрении"; + case 2: + return "Одобрена"; + case 3: + return "Отклонена"; + case 4: + return "В архиве"; + default: + return `Статус ${status}`; + } + }; + + return { + id: claim.id, + productName: claim.imt_name, + nmId: claim.nm_id, + returnDate: claim.dt, + status: getStatusLabel(claim.status, claim.status_ex), + reason: claim.user_comment || "Не указана", + price: claim.price, + userComment: claim.user_comment, + wbComment: claim.wb_comment, + photos: claim.photos || [], + videoPaths: claim.video_paths || [], + actions: claim.actions || [], + orderDate: claim.order_dt, + lastUpdate: claim.dt_update, + }; + }; + + // Загрузка при монтировании компонента + useEffect(() => { + loadClaims(); + }, [user, archiveFilter]); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("ru-RU", { @@ -77,26 +156,28 @@ export function PvzReturnsTab() { const getStatusBadge = (status: string) => { const statusConfig = { - pending: { + "На рассмотрении": { color: "text-yellow-300 border-yellow-400/30", - label: "Ожидает сбора", + label: "На рассмотрении", }, - collected: { - color: "text-blue-300 border-blue-400/30", - label: "Собрано", - }, - processed: { + "Одобрена": { color: "text-green-300 border-green-400/30", - label: "Обработано", + label: "Одобрена", }, - disposed: { + "Отклонена": { color: "text-red-300 border-red-400/30", - label: "Утилизировано", + label: "Отклонена", + }, + "В архиве": { + color: "text-gray-300 border-gray-400/30", + label: "В архиве", }, }; - const config = - statusConfig[status as keyof typeof statusConfig] || statusConfig.pending; + const config = statusConfig[status as keyof typeof statusConfig] || { + color: "text-gray-300 border-gray-400/30", + label: status, + }; return ( @@ -105,64 +186,57 @@ export function PvzReturnsTab() { ); }; - const getReasonBadge = (reason: string) => { - const reasonConfig = { - "Брак товара": { color: "text-red-300 border-red-400/30" }, - "Не подошел размер": { color: "text-orange-300 border-orange-400/30" }, - "Передумал покупать": { color: "text-blue-300 border-blue-400/30" }, - Другое: { color: "text-gray-300 border-gray-400/30" }, - }; - - const config = - reasonConfig[reason as keyof typeof reasonConfig] || - reasonConfig["Другое"]; - - return ( - - {reason} - - ); - }; - - const filteredReturns = mockPvzReturns.filter((returnItem) => { + const filteredClaims = claims.filter((claim) => { const matchesSearch = - returnItem.productName.toLowerCase().includes(searchTerm.toLowerCase()) || - returnItem.sku.toLowerCase().includes(searchTerm.toLowerCase()) || - returnItem.pvzAddress.toLowerCase().includes(searchTerm.toLowerCase()) || - returnItem.seller.toLowerCase().includes(searchTerm.toLowerCase()); + claim.productName.toLowerCase().includes(searchTerm.toLowerCase()) || + claim.nmId.toString().includes(searchTerm) || + claim.reason.toLowerCase().includes(searchTerm.toLowerCase()) || + claim.id.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = - statusFilter === "all" || returnItem.status === statusFilter; + statusFilter === "all" || claim.status === statusFilter; return matchesSearch && matchesStatus; }); const getTotalValue = () => { - return filteredReturns.reduce( - (sum, returnItem) => sum + returnItem.estimatedValue, - 0 - ); - }; - - const getTotalQuantity = () => { - return filteredReturns.reduce( - (sum, returnItem) => sum + returnItem.quantity, - 0 - ); + return filteredClaims.reduce((sum, claim) => sum + claim.price, 0); }; const getPendingCount = () => { - return filteredReturns.filter( - (returnItem) => returnItem.status === "pending" - ).length; + return filteredClaims.filter((claim) => claim.status === "На рассмотрении") + .length; }; + const getUniqueStatuses = () => { + const statuses = [...new Set(claims.map((claim) => claim.status))]; + return statuses; + }; + + const hasWBApiKey = user?.organization?.apiKeys?.some( + (key) => key.marketplace === "WILDBERRIES" && key.isActive + ); + + if (!hasWBApiKey) { + return ( +
+ +
+

+ API ключ Wildberries не настроен +

+

+ Для просмотра заявок на возврат необходимо настроить API ключ + Wildberries в настройках организации. +

+
+
+ ); + } + return (
- {/* Статистика с кнопкой */} + {/* Статистика с кнопками */}
@@ -171,9 +245,9 @@ export function PvzReturnsTab() {
-

Возвратов

+

Заявок

- {filteredReturns.length} + {filteredClaims.length}

@@ -185,7 +259,7 @@ export function PvzReturnsTab() {
-

Ожидает сбора

+

На рассмотрении

{getPendingCount()}

@@ -199,7 +273,7 @@ export function PvzReturnsTab() {
-

Стоимость

+

Общая стоимость

{formatCurrency(getTotalValue())}

@@ -213,22 +287,27 @@ export function PvzReturnsTab() {
-

Товаров

-

- {getTotalQuantity()} -

+

Всего в базе

+

{total}

- +
+ +
{/* Фильтры */} @@ -236,7 +315,7 @@ export function PvzReturnsTab() {
setSearchTerm(e.target.value)} className="glass-input pl-10 text-white placeholder:text-white/40" @@ -249,87 +328,175 @@ export function PvzReturnsTab() { className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10" > - - - - + {getUniqueStatuses().map((status) => ( + + ))} + +
- {/* Список возвратов */} + {/* Список заявок */}
-
- {filteredReturns.map((returnItem) => ( - -
-
-
-

- {returnItem.productName} -

- {getStatusBadge(returnItem.status)} - {getReasonBadge(returnItem.reason)} -
+ {loading ? ( +
+
+ Загрузка заявок... +
+ ) : ( +
+ {filteredClaims.length === 0 ? ( + + +

+ {claims.length === 0 + ? "Нет заявок на возврат" + : "Нет заявок по фильтру"} +

+

+ {claims.length === 0 + ? "Заявки на возврат товаров появятся здесь" + : "Попробуйте изменить параметры фильтрации"} +

+
+ ) : ( + filteredClaims.map((claim) => ( + +
+
+
+

+ {claim.productName} +

+ {getStatusBadge(claim.status)} +
-
-
-

SKU

-

{returnItem.sku}

+
+
+

Артикул WB

+

{claim.nmId}

+
+
+

ID заявки

+

+ {claim.id.substring(0, 8)}... +

+
+
+

Стоимость

+

+ {formatCurrency(claim.price)} +

+
+
+

Дата заявки

+

+ {formatDate(claim.returnDate)} +

+
+
+ + {claim.userComment && ( +
+

+ Комментарий покупателя: +

+

+ {claim.userComment} +

+
+ )} + + {claim.wbComment && ( +
+

+ Ответ WB: +

+

+ {claim.wbComment} +

+
+ )} + + {claim.photos.length > 0 && ( +
+

+ Фотографии ({claim.photos.length}): +

+
+ {claim.photos.slice(0, 3).map((photo, index) => ( + + {`Фото + + ))} + {claim.photos.length > 3 && ( +
+ +{claim.photos.length - 3} +
+ )} +
+
+ )} + +
+ + Заказ от: {formatDate(claim.orderDate)} + + + Обновлено: {formatDate(claim.lastUpdate)} + +
-
-

Селлер

-

{returnItem.seller}

-
-
-

Количество

-

- {returnItem.quantity} шт. -

-
-
-

Дата возврата

-

- {formatDate(returnItem.returnDate)} -

+ +
+ + {claim.actions.length > 0 && ( + + )}
- -
-

- - ПВЗ:{" "} - - {returnItem.pvzAddress} - -

-
- -
- - Оценочная стоимость:{" "} - - {formatCurrency(returnItem.estimatedValue)} - - -
-
- -
- -
-
- - ))} -
+
+ )) + )} +
+ )}
); diff --git a/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx b/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx index aa98477..58056b6 100644 --- a/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx +++ b/src/components/supplies/fulfillment-supplies/pvz-returns-tab.tsx @@ -1,301 +1,142 @@ "use client"; -import React, { useState } from "react"; +import { 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 { StatsCard } from "../ui/stats-card"; -import { StatsGrid } from "../ui/stats-grid"; import { - ChevronDown, - ChevronRight, - Calendar, - Package, - MapPin, - Building2, - TrendingDown, - AlertTriangle, - DollarSign, RotateCcw, - RefreshCcw, + Plus, + Search, + TrendingUp, + AlertCircle, + Eye, + MapPin, + RefreshCw, + ExternalLink, + MessageCircle, } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; +import { WildberriesService, type WBClaim, type WBClaimsResponse } from "@/services/wildberries-service"; +import { toast } from "sonner"; -// Типы данных для возвратов с ПВЗ -interface ReturnProduct { +// Интерфейс для обработанных данных возврата +interface ProcessedClaim { id: string; - name: string; - sku: string; - category: string; - returnQty: number; - defectQty: number; - returnPrice: number; - returnReason: - | "customer_return" - | "defect" - | "damage" - | "wrong_item" - | "other"; -} - -interface PvzPoint { - id: string; - name: string; - address: string; - contact: string; - products: ReturnProduct[]; - totalAmount: number; -} - -interface ReturnRoute { - id: string; - from: string; - fromAddress: string; - to: string; - toAddress: string; - pvzPoints: PvzPoint[]; - totalReturnPrice: number; - logisticsPrice: number; - totalAmount: number; -} - -interface PvzReturnSupply { - id: string; - number: number; + productName: string; + nmId: number; returnDate: string; - createdDate: string; - routes: ReturnRoute[]; - totalReturnQty: number; - totalDefectQty: number; - totalReturnPrice: number; - totalLogisticsPrice: number; - grandTotal: number; - status: "planned" | "in-transit" | "delivered" | "completed"; + status: string; + reason: string; + price: number; + userComment: string; + wbComment: string; + photos: string[]; + videoPaths: string[]; + actions: string[]; + orderDate: string; + lastUpdate: string; } -// Моковые данные для возвратов с ПВЗ -const mockPvzReturns: PvzReturnSupply[] = [ - { - id: "pvz1", - number: 3001, - returnDate: "2024-01-20", - createdDate: "2024-01-15", - status: "delivered", - totalReturnQty: 45, - totalDefectQty: 12, - totalReturnPrice: 890000, - totalLogisticsPrice: 15000, - grandTotal: 905000, - routes: [ - { - id: "pvzr1", - from: "ПВЗ Сеть", - fromAddress: "Москва, различные точки", - to: "SFERAV Logistics ФФ", - toAddress: "Москва, ул. Складская, 15", - totalReturnPrice: 890000, - logisticsPrice: 15000, - totalAmount: 905000, - pvzPoints: [ - { - id: "pvzp1", - name: 'ПВЗ "На Тверской"', - address: "Москва, ул. Тверская, 15", - contact: "+7 (495) 123-45-67", - totalAmount: 450000, - products: [ - { - id: "pvzprod1", - name: "Смартфон iPhone 15 (возврат)", - sku: "APL-IP15-128-RET", - category: "Электроника", - returnQty: 15, - defectQty: 3, - returnPrice: 70000, - returnReason: "customer_return", - }, - { - id: "pvzprod2", - name: "Наушники AirPods (брак)", - sku: "APL-AP-PRO2-DEF", - category: "Аудио", - returnQty: 8, - defectQty: 8, - returnPrice: 22000, - returnReason: "defect", - }, - ], - }, - { - id: "pvzp2", - name: 'ПВЗ "Арбатский"', - address: "Москва, ул. Арбат, 25", - contact: "+7 (495) 987-65-43", - totalAmount: 440000, - products: [ - { - id: "pvzprod3", - name: "Планшет iPad Air (повреждение)", - sku: "APL-IPAD-AIR-DMG", - category: "Планшеты", - returnQty: 12, - defectQty: 1, - returnPrice: 35000, - returnReason: "damage", - }, - ], - }, - ], - }, - ], - }, - { - id: "pvz2", - number: 3002, - returnDate: "2024-01-25", - createdDate: "2024-01-18", - status: "in-transit", - totalReturnQty: 28, - totalDefectQty: 5, - totalReturnPrice: 560000, - totalLogisticsPrice: 12000, - grandTotal: 572000, - routes: [ - { - id: "pvzr2", - from: "ПВЗ Сеть Подольск", - fromAddress: "Подольск, различные точки", - to: "MegaFulfillment", - toAddress: "Подольск, ул. Складская, 25", - totalReturnPrice: 560000, - logisticsPrice: 12000, - totalAmount: 572000, - pvzPoints: [ - { - id: "pvzp3", - name: 'ПВЗ "Центральный"', - address: "Подольск, ул. Центральная, 10", - contact: "+7 (4967) 55-66-77", - totalAmount: 560000, - products: [ - { - id: "pvzprod4", - name: "Ноутбук MacBook (возврат)", - sku: "APL-MBP-14-RET", - category: "Компьютеры", - returnQty: 8, - defectQty: 2, - returnPrice: 180000, - returnReason: "customer_return", - }, - ], - }, - ], - }, - ], - }, -]; - export function PvzReturnsTab() { - const [expandedSupplies, setExpandedSupplies] = useState>( - new Set() - ); - const [expandedRoutes, setExpandedRoutes] = useState>(new Set()); - const [expandedPvzPoints, setExpandedPvzPoints] = useState>( - new Set() - ); - const [expandedProducts, setExpandedProducts] = useState>( - new Set() - ); + const { user } = useAuth(); + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [archiveFilter, setArchiveFilter] = useState(false); + const [claims, setClaims] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [total, setTotal] = useState(0); - const toggleSupplyExpansion = (supplyId: string) => { - const newExpanded = new Set(expandedSupplies); - if (newExpanded.has(supplyId)) { - newExpanded.delete(supplyId); - } else { - newExpanded.add(supplyId); + // Загрузка заявок + const loadClaims = async (showToast = false) => { + const isInitialLoad = !refreshing; + if (isInitialLoad) setLoading(true); + else setRefreshing(true); + + try { + const wbApiKey = user?.organization?.apiKeys?.find( + (key) => key.marketplace === "WILDBERRIES" && key.isActive + ); + + if (!wbApiKey) { + if (showToast) { + toast.error("API ключ Wildberries не настроен"); + } + return; + } + + const apiToken = wbApiKey.apiKey; + + console.log("WB Claims: Loading claims with archive =", archiveFilter); + + const response = await WildberriesService.getClaims(apiToken, { + isArchive: archiveFilter, + limit: 100, + offset: 0, + }); + + const processedClaims = response.claims.map(processClaim); + setClaims(processedClaims); + setTotal(response.total); + + console.log(`WB Claims: Loaded ${processedClaims.length} claims`); + + if (showToast) { + toast.success(`Загружено заявок: ${processedClaims.length}`); + } + } catch (error) { + console.error("Error loading claims:", error); + if (showToast) { + toast.error("Ошибка загрузки заявок на возврат"); + } + } finally { + setLoading(false); + setRefreshing(false); } - setExpandedSupplies(newExpanded); }; - const toggleRouteExpansion = (routeId: string) => { - const newExpanded = new Set(expandedRoutes); - if (newExpanded.has(routeId)) { - newExpanded.delete(routeId); - } else { - newExpanded.add(routeId); - } - setExpandedRoutes(newExpanded); - }; - - const togglePvzPointExpansion = (pvzPointId: string) => { - const newExpanded = new Set(expandedPvzPoints); - if (newExpanded.has(pvzPointId)) { - newExpanded.delete(pvzPointId); - } else { - newExpanded.add(pvzPointId); - } - setExpandedPvzPoints(newExpanded); - }; - - const toggleProductExpansion = (productId: string) => { - const newExpanded = new Set(expandedProducts); - if (newExpanded.has(productId)) { - newExpanded.delete(productId); - } else { - newExpanded.add(productId); - } - setExpandedProducts(newExpanded); - }; - - const getStatusBadge = (status: PvzReturnSupply["status"]) => { - const statusMap = { - planned: { - label: "Запланирован", - color: "bg-blue-500/20 text-blue-300 border-blue-500/30", - }, - "in-transit": { - label: "В пути", - color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", - }, - delivered: { - label: "Доставлен", - color: "bg-green-500/20 text-green-300 border-green-500/30", - }, - completed: { - label: "Завершен", - color: "bg-purple-500/20 text-purple-300 border-purple-500/30", - }, + // Обработка данных из API в удобный формат + const processClaim = (claim: WBClaim): ProcessedClaim => { + const getStatusLabel = (status: number, statusEx: number) => { + // Мапинг статусов на основе документации API + switch (status) { + case 1: + return "На рассмотрении"; + case 2: + return "Одобрена"; + case 3: + return "Отклонена"; + case 4: + return "В архиве"; + default: + return `Статус ${status}`; + } + }; + + return { + id: claim.id, + productName: claim.imt_name, + nmId: claim.nm_id, + returnDate: claim.dt, + status: getStatusLabel(claim.status, claim.status_ex), + reason: claim.user_comment || "Не указана", + price: claim.price, + userComment: claim.user_comment, + wbComment: claim.wb_comment, + photos: claim.photos || [], + videoPaths: claim.video_paths || [], + actions: claim.actions || [], + orderDate: claim.order_dt, + lastUpdate: claim.dt_update, }; - const { label, color } = statusMap[status]; - return {label}; }; - const getReturnReasonBadge = (reason: ReturnProduct["returnReason"]) => { - const reasonMap = { - customer_return: { - label: "Возврат клиента", - color: "bg-blue-500/20 text-blue-300 border-blue-500/30", - }, - defect: { - label: "Брак", - color: "bg-red-500/20 text-red-300 border-red-500/30", - }, - damage: { - label: "Повреждение", - color: "bg-orange-500/20 text-orange-300 border-orange-500/30", - }, - wrong_item: { - label: "Неправильный товар", - color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30", - }, - other: { - label: "Прочее", - color: "bg-gray-500/20 text-gray-300 border-gray-500/30", - }, - }; - const { label, color } = reasonMap[reason]; - return {label}; - }; + // Загрузка при монтировании компонента + useEffect(() => { + loadClaims(); + }, [user, archiveFilter]); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("ru-RU", { @@ -313,420 +154,350 @@ export function PvzReturnsTab() { }); }; - const calculateProductTotal = (product: ReturnProduct) => { - return product.returnQty * product.returnPrice; + const getStatusBadge = (status: string) => { + const statusConfig = { + "На рассмотрении": { + color: "text-yellow-300 border-yellow-400/30", + label: "На рассмотрении", + }, + "Одобрена": { + color: "text-green-300 border-green-400/30", + label: "Одобрена", + }, + "Отклонена": { + color: "text-red-300 border-red-400/30", + label: "Отклонена", + }, + "В архиве": { + color: "text-gray-300 border-gray-400/30", + label: "В архиве", + }, + }; + + const config = statusConfig[status as keyof typeof statusConfig] || { + color: "text-gray-300 border-gray-400/30", + label: status, + }; + + return ( + + {config.label} + + ); }; - return ( -
- {/* Статистика возвратов с ПВЗ */} - - + const filteredClaims = claims.filter((claim) => { + const matchesSearch = + claim.productName.toLowerCase().includes(searchTerm.toLowerCase()) || + claim.nmId.toString().includes(searchTerm) || + claim.reason.toLowerCase().includes(searchTerm.toLowerCase()) || + claim.id.toLowerCase().includes(searchTerm.toLowerCase()); - sum + supply.grandTotal, 0) - )} - icon={TrendingDown} - iconColor="text-orange-400" - iconBg="bg-orange-500/20" - trend={{ value: 4, isPositive: false }} - subtitle="Общая стоимость" - /> + const matchesStatus = + statusFilter === "all" || claim.status === statusFilter; - supply.status === "in-transit") - .length - } - icon={Calendar} - iconColor="text-yellow-400" - iconBg="bg-yellow-500/20" - subtitle="Активные возвраты" - /> + return matchesSearch && matchesStatus; + }); - supply.totalDefectQty > 0).length - } - icon={AlertTriangle} - iconColor="text-red-400" - iconBg="bg-red-500/20" - trend={{ value: 15, isPositive: false }} - subtitle="Бракованные товары" - /> - + const getTotalValue = () => { + return filteredClaims.reduce((sum, claim) => sum + claim.price, 0); + }; - {/* Таблица возвратов с ПВЗ */} - -
- - - - - - - - - - - - - - - - {mockPvzReturns.map((supply) => { - const isSupplyExpanded = expandedSupplies.has(supply.id); + const getPendingCount = () => { + return filteredClaims.filter((claim) => claim.status === "На рассмотрении") + .length; + }; - return ( - - {/* Основная строка возврата */} - toggleSupplyExpansion(supply.id)} - > - - - - - - - - - - + const getUniqueStatuses = () => { + const statuses = [...new Set(claims.map((claim) => claim.status))]; + return statuses; + }; - {/* Развернутые уровни - аналогично другим компонентам */} - {isSupplyExpanded && - supply.routes.map((route) => { - const isRouteExpanded = expandedRoutes.has(route.id); - return ( - - - - - - - - - - - + const hasWBApiKey = user?.organization?.apiKeys?.some( + (key) => key.marketplace === "WILDBERRIES" && key.isActive + ); - {/* ПВЗ точки */} - {isRouteExpanded && - route.pvzPoints.map((pvzPoint) => { - const isPvzPointExpanded = - expandedPvzPoints.has(pvzPoint.id); - return ( - - - - - - - - - - - - - {/* Возвращенные товары */} - {isPvzPointExpanded && - pvzPoint.products.map((product) => ( - - - - - - - - - - - ))} - - ); - })} - - ); - })} - - ); - })} - -
- Дата возврата - - Дата создания - - Возвратов - - Дефектов - - Сумма возвратов - - Логистика - - Итого сумма - - Статус -
-
- - {supply.number} - -
-
-
- - - {formatDate(supply.returnDate)} - -
-
- - {formatDate(supply.createdDate)} - - - - {supply.totalReturnQty} - - - 0 - ? "text-red-400" - : "text-white" - }`} - > - {supply.totalDefectQty} - - - - {formatCurrency(supply.totalReturnPrice)} - - - - {formatCurrency(supply.totalLogisticsPrice)} - - -
- - - {formatCurrency(supply.grandTotal)} - -
-
{getStatusBadge(supply.status)}
-
- - - - Маршрут возврата - -
-
-
-
- - {route.from} - - - - {route.to} - -
-
- {route.fromAddress} → {route.toAddress} -
-
-
- - {route.pvzPoints.reduce( - (sum, p) => - sum + - p.products.reduce( - (pSum, prod) => pSum + prod.returnQty, - 0 - ), - 0 - )} - - - - {route.pvzPoints.reduce( - (sum, p) => - sum + - p.products.reduce( - (pSum, prod) => pSum + prod.defectQty, - 0 - ), - 0 - )} - - - - {formatCurrency(route.totalReturnPrice)} - - - - {formatCurrency(route.logisticsPrice)} - - - - {formatCurrency(route.totalAmount)} - -
-
- - - - ПВЗ - -
-
-
-
- {pvzPoint.name} -
-
- {pvzPoint.address} -
-
- {pvzPoint.contact} -
-
-
- - {pvzPoint.products.reduce( - (sum, p) => sum + p.returnQty, - 0 - )} - - - - {pvzPoint.products.reduce( - (sum, p) => sum + p.defectQty, - 0 - )} - - - - {formatCurrency( - pvzPoint.products.reduce( - (sum, p) => - sum + calculateProductTotal(p), - 0 - ) - )} - - - - {formatCurrency(pvzPoint.totalAmount)} - -
-
- - - Возврат - -
-
-
-
- {product.name} -
-
- Артикул: {product.sku} -
-
- - {product.category} - - {getReturnReasonBadge( - product.returnReason - )} -
-
-
- - {product.returnQty} - - - 0 - ? "text-red-400" - : "text-white" - }`} - > - {product.defectQty} - - -
-
- {formatCurrency( - calculateProductTotal(product) - )} -
-
- {formatCurrency( - product.returnPrice - )}{" "} - за шт. -
-
-
- - {formatCurrency( - calculateProductTotal(product) - )} - -
+ if (!hasWBApiKey) { + return ( +
+ +
+

+ API ключ Wildberries не настроен +

+

+ Для просмотра заявок на возврат необходимо настроить API ключ + Wildberries в настройках организации. +

- +
+ ); + } + + return ( +
+ {/* Статистика с кнопками */} +
+
+ +
+
+ +
+
+

Заявок

+

+ {filteredClaims.length} +

+
+
+
+ + +
+
+ +
+
+

На рассмотрении

+

+ {getPendingCount()} +

+
+
+
+ + +
+
+ +
+
+

Общая стоимость

+

+ {formatCurrency(getTotalValue())} +

+
+
+
+ + +
+
+ +
+
+

Всего в базе

+

{total}

+
+
+
+
+ +
+ +
+
+ + {/* Фильтры */} +
+
+ + setSearchTerm(e.target.value)} + className="glass-input pl-10 text-white placeholder:text-white/40" + /> +
+ + + + +
+ + {/* Список заявок */} +
+ {loading ? ( +
+
+ Загрузка заявок... +
+ ) : ( +
+ {filteredClaims.length === 0 ? ( + + +

+ {claims.length === 0 + ? "Нет заявок на возврат" + : "Нет заявок по фильтру"} +

+

+ {claims.length === 0 + ? "Заявки на возврат товаров появятся здесь" + : "Попробуйте изменить параметры фильтрации"} +

+
+ ) : ( + filteredClaims.map((claim) => ( + +
+
+
+

+ {claim.productName} +

+ {getStatusBadge(claim.status)} +
+ +
+
+

Артикул WB

+

{claim.nmId}

+
+
+

ID заявки

+

+ {claim.id.substring(0, 8)}... +

+
+
+

Стоимость

+

+ {formatCurrency(claim.price)} +

+
+
+

Дата заявки

+

+ {formatDate(claim.returnDate)} +

+
+
+ + {claim.userComment && ( +
+

+ Комментарий покупателя: +

+

+ {claim.userComment} +

+
+ )} + + {claim.wbComment && ( +
+

+ Ответ WB: +

+

+ {claim.wbComment} +

+
+ )} + + {claim.photos.length > 0 && ( +
+

+ Фотографии ({claim.photos.length}): +

+
+ {claim.photos.slice(0, 3).map((photo, index) => ( + + {`Фото + + ))} + {claim.photos.length > 3 && ( +
+ +{claim.photos.length - 3} +
+ )} +
+
+ )} + +
+ + Заказ от: {formatDate(claim.orderDate)} + + + Обновлено: {formatDate(claim.lastUpdate)} + +
+
+ +
+ + {claim.actions.length > 0 && ( + + )} +
+
+
+ )) + )} +
+ )} +
); } diff --git a/src/services/wildberries-service.ts b/src/services/wildberries-service.ts index 3a60533..a1f7b34 100644 --- a/src/services/wildberries-service.ts +++ b/src/services/wildberries-service.ts @@ -335,6 +335,49 @@ export interface WBStatisticsData { buyoutPercentage: number } +export interface WBClaimStatus { + 1: 'На рассмотрении' + 2: 'Одобрена' + 3: 'Отклонена' + 4: 'В архиве' +} + +export interface WBClaimType { + 1: 'Претензия' + 2: 'Возврат' +} + +export interface WBClaim { + id: string + claim_type: number + status: number + status_ex: number + nm_id: number + user_comment: string + wb_comment: string + dt: string + imt_name: string + order_dt: string + dt_update: string + photos: string[] + video_paths: string[] + actions: string[] + price: number + currency_code: string + srid: string +} + +export interface WBClaimsResponse { + claims: WBClaim[] + total: number +} + +export interface WBClaimResponseRequest { + id: string + action: string + comment?: string +} + class WildberriesService { private apiKey: string private baseURL = 'https://statistics-api.wildberries.ru' @@ -1260,6 +1303,70 @@ class WildberriesService { } } + // Получение заявок покупателей на возврат товаров + async getClaims(options: { + isArchive: boolean + id?: string + limit?: number + offset?: number + nmId?: number + }): Promise { + const { isArchive, id, limit = 50, offset = 0, nmId } = options + + // Используем правильный API endpoint для возвратов + let url = `https://returns-api.wildberries.ru/api/v1/claims?is_archive=${isArchive}` + + if (id) url += `&id=${id}` + if (limit) url += `&limit=${limit}` + if (offset) url += `&offset=${offset}` + if (nmId) url += `&nm_id=${nmId}` + + console.log(`WB Claims API: Getting customer claims from ${url}`) + + try { + const response = await this.makeRequest(url, { + method: 'GET' + }) + + console.log(`WB Claims API: Got ${response.claims?.length || 0} claims, total: ${response.total || 0}`) + return response + } catch (error) { + console.error(`WB Claims API: Error getting claims:`, error) + return { claims: [], total: 0 } + } + } + + // Ответ на заявку покупателя на возврат + async respondToClaim(request: WBClaimResponseRequest): Promise { + const url = `https://returns-api.wildberries.ru/api/v1/claim` + + console.log(`WB Claims API: Responding to claim ${request.id} with action ${request.action}`) + + try { + await this.makeRequest(url, { + method: 'PATCH', + body: JSON.stringify(request) + }) + + console.log(`WB Claims API: Successfully responded to claim ${request.id}`) + return true + } catch (error) { + console.error(`WB Claims API: Error responding to claim:`, error) + return false + } + } + + // Статический метод для получения заявок с токеном + static async getClaims(apiKey: string, options: { + isArchive: boolean + id?: string + limit?: number + offset?: number + nmId?: number + }): Promise { + const service = new WildberriesService(apiKey) + return service.getClaims(options) + } }