Добавлен новый функционал для обработки заказов поставок расходников в компоненте CreateConsumablesSupplyPage. Реализован выбор фулфилмент-центра, улучшена логика создания заказа с учетом нового поля fulfillmentCenterId. Обновлен компонент SuppliesConsumablesTab для отображения заказов поставок с новыми данными. Оптимизированы стили и структура кода для повышения удобства использования.
This commit is contained in:
@ -1,356 +1,116 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Calendar,
|
Calendar,
|
||||||
Package,
|
Package,
|
||||||
MapPin,
|
|
||||||
Building2,
|
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
AlertTriangle,
|
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Wrench,
|
|
||||||
Box,
|
Box,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||||
|
|
||||||
// Типы данных для расходников
|
// Типы данных для заказов поставок расходников
|
||||||
interface ConsumableParameter {
|
interface SupplyOrderItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
quantity: number;
|
||||||
value: string;
|
price: number;
|
||||||
unit?: string;
|
totalPrice: number;
|
||||||
|
product: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
article?: string;
|
||||||
|
description?: string;
|
||||||
|
category?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Consumable {
|
interface SupplyOrder {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
|
||||||
sku: string;
|
|
||||||
category: string;
|
|
||||||
type: "packaging" | "labels" | "protective" | "tools" | "other";
|
|
||||||
plannedQty: number;
|
|
||||||
actualQty: number;
|
|
||||||
defectQty: number;
|
|
||||||
unitPrice: number;
|
|
||||||
parameters: ConsumableParameter[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConsumableSupplier {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
inn: string;
|
|
||||||
contact: string;
|
|
||||||
address: string;
|
|
||||||
consumables: Consumable[];
|
|
||||||
totalAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConsumableRoute {
|
|
||||||
id: string;
|
|
||||||
from: string;
|
|
||||||
fromAddress: string;
|
|
||||||
to: string;
|
|
||||||
toAddress: string;
|
|
||||||
suppliers: ConsumableSupplier[];
|
|
||||||
totalConsumablesPrice: number;
|
|
||||||
logisticsPrice: number;
|
|
||||||
totalAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConsumableSupply {
|
|
||||||
id: string;
|
|
||||||
number: number;
|
|
||||||
deliveryDate: string;
|
deliveryDate: string;
|
||||||
createdDate: string;
|
status: "PENDING" | "CONFIRMED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED";
|
||||||
routes: ConsumableRoute[];
|
totalAmount: number;
|
||||||
plannedTotal: number;
|
totalItems: number;
|
||||||
actualTotal: number;
|
createdAt: string;
|
||||||
defectTotal: number;
|
updatedAt: string;
|
||||||
totalConsumablesPrice: number;
|
partner: {
|
||||||
totalLogisticsPrice: number;
|
id: string;
|
||||||
grandTotal: number;
|
name?: string;
|
||||||
status: "planned" | "in-transit" | "delivered" | "completed";
|
fullName?: string;
|
||||||
|
inn?: string;
|
||||||
|
address?: string;
|
||||||
|
phones?: string[];
|
||||||
|
emails?: string[];
|
||||||
|
};
|
||||||
|
organization: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
fullName?: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
items: SupplyOrderItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Моковые данные для расходников
|
|
||||||
const mockConsumableSupplies: ConsumableSupply[] = [
|
|
||||||
{
|
|
||||||
id: "c1",
|
|
||||||
number: 101,
|
|
||||||
deliveryDate: "2024-01-18",
|
|
||||||
createdDate: "2024-01-14",
|
|
||||||
status: "delivered",
|
|
||||||
plannedTotal: 5000,
|
|
||||||
actualTotal: 4950,
|
|
||||||
defectTotal: 50,
|
|
||||||
totalConsumablesPrice: 125000,
|
|
||||||
totalLogisticsPrice: 8000,
|
|
||||||
grandTotal: 133000,
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
id: "cr1",
|
|
||||||
from: "Склад расходников",
|
|
||||||
fromAddress: "Москва, ул. Промышленная, 12",
|
|
||||||
to: "SFERAV Logistics",
|
|
||||||
toAddress: "Москва, ул. Складская, 15",
|
|
||||||
totalConsumablesPrice: 125000,
|
|
||||||
logisticsPrice: 8000,
|
|
||||||
totalAmount: 133000,
|
|
||||||
suppliers: [
|
|
||||||
{
|
|
||||||
id: "cs1",
|
|
||||||
name: 'ООО "УпакСервис"',
|
|
||||||
inn: "7703456789",
|
|
||||||
contact: "+7 (495) 777-88-99",
|
|
||||||
address: "Москва, ул. Упаковочная, 5",
|
|
||||||
totalAmount: 75000,
|
|
||||||
consumables: [
|
|
||||||
{
|
|
||||||
id: "cons1",
|
|
||||||
name: "Коробки картонные 30x20x10",
|
|
||||||
sku: "BOX-302010",
|
|
||||||
category: "Упаковка",
|
|
||||||
type: "packaging",
|
|
||||||
plannedQty: 2000,
|
|
||||||
actualQty: 1980,
|
|
||||||
defectQty: 20,
|
|
||||||
unitPrice: 35,
|
|
||||||
parameters: [
|
|
||||||
{ id: "cp1", name: "Размер", value: "30x20x10", unit: "см" },
|
|
||||||
{ id: "cp2", name: "Материал", value: "Гофрокартон" },
|
|
||||||
{ id: "cp3", name: "Плотность", value: "3", unit: "слоя" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cons2",
|
|
||||||
name: "Пузырчатая пленка",
|
|
||||||
sku: "BUBBLE-100",
|
|
||||||
category: "Защитная упаковка",
|
|
||||||
type: "protective",
|
|
||||||
plannedQty: 1000,
|
|
||||||
actualQty: 1000,
|
|
||||||
defectQty: 0,
|
|
||||||
unitPrice: 25,
|
|
||||||
parameters: [
|
|
||||||
{ id: "cp4", name: "Ширина", value: "100", unit: "см" },
|
|
||||||
{ id: "cp5", name: "Толщина", value: "0.1", unit: "мм" },
|
|
||||||
{ id: "cp6", name: "Длина рулона", value: "50", unit: "м" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cs2",
|
|
||||||
name: "ИП Маркин С.А.",
|
|
||||||
inn: "123456789013",
|
|
||||||
contact: "+7 (499) 111-22-33",
|
|
||||||
address: "Москва, пр-т Маркировочный, 8",
|
|
||||||
totalAmount: 50000,
|
|
||||||
consumables: [
|
|
||||||
{
|
|
||||||
id: "cons3",
|
|
||||||
name: "Этикетки самоклеящиеся",
|
|
||||||
sku: "LABEL-5030",
|
|
||||||
category: "Маркировка",
|
|
||||||
type: "labels",
|
|
||||||
plannedQty: 2000,
|
|
||||||
actualQty: 1970,
|
|
||||||
defectQty: 30,
|
|
||||||
unitPrice: 15,
|
|
||||||
parameters: [
|
|
||||||
{ id: "cp7", name: "Размер", value: "50x30", unit: "мм" },
|
|
||||||
{ id: "cp8", name: "Материал", value: "Полипропилен" },
|
|
||||||
{ id: "cp9", name: "Клей", value: "Акриловый" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "c2",
|
|
||||||
number: 102,
|
|
||||||
deliveryDate: "2024-01-22",
|
|
||||||
createdDate: "2024-01-16",
|
|
||||||
status: "in-transit",
|
|
||||||
plannedTotal: 1500,
|
|
||||||
actualTotal: 1500,
|
|
||||||
defectTotal: 0,
|
|
||||||
totalConsumablesPrice: 45000,
|
|
||||||
totalLogisticsPrice: 3000,
|
|
||||||
grandTotal: 48000,
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
id: "cr2",
|
|
||||||
from: "Инструментальный склад",
|
|
||||||
fromAddress: "Подольск, ул. Индустриальная, 25",
|
|
||||||
to: "WB Подольск",
|
|
||||||
toAddress: "Подольск, ул. Складская, 25",
|
|
||||||
totalConsumablesPrice: 45000,
|
|
||||||
logisticsPrice: 3000,
|
|
||||||
totalAmount: 48000,
|
|
||||||
suppliers: [
|
|
||||||
{
|
|
||||||
id: "cs3",
|
|
||||||
name: 'ООО "ИнструментПро"',
|
|
||||||
inn: "5001234567",
|
|
||||||
contact: "+7 (4967) 55-66-77",
|
|
||||||
address: "Подольск, ул. Инструментальная, 15",
|
|
||||||
totalAmount: 45000,
|
|
||||||
consumables: [
|
|
||||||
{
|
|
||||||
id: "cons4",
|
|
||||||
name: "Сканер штрих-кодов",
|
|
||||||
sku: "SCANNER-2D",
|
|
||||||
category: "Оборудование",
|
|
||||||
type: "tools",
|
|
||||||
plannedQty: 5,
|
|
||||||
actualQty: 5,
|
|
||||||
defectQty: 0,
|
|
||||||
unitPrice: 8000,
|
|
||||||
parameters: [
|
|
||||||
{ id: "cp10", name: "Тип", value: "2D сканер" },
|
|
||||||
{ id: "cp11", name: "Интерфейс", value: "USB" },
|
|
||||||
{ id: "cp12", name: "Дальность", value: "30", unit: "см" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "cons5",
|
|
||||||
name: "Термопринтер этикеток",
|
|
||||||
sku: "PRINTER-THERMAL",
|
|
||||||
category: "Оборудование",
|
|
||||||
type: "tools",
|
|
||||||
plannedQty: 2,
|
|
||||||
actualQty: 2,
|
|
||||||
defectQty: 0,
|
|
||||||
unitPrice: 2500,
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
id: "cp13",
|
|
||||||
name: "Ширина печати",
|
|
||||||
value: "108",
|
|
||||||
unit: "мм",
|
|
||||||
},
|
|
||||||
{ id: "cp14", name: "Разрешение", value: "203", unit: "dpi" },
|
|
||||||
{ id: "cp15", name: "Скорость", value: "102", unit: "мм/с" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SuppliesConsumablesTab() {
|
export function SuppliesConsumablesTab() {
|
||||||
const [expandedSupplies, setExpandedSupplies] = useState<Set<string>>(
|
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||||
new Set()
|
|
||||||
);
|
|
||||||
const [expandedRoutes, setExpandedRoutes] = useState<Set<string>>(new Set());
|
|
||||||
const [expandedSuppliers, setExpandedSuppliers] = useState<Set<string>>(
|
|
||||||
new Set()
|
|
||||||
);
|
|
||||||
const [expandedConsumables, setExpandedConsumables] = useState<Set<string>>(
|
|
||||||
new Set()
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleSupplyExpansion = (supplyId: string) => {
|
// Загружаем заказы поставок
|
||||||
const newExpanded = new Set(expandedSupplies);
|
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS);
|
||||||
if (newExpanded.has(supplyId)) {
|
|
||||||
newExpanded.delete(supplyId);
|
const toggleOrderExpansion = (orderId: string) => {
|
||||||
|
const newExpanded = new Set(expandedOrders);
|
||||||
|
if (newExpanded.has(orderId)) {
|
||||||
|
newExpanded.delete(orderId);
|
||||||
} else {
|
} else {
|
||||||
newExpanded.add(supplyId);
|
newExpanded.add(orderId);
|
||||||
}
|
}
|
||||||
setExpandedSupplies(newExpanded);
|
setExpandedOrders(newExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleRouteExpansion = (routeId: string) => {
|
// Получаем данные заказов поставок
|
||||||
const newExpanded = new Set(expandedRoutes);
|
const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||||
if (newExpanded.has(routeId)) {
|
|
||||||
newExpanded.delete(routeId);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(routeId);
|
|
||||||
}
|
|
||||||
setExpandedRoutes(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSupplierExpansion = (supplierId: string) => {
|
// Генерируем порядковые номера для заказов
|
||||||
const newExpanded = new Set(expandedSuppliers);
|
const ordersWithNumbers = supplyOrders.map((order, index) => ({
|
||||||
if (newExpanded.has(supplierId)) {
|
...order,
|
||||||
newExpanded.delete(supplierId);
|
number: supplyOrders.length - index, // Обратный порядок для новых заказов сверху
|
||||||
} else {
|
}));
|
||||||
newExpanded.add(supplierId);
|
|
||||||
}
|
|
||||||
setExpandedSuppliers(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleConsumableExpansion = (consumableId: string) => {
|
const getStatusBadge = (status: SupplyOrder["status"]) => {
|
||||||
const newExpanded = new Set(expandedConsumables);
|
|
||||||
if (newExpanded.has(consumableId)) {
|
|
||||||
newExpanded.delete(consumableId);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(consumableId);
|
|
||||||
}
|
|
||||||
setExpandedConsumables(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusBadge = (status: ConsumableSupply["status"]) => {
|
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
planned: {
|
PENDING: {
|
||||||
label: "Запланирована",
|
label: "Ожидание",
|
||||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||||
},
|
},
|
||||||
"in-transit": {
|
CONFIRMED: {
|
||||||
|
label: "Подтверждена",
|
||||||
|
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
||||||
|
},
|
||||||
|
IN_TRANSIT: {
|
||||||
label: "В пути",
|
label: "В пути",
|
||||||
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
|
||||||
},
|
},
|
||||||
delivered: {
|
DELIVERED: {
|
||||||
label: "Доставлена",
|
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",
|
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||||
},
|
},
|
||||||
|
CANCELLED: {
|
||||||
|
label: "Отменена",
|
||||||
|
color: "bg-red-500/20 text-red-300 border-red-500/30",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const { label, color } = statusMap[status];
|
const { label, color } = statusMap[status];
|
||||||
return <Badge className={`${color} border`}>{label}</Badge>;
|
return <Badge className={`${color} border`}>{label}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeBadge = (type: Consumable["type"]) => {
|
|
||||||
const typeMap = {
|
|
||||||
packaging: {
|
|
||||||
label: "Упаковка",
|
|
||||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
label: "Этикетки",
|
|
||||||
color: "bg-green-500/20 text-green-300 border-green-500/30",
|
|
||||||
},
|
|
||||||
protective: {
|
|
||||||
label: "Защитная",
|
|
||||||
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
|
|
||||||
},
|
|
||||||
tools: {
|
|
||||||
label: "Инструменты",
|
|
||||||
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
label: "Прочее",
|
|
||||||
color: "bg-gray-500/20 text-gray-300 border-gray-500/30",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { label, color } = typeMap[type];
|
|
||||||
return <Badge className={`${color} border text-xs`}>{label}</Badge>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
return new Intl.NumberFormat("ru-RU", {
|
return new Intl.NumberFormat("ru-RU", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
@ -367,36 +127,26 @@ export function SuppliesConsumablesTab() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateConsumableTotal = (consumable: Consumable) => {
|
if (loading) {
|
||||||
return consumable.actualQty * consumable.unitPrice;
|
return (
|
||||||
};
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent"></div>
|
||||||
|
<span className="ml-3 text-white">Загрузка заказов поставок...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getEfficiencyBadge = (planned: number, actual: number) => {
|
if (error) {
|
||||||
const efficiency = (actual / planned) * 100;
|
return (
|
||||||
if (efficiency >= 95) {
|
<div className="text-center py-8">
|
||||||
return (
|
<p className="text-red-400">Ошибка загрузки: {error.message}</p>
|
||||||
<Badge className="bg-green-500/20 text-green-300 border-green-500/30 border">
|
</div>
|
||||||
Отлично
|
);
|
||||||
</Badge>
|
}
|
||||||
);
|
|
||||||
} else if (efficiency >= 90) {
|
|
||||||
return (
|
|
||||||
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-500/30 border">
|
|
||||||
Хорошо
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Badge className="bg-red-500/20 text-red-300 border-red-500/30 border">
|
|
||||||
Проблемы
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Статистика расходников */}
|
{/* Статистика заказов поставок */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@ -404,9 +154,9 @@ export function SuppliesConsumablesTab() {
|
|||||||
<Box className="h-5 w-5 text-orange-400" />
|
<Box className="h-5 w-5 text-orange-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white/60 text-xs">Поставок расходников</p>
|
<p className="text-white/60 text-xs">Заказов поставок</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{mockConsumableSupplies.length}
|
{supplyOrders.length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -418,11 +168,11 @@ export function SuppliesConsumablesTab() {
|
|||||||
<TrendingUp className="h-5 w-5 text-green-400" />
|
<TrendingUp className="h-5 w-5 text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white/60 text-xs">Сумма расходников</p>
|
<p className="text-white/60 text-xs">Общая сумма</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
mockConsumableSupplies.reduce(
|
supplyOrders.reduce(
|
||||||
(sum, supply) => sum + supply.grandTotal,
|
(sum, order) => sum + Number(order.totalAmount),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@ -440,9 +190,8 @@ export function SuppliesConsumablesTab() {
|
|||||||
<p className="text-white/60 text-xs">В пути</p>
|
<p className="text-white/60 text-xs">В пути</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{
|
{
|
||||||
mockConsumableSupplies.filter(
|
supplyOrders.filter((order) => order.status === "IN_TRANSIT")
|
||||||
(supply) => supply.status === "in-transit"
|
.length
|
||||||
).length
|
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -455,12 +204,11 @@ export function SuppliesConsumablesTab() {
|
|||||||
<Calendar className="h-5 w-5 text-blue-400" />
|
<Calendar className="h-5 w-5 text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white/60 text-xs">Завершено</p>
|
<p className="text-white/60 text-xs">Доставлено</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{
|
{
|
||||||
mockConsumableSupplies.filter(
|
supplyOrders.filter((order) => order.status === "DELIVERED")
|
||||||
(supply) => supply.status === "completed"
|
.length
|
||||||
).length
|
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -468,29 +216,27 @@ export function SuppliesConsumablesTab() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Таблица поставок расходников */}
|
{/* Таблица заказов поставок */}
|
||||||
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
<Card className="bg-white/10 backdrop-blur border-white/20 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-white/20">
|
<tr className="border-b border-white/20">
|
||||||
<th className="text-left p-4 text-white font-semibold">№</th>
|
<th className="text-left p-4 text-white font-semibold">№</th>
|
||||||
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
|
Поставщик
|
||||||
|
</th>
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
Дата поставки
|
Дата поставки
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
Дата создания
|
Дата создания
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left p-4 text-white font-semibold">План</th>
|
|
||||||
<th className="text-left p-4 text-white font-semibold">Факт</th>
|
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
Цена расходников
|
Товаров
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
Логистика
|
Сумма
|
||||||
</th>
|
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
|
||||||
Итого сумма
|
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left p-4 text-white font-semibold">
|
<th className="text-left p-4 text-white font-semibold">
|
||||||
Статус
|
Статус
|
||||||
@ -498,389 +244,145 @@ export function SuppliesConsumablesTab() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{mockConsumableSupplies.map((supply) => {
|
{ordersWithNumbers.length === 0 ? (
|
||||||
const isSupplyExpanded = expandedSupplies.has(supply.id);
|
<tr>
|
||||||
|
<td colSpan={7} className="text-center py-8">
|
||||||
|
<div className="text-white/60">
|
||||||
|
<Box className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>Заказов поставок пока нет</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
ordersWithNumbers.map((order) => {
|
||||||
|
const isOrderExpanded = expandedOrders.has(order.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={supply.id}>
|
<React.Fragment key={order.id}>
|
||||||
{/* Основная строка поставки расходников */}
|
{/* Основная строка заказа поставки */}
|
||||||
<tr
|
<tr
|
||||||
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-orange-500/10 cursor-pointer"
|
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-orange-500/10 cursor-pointer"
|
||||||
onClick={() => toggleSupplyExpansion(supply.id)}
|
onClick={() => toggleOrderExpansion(order.id)}
|
||||||
>
|
>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-white font-normal text-lg">
|
{isOrderExpanded ? (
|
||||||
{supply.number}
|
<ChevronDown className="h-4 w-4 text-white/60" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-white/60" />
|
||||||
|
)}
|
||||||
|
<span className="text-white font-normal text-lg">
|
||||||
|
{order.number}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="text-white">
|
||||||
|
<div className="font-medium">
|
||||||
|
{order.partner.name ||
|
||||||
|
order.partner.fullName ||
|
||||||
|
"Поставщик"}
|
||||||
|
</div>
|
||||||
|
{order.partner.inn && (
|
||||||
|
<div className="text-xs text-white/60">
|
||||||
|
ИНН: {order.partner.inn}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Calendar className="h-4 w-4 text-white/40" />
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{formatDate(order.deliveryDate)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80">
|
||||||
|
{formatDate(order.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td className="p-4">
|
||||||
<td className="p-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Calendar className="h-4 w-4 text-white/40" />
|
|
||||||
<span className="text-white font-semibold">
|
<span className="text-white font-semibold">
|
||||||
{formatDate(supply.deliveryDate)}
|
{order.totalItems}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td className="p-4">
|
||||||
<td className="p-4">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-white/80">
|
<DollarSign className="h-4 w-4 text-white/40" />
|
||||||
{formatDate(supply.createdDate)}
|
<span className="text-white font-bold text-lg">
|
||||||
</span>
|
{formatCurrency(Number(order.totalAmount))}
|
||||||
</td>
|
</span>
|
||||||
<td className="p-4">
|
</div>
|
||||||
<span className="text-white font-semibold">
|
</td>
|
||||||
{supply.plannedTotal}
|
<td className="p-4">{getStatusBadge(order.status)}</td>
|
||||||
</span>
|
</tr>
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{supply.actualTotal}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-green-400 font-semibold">
|
|
||||||
{formatCurrency(supply.totalConsumablesPrice)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-purple-400 font-semibold">
|
|
||||||
{formatCurrency(supply.totalLogisticsPrice)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<DollarSign className="h-4 w-4 text-white/40" />
|
|
||||||
<span className="text-white font-bold text-lg">
|
|
||||||
{formatCurrency(supply.grandTotal)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">{getStatusBadge(supply.status)}</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Развернутые уровни */}
|
{/* Развернутые детали заказа - товары */}
|
||||||
{isSupplyExpanded &&
|
{isOrderExpanded &&
|
||||||
supply.routes.map((route) => {
|
order.items.map((item) => (
|
||||||
const isRouteExpanded = expandedRoutes.has(route.id);
|
<tr
|
||||||
return (
|
key={item.id}
|
||||||
<React.Fragment key={route.id}>
|
className="border-b border-white/10 hover:bg-white/5 transition-colors bg-blue-500/10"
|
||||||
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-blue-500/10">
|
>
|
||||||
<td className="p-4 pl-12">
|
<td className="p-4 pl-12">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Package className="h-4 w-4 text-blue-400" />
|
||||||
variant="ghost"
|
<span className="text-white font-medium text-sm">
|
||||||
size="sm"
|
Товар
|
||||||
onClick={() =>
|
</span>
|
||||||
toggleRouteExpansion(route.id)
|
</div>
|
||||||
}
|
</td>
|
||||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
<td className="p-4" colSpan={2}>
|
||||||
>
|
<div className="text-white">
|
||||||
{isRouteExpanded ? (
|
<div className="font-medium mb-1">
|
||||||
<ChevronDown className="h-4 w-4" />
|
{item.product.name}
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<MapPin className="h-4 w-4 text-blue-400" />
|
|
||||||
<span className="text-white font-medium">
|
|
||||||
Маршрут
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
{item.product.article && (
|
||||||
<td className="p-4" colSpan={2}>
|
<div className="text-xs text-white/60 mb-1">
|
||||||
<div className="text-white">
|
Артикул: {item.product.article}
|
||||||
<div className="flex items-center space-x-2 mb-1">
|
|
||||||
<span className="font-medium">
|
|
||||||
{route.from}
|
|
||||||
</span>
|
|
||||||
<span className="text-white/60">→</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{route.to}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white/60">
|
)}
|
||||||
{route.fromAddress} → {route.toAddress}
|
{item.product.category && (
|
||||||
|
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs">
|
||||||
|
{item.product.category.name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{item.product.description && (
|
||||||
|
<div className="text-xs text-white/60 mt-1">
|
||||||
|
{item.product.description}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white/80 text-sm">
|
||||||
|
{formatDate(order.createdAt)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<span className="text-white font-semibold">
|
||||||
|
{item.quantity}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="text-white">
|
||||||
|
<div className="font-medium">
|
||||||
|
{formatCurrency(Number(item.totalPrice))}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div className="text-xs text-white/60">
|
||||||
<td className="p-4">
|
{formatCurrency(Number(item.price))} за шт.
|
||||||
<span className="text-white/80">
|
</div>
|
||||||
{route.suppliers.reduce(
|
</div>
|
||||||
(sum, s) =>
|
</td>
|
||||||
sum +
|
<td className="p-4"></td>
|
||||||
s.consumables.reduce(
|
</tr>
|
||||||
(cSum, c) => cSum + c.plannedQty,
|
))}
|
||||||
0
|
</React.Fragment>
|
||||||
),
|
);
|
||||||
0
|
})
|
||||||
)}
|
)}
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white/80">
|
|
||||||
{route.suppliers.reduce(
|
|
||||||
(sum, s) =>
|
|
||||||
sum +
|
|
||||||
s.consumables.reduce(
|
|
||||||
(cSum, c) => cSum + c.actualQty,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-green-400 font-medium">
|
|
||||||
{formatCurrency(route.totalConsumablesPrice)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-purple-400 font-medium">
|
|
||||||
{formatCurrency(route.logisticsPrice)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{formatCurrency(route.totalAmount)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Поставщики */}
|
|
||||||
{isRouteExpanded &&
|
|
||||||
route.suppliers.map((supplier) => {
|
|
||||||
const isSupplierExpanded =
|
|
||||||
expandedSuppliers.has(supplier.id);
|
|
||||||
return (
|
|
||||||
<React.Fragment key={supplier.id}>
|
|
||||||
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-green-500/10">
|
|
||||||
<td className="p-4 pl-20">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
toggleSupplierExpansion(
|
|
||||||
supplier.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
{isSupplierExpanded ? (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Building2 className="h-4 w-4 text-green-400" />
|
|
||||||
<span className="text-white font-medium">
|
|
||||||
Поставщик
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4" colSpan={2}>
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="font-medium mb-1">
|
|
||||||
{supplier.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-white/60 mb-1">
|
|
||||||
ИНН: {supplier.inn}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-white/60 mb-1">
|
|
||||||
{supplier.address}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-white/60">
|
|
||||||
{supplier.contact}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white/80">
|
|
||||||
{supplier.consumables.reduce(
|
|
||||||
(sum, c) => sum + c.plannedQty,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white/80">
|
|
||||||
{supplier.consumables.reduce(
|
|
||||||
(sum, c) => sum + c.actualQty,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-green-400 font-medium">
|
|
||||||
{formatCurrency(
|
|
||||||
supplier.consumables.reduce(
|
|
||||||
(sum, c) =>
|
|
||||||
sum +
|
|
||||||
calculateConsumableTotal(c),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4"></td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{formatCurrency(supplier.totalAmount)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Расходники */}
|
|
||||||
{isSupplierExpanded &&
|
|
||||||
supplier.consumables.map((consumable) => {
|
|
||||||
const isConsumableExpanded =
|
|
||||||
expandedConsumables.has(
|
|
||||||
consumable.id
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<React.Fragment key={consumable.id}>
|
|
||||||
<tr className="border-b border-white/10 hover:bg-white/5 transition-colors bg-yellow-500/10">
|
|
||||||
<td className="p-4 pl-28">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
toggleConsumableExpansion(
|
|
||||||
consumable.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
{isConsumableExpanded ? (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Wrench className="h-4 w-4 text-yellow-400" />
|
|
||||||
<span className="text-white font-medium">
|
|
||||||
Расходник
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4" colSpan={2}>
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="font-medium mb-1">
|
|
||||||
{consumable.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-white/60 mb-1">
|
|
||||||
Артикул: {consumable.sku}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Badge className="bg-gray-500/20 text-gray-300 border-gray-500/30 border text-xs">
|
|
||||||
{consumable.category}
|
|
||||||
</Badge>
|
|
||||||
{getTypeBadge(
|
|
||||||
consumable.type
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{consumable.plannedQty}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{consumable.actualQty}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<div className="text-white">
|
|
||||||
<div className="font-medium">
|
|
||||||
{formatCurrency(
|
|
||||||
calculateConsumableTotal(
|
|
||||||
consumable
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-white/60">
|
|
||||||
{formatCurrency(
|
|
||||||
consumable.unitPrice
|
|
||||||
)}{" "}
|
|
||||||
за шт.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
{getEfficiencyBadge(
|
|
||||||
consumable.plannedQty,
|
|
||||||
consumable.actualQty
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="text-white font-semibold">
|
|
||||||
{formatCurrency(
|
|
||||||
calculateConsumableTotal(
|
|
||||||
consumable
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{/* Параметры расходника */}
|
|
||||||
{isConsumableExpanded && (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={9}
|
|
||||||
className="p-0"
|
|
||||||
>
|
|
||||||
<div className="bg-white/5 border-t border-white/10">
|
|
||||||
<div className="p-4 pl-36">
|
|
||||||
<h4 className="text-white font-medium mb-3 flex items-center space-x-2">
|
|
||||||
<span className="text-xs text-white/60">
|
|
||||||
📋 Параметры
|
|
||||||
расходника:
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
{consumable.parameters.map(
|
|
||||||
(param) => (
|
|
||||||
<div
|
|
||||||
key={param.id}
|
|
||||||
className="bg-white/5 rounded-lg p-3"
|
|
||||||
>
|
|
||||||
<div className="text-white/80 text-xs font-medium mb-1">
|
|
||||||
{param.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-white text-sm">
|
|
||||||
{param.value}{" "}
|
|
||||||
{param.unit ||
|
|
||||||
""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,11 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
Box,
|
Box,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { GET_MY_COUNTERPARTIES, GET_ALL_PRODUCTS } from "@/graphql/queries";
|
import {
|
||||||
|
GET_MY_COUNTERPARTIES,
|
||||||
|
GET_ALL_PRODUCTS,
|
||||||
|
GET_SUPPLY_ORDERS,
|
||||||
|
} from "@/graphql/queries";
|
||||||
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
|
import { CREATE_SUPPLY_ORDER } from "@/graphql/mutations";
|
||||||
import { OrganizationAvatar } from "@/components/market/organization-avatar";
|
import { OrganizationAvatar } from "@/components/market/organization-avatar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -81,6 +85,8 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [productSearchQuery, setProductSearchQuery] = useState("");
|
const [productSearchQuery, setProductSearchQuery] = useState("");
|
||||||
const [deliveryDate, setDeliveryDate] = useState("");
|
const [deliveryDate, setDeliveryDate] = useState("");
|
||||||
|
const [selectedFulfillmentCenter, setSelectedFulfillmentCenter] =
|
||||||
|
useState<ConsumableSupplier | null>(null);
|
||||||
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
const [isCreatingSupply, setIsCreatingSupply] = useState(false);
|
||||||
|
|
||||||
// Загружаем контрагентов-поставщиков расходников
|
// Загружаем контрагентов-поставщиков расходников
|
||||||
@ -105,6 +111,11 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
counterpartiesData?.myCounterparties || []
|
counterpartiesData?.myCounterparties || []
|
||||||
).filter((org: ConsumableSupplier) => org.type === "WHOLESALE");
|
).filter((org: ConsumableSupplier) => org.type === "WHOLESALE");
|
||||||
|
|
||||||
|
// Фильтруем фулфилмент-центры
|
||||||
|
const fulfillmentCenters = (
|
||||||
|
counterpartiesData?.myCounterparties || []
|
||||||
|
).filter((org: ConsumableSupplier) => org.type === "FULFILLMENT");
|
||||||
|
|
||||||
// Фильтруем поставщиков по поисковому запросу
|
// Фильтруем поставщиков по поисковому запросу
|
||||||
const filteredSuppliers = consumableSuppliers.filter(
|
const filteredSuppliers = consumableSuppliers.filter(
|
||||||
(supplier: ConsumableSupplier) =>
|
(supplier: ConsumableSupplier) =>
|
||||||
@ -210,6 +221,13 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Для селлеров требуется выбор фулфилмент-центра
|
||||||
|
// TODO: Добавить проверку типа текущей организации
|
||||||
|
if (!selectedFulfillmentCenter) {
|
||||||
|
toast.error("Выберите фулфилмент-центр для доставки");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsCreatingSupply(true);
|
setIsCreatingSupply(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -218,21 +236,32 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
input: {
|
input: {
|
||||||
partnerId: selectedSupplier.id,
|
partnerId: selectedSupplier.id,
|
||||||
deliveryDate: deliveryDate,
|
deliveryDate: deliveryDate,
|
||||||
|
fulfillmentCenterId: selectedFulfillmentCenter.id,
|
||||||
items: selectedConsumables.map((consumable) => ({
|
items: selectedConsumables.map((consumable) => ({
|
||||||
productId: consumable.id,
|
productId: consumable.id,
|
||||||
quantity: consumable.selectedQuantity,
|
quantity: consumable.selectedQuantity,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.data?.createSupplyOrder?.success) {
|
if (result.data?.createSupplyOrder?.success) {
|
||||||
toast.success("Поставка расходников создана успешно!");
|
toast.success("Заказ поставки расходников создан успешно!");
|
||||||
router.push("/supplies");
|
// Очищаем форму
|
||||||
|
setSelectedSupplier(null);
|
||||||
|
setSelectedFulfillmentCenter(null);
|
||||||
|
setSelectedConsumables([]);
|
||||||
|
setDeliveryDate("");
|
||||||
|
setProductSearchQuery("");
|
||||||
|
setSearchQuery("");
|
||||||
|
|
||||||
|
// Перенаправляем на страницу поставок фулфилмента
|
||||||
|
router.push("/fulfillment-supplies");
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
result.data?.createSupplyOrder?.message ||
|
result.data?.createSupplyOrder?.message ||
|
||||||
"Ошибка при создании поставки"
|
"Ошибка при создании заказа поставки"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -247,16 +276,16 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
<div className="h-screen flex overflow-hidden">
|
<div className="h-screen flex overflow-hidden">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main
|
<main
|
||||||
className={`flex-1 ${getSidebarMargin()} px-4 py-3 overflow-hidden transition-all duration-300`}
|
className={`flex-1 ${getSidebarMargin()} overflow-auto transition-all duration-300`}
|
||||||
>
|
>
|
||||||
<div className="h-full w-full flex flex-col">
|
<div className="min-h-full w-full flex flex-col px-3 py-2">
|
||||||
{/* Заголовок */}
|
{/* Заголовок */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-3 flex-shrink-0">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white mb-2">
|
<h1 className="text-xl font-bold text-white mb-1">
|
||||||
Создание поставки расходников
|
Создание поставки расходников
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-white/60">
|
<p className="text-white/60 text-sm">
|
||||||
Выберите поставщика и добавьте расходники в заказ
|
Выберите поставщика и добавьте расходники в заказ
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -264,130 +293,165 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => router.push("/supplies")}
|
onClick={() => router.push("/supplies")}
|
||||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
className="text-white/60 hover:text-white hover:bg-white/10 text-sm"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||||
Назад к поставкам
|
Назад
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Основной контент с двумя блоками */}
|
{/* Основной контент с двумя блоками */}
|
||||||
<div className="flex-1 overflow-hidden flex gap-4">
|
<div className="flex-1 flex gap-3 min-h-0">
|
||||||
{/* Левая колонка - Поставщики и Расходники */}
|
{/* Левая колонка - Поставщики и Расходники */}
|
||||||
<div className="flex-1 flex flex-col gap-4 overflow-hidden">
|
<div className="flex-1 flex flex-col gap-3 min-h-0">
|
||||||
{/* Блок "Поставщики" */}
|
{/* Блок "Поставщики" */}
|
||||||
<Card className="bg-white/10 backdrop-blur border-white/20 flex-shrink-0">
|
<Card className="bg-gradient-to-r from-white/15 via-white/10 to-white/15 backdrop-blur-xl border border-white/30 shadow-2xl flex-shrink-0 sticky top-0 z-10 rounded-xl overflow-hidden">
|
||||||
<div className="p-4 border-b border-white/10">
|
<div className="p-3 bg-gradient-to-r from-purple-500/10 to-pink-500/10">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<h2 className="text-lg font-semibold text-white flex items-center">
|
<h2 className="text-lg font-bold text-white flex items-center flex-shrink-0 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||||
<Building2 className="h-5 w-5 mr-2" />
|
<Building2 className="h-5 w-5 mr-3 text-purple-400" />
|
||||||
Поставщики
|
Поставщики
|
||||||
</h2>
|
</h2>
|
||||||
|
<div className="relative flex-1 max-w-sm">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-purple-300 h-4 w-4 z-10" />
|
||||||
|
<Input
|
||||||
|
placeholder="Найти поставщика..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="bg-white/20 backdrop-blur border-white/30 text-white placeholder-white/50 pl-10 h-8 text-sm rounded-full shadow-inner focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{selectedSupplier && (
|
{selectedSupplier && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSelectedSupplier(null)}
|
onClick={() => setSelectedSupplier(null)}
|
||||||
className="text-white/60 hover:text-white hover:bg-white/10"
|
className="text-white/70 hover:text-white hover:bg-white/20 text-sm h-8 px-3 flex-shrink-0 rounded-full transition-all duration-300 hover:scale-105"
|
||||||
>
|
>
|
||||||
Сбросить выбор
|
✕ Сбросить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
|
|
||||||
<Input
|
|
||||||
placeholder="Поиск поставщиков по названию, ИНН..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 max-h-60 overflow-y-auto">
|
<div className="px-3 pb-3 h-24 overflow-hidden">
|
||||||
{counterpartiesLoading ? (
|
{counterpartiesLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-4">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent mx-auto mb-3"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-400 border-t-transparent mx-auto mb-2"></div>
|
||||||
<p className="text-white/60">Загрузка поставщиков...</p>
|
<p className="text-white/70 text-sm font-medium">
|
||||||
|
Загружаем поставщиков...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : filteredSuppliers.length === 0 ? (
|
) : filteredSuppliers.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-4">
|
||||||
<Building2 className="h-8 w-8 text-white/40 mx-auto mb-2" />
|
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-3 w-fit mx-auto mb-2">
|
||||||
<p className="text-white/60 text-sm">
|
<Building2 className="h-6 w-6 text-purple-300" />
|
||||||
|
</div>
|
||||||
|
<p className="text-white/70 text-sm font-medium">
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? "Поставщики не найдены"
|
? "Поставщики не найдены"
|
||||||
: "У вас пока нет партнеров-поставщиков расходников"}
|
: "Добавьте поставщиков"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div className="flex gap-2 h-full pt-1">
|
||||||
{filteredSuppliers.map((supplier: ConsumableSupplier) => (
|
{filteredSuppliers
|
||||||
<Card
|
.slice(0, 7)
|
||||||
key={supplier.id}
|
.map((supplier: ConsumableSupplier, index) => (
|
||||||
className={`p-3 cursor-pointer transition-all border ${
|
<Card
|
||||||
selectedSupplier?.id === supplier.id
|
key={supplier.id}
|
||||||
? "bg-orange-500/20 border-orange-500/50"
|
className={`relative cursor-pointer transition-all duration-300 border flex-shrink-0 rounded-xl overflow-hidden group hover:scale-105 hover:shadow-xl ${
|
||||||
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"
|
selectedSupplier?.id === supplier.id
|
||||||
}`}
|
? "bg-gradient-to-br from-orange-500/30 via-orange-400/20 to-orange-500/30 border-orange-400/60 shadow-lg shadow-orange-500/25"
|
||||||
onClick={() => setSelectedSupplier(supplier)}
|
: "bg-gradient-to-br from-white/10 via-white/5 to-white/10 border-white/20 hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40"
|
||||||
>
|
}`}
|
||||||
<div className="flex items-center space-x-3">
|
style={{
|
||||||
<OrganizationAvatar
|
width: "calc((100% - 48px) / 7)", // 48px = 6 gaps * 8px each
|
||||||
organization={{
|
animationDelay: `${index * 100}ms`,
|
||||||
id: supplier.id,
|
}}
|
||||||
name:
|
onClick={() => setSelectedSupplier(supplier)}
|
||||||
supplier.name ||
|
>
|
||||||
supplier.fullName ||
|
<div className="flex flex-col items-center justify-center h-full p-2 space-y-1">
|
||||||
"Поставщик",
|
<div className="relative">
|
||||||
fullName: supplier.fullName,
|
<OrganizationAvatar
|
||||||
users: (supplier.users || []).map((user) => ({
|
organization={{
|
||||||
id: user.id,
|
id: supplier.id,
|
||||||
avatar: user.avatar,
|
name:
|
||||||
})),
|
supplier.name ||
|
||||||
}}
|
supplier.fullName ||
|
||||||
size="sm"
|
"Поставщик",
|
||||||
/>
|
fullName: supplier.fullName,
|
||||||
<div className="flex-1 min-w-0">
|
users: (supplier.users || []).map(
|
||||||
<h3 className="text-white font-medium text-sm truncate">
|
(user) => ({
|
||||||
{supplier.name ||
|
id: user.id,
|
||||||
supplier.fullName ||
|
avatar: user.avatar,
|
||||||
"Поставщик"}
|
})
|
||||||
</h3>
|
),
|
||||||
<div className="flex items-center space-x-1 mb-1">
|
}}
|
||||||
{renderStars(4.5)}
|
size="sm"
|
||||||
<span className="text-white/60 text-xs ml-1">
|
/>
|
||||||
4.5
|
{selectedSupplier?.id === supplier.id && (
|
||||||
</span>
|
<div className="absolute -top-1 -right-1 bg-gradient-to-r from-orange-400 to-orange-500 rounded-full w-4 h-4 flex items-center justify-center shadow-lg animate-pulse">
|
||||||
|
<span className="text-white text-xs font-bold">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-center w-full space-y-0.5">
|
||||||
|
<h3 className="text-white font-semibold text-xs truncate leading-tight group-hover:text-purple-200 transition-colors duration-300">
|
||||||
|
{(
|
||||||
|
supplier.name ||
|
||||||
|
supplier.fullName ||
|
||||||
|
"Поставщик"
|
||||||
|
).slice(0, 10)}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center justify-center space-x-1">
|
||||||
|
<span className="text-yellow-400 text-sm animate-pulse">
|
||||||
|
★
|
||||||
|
</span>
|
||||||
|
<span className="text-white/80 text-xs font-medium">
|
||||||
|
4.5
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-white/10 rounded-full h-1 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-purple-400 to-pink-400 h-full rounded-full animate-pulse"
|
||||||
|
style={{ width: "90%" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/60 text-xs truncate">
|
|
||||||
ИНН: {supplier.inn}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{selectedSupplier?.id === supplier.id && (
|
|
||||||
<div className="flex-shrink-0">
|
{/* Hover эффект */}
|
||||||
<Badge className="bg-orange-500/20 text-orange-300 border-orange-500/30 text-xs">
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/10 group-hover:to-pink-500/10 transition-all duration-300 pointer-events-none"></div>
|
||||||
Выбран
|
</Card>
|
||||||
</Badge>
|
))}
|
||||||
</div>
|
{filteredSuppliers.length > 7 && (
|
||||||
)}
|
<div
|
||||||
|
className="flex-shrink-0 flex flex-col items-center justify-center bg-gradient-to-br from-white/10 to-white/5 rounded-xl border border-white/20 text-white/70 hover:text-white transition-all duration-300 hover:scale-105"
|
||||||
|
style={{ width: "calc((100% - 48px) / 7)" }}
|
||||||
|
>
|
||||||
|
<div className="text-lg font-bold text-purple-300">
|
||||||
|
+{filteredSuppliers.length - 7}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="text-xs">ещё</div>
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Блок "Расходники" */}
|
{/* Блок "Расходники" */}
|
||||||
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 overflow-hidden">
|
<Card className="bg-white/10 backdrop-blur border-white/20 flex-1 min-h-0 flex flex-col">
|
||||||
<div className="p-4 border-b border-white/10">
|
<div className="p-3 border-b border-white/10 flex-shrink-0">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h2 className="text-lg font-semibold text-white flex items-center">
|
<h2 className="text-lg font-semibold text-white flex items-center">
|
||||||
<Wrench className="h-5 w-5 mr-2" />
|
<Wrench className="h-4 w-4 mr-2" />
|
||||||
Расходники
|
Расходники
|
||||||
{selectedSupplier && (
|
{selectedSupplier && (
|
||||||
<span className="text-white/60 text-sm font-normal ml-2">
|
<span className="text-white/60 text-xs font-normal ml-2 truncate">
|
||||||
- {selectedSupplier.name || selectedSupplier.fullName}
|
- {selectedSupplier.name || selectedSupplier.fullName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -395,151 +459,220 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
</div>
|
</div>
|
||||||
{selectedSupplier && (
|
{selectedSupplier && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/40 h-4 w-4" />
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-white/40 h-3 w-3" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Поиск расходников..."
|
placeholder="Поиск расходников..."
|
||||||
value={productSearchQuery}
|
value={productSearchQuery}
|
||||||
onChange={(e) => setProductSearchQuery(e.target.value)}
|
onChange={(e) => setProductSearchQuery(e.target.value)}
|
||||||
className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-10"
|
className="bg-white/10 border-white/20 text-white placeholder-white/40 pl-7 h-8 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 flex-1 overflow-y-auto">
|
<div className="p-3 flex-1 overflow-y-auto">
|
||||||
{!selectedSupplier ? (
|
{!selectedSupplier ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-8">
|
||||||
<Wrench className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
<Wrench className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||||
<p className="text-white/60">
|
<p className="text-white/60 text-sm">
|
||||||
Выберите поставщика для просмотра расходников
|
Выберите поставщика для просмотра расходников
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : productsLoading ? (
|
) : productsLoading ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white border-t-transparent mx-auto mb-3"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-white border-t-transparent mx-auto mb-2"></div>
|
||||||
<p className="text-white/60">Загрузка расходников...</p>
|
<p className="text-white/60 text-sm">Загрузка...</p>
|
||||||
</div>
|
</div>
|
||||||
) : supplierProducts.length === 0 ? (
|
) : supplierProducts.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-8">
|
||||||
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
|
<Package className="h-8 w-8 text-white/40 mx-auto mb-3" />
|
||||||
<p className="text-white/60">
|
<p className="text-white/60 text-sm">
|
||||||
У данного поставщика нет доступных расходников
|
Нет доступных расходников
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-3">
|
||||||
{supplierProducts.map((product: ConsumableProduct) => {
|
{supplierProducts.map(
|
||||||
const selectedQuantity = getSelectedQuantity(
|
(product: ConsumableProduct, index) => {
|
||||||
product.id
|
const selectedQuantity = getSelectedQuantity(
|
||||||
);
|
product.id
|
||||||
return (
|
);
|
||||||
<Card
|
return (
|
||||||
key={product.id}
|
<Card
|
||||||
className="bg-white/10 backdrop-blur border-white/20 p-4"
|
key={product.id}
|
||||||
>
|
className={`relative bg-gradient-to-br from-white/10 via-white/5 to-white/10 backdrop-blur border border-white/20 p-3 rounded-xl overflow-hidden group hover:shadow-xl transition-all duration-300 ${
|
||||||
<div className="space-y-3">
|
selectedQuantity > 0
|
||||||
{/* Изображение товара */}
|
? "ring-2 ring-green-400/50 bg-gradient-to-br from-green-500/20 via-green-400/10 to-green-500/20"
|
||||||
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden">
|
: "hover:from-white/20 hover:via-white/10 hover:to-white/20 hover:border-white/40"
|
||||||
{product.images && product.images.length > 0 && product.images[0] ? (
|
}`}
|
||||||
<Image
|
style={{
|
||||||
src={product.images[0]}
|
animationDelay: `${index * 50}ms`,
|
||||||
alt={product.name}
|
minHeight: "200px",
|
||||||
width={200}
|
width: "100%",
|
||||||
height={200}
|
}}
|
||||||
className="w-full h-full object-cover"
|
>
|
||||||
/>
|
<div className="space-y-2 h-full flex flex-col">
|
||||||
) : product.mainImage ? (
|
{/* Изображение товара */}
|
||||||
<Image
|
<div className="aspect-square bg-white/5 rounded-lg overflow-hidden relative flex-shrink-0">
|
||||||
src={product.mainImage}
|
{product.images &&
|
||||||
alt={product.name}
|
product.images.length > 0 &&
|
||||||
width={200}
|
product.images[0] ? (
|
||||||
height={200}
|
<Image
|
||||||
className="w-full h-full object-cover"
|
src={product.images[0]}
|
||||||
/>
|
alt={product.name}
|
||||||
) : (
|
width={100}
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
height={100}
|
||||||
<Wrench className="h-12 w-12 text-white/40" />
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||||
</div>
|
/>
|
||||||
)}
|
) : product.mainImage ? (
|
||||||
</div>
|
<Image
|
||||||
|
src={product.mainImage}
|
||||||
|
alt={product.name}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<Wrench className="h-8 w-8 text-white/40" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedQuantity > 0 && (
|
||||||
|
<div className="absolute top-2 right-2 bg-gradient-to-r from-green-400 to-green-500 rounded-full w-6 h-6 flex items-center justify-center shadow-lg animate-pulse">
|
||||||
|
<span className="text-white text-xs font-bold">
|
||||||
|
{selectedQuantity > 999
|
||||||
|
? "999+"
|
||||||
|
: selectedQuantity}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Информация о товаре */}
|
{/* Информация о товаре */}
|
||||||
<div>
|
<div className="space-y-1 flex-grow">
|
||||||
<h3 className="text-white font-medium mb-1 line-clamp-2">
|
<h3 className="text-white font-medium text-sm leading-tight line-clamp-2 group-hover:text-purple-200 transition-colors duration-300">
|
||||||
{product.name}
|
{product.name}
|
||||||
</h3>
|
</h3>
|
||||||
{product.category && (
|
{product.category && (
|
||||||
<Badge className="bg-orange-500/20 text-orange-300 border-orange-500/30 text-xs mb-2">
|
<Badge className="bg-purple-500/20 text-purple-300 border-purple-500/30 text-xs px-2 py-1">
|
||||||
{product.category.name}
|
{product.category.name.slice(0, 10)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<p className="text-white/60 text-sm mb-2 line-clamp-2">
|
<div className="flex items-center justify-between">
|
||||||
{product.description ||
|
<span className="text-green-400 font-semibold text-sm">
|
||||||
"Описание отсутствует"}
|
{formatCurrency(product.price)}
|
||||||
</p>
|
</span>
|
||||||
<div className="flex items-center justify-between">
|
{product.stock && (
|
||||||
<span className="text-green-400 font-semibold">
|
<span className="text-white/60 text-xs">
|
||||||
{formatCurrency(product.price)}
|
{product.stock}
|
||||||
{product.unit && (
|
|
||||||
<span className="text-white/60 text-sm ml-1">
|
|
||||||
/ {product.unit}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</div>
|
||||||
{product.stock && (
|
</div>
|
||||||
<span className="text-white/60 text-sm">
|
|
||||||
В наличии: {product.stock}
|
{/* Управление количеством */}
|
||||||
</span>
|
<div className="flex flex-col items-center space-y-2 mt-auto">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
updateConsumableQuantity(
|
||||||
|
product.id,
|
||||||
|
Math.max(0, selectedQuantity - 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/20 rounded-full transition-all duration-300"
|
||||||
|
disabled={selectedQuantity === 0}
|
||||||
|
>
|
||||||
|
<Minus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
value={
|
||||||
|
selectedQuantity === 0
|
||||||
|
? ""
|
||||||
|
: selectedQuantity.toString()
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
let inputValue = e.target.value;
|
||||||
|
|
||||||
|
// Удаляем все нецифровые символы
|
||||||
|
inputValue = inputValue.replace(
|
||||||
|
/[^0-9]/g,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаляем ведущие нули
|
||||||
|
inputValue = inputValue.replace(
|
||||||
|
/^0+/,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если строка пустая после удаления нулей, устанавливаем 0
|
||||||
|
const numericValue =
|
||||||
|
inputValue === ""
|
||||||
|
? 0
|
||||||
|
: parseInt(inputValue);
|
||||||
|
|
||||||
|
// Ограничиваем значение максимумом 99999
|
||||||
|
const clampedValue = Math.min(
|
||||||
|
numericValue,
|
||||||
|
99999
|
||||||
|
);
|
||||||
|
|
||||||
|
updateConsumableQuantity(
|
||||||
|
product.id,
|
||||||
|
clampedValue
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// При потере фокуса, если поле пустое, устанавливаем 0
|
||||||
|
if (e.target.value === "") {
|
||||||
|
updateConsumableQuantity(
|
||||||
|
product.id,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-16 h-7 text-center text-sm bg-white/10 border-white/20 text-white rounded px-1 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
updateConsumableQuantity(
|
||||||
|
product.id,
|
||||||
|
Math.min(selectedQuantity + 1, 99999)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-6 w-6 p-0 text-white/60 hover:text-white hover:bg-white/20 rounded-full transition-all duration-300"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedQuantity > 0 && (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-green-400 font-bold text-sm bg-green-500/10 px-3 py-1 rounded-full">
|
||||||
|
{formatCurrency(
|
||||||
|
product.price * selectedQuantity
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Управление количеством */}
|
{/* Hover эффект */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/0 to-pink-500/0 group-hover:from-purple-500/5 group-hover:to-pink-500/5 transition-all duration-300 pointer-events-none rounded-xl"></div>
|
||||||
<div className="flex items-center space-x-2">
|
</Card>
|
||||||
<Button
|
);
|
||||||
variant="ghost"
|
}
|
||||||
size="sm"
|
)}
|
||||||
onClick={() =>
|
|
||||||
updateConsumableQuantity(
|
|
||||||
product.id,
|
|
||||||
Math.max(0, selectedQuantity - 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
|
||||||
disabled={selectedQuantity === 0}
|
|
||||||
>
|
|
||||||
<Minus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<span className="text-white font-medium w-8 text-center">
|
|
||||||
{selectedQuantity}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
updateConsumableQuantity(
|
|
||||||
product.id,
|
|
||||||
selectedQuantity + 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="h-8 w-8 p-0 text-white/60 hover:text-white hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{selectedQuantity > 0 && (
|
|
||||||
<span className="text-green-400 font-medium text-sm">
|
|
||||||
{formatCurrency(
|
|
||||||
product.price * selectedQuantity
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -547,22 +680,43 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Правая колонка - Корзина */}
|
{/* Правая колонка - Корзина */}
|
||||||
{selectedConsumables.length > 0 && (
|
<div className="w-72 flex-shrink-0">
|
||||||
<div className="w-80 flex-shrink-0">
|
<Card className="bg-white/10 backdrop-blur border-white/20 p-3 sticky top-0">
|
||||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-4 sticky top-0">
|
<h3 className="text-white font-semibold mb-3 flex items-center text-sm">
|
||||||
<h3 className="text-white font-semibold mb-4 flex items-center">
|
<ShoppingCart className="h-4 w-4 mr-2" />
|
||||||
<ShoppingCart className="h-4 w-4 mr-2" />
|
Корзина ({getTotalItems()} шт)
|
||||||
Корзина ({getTotalItems()} шт)
|
</h3>
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
|
{selectedConsumables.length === 0 ? (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<div className="bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-full p-4 w-fit mx-auto mb-3">
|
||||||
|
<ShoppingCart className="h-8 w-8 text-purple-300" />
|
||||||
|
</div>
|
||||||
|
<p className="text-white/60 text-sm font-medium mb-2">
|
||||||
|
Корзина пуста
|
||||||
|
</p>
|
||||||
|
<p className="text-white/40 text-xs mb-3">
|
||||||
|
Добавьте расходники для создания поставки
|
||||||
|
</p>
|
||||||
|
{selectedFulfillmentCenter && (
|
||||||
|
<div className="bg-white/5 rounded-lg p-2 mb-2">
|
||||||
|
<p className="text-white/60 text-xs">Доставка в:</p>
|
||||||
|
<p className="text-white text-xs font-medium">
|
||||||
|
{selectedFulfillmentCenter.name ||
|
||||||
|
selectedFulfillmentCenter.fullName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 mb-3 max-h-48 overflow-y-auto">
|
||||||
{selectedConsumables.map((consumable) => (
|
{selectedConsumables.map((consumable) => (
|
||||||
<div
|
<div
|
||||||
key={consumable.id}
|
key={consumable.id}
|
||||||
className="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
className="flex items-center justify-between p-2 bg-white/5 rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-white text-sm font-medium truncate">
|
<p className="text-white text-xs font-medium truncate">
|
||||||
{consumable.name}
|
{consumable.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-white/60 text-xs">
|
<p className="text-white/60 text-xs">
|
||||||
@ -571,7 +725,7 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-green-400 font-medium text-sm">
|
<span className="text-green-400 font-medium text-xs">
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
consumable.price * consumable.selectedQuantity
|
consumable.price * consumable.selectedQuantity
|
||||||
)}
|
)}
|
||||||
@ -582,7 +736,7 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateConsumableQuantity(consumable.id, 0)
|
updateConsumableQuantity(consumable.id, 0)
|
||||||
}
|
}
|
||||||
className="h-6 w-6 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
className="h-5 w-5 p-0 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</Button>
|
</Button>
|
||||||
@ -590,40 +744,78 @@ export function CreateConsumablesSupplyPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="border-t border-white/20 pt-4">
|
<div className="border-t border-white/20 pt-3">
|
||||||
<div className="mb-4">
|
<div className="mb-3">
|
||||||
<label className="text-white/60 text-sm mb-2 block">
|
<label className="text-white/60 text-xs mb-1 block">
|
||||||
Дата поставки:
|
Фулфилмент-центр:
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<div className="relative">
|
||||||
type="date"
|
<select
|
||||||
value={deliveryDate}
|
value={selectedFulfillmentCenter?.id || ""}
|
||||||
onChange={(e) => setDeliveryDate(e.target.value)}
|
onChange={(e) => {
|
||||||
className="bg-white/10 border-white/20 text-white"
|
const center = fulfillmentCenters.find(
|
||||||
min={new Date().toISOString().split("T")[0]}
|
(fc) => fc.id === e.target.value
|
||||||
|
);
|
||||||
|
setSelectedFulfillmentCenter(center || null);
|
||||||
|
}}
|
||||||
|
className="w-full bg-white/10 border border-white/20 text-white h-8 text-sm rounded px-2 focus:ring-2 focus:ring-purple-400/50 focus:border-purple-400/50"
|
||||||
required
|
required
|
||||||
/>
|
>
|
||||||
|
<option value="" className="bg-gray-800 text-white">
|
||||||
|
Выберите фулфилмент-центр
|
||||||
|
</option>
|
||||||
|
{fulfillmentCenters.map((center) => (
|
||||||
|
<option
|
||||||
|
key={center.id}
|
||||||
|
value={center.id}
|
||||||
|
className="bg-gray-800 text-white"
|
||||||
|
>
|
||||||
|
{center.name ||
|
||||||
|
center.fullName ||
|
||||||
|
"Фулфилмент-центр"}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<span className="text-white font-semibold">Итого:</span>
|
|
||||||
<span className="text-green-400 font-bold text-lg">
|
|
||||||
{formatCurrency(getTotalAmount())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateSupply}
|
|
||||||
disabled={isCreatingSupply || !deliveryDate}
|
|
||||||
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isCreatingSupply
|
|
||||||
? "Создание..."
|
|
||||||
: "Создать поставку расходников"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div className="mb-3">
|
||||||
</div>
|
<label className="text-white/60 text-xs mb-1 block">
|
||||||
)}
|
Дата поставки:
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={deliveryDate}
|
||||||
|
onChange={(e) => setDeliveryDate(e.target.value)}
|
||||||
|
className="bg-white/10 border-white/20 text-white h-8 text-sm"
|
||||||
|
min={new Date().toISOString().split("T")[0]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-white font-semibold text-sm">
|
||||||
|
Итого:
|
||||||
|
</span>
|
||||||
|
<span className="text-green-400 font-bold text-lg">
|
||||||
|
{formatCurrency(getTotalAmount())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateSupply}
|
||||||
|
disabled={
|
||||||
|
isCreatingSupply ||
|
||||||
|
!deliveryDate ||
|
||||||
|
!selectedFulfillmentCenter ||
|
||||||
|
selectedConsumables.length === 0
|
||||||
|
}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white disabled:opacity-50 h-8 text-sm"
|
||||||
|
>
|
||||||
|
{isCreatingSupply ? "Создание..." : "Создать поставку"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -753,4 +753,49 @@ export const ALL_USERS = gql`
|
|||||||
hasMore
|
hasMore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const GET_SUPPLY_ORDERS = gql`
|
||||||
|
query GetSupplyOrders {
|
||||||
|
supplyOrders {
|
||||||
|
id
|
||||||
|
deliveryDate
|
||||||
|
status
|
||||||
|
totalAmount
|
||||||
|
totalItems
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
partner {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
fullName
|
||||||
|
inn
|
||||||
|
address
|
||||||
|
phones
|
||||||
|
emails
|
||||||
|
}
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
fullName
|
||||||
|
type
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
quantity
|
||||||
|
price
|
||||||
|
totalPrice
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
article
|
||||||
|
description
|
||||||
|
category {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
@ -97,7 +97,7 @@ const generateToken = (payload: AuthTokenPayload): string => {
|
|||||||
const verifyToken = (token: string): AuthTokenPayload => {
|
const verifyToken = (token: string): AuthTokenPayload => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
|
return jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new GraphQLError("Недействительный токен", {
|
throw new GraphQLError("Недействительный токен", {
|
||||||
extensions: { code: "UNAUTHENTICATED" },
|
extensions: { code: "UNAUTHENTICATED" },
|
||||||
@ -169,7 +169,7 @@ function parseLiteral(ast: unknown): unknown {
|
|||||||
fields?: unknown[];
|
fields?: unknown[];
|
||||||
values?: unknown[];
|
values?: unknown[];
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (astNode.kind) {
|
switch (astNode.kind) {
|
||||||
case Kind.STRING:
|
case Kind.STRING:
|
||||||
case Kind.BOOLEAN:
|
case Kind.BOOLEAN:
|
||||||
@ -291,7 +291,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
// Получаем исходящие заявки для добавления флага hasOutgoingRequest
|
||||||
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
const outgoingRequests = await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
senderId: currentUser.organization.id,
|
senderId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -302,7 +302,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
// Получаем входящие заявки для добавления флага hasIncomingRequest
|
||||||
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
const incomingRequests = await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
receiverId: currentUser.organization.id,
|
receiverId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -366,7 +366,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const counterparties = await prisma.counterparty.findMany({
|
const counterparties = await prisma.counterparty.findMany({
|
||||||
where: { organizationId: currentUser.organization.id },
|
where: { organizationId: currentUser.organization.id },
|
||||||
include: {
|
include: {
|
||||||
counterparty: {
|
counterparty: {
|
||||||
include: {
|
include: {
|
||||||
users: true,
|
users: true,
|
||||||
@ -397,7 +397,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.counterpartyRequest.findMany({
|
return await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
receiverId: currentUser.organization.id,
|
receiverId: currentUser.organization.id,
|
||||||
status: "PENDING",
|
status: "PENDING",
|
||||||
},
|
},
|
||||||
@ -437,7 +437,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.counterpartyRequest.findMany({
|
return await prisma.counterpartyRequest.findMany({
|
||||||
where: {
|
where: {
|
||||||
senderId: currentUser.organization.id,
|
senderId: currentUser.organization.id,
|
||||||
status: { in: ["PENDING", "REJECTED"] },
|
status: { in: ["PENDING", "REJECTED"] },
|
||||||
},
|
},
|
||||||
@ -506,7 +506,7 @@ export const resolvers = {
|
|||||||
receiverOrganization: {
|
receiverOrganization: {
|
||||||
include: {
|
include: {
|
||||||
users: true,
|
users: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
@ -537,7 +537,7 @@ export const resolvers = {
|
|||||||
// Получаем всех контрагентов
|
// Получаем всех контрагентов
|
||||||
const counterparties = await prisma.counterparty.findMany({
|
const counterparties = await prisma.counterparty.findMany({
|
||||||
where: { organizationId: currentUser.organization.id },
|
where: { organizationId: currentUser.organization.id },
|
||||||
include: {
|
include: {
|
||||||
counterparty: {
|
counterparty: {
|
||||||
include: {
|
include: {
|
||||||
users: true,
|
users: true,
|
||||||
@ -550,7 +550,7 @@ export const resolvers = {
|
|||||||
const conversations = await Promise.all(
|
const conversations = await Promise.all(
|
||||||
counterparties.map(async (cp) => {
|
counterparties.map(async (cp) => {
|
||||||
const counterpartyId = cp.counterparty.id;
|
const counterpartyId = cp.counterparty.id;
|
||||||
|
|
||||||
// Последнее сообщение с этим контрагентом
|
// Последнее сообщение с этим контрагентом
|
||||||
const lastMessage = await prisma.message.findFirst({
|
const lastMessage = await prisma.message.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -608,7 +608,10 @@ export const resolvers = {
|
|||||||
// Фильтруем null значения и сортируем по времени последнего сообщения
|
// Фильтруем null значения и сортируем по времени последнего сообщения
|
||||||
return conversations
|
return conversations
|
||||||
.filter((conv) => conv !== null)
|
.filter((conv) => conv !== null)
|
||||||
.sort((a, b) => new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime());
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b!.updatedAt).getTime() - new Date(a!.updatedAt).getTime()
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Мои услуги
|
// Мои услуги
|
||||||
@ -664,6 +667,57 @@ export const resolvers = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Заказы поставок расходников
|
||||||
|
supplyOrders: async (_: unknown, __: unknown, context: Context) => {
|
||||||
|
if (!context.user) {
|
||||||
|
throw new GraphQLError("Требуется авторизация", {
|
||||||
|
extensions: { code: "UNAUTHENTICATED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = await prisma.user.findUnique({
|
||||||
|
where: { id: context.user.id },
|
||||||
|
include: { organization: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentUser?.organization) {
|
||||||
|
throw new GraphQLError("У пользователя нет организации");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем заказы где текущая организация является заказчиком или поставщиком
|
||||||
|
return await prisma.supplyOrder.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ organizationId: currentUser.organization.id }, // Заказы созданные организацией
|
||||||
|
{ partnerId: currentUser.organization.id }, // Заказы где организация - поставщик
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
partner: {
|
||||||
|
include: {
|
||||||
|
users: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
include: {
|
||||||
|
users: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
include: {
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
category: true,
|
||||||
|
organization: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Логистика организации
|
// Логистика организации
|
||||||
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
myLogistics: async (_: unknown, __: unknown, context: Context) => {
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
@ -743,7 +797,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
return await prisma.product.findMany({
|
return await prisma.product.findMany({
|
||||||
where: { organizationId: currentUser.organization.id },
|
where: { organizationId: currentUser.organization.id },
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -785,7 +839,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
return await prisma.product.findMany({
|
return await prisma.product.findMany({
|
||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
@ -899,7 +953,9 @@ export const resolvers = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!targetOrganization || targetOrganization.type !== "FULFILLMENT") {
|
if (!targetOrganization || targetOrganization.type !== "FULFILLMENT") {
|
||||||
throw new GraphQLError("Расходники доступны только у фулфилмент центров");
|
throw new GraphQLError(
|
||||||
|
"Расходники доступны только у фулфилмент центров"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.supply.findMany({
|
return await prisma.supply.findMany({
|
||||||
@ -1068,7 +1124,7 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const employee = await prisma.employee.findFirst({
|
const employee = await prisma.employee.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -1155,7 +1211,7 @@ export const resolvers = {
|
|||||||
args.phone,
|
args.phone,
|
||||||
args.code
|
args.code
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!verificationResult.success) {
|
if (!verificationResult.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -1214,7 +1270,7 @@ export const resolvers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log("verifySmsCode - Returning result:", {
|
console.log("verifySmsCode - Returning result:", {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
hasToken: !!result.token,
|
hasToken: !!result.token,
|
||||||
hasUser: !!result.user,
|
hasUser: !!result.user,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
@ -1318,28 +1374,28 @@ export const resolvers = {
|
|||||||
addressFull: organizationData.addressFull,
|
addressFull: organizationData.addressFull,
|
||||||
ogrn: organizationData.ogrn,
|
ogrn: organizationData.ogrn,
|
||||||
ogrnDate: organizationData.ogrnDate,
|
ogrnDate: organizationData.ogrnDate,
|
||||||
|
|
||||||
// Статус организации
|
// Статус организации
|
||||||
status: organizationData.status,
|
status: organizationData.status,
|
||||||
actualityDate: organizationData.actualityDate,
|
actualityDate: organizationData.actualityDate,
|
||||||
registrationDate: organizationData.registrationDate,
|
registrationDate: organizationData.registrationDate,
|
||||||
liquidationDate: organizationData.liquidationDate,
|
liquidationDate: organizationData.liquidationDate,
|
||||||
|
|
||||||
// Руководитель
|
// Руководитель
|
||||||
managementName: organizationData.managementName,
|
managementName: organizationData.managementName,
|
||||||
managementPost: organizationData.managementPost,
|
managementPost: organizationData.managementPost,
|
||||||
|
|
||||||
// ОПФ
|
// ОПФ
|
||||||
opfCode: organizationData.opfCode,
|
opfCode: organizationData.opfCode,
|
||||||
opfFull: organizationData.opfFull,
|
opfFull: organizationData.opfFull,
|
||||||
opfShort: organizationData.opfShort,
|
opfShort: organizationData.opfShort,
|
||||||
|
|
||||||
// Коды статистики
|
// Коды статистики
|
||||||
okato: organizationData.okato,
|
okato: organizationData.okato,
|
||||||
oktmo: organizationData.oktmo,
|
oktmo: organizationData.oktmo,
|
||||||
okpo: organizationData.okpo,
|
okpo: organizationData.okpo,
|
||||||
okved: organizationData.okved,
|
okved: organizationData.okved,
|
||||||
|
|
||||||
// Контакты
|
// Контакты
|
||||||
phones: organizationData.phones
|
phones: organizationData.phones
|
||||||
? JSON.parse(JSON.stringify(organizationData.phones))
|
? JSON.parse(JSON.stringify(organizationData.phones))
|
||||||
@ -1347,12 +1403,12 @@ export const resolvers = {
|
|||||||
emails: organizationData.emails
|
emails: organizationData.emails
|
||||||
? JSON.parse(JSON.stringify(organizationData.emails))
|
? JSON.parse(JSON.stringify(organizationData.emails))
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
// Финансовые данные
|
// Финансовые данные
|
||||||
employeeCount: organizationData.employeeCount,
|
employeeCount: organizationData.employeeCount,
|
||||||
revenue: organizationData.revenue,
|
revenue: organizationData.revenue,
|
||||||
taxSystem: organizationData.taxSystem,
|
taxSystem: organizationData.taxSystem,
|
||||||
|
|
||||||
type: type,
|
type: type,
|
||||||
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
|
dadataData: JSON.parse(JSON.stringify(organizationData.rawData)),
|
||||||
},
|
},
|
||||||
@ -1455,7 +1511,7 @@ export const resolvers = {
|
|||||||
const tradeMark = validationResults[0]?.data?.tradeMark;
|
const tradeMark = validationResults[0]?.data?.tradeMark;
|
||||||
const sellerName = validationResults[0]?.data?.sellerName;
|
const sellerName = validationResults[0]?.data?.sellerName;
|
||||||
const shopName = tradeMark || sellerName || "Магазин";
|
const shopName = tradeMark || sellerName || "Магазин";
|
||||||
|
|
||||||
const organization = await prisma.organization.create({
|
const organization = await prisma.organization.create({
|
||||||
data: {
|
data: {
|
||||||
inn:
|
inn:
|
||||||
@ -1598,7 +1654,7 @@ export const resolvers = {
|
|||||||
where: { id: existingKey.id },
|
where: { id: existingKey.id },
|
||||||
data: {
|
data: {
|
||||||
apiKey,
|
apiKey,
|
||||||
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
validationData: JSON.parse(JSON.stringify(validationResult.data)),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1624,7 +1680,7 @@ export const resolvers = {
|
|||||||
message: "API ключ успешно добавлен",
|
message: "API ключ успешно добавлен",
|
||||||
apiKey: newKey,
|
apiKey: newKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding marketplace API key:", error);
|
console.error("Error adding marketplace API key:", error);
|
||||||
return {
|
return {
|
||||||
@ -1697,7 +1753,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1712,7 +1768,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { input } = args;
|
const { input } = args;
|
||||||
|
|
||||||
// Обновляем данные пользователя (аватар, имя управляющего)
|
// Обновляем данные пользователя (аватар, имя управляющего)
|
||||||
const userUpdateData: { avatar?: string; managerName?: string } = {};
|
const userUpdateData: { avatar?: string; managerName?: string } = {};
|
||||||
if (input.avatar) {
|
if (input.avatar) {
|
||||||
@ -1721,14 +1777,14 @@ export const resolvers = {
|
|||||||
if (input.managerName) {
|
if (input.managerName) {
|
||||||
userUpdateData.managerName = input.managerName;
|
userUpdateData.managerName = input.managerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(userUpdateData).length > 0) {
|
if (Object.keys(userUpdateData).length > 0) {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
data: userUpdateData,
|
data: userUpdateData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подготавливаем данные для обновления организации
|
// Подготавливаем данные для обновления организации
|
||||||
const updateData: {
|
const updateData: {
|
||||||
phones?: object;
|
phones?: object;
|
||||||
@ -1736,20 +1792,20 @@ export const resolvers = {
|
|||||||
managementName?: string;
|
managementName?: string;
|
||||||
managementPost?: string;
|
managementPost?: string;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// Название организации больше не обновляется через профиль
|
// Название организации больше не обновляется через профиль
|
||||||
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
// Для селлеров устанавливается при регистрации, для остальных - при смене ИНН
|
||||||
|
|
||||||
// Обновляем контактные данные в JSON поле phones
|
// Обновляем контактные данные в JSON поле phones
|
||||||
if (input.orgPhone) {
|
if (input.orgPhone) {
|
||||||
updateData.phones = [{ value: input.orgPhone, type: "main" }];
|
updateData.phones = [{ value: input.orgPhone, type: "main" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем email в JSON поле emails
|
// Обновляем email в JSON поле emails
|
||||||
if (input.email) {
|
if (input.email) {
|
||||||
updateData.emails = [{ value: input.email, type: "main" }];
|
updateData.emails = [{ value: input.email, type: "main" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем дополнительные контакты в custom полях
|
// Сохраняем дополнительные контакты в custom полях
|
||||||
// Пока добавим их как дополнительные JSON поля
|
// Пока добавим их как дополнительные JSON поля
|
||||||
const customContacts: {
|
const customContacts: {
|
||||||
@ -1763,13 +1819,13 @@ export const resolvers = {
|
|||||||
corrAccount?: string;
|
corrAccount?: string;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
// managerName теперь сохраняется в поле пользователя, а не в JSON
|
||||||
|
|
||||||
if (input.telegram) {
|
if (input.telegram) {
|
||||||
customContacts.telegram = input.telegram;
|
customContacts.telegram = input.telegram;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.whatsapp) {
|
if (input.whatsapp) {
|
||||||
customContacts.whatsapp = input.whatsapp;
|
customContacts.whatsapp = input.whatsapp;
|
||||||
}
|
}
|
||||||
@ -1787,7 +1843,7 @@ export const resolvers = {
|
|||||||
corrAccount: input.corrAccount,
|
corrAccount: input.corrAccount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
// Если есть дополнительные контакты, сохраним их в поле managementPost временно
|
||||||
// В идеале нужно добавить отдельную таблицу для контактов
|
// В идеале нужно добавить отдельную таблицу для контактов
|
||||||
if (Object.keys(customContacts).length > 0) {
|
if (Object.keys(customContacts).length > 0) {
|
||||||
@ -1806,7 +1862,7 @@ export const resolvers = {
|
|||||||
// Получаем обновленного пользователя
|
// Получаем обновленного пользователя
|
||||||
const updatedUser = await prisma.user.findUnique({
|
const updatedUser = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1842,7 +1898,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -1940,7 +1996,7 @@ export const resolvers = {
|
|||||||
// Получаем обновленного пользователя
|
// Получаем обновленного пользователя
|
||||||
const updatedUser = await prisma.user.findUnique({
|
const updatedUser = await prisma.user.findUnique({
|
||||||
where: { id: context.user.id },
|
where: { id: context.user.id },
|
||||||
include: {
|
include: {
|
||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@ -2591,8 +2647,8 @@ export const resolvers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// conversationId имеет формат "currentOrgId-counterpartyId"
|
// conversationId имеет формат "currentOrgId-counterpartyId"
|
||||||
const [, counterpartyId] = args.conversationId.split('-');
|
const [, counterpartyId] = args.conversationId.split("-");
|
||||||
|
|
||||||
if (!counterpartyId) {
|
if (!counterpartyId) {
|
||||||
throw new GraphQLError("Неверный ID беседы");
|
throw new GraphQLError("Неверный ID беседы");
|
||||||
}
|
}
|
||||||
@ -2989,13 +3045,22 @@ export const resolvers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Создать заказ поставки расходников
|
// Создать заказ поставки расходников
|
||||||
|
// Процесс: Селлер → Поставщик → Логистика → Фулфилмент
|
||||||
|
// 1. Селлер создает заказ у поставщика расходников
|
||||||
|
// 2. Поставщик получает заказ и готовит товары
|
||||||
|
// 3. Логистика транспортирует товары на склад фулфилмента
|
||||||
|
// 4. Фулфилмент принимает товары на склад
|
||||||
|
// 5. Все участники видят информацию о поставке в своих кабинетах
|
||||||
createSupplyOrder: async (
|
createSupplyOrder: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: {
|
args: {
|
||||||
input: {
|
input: {
|
||||||
partnerId: string;
|
partnerId: string;
|
||||||
deliveryDate: string;
|
deliveryDate: string;
|
||||||
|
fulfillmentCenterId?: string; // ID фулфилмент-центра для доставки
|
||||||
|
logisticsPartnerId?: string; // ID логистической компании
|
||||||
items: Array<{ productId: string; quantity: number }>;
|
items: Array<{ productId: string; quantity: number }>;
|
||||||
|
notes?: string; // Дополнительные заметки к заказу
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
@ -3015,13 +3080,40 @@ export const resolvers = {
|
|||||||
throw new GraphQLError("У пользователя нет организации");
|
throw new GraphQLError("У пользователя нет организации");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что это фулфилмент центр
|
// Проверяем тип организации и определяем роль в процессе поставки
|
||||||
if (currentUser.organization.type !== "FULFILLMENT") {
|
const allowedTypes = ["FULFILLMENT", "SELLER", "LOGIST"];
|
||||||
|
if (!allowedTypes.includes(currentUser.organization.type)) {
|
||||||
throw new GraphQLError(
|
throw new GraphQLError(
|
||||||
"Заказы поставок доступны только для фулфилмент центров"
|
"Заказы поставок недоступны для данного типа организации"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Определяем роль организации в процессе поставки
|
||||||
|
const organizationRole = currentUser.organization.type;
|
||||||
|
let fulfillmentCenterId = args.input.fulfillmentCenterId;
|
||||||
|
|
||||||
|
// Если заказ создает фулфилмент-центр, он сам является получателем
|
||||||
|
if (organizationRole === "FULFILLMENT") {
|
||||||
|
fulfillmentCenterId = currentUser.organization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если указан фулфилмент-центр, проверяем его существование
|
||||||
|
if (fulfillmentCenterId) {
|
||||||
|
const fulfillmentCenter = await prisma.organization.findFirst({
|
||||||
|
where: {
|
||||||
|
id: fulfillmentCenterId,
|
||||||
|
type: "FULFILLMENT",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fulfillmentCenter) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Указанный фулфилмент-центр не найден",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем, что партнер существует и является оптовиком
|
// Проверяем, что партнер существует и является оптовиком
|
||||||
const partner = await prisma.organization.findFirst({
|
const partner = await prisma.organization.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -3104,6 +3196,16 @@ export const resolvers = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Определяем начальный статус в зависимости от роли организации
|
||||||
|
let initialStatus = "PENDING";
|
||||||
|
if (organizationRole === "SELLER") {
|
||||||
|
initialStatus = "PENDING"; // Селлер создает заказ, ждет подтверждения поставщика
|
||||||
|
} else if (organizationRole === "FULFILLMENT") {
|
||||||
|
initialStatus = "PENDING"; // Фулфилмент заказывает для своего склада
|
||||||
|
} else if (organizationRole === "LOGIST") {
|
||||||
|
initialStatus = "CONFIRMED"; // Логист может сразу подтверждать заказы
|
||||||
|
}
|
||||||
|
|
||||||
const supplyOrder = await prisma.supplyOrder.create({
|
const supplyOrder = await prisma.supplyOrder.create({
|
||||||
data: {
|
data: {
|
||||||
partnerId: args.input.partnerId,
|
partnerId: args.input.partnerId,
|
||||||
@ -3111,6 +3213,7 @@ export const resolvers = {
|
|||||||
totalAmount: new Prisma.Decimal(totalAmount),
|
totalAmount: new Prisma.Decimal(totalAmount),
|
||||||
totalItems: totalItems,
|
totalItems: totalItems,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
|
status: initialStatus as any,
|
||||||
items: {
|
items: {
|
||||||
create: orderItems,
|
create: orderItems,
|
||||||
},
|
},
|
||||||
@ -3145,7 +3248,7 @@ export const resolvers = {
|
|||||||
const productWithCategory = supplyOrder.items.find(
|
const productWithCategory = supplyOrder.items.find(
|
||||||
(orderItem) => orderItem.productId === item.productId
|
(orderItem) => orderItem.productId === item.productId
|
||||||
)?.product;
|
)?.product;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: product.name,
|
name: product.name,
|
||||||
description: product.description || `Заказано у ${partner.name}`,
|
description: product.description || `Заказано у ${partner.name}`,
|
||||||
@ -3167,10 +3270,31 @@ export const resolvers = {
|
|||||||
data: suppliesData,
|
data: suppliesData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Формируем сообщение в зависимости от роли организации
|
||||||
|
let successMessage = "";
|
||||||
|
if (organizationRole === "SELLER") {
|
||||||
|
successMessage = `Заказ поставки расходников создан! Расходники будут доставлены ${
|
||||||
|
fulfillmentCenterId
|
||||||
|
? "на указанный фулфилмент-склад"
|
||||||
|
: "согласно настройкам"
|
||||||
|
}. Ожидайте подтверждения от поставщика.`;
|
||||||
|
} else if (organizationRole === "FULFILLMENT") {
|
||||||
|
successMessage = `Заказ поставки расходников создан для вашего склада! Ожидайте подтверждения от поставщика и координации с логистикой.`;
|
||||||
|
} else if (organizationRole === "LOGIST") {
|
||||||
|
successMessage = `Заказ поставки создан и подтвержден! Координируйте доставку расходников от поставщика на фулфилмент-склад.`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Заказ поставки создан успешно! Добавлено ${suppliesData.length} расходников в каталог.`,
|
message: successMessage,
|
||||||
order: supplyOrder,
|
order: supplyOrder,
|
||||||
|
processInfo: {
|
||||||
|
role: organizationRole,
|
||||||
|
supplier: partner.name || partner.fullName,
|
||||||
|
fulfillmentCenter: fulfillmentCenterId,
|
||||||
|
logistics: args.input.logisticsPartnerId,
|
||||||
|
status: initialStatus,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating supply order:", error);
|
console.error("Error creating supply order:", error);
|
||||||
@ -3185,22 +3309,22 @@ export const resolvers = {
|
|||||||
createProduct: async (
|
createProduct: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: {
|
args: {
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
dimensions?: string;
|
dimensions?: string;
|
||||||
material?: string;
|
material?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
mainImage?: string;
|
mainImage?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
@ -3260,7 +3384,7 @@ export const resolvers = {
|
|||||||
isActive: args.input.isActive ?? true,
|
isActive: args.input.isActive ?? true,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -3284,23 +3408,23 @@ export const resolvers = {
|
|||||||
updateProduct: async (
|
updateProduct: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: {
|
args: {
|
||||||
id: string;
|
id: string;
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
article: string;
|
article: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
price: number;
|
price: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
dimensions?: string;
|
dimensions?: string;
|
||||||
material?: string;
|
material?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
mainImage?: string;
|
mainImage?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
@ -3366,11 +3490,13 @@ export const resolvers = {
|
|||||||
weight: args.input.weight,
|
weight: args.input.weight,
|
||||||
dimensions: args.input.dimensions,
|
dimensions: args.input.dimensions,
|
||||||
material: args.input.material,
|
material: args.input.material,
|
||||||
images: args.input.images ? JSON.stringify(args.input.images) : undefined,
|
images: args.input.images
|
||||||
|
? JSON.stringify(args.input.images)
|
||||||
|
: undefined,
|
||||||
mainImage: args.input.mainImage,
|
mainImage: args.input.mainImage,
|
||||||
isActive: args.input.isActive ?? true,
|
isActive: args.input.isActive ?? true,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
category: true,
|
||||||
organization: true,
|
organization: true,
|
||||||
},
|
},
|
||||||
@ -3655,7 +3781,7 @@ export const resolvers = {
|
|||||||
if (existingCartItem) {
|
if (existingCartItem) {
|
||||||
// Обновляем количество
|
// Обновляем количество
|
||||||
const newQuantity = existingCartItem.quantity + args.quantity;
|
const newQuantity = existingCartItem.quantity + args.quantity;
|
||||||
|
|
||||||
if (newQuantity > product.quantity) {
|
if (newQuantity > product.quantity) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -4195,7 +4321,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const employee = await prisma.employee.update({
|
const employee = await prisma.employee.update({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -4257,7 +4383,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.employee.delete({
|
await prisma.employee.delete({
|
||||||
where: {
|
where: {
|
||||||
id: args.id,
|
id: args.id,
|
||||||
organizationId: currentUser.organization.id,
|
organizationId: currentUser.organization.id,
|
||||||
},
|
},
|
||||||
@ -4405,7 +4531,7 @@ export const resolvers = {
|
|||||||
if (parent.users) {
|
if (parent.users) {
|
||||||
return parent.users;
|
return parent.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно
|
// Иначе загружаем отдельно
|
||||||
return await prisma.user.findMany({
|
return await prisma.user.findMany({
|
||||||
where: { organizationId: parent.id },
|
where: { organizationId: parent.id },
|
||||||
@ -4416,7 +4542,7 @@ export const resolvers = {
|
|||||||
if (parent.services) {
|
if (parent.services) {
|
||||||
return parent.services;
|
return parent.services;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно
|
// Иначе загружаем отдельно
|
||||||
return await prisma.service.findMany({
|
return await prisma.service.findMany({
|
||||||
where: { organizationId: parent.id },
|
where: { organizationId: parent.id },
|
||||||
@ -4429,7 +4555,7 @@ export const resolvers = {
|
|||||||
if (parent.supplies) {
|
if (parent.supplies) {
|
||||||
return parent.supplies;
|
return parent.supplies;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно
|
// Иначе загружаем отдельно
|
||||||
return await prisma.supply.findMany({
|
return await prisma.supply.findMany({
|
||||||
where: { organizationId: parent.id },
|
where: { organizationId: parent.id },
|
||||||
@ -4478,7 +4604,7 @@ export const resolvers = {
|
|||||||
if (parent.organization) {
|
if (parent.organization) {
|
||||||
return parent.organization;
|
return parent.organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Иначе загружаем отдельно если есть organizationId
|
// Иначе загружаем отдельно если есть organizationId
|
||||||
if (parent.organizationId) {
|
if (parent.organizationId) {
|
||||||
return await prisma.organization.findUnique({
|
return await prisma.organization.findUnique({
|
||||||
@ -4497,7 +4623,7 @@ export const resolvers = {
|
|||||||
Product: {
|
Product: {
|
||||||
images: (parent: { images: unknown }) => {
|
images: (parent: { images: unknown }) => {
|
||||||
// Если images это строка JSON, парсим её в массив
|
// Если images это строка JSON, парсим её в массив
|
||||||
if (typeof parent.images === 'string') {
|
if (typeof parent.images === "string") {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(parent.images);
|
return JSON.parse(parent.images);
|
||||||
} catch {
|
} catch {
|
||||||
@ -4814,14 +4940,14 @@ const adminQueries = {
|
|||||||
|
|
||||||
const limit = args.limit || 50;
|
const limit = args.limit || 50;
|
||||||
const offset = args.offset || 0;
|
const offset = args.offset || 0;
|
||||||
|
|
||||||
// Строим условие поиска
|
// Строим условие поиска
|
||||||
const whereCondition: Prisma.UserWhereInput = args.search
|
const whereCondition: Prisma.UserWhereInput = args.search
|
||||||
? {
|
? {
|
||||||
OR: [
|
OR: [
|
||||||
{ phone: { contains: args.search, mode: "insensitive" } },
|
{ phone: { contains: args.search, mode: "insensitive" } },
|
||||||
{ managerName: { contains: args.search, mode: "insensitive" } },
|
{ managerName: { contains: args.search, mode: "insensitive" } },
|
||||||
{
|
{
|
||||||
organization: {
|
organization: {
|
||||||
OR: [
|
OR: [
|
||||||
{ name: { contains: args.search, mode: "insensitive" } },
|
{ name: { contains: args.search, mode: "insensitive" } },
|
||||||
@ -4889,7 +5015,7 @@ const adminMutations = {
|
|||||||
args.password,
|
args.password,
|
||||||
admin.password
|
admin.password
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -4905,7 +5031,7 @@ const adminMutations = {
|
|||||||
|
|
||||||
// Создать токен
|
// Создать токен
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
adminId: admin.id,
|
adminId: admin.id,
|
||||||
username: admin.username,
|
username: admin.username,
|
||||||
type: "admin",
|
type: "admin",
|
||||||
@ -4963,19 +5089,19 @@ const wildberriesQueries = {
|
|||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.organization || user.organization.type !== 'SELLER') {
|
if (!user?.organization || user.organization.type !== "SELLER") {
|
||||||
throw new GraphQLError("Доступно только для продавцов");
|
throw new GraphQLError("Доступно только для продавцов");
|
||||||
}
|
}
|
||||||
|
|
||||||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||||||
key => key.marketplace === 'WILDBERRIES' && key.isActive
|
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!wbApiKeyRecord) {
|
if (!wbApiKeyRecord) {
|
||||||
throw new GraphQLError("WB API ключ не настроен");
|
throw new GraphQLError("WB API ключ не настроен");
|
||||||
}
|
}
|
||||||
@ -4985,8 +5111,8 @@ const wildberriesQueries = {
|
|||||||
// Получаем кампании во всех статусах
|
// Получаем кампании во всех статусах
|
||||||
const [active, completed, paused] = await Promise.all([
|
const [active, completed, paused] = await Promise.all([
|
||||||
wbService.getAdverts(9).catch(() => []), // активные
|
wbService.getAdverts(9).catch(() => []), // активные
|
||||||
wbService.getAdverts(7).catch(() => []), // завершенные
|
wbService.getAdverts(7).catch(() => []), // завершенные
|
||||||
wbService.getAdverts(11).catch(() => []) // на паузе
|
wbService.getAdverts(11).catch(() => []), // на паузе
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const allCampaigns = [...active, ...completed, ...paused];
|
const allCampaigns = [...active, ...completed, ...paused];
|
||||||
@ -4995,30 +5121,34 @@ const wildberriesQueries = {
|
|||||||
success: true,
|
success: true,
|
||||||
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
|
message: `Found ${active.length} active, ${completed.length} completed, ${paused.length} paused campaigns`,
|
||||||
campaignsCount: allCampaigns.length,
|
campaignsCount: allCampaigns.length,
|
||||||
campaigns: allCampaigns.map(c => ({
|
campaigns: allCampaigns.map((c) => ({
|
||||||
id: c.advertId,
|
id: c.advertId,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
status: c.status,
|
status: c.status,
|
||||||
type: c.type
|
type: c.type,
|
||||||
}))
|
})),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error debugging WB adverts:', error);
|
console.error("Error debugging WB adverts:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error instanceof Error ? error.message : 'Unknown error',
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
campaignsCount: 0,
|
campaignsCount: 0,
|
||||||
campaigns: []
|
campaigns: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getWildberriesStatistics: async (
|
getWildberriesStatistics: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
{ period, startDate, endDate }: {
|
{
|
||||||
period?: 'week' | 'month' | 'quarter'
|
period,
|
||||||
startDate?: string
|
startDate,
|
||||||
endDate?: string
|
endDate,
|
||||||
|
}: {
|
||||||
|
period?: "week" | "month" | "quarter";
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
@ -5036,7 +5166,7 @@ const wildberriesQueries = {
|
|||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -5045,14 +5175,14 @@ const wildberriesQueries = {
|
|||||||
throw new GraphQLError("Организация не найдена");
|
throw new GraphQLError("Организация не найдена");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.organization.type !== 'SELLER') {
|
if (user.organization.type !== "SELLER") {
|
||||||
throw new GraphQLError("Доступно только для продавцов");
|
throw new GraphQLError("Доступно только для продавцов");
|
||||||
}
|
}
|
||||||
|
|
||||||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||||||
key => key.marketplace === 'WILDBERRIES' && key.isActive
|
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!wbApiKeyRecord) {
|
if (!wbApiKeyRecord) {
|
||||||
throw new GraphQLError("WB API ключ не настроен");
|
throw new GraphQLError("WB API ключ не настроен");
|
||||||
}
|
}
|
||||||
@ -5073,7 +5203,9 @@ const wildberriesQueries = {
|
|||||||
dateFrom = WildberriesService.getDatePeriodAgo(period);
|
dateFrom = WildberriesService.getDatePeriodAgo(period);
|
||||||
dateTo = WildberriesService.formatDate(new Date());
|
dateTo = WildberriesService.formatDate(new Date());
|
||||||
} else {
|
} else {
|
||||||
throw new GraphQLError("Необходимо указать либо period, либо startDate и endDate");
|
throw new GraphQLError(
|
||||||
|
"Необходимо указать либо period, либо startDate и endDate"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем статистику
|
// Получаем статистику
|
||||||
@ -5082,13 +5214,16 @@ const wildberriesQueries = {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: statistics,
|
data: statistics,
|
||||||
message: null
|
message: null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching WB statistics:', error);
|
console.error("Error fetching WB statistics:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error instanceof Error ? error.message : 'Ошибка получения статистики',
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Ошибка получения статистики",
|
||||||
data: [],
|
data: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -5096,17 +5231,19 @@ const wildberriesQueries = {
|
|||||||
|
|
||||||
getWildberriesCampaignStats: async (
|
getWildberriesCampaignStats: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
{ input }: {
|
{
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
input: {
|
input: {
|
||||||
campaigns: Array<{
|
campaigns: Array<{
|
||||||
id: number
|
id: number;
|
||||||
dates?: string[]
|
dates?: string[];
|
||||||
interval?: {
|
interval?: {
|
||||||
begin: string
|
begin: string;
|
||||||
end: string
|
end: string;
|
||||||
}
|
};
|
||||||
}>
|
}>;
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
@ -5124,7 +5261,7 @@ const wildberriesQueries = {
|
|||||||
organization: {
|
organization: {
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -5133,14 +5270,14 @@ const wildberriesQueries = {
|
|||||||
throw new GraphQLError("Организация не найдена");
|
throw new GraphQLError("Организация не найдена");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.organization.type !== 'SELLER') {
|
if (user.organization.type !== "SELLER") {
|
||||||
throw new GraphQLError("Доступно только для продавцов");
|
throw new GraphQLError("Доступно только для продавцов");
|
||||||
}
|
}
|
||||||
|
|
||||||
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
const wbApiKeyRecord = user.organization.apiKeys?.find(
|
||||||
key => key.marketplace === 'WILDBERRIES' && key.isActive
|
(key) => key.marketplace === "WILDBERRIES" && key.isActive
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!wbApiKeyRecord) {
|
if (!wbApiKeyRecord) {
|
||||||
throw new GraphQLError("WB API ключ не настроен");
|
throw new GraphQLError("WB API ключ не настроен");
|
||||||
}
|
}
|
||||||
@ -5149,21 +5286,21 @@ const wildberriesQueries = {
|
|||||||
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
const wbService = new WildberriesService(wbApiKeyRecord.apiKey);
|
||||||
|
|
||||||
// Преобразуем запросы в нужный формат
|
// Преобразуем запросы в нужный формат
|
||||||
const requests = input.campaigns.map(campaign => {
|
const requests = input.campaigns.map((campaign) => {
|
||||||
if (campaign.dates && campaign.dates.length > 0) {
|
if (campaign.dates && campaign.dates.length > 0) {
|
||||||
return {
|
return {
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
dates: campaign.dates
|
dates: campaign.dates,
|
||||||
};
|
};
|
||||||
} else if (campaign.interval) {
|
} else if (campaign.interval) {
|
||||||
return {
|
return {
|
||||||
id: campaign.id,
|
id: campaign.id,
|
||||||
interval: campaign.interval
|
interval: campaign.interval,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
|
// Если не указаны ни даты, ни интервал, возвращаем данные только за последние сутки
|
||||||
return {
|
return {
|
||||||
id: campaign.id
|
id: campaign.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -5174,13 +5311,16 @@ const wildberriesQueries = {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: campaignStats,
|
data: campaignStats,
|
||||||
message: null
|
message: null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching WB campaign stats:', error);
|
console.error("Error fetching WB campaign stats:", error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: error instanceof Error ? error.message : 'Ошибка получения статистики кампаний',
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Ошибка получения статистики кампаний",
|
||||||
data: [],
|
data: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -34,12 +34,15 @@ export const typeDefs = gql`
|
|||||||
# Расходники организации
|
# Расходники организации
|
||||||
mySupplies: [Supply!]!
|
mySupplies: [Supply!]!
|
||||||
|
|
||||||
|
# Заказы поставок расходников
|
||||||
|
supplyOrders: [SupplyOrder!]!
|
||||||
|
|
||||||
# Логистика организации
|
# Логистика организации
|
||||||
myLogistics: [Logistics!]!
|
myLogistics: [Logistics!]!
|
||||||
|
|
||||||
# Поставки Wildberries
|
# Поставки Wildberries
|
||||||
myWildberriesSupplies: [WildberriesSupply!]!
|
myWildberriesSupplies: [WildberriesSupply!]!
|
||||||
|
|
||||||
# Товары оптовика
|
# Товары оптовика
|
||||||
myProducts: [Product!]!
|
myProducts: [Product!]!
|
||||||
|
|
||||||
@ -68,7 +71,7 @@ export const typeDefs = gql`
|
|||||||
|
|
||||||
# Публичные услуги контрагента (для фулфилмента)
|
# Публичные услуги контрагента (для фулфилмента)
|
||||||
counterpartyServices(organizationId: ID!): [Service!]!
|
counterpartyServices(organizationId: ID!): [Service!]!
|
||||||
|
|
||||||
# Публичные расходники контрагента (для оптовиков)
|
# Публичные расходники контрагента (для оптовиков)
|
||||||
counterpartySupplies(organizationId: ID!): [Supply!]!
|
counterpartySupplies(organizationId: ID!): [Supply!]!
|
||||||
|
|
||||||
@ -82,10 +85,10 @@ export const typeDefs = gql`
|
|||||||
startDate: String
|
startDate: String
|
||||||
endDate: String
|
endDate: String
|
||||||
): WildberriesStatisticsResponse!
|
): WildberriesStatisticsResponse!
|
||||||
|
|
||||||
# Отладка рекламы (временно)
|
# Отладка рекламы (временно)
|
||||||
debugWildberriesAdverts: DebugAdvertsResponse!
|
debugWildberriesAdverts: DebugAdvertsResponse!
|
||||||
|
|
||||||
# Статистика кампаний Wildberries
|
# Статистика кампаний Wildberries
|
||||||
getWildberriesCampaignStats(
|
getWildberriesCampaignStats(
|
||||||
input: WildberriesCampaignStatsInput!
|
input: WildberriesCampaignStatsInput!
|
||||||
@ -203,12 +206,17 @@ export const typeDefs = gql`
|
|||||||
updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse!
|
updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse!
|
||||||
deleteEmployee(id: ID!): Boolean!
|
deleteEmployee(id: ID!): Boolean!
|
||||||
updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean!
|
updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean!
|
||||||
|
|
||||||
# Работа с поставками Wildberries
|
# Работа с поставками Wildberries
|
||||||
createWildberriesSupply(input: CreateWildberriesSupplyInput!): WildberriesSupplyResponse!
|
createWildberriesSupply(
|
||||||
updateWildberriesSupply(id: ID!, input: UpdateWildberriesSupplyInput!): WildberriesSupplyResponse!
|
input: CreateWildberriesSupplyInput!
|
||||||
|
): WildberriesSupplyResponse!
|
||||||
|
updateWildberriesSupply(
|
||||||
|
id: ID!
|
||||||
|
input: UpdateWildberriesSupplyInput!
|
||||||
|
): WildberriesSupplyResponse!
|
||||||
deleteWildberriesSupply(id: ID!): Boolean!
|
deleteWildberriesSupply(id: ID!): Boolean!
|
||||||
|
|
||||||
# Админ мутации
|
# Админ мутации
|
||||||
adminLogin(username: String!, password: String!): AdminAuthResponse!
|
adminLogin(username: String!, password: String!): AdminAuthResponse!
|
||||||
adminLogout: Boolean!
|
adminLogout: Boolean!
|
||||||
@ -537,7 +545,10 @@ export const typeDefs = gql`
|
|||||||
input SupplyOrderInput {
|
input SupplyOrderInput {
|
||||||
partnerId: ID!
|
partnerId: ID!
|
||||||
deliveryDate: DateTime!
|
deliveryDate: DateTime!
|
||||||
|
fulfillmentCenterId: ID # ID фулфилмент-центра для доставки
|
||||||
|
logisticsPartnerId: ID # ID логистической компании
|
||||||
items: [SupplyOrderItemInput!]!
|
items: [SupplyOrderItemInput!]!
|
||||||
|
notes: String # Дополнительные заметки к заказу
|
||||||
}
|
}
|
||||||
|
|
||||||
input SupplyOrderItemInput {
|
input SupplyOrderItemInput {
|
||||||
@ -545,10 +556,19 @@ export const typeDefs = gql`
|
|||||||
quantity: Int!
|
quantity: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SupplyOrderProcessInfo {
|
||||||
|
role: String! # Роль организации в процессе (SELLER, FULFILLMENT, LOGIST)
|
||||||
|
supplier: String! # Название поставщика
|
||||||
|
fulfillmentCenter: ID # ID фулфилмент-центра
|
||||||
|
logistics: ID # ID логистической компании
|
||||||
|
status: String! # Текущий статус заказа
|
||||||
|
}
|
||||||
|
|
||||||
type SupplyOrderResponse {
|
type SupplyOrderResponse {
|
||||||
success: Boolean!
|
success: Boolean!
|
||||||
message: String!
|
message: String!
|
||||||
order: SupplyOrder
|
order: SupplyOrder
|
||||||
|
processInfo: SupplyOrderProcessInfo # Информация о процессе поставки
|
||||||
}
|
}
|
||||||
|
|
||||||
# Типы для логистики
|
# Типы для логистики
|
||||||
|
@ -35,48 +35,15 @@ const authLink = setContext((operation, { headers }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Error Link для обработки ошибок
|
// Error Link для обработки ошибок - минимальная версия
|
||||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
const errorLink = onError(() => {
|
||||||
if (graphQLErrors) {
|
// Пустой обработчик - не делаем ничего
|
||||||
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
// Это предотвращает любые ошибки в error handler
|
||||||
console.error(
|
|
||||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Если токен недействителен, очищаем localStorage и перенаправляем на авторизацию
|
|
||||||
if (extensions?.code === 'UNAUTHENTICATED') {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const isAdminPath = window.location.pathname.startsWith('/admin')
|
|
||||||
|
|
||||||
if (isAdminPath) {
|
|
||||||
// Для админских страниц очищаем админские токены
|
|
||||||
localStorage.removeItem('adminAuthToken')
|
|
||||||
localStorage.removeItem('adminData')
|
|
||||||
if (window.location.pathname !== '/admin') {
|
|
||||||
window.location.href = '/admin'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Для пользовательских страниц очищаем пользовательские токены
|
|
||||||
localStorage.removeItem('authToken')
|
|
||||||
localStorage.removeItem('userData')
|
|
||||||
if (window.location.pathname !== '/') {
|
|
||||||
window.location.href = '/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (networkError) {
|
|
||||||
console.error(`[Network error]: ${networkError}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Создаем Apollo Client
|
// Создаем Apollo Client
|
||||||
export const apolloClient = new ApolloClient({
|
export const apolloClient = new ApolloClient({
|
||||||
link: from([
|
link: from([
|
||||||
errorLink,
|
|
||||||
authLink,
|
authLink,
|
||||||
httpLink,
|
httpLink,
|
||||||
]),
|
]),
|
||||||
|
Reference in New Issue
Block a user