Обновлены компоненты для управления поставками: добавлены фильтрация и отображение заказов расходников селлеров и наших расходников. Реализованы новые GraphQL запросы и резолверы для получения статистики по ожидающим поставкам. Оптимизирована логика отображения уведомлений и статусов заказов.
This commit is contained in:
@ -170,15 +170,17 @@ export function FulfillmentConsumablesOrdersTab() {
|
||||
// Получаем данные заказов поставок
|
||||
const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
|
||||
|
||||
// Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем)
|
||||
// Фильтруем заказы для фулфилмента (расходники селлеров)
|
||||
const fulfillmentOrders = supplyOrders.filter((order) => {
|
||||
// Показываем только заказы где текущий фулфилмент-центр является получателем
|
||||
const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id;
|
||||
// И статус не PENDING и не CANCELLED (одобренные заявки)
|
||||
// НО создатель заказа НЕ мы (т.е. селлер создал заказ для нас)
|
||||
const isCreatedByOther = order.organization?.id !== user?.organization?.id;
|
||||
// И статус не PENDING и не CANCELLED (одобренные поставщиком заявки)
|
||||
const isApproved =
|
||||
order.status !== "CANCELLED" && order.status !== "PENDING";
|
||||
|
||||
return isRecipient && isApproved;
|
||||
return isRecipient && isCreatedByOther && isApproved;
|
||||
});
|
||||
|
||||
// Генерируем порядковые номера для заказов
|
||||
|
@ -7,12 +7,14 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { StatsCard } from "../../supplies/ui/stats-card";
|
||||
import { StatsGrid } from "../../supplies/ui/stats-grid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import {
|
||||
GET_SUPPLY_ORDERS,
|
||||
GET_PENDING_SUPPLIES_COUNT,
|
||||
} from "@/graphql/queries";
|
||||
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Calendar,
|
||||
Building2,
|
||||
@ -25,6 +27,8 @@ import {
|
||||
ChevronRight,
|
||||
Bell,
|
||||
AlertTriangle,
|
||||
Truck,
|
||||
CheckCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
// Компонент уведомлений о непринятых поставках
|
||||
@ -36,8 +40,10 @@ function PendingSuppliesAlert() {
|
||||
});
|
||||
|
||||
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
|
||||
const supplyOrdersCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0;
|
||||
const incomingRequestsCount = pendingData?.pendingSuppliesCount?.incomingRequests || 0;
|
||||
const supplyOrdersCount =
|
||||
pendingData?.pendingSuppliesCount?.supplyOrders || 0;
|
||||
const incomingRequestsCount =
|
||||
pendingData?.pendingSuppliesCount?.incomingRequests || 0;
|
||||
|
||||
if (pendingCount === 0) return null;
|
||||
|
||||
@ -52,14 +58,19 @@ function PendingSuppliesAlert() {
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Требует вашего внимания
|
||||
</h3>
|
||||
<div className="text-orange-100 text-xs mt-1 space-y-1">
|
||||
{supplyOrdersCount > 0 && (
|
||||
<p>• {supplyOrdersCount} поставок требуют вашего действия (подтверждение/получение)</p>
|
||||
)}
|
||||
{incomingRequestsCount > 0 && (
|
||||
<p>• {incomingRequestsCount} заявок на партнерство ожидают ответа</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-orange-100 text-xs mt-1 space-y-1">
|
||||
{supplyOrdersCount > 0 && (
|
||||
<p>
|
||||
• {supplyOrdersCount} поставок требуют вашего действия
|
||||
(подтверждение/получение)
|
||||
</p>
|
||||
)}
|
||||
{incomingRequestsCount > 0 && (
|
||||
<p>
|
||||
• {incomingRequestsCount} заявок на партнерство ожидают ответа
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="bg-orange-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center">
|
||||
@ -80,6 +91,18 @@ interface SupplyOrder {
|
||||
totalItems: number;
|
||||
totalAmount: number;
|
||||
status: string;
|
||||
fulfillmentCenterId: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
partner: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
};
|
||||
items: {
|
||||
id: string;
|
||||
quantity: number;
|
||||
@ -120,12 +143,8 @@ const getStatusBadge = (status: string) => {
|
||||
label: "Подтверждён",
|
||||
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
|
||||
},
|
||||
IN_PROGRESS: {
|
||||
label: "В работе",
|
||||
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
|
||||
},
|
||||
SHIPPED: {
|
||||
label: "Отправлен",
|
||||
IN_TRANSIT: {
|
||||
label: "В пути",
|
||||
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
|
||||
},
|
||||
DELIVERED: {
|
||||
@ -149,6 +168,15 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
const { user } = useAuth();
|
||||
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
|
||||
|
||||
// Мутация для обновления статуса заказа
|
||||
const [updateSupplyOrderStatus] = useMutation(UPDATE_SUPPLY_ORDER_STATUS, {
|
||||
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
|
||||
onError: (error) => {
|
||||
console.error("Error updating supply order status:", error);
|
||||
toast.error("Ошибка при обновлении статуса заказа");
|
||||
},
|
||||
});
|
||||
|
||||
// Загружаем реальные данные заказов расходников
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network", // Принудительно проверяем сервер
|
||||
@ -158,11 +186,19 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
// Получаем ID текущей организации (фулфилмент-центра)
|
||||
const currentOrganizationId = user?.organization?.id;
|
||||
|
||||
// Фильтруем заказы созданные текущей организацией (наши расходники)
|
||||
// "Наши расходники" = расходники, которые МЫ (фулфилмент-центр) заказали для себя
|
||||
// Критерии: создатель = мы И получатель = мы (ОБА условия)
|
||||
const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
|
||||
(order: SupplyOrder) => order.organizationId === currentOrganizationId
|
||||
(order: SupplyOrder) => {
|
||||
return (
|
||||
order.organizationId === currentOrganizationId && // Создали мы
|
||||
order.fulfillmentCenterId === currentOrganizationId // Получатель - мы
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Убираем разделение на createdByUs и createdForUs, так как здесь только наши поставки
|
||||
|
||||
const toggleOrderExpansion = (orderId: string) => {
|
||||
const newExpanded = new Set(expandedOrders);
|
||||
if (newExpanded.has(orderId)) {
|
||||
@ -173,6 +209,31 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
setExpandedOrders(newExpanded);
|
||||
};
|
||||
|
||||
// Функция для обновления статуса заказа
|
||||
const handleStatusUpdate = async (orderId: string, newStatus: string) => {
|
||||
try {
|
||||
await updateSupplyOrderStatus({
|
||||
variables: {
|
||||
id: orderId,
|
||||
status: newStatus,
|
||||
},
|
||||
});
|
||||
toast.success("Статус заказа обновлен");
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Проверяем, можно ли отметить как доставленный
|
||||
const canMarkAsDelivered = (status: string) => {
|
||||
return status === "IN_TRANSIT";
|
||||
};
|
||||
|
||||
// Проверяем, можно ли отметить как в пути
|
||||
const canMarkAsInTransit = (status: string) => {
|
||||
return status === "CONFIRMED";
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
@ -208,7 +269,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-1">Наши расходники</h2>
|
||||
<p className="text-white/60 text-sm">
|
||||
Управление поставками расходников фулфилмента
|
||||
Поставки расходников, поступающие на склад фулфилмент-центра
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -225,12 +286,12 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
{/* Статистика наших расходников */}
|
||||
<StatsGrid>
|
||||
<StatsCard
|
||||
title="Наши расходники"
|
||||
title="Наши поставки"
|
||||
value={ourSupplyOrders.length}
|
||||
icon={Package2}
|
||||
iconColor="text-orange-400"
|
||||
iconBg="bg-orange-500/20"
|
||||
subtitle="Поставки расходников"
|
||||
subtitle="Заказано нами для склада"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
@ -244,7 +305,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
icon={TrendingUp}
|
||||
iconColor="text-green-400"
|
||||
iconBg="bg-green-500/20"
|
||||
subtitle="Стоимость заказов"
|
||||
subtitle="Стоимость всех поставок"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
@ -269,7 +330,7 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
icon={Calendar}
|
||||
iconColor="text-purple-400"
|
||||
iconBg="bg-purple-500/20"
|
||||
subtitle="Доставленные заказы"
|
||||
subtitle="Доставленные поставки"
|
||||
/>
|
||||
</StatsGrid>
|
||||
|
||||
@ -279,11 +340,12 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
<div className="text-center">
|
||||
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Пока нет заказов расходников
|
||||
Пока нет поставок расходников
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Создайте первый заказ расходников через кнопку "Создать
|
||||
поставку"
|
||||
Здесь будут отображаться поставки расходников, поступающие на ваш
|
||||
склад. Создайте заказ через кнопку "Создать поставку"
|
||||
или ожидайте поставки от партнеров.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@ -297,9 +359,6 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
<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>
|
||||
@ -333,11 +392,6 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isOrderExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-white/60" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-white/60" />
|
||||
)}
|
||||
<span className="text-white font-bold text-lg">
|
||||
#{order.id.slice(-8)}
|
||||
</span>
|
||||
@ -351,11 +405,6 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-white/80">
|
||||
{formatDate(order.createdAt)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-white font-semibold">
|
||||
{order.totalItems}
|
||||
@ -384,15 +433,65 @@ export function FulfillmentDetailedSuppliesTab() {
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(order.status)}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusBadge(order.status)}
|
||||
|
||||
{/* Кнопка "В пути" для подтвержденных заказов */}
|
||||
{canMarkAsInTransit(order.status) && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStatusUpdate(order.id, "IN_TRANSIT");
|
||||
}}
|
||||
className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-300 border border-yellow-500/30 text-xs px-3 py-1 h-7"
|
||||
>
|
||||
<Truck className="h-3 w-3 mr-1" />В пути
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Кнопка "Получено" для заказов в пути */}
|
||||
{canMarkAsDelivered(order.status) && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStatusUpdate(order.id, "DELIVERED");
|
||||
}}
|
||||
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30 text-xs px-3 py-1 h-7"
|
||||
>
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Получено
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Развернутая информация о заказе */}
|
||||
{isOrderExpanded && (
|
||||
<tr>
|
||||
<td colSpan={9} className="p-0">
|
||||
<td colSpan={8} className="p-0">
|
||||
<div className="bg-white/5 border-t border-white/10">
|
||||
<div className="p-6">
|
||||
<div className="mb-4 space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Calendar className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white/80 text-sm">
|
||||
Дата создания:{" "}
|
||||
{formatDate(order.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Package2 className="h-4 w-4 text-white/40" />
|
||||
<span className="text-white/80 text-sm">
|
||||
Поставщик:{" "}
|
||||
{order.partner.name ||
|
||||
order.partner.fullName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-white font-semibold mb-4">
|
||||
Состав заказа:
|
||||
</h4>
|
||||
|
@ -39,6 +39,10 @@ export function FulfillmentSuppliesTab() {
|
||||
});
|
||||
|
||||
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
|
||||
const ourSupplyOrdersCount =
|
||||
pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0;
|
||||
const sellerSupplyOrdersCount =
|
||||
pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0;
|
||||
|
||||
// Проверяем URL параметр при загрузке
|
||||
useEffect(() => {
|
||||
@ -79,12 +83,13 @@ export function FulfillmentSuppliesTab() {
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="detailed-supplies"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs"
|
||||
className="data-[state=active]:bg-white/20 data-[state=active]:text-white text-white/70 flex items-center gap-1 text-[10px] xl:text-xs relative"
|
||||
>
|
||||
<Building2 className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
|
||||
<span className="hidden md:inline">Наши расходники</span>
|
||||
<span className="md:hidden hidden sm:inline">Наши</span>
|
||||
<span className="sm:hidden">Н</span>
|
||||
<NotificationBadge count={ourSupplyOrdersCount} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="consumables"
|
||||
@ -94,7 +99,7 @@ export function FulfillmentSuppliesTab() {
|
||||
<span className="hidden md:inline">Расходники селлеров</span>
|
||||
<span className="md:hidden hidden sm:inline">Селлеры</span>
|
||||
<span className="sm:hidden">С</span>
|
||||
<NotificationBadge count={pendingCount} />
|
||||
<NotificationBadge count={sellerSupplyOrdersCount} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="returns"
|
||||
|
@ -5,6 +5,9 @@ import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import {
|
||||
Wrench,
|
||||
Plus,
|
||||
@ -12,52 +15,72 @@ import {
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
Calendar,
|
||||
Package2,
|
||||
Building2,
|
||||
} from "lucide-react";
|
||||
|
||||
// Мок данные для расходников селлеров
|
||||
const mockSellerMaterials = [
|
||||
{
|
||||
id: "1",
|
||||
materialName: "Пакеты полиэтиленовые 30х40",
|
||||
seller: "PackStore LLC",
|
||||
category: "Расходники",
|
||||
quantity: 10000,
|
||||
expectedDate: "2024-01-14",
|
||||
status: "planned",
|
||||
unitPrice: 2.5,
|
||||
totalValue: 25000,
|
||||
purpose: "Упаковка мелких товаров",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
materialName: "Скотч упаковочный прозрачный",
|
||||
seller: "Packaging Pro",
|
||||
category: "Расходники",
|
||||
quantity: 500,
|
||||
expectedDate: "2024-01-11",
|
||||
status: "in-transit",
|
||||
unitPrice: 85,
|
||||
totalValue: 42500,
|
||||
purpose: "Заклейка коробок",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
materialName: "Этикетки штрих-код 58х40",
|
||||
seller: "LabelTech",
|
||||
category: "Маркировка",
|
||||
quantity: 50000,
|
||||
expectedDate: "2024-01-09",
|
||||
status: "delivered",
|
||||
unitPrice: 0.8,
|
||||
totalValue: 40000,
|
||||
purpose: "Маркировка товаров",
|
||||
},
|
||||
];
|
||||
// Интерфейс для заказа поставки от селлера
|
||||
interface SellerSupplyOrder {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
deliveryDate: string;
|
||||
createdAt: string;
|
||||
totalItems: number;
|
||||
totalAmount: number;
|
||||
status: string;
|
||||
fulfillmentCenterId: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
type: string;
|
||||
};
|
||||
partner: {
|
||||
id: string;
|
||||
name?: string;
|
||||
fullName?: string;
|
||||
};
|
||||
items: {
|
||||
id: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
totalPrice: number;
|
||||
product: {
|
||||
name: string;
|
||||
article: string;
|
||||
category?: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export function SellerMaterialsTab() {
|
||||
const { user } = useAuth();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
// Загружаем реальные данные заказов поставок
|
||||
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
|
||||
fetchPolicy: "cache-and-network",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
// Получаем ID текущей организации (фулфилмент-центра)
|
||||
const currentOrganizationId = user?.organization?.id;
|
||||
|
||||
// "Расходники селлеров" = расходники, которые СЕЛЛЕРЫ заказали для доставки на наш склад
|
||||
// Критерии: создатель != мы И получатель = мы
|
||||
const sellerSupplyOrders: SellerSupplyOrder[] = (
|
||||
data?.supplyOrders || []
|
||||
).filter((order: SellerSupplyOrder) => {
|
||||
return (
|
||||
order.organizationId !== currentOrganizationId && // Создали НЕ мы (селлер)
|
||||
order.fulfillmentCenterId === currentOrganizationId // Получатель - мы
|
||||
);
|
||||
});
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
@ -76,58 +99,83 @@ export function SellerMaterialsTab() {
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
planned: {
|
||||
PENDING: {
|
||||
color: "text-blue-300 border-blue-400/30",
|
||||
label: "Запланировано",
|
||||
label: "Ожидает подтверждения",
|
||||
},
|
||||
"in-transit": {
|
||||
CONFIRMED: {
|
||||
color: "text-yellow-300 border-yellow-400/30",
|
||||
label: "Подтверждено",
|
||||
},
|
||||
IN_TRANSIT: {
|
||||
color: "text-orange-300 border-orange-400/30",
|
||||
label: "В пути",
|
||||
},
|
||||
delivered: {
|
||||
DELIVERED: {
|
||||
color: "text-green-300 border-green-400/30",
|
||||
label: "Доставлено",
|
||||
},
|
||||
"in-processing": {
|
||||
color: "text-purple-300 border-purple-400/30",
|
||||
label: "Обрабатывается",
|
||||
CANCELLED: {
|
||||
color: "text-red-300 border-red-400/30",
|
||||
label: "Отменено",
|
||||
},
|
||||
};
|
||||
|
||||
const config =
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned;
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING;
|
||||
return <Badge className={`${config.color}`}>{config.label}</Badge>;
|
||||
};
|
||||
|
||||
const filteredMaterials = mockSellerMaterials.filter((material) => {
|
||||
// Фильтрация поставок
|
||||
const filteredOrders = sellerSupplyOrders.filter((order) => {
|
||||
const matchesSearch =
|
||||
material.materialName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
material.seller.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
material.category.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
order.organization.name
|
||||
?.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()) ||
|
||||
order.organization.fullName
|
||||
?.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()) ||
|
||||
order.items.some((item) =>
|
||||
item.product.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" || material.status === statusFilter;
|
||||
statusFilter === "all" || order.status === statusFilter;
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredMaterials.reduce(
|
||||
(sum, material) => sum + material.totalValue,
|
||||
0
|
||||
if (loading) {
|
||||
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/60">
|
||||
Загрузка расходников селлеров...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-red-400 font-medium">
|
||||
Ошибка загрузки расходников селлеров
|
||||
</p>
|
||||
<p className="text-white/60 text-sm mt-2">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getTotalValue = () => {
|
||||
return filteredOrders.reduce((sum, order) => sum + order.totalAmount, 0);
|
||||
};
|
||||
|
||||
const getTotalQuantity = () => {
|
||||
return filteredMaterials.reduce(
|
||||
(sum, material) => sum + material.quantity,
|
||||
0
|
||||
);
|
||||
return filteredOrders.reduce((sum, order) => sum + order.totalItems, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -143,7 +191,7 @@ export function SellerMaterialsTab() {
|
||||
<div>
|
||||
<p className="text-white/60 text-xs">Поставок</p>
|
||||
<p className="text-lg font-bold text-white">
|
||||
{filteredMaterials.length}
|
||||
{filteredOrders.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -205,49 +253,67 @@ export function SellerMaterialsTab() {
|
||||
className="glass-input text-white text-sm px-3 py-2 rounded-lg bg-white/5 border border-white/10"
|
||||
>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="planned">Запланировано</option>
|
||||
<option value="in-transit">В пути</option>
|
||||
<option value="delivered">Доставлено</option>
|
||||
<option value="in-processing">Обрабатывается</option>
|
||||
<option value="PENDING">Ожидает подтверждения</option>
|
||||
<option value="CONFIRMED">Подтверждено</option>
|
||||
<option value="IN_TRANSIT">В пути</option>
|
||||
<option value="DELIVERED">Доставлено</option>
|
||||
<option value="CANCELLED">Отменено</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Список материалов */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredMaterials.map((material) => (
|
||||
{filteredOrders.length === 0 ? (
|
||||
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
|
||||
<div className="text-center">
|
||||
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Нет поставок от селлеров
|
||||
</h3>
|
||||
<p className="text-white/60">
|
||||
Здесь будут отображаться расходники, которые селлеры заказывают для доставки на ваш склад.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto space-y-3">
|
||||
{filteredOrders.map((order) => (
|
||||
<Card
|
||||
key={material.id}
|
||||
key={order.id}
|
||||
className="glass-card p-4 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-white font-medium">
|
||||
{material.materialName}
|
||||
{order.organization.name || order.organization.fullName}
|
||||
</h3>
|
||||
{getStatusBadge(material.status)}
|
||||
{getStatusBadge(order.status)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-white/60">Селлер</p>
|
||||
<p className="text-white">{material.seller}</p>
|
||||
<p className="text-white">
|
||||
{order.organization.name || order.organization.fullName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Категория</p>
|
||||
<p className="text-white">{material.category}</p>
|
||||
<p className="text-white/60">Дата доставки</p>
|
||||
<p className="text-white">
|
||||
{formatDate(order.deliveryDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Количество</p>
|
||||
<p className="text-white font-semibold">
|
||||
{material.quantity.toLocaleString()} шт.
|
||||
{order.totalItems.toLocaleString()} шт.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60">Ожидается</p>
|
||||
<p className="text-white">
|
||||
{formatDate(material.expectedDate)}
|
||||
<p className="text-white/60">Общая стоимость</p>
|
||||
<p className="text-white font-semibold">
|
||||
{formatCurrency(order.totalAmount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -255,15 +321,9 @@ export function SellerMaterialsTab() {
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-white/60">
|
||||
Цена за ед.:{" "}
|
||||
Дата заказа:{" "}
|
||||
<span className="text-white">
|
||||
{formatCurrency(material.unitPrice)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-white/60">
|
||||
Общая стоимость:{" "}
|
||||
<span className="text-green-400 font-semibold">
|
||||
{formatCurrency(material.totalValue)}
|
||||
{formatDate(order.createdAt)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@ -271,8 +331,8 @@ export function SellerMaterialsTab() {
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-white/60 text-xs">
|
||||
Назначение:{" "}
|
||||
<span className="text-white/80">{material.purpose}</span>
|
||||
Статус:{" "}
|
||||
<span className="text-white/80">{order.status}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -289,7 +349,8 @@ export function SellerMaterialsTab() {
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -827,7 +827,7 @@ export const GET_WILDBERRIES_CAMPAIGNS_LIST = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
// Админ запросы
|
||||
export const ADMIN_ME = gql`
|
||||
@ -927,6 +927,8 @@ export const GET_PENDING_SUPPLIES_COUNT = gql`
|
||||
query GetPendingSuppliesCount {
|
||||
pendingSuppliesCount {
|
||||
supplyOrders
|
||||
ourSupplyOrders
|
||||
sellerSupplyOrders
|
||||
incomingRequests
|
||||
total
|
||||
}
|
||||
|
@ -790,28 +790,28 @@ export const resolvers = {
|
||||
}
|
||||
|
||||
// Считаем заказы поставок, требующие действий
|
||||
const pendingSupplyOrders = await prisma.supplyOrder.count({
|
||||
|
||||
// Наши расходники (созданные нами для себя) - требуют действий по статусам
|
||||
const ourSupplyOrders = await prisma.supplyOrder.count({
|
||||
where: {
|
||||
OR: [
|
||||
// Заказы со статусом PENDING где мы - поставщик (нужно подтвердить)
|
||||
{
|
||||
status: "PENDING",
|
||||
partnerId: currentUser.organization.id,
|
||||
},
|
||||
// Заказы со статусом PENDING где мы - получатель ФФ (нужно подтвердить)
|
||||
{
|
||||
status: "PENDING",
|
||||
fulfillmentCenterId: currentUser.organization.id,
|
||||
},
|
||||
// Заказы со статусом IN_TRANSIT где мы - получатель ФФ (нужно подтвердить получение)
|
||||
{
|
||||
status: "IN_TRANSIT",
|
||||
fulfillmentCenterId: currentUser.organization.id,
|
||||
},
|
||||
],
|
||||
organizationId: currentUser.organization.id, // Создали мы
|
||||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||||
status: { in: ["CONFIRMED", "IN_TRANSIT"] }, // Подтверждено или в пути
|
||||
},
|
||||
});
|
||||
|
||||
// Расходники селлеров (созданные другими для нас) - требуют подтверждения получения
|
||||
const sellerSupplyOrders = await prisma.supplyOrder.count({
|
||||
where: {
|
||||
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
|
||||
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
|
||||
status: "IN_TRANSIT", // В пути - нужно подтвердить получение
|
||||
},
|
||||
});
|
||||
|
||||
// Общий счетчик поставок
|
||||
const pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders;
|
||||
|
||||
// Считаем входящие заявки на партнерство со статусом PENDING
|
||||
const pendingIncomingRequests = await prisma.counterpartyRequest.count({
|
||||
where: {
|
||||
@ -822,6 +822,8 @@ export const resolvers = {
|
||||
|
||||
return {
|
||||
supplyOrders: pendingSupplyOrders,
|
||||
ourSupplyOrders: ourSupplyOrders, // Наши расходники
|
||||
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
|
||||
incomingRequests: pendingIncomingRequests,
|
||||
total: pendingSupplyOrders + pendingIncomingRequests,
|
||||
};
|
||||
@ -3284,12 +3286,16 @@ export const resolvers = {
|
||||
},
|
||||
|
||||
// Создать заказ поставки расходников
|
||||
// Процесс: Селлер → Поставщик → Логистика → Фулфилмент
|
||||
// 1. Селлер создает заказ у поставщика расходников
|
||||
// Два сценария:
|
||||
// 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра)
|
||||
// 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя)
|
||||
//
|
||||
// Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент
|
||||
// 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников
|
||||
// 2. Поставщик получает заказ и готовит товары
|
||||
// 3. Логистика транспортирует товары на склад фулфилмента
|
||||
// 4. Фулфилмент принимает товары на склад
|
||||
// 5. Все участники видят информацию о поставке в своих кабинетах
|
||||
// 5. Расходники создаются в системе фулфилмент-центра
|
||||
createSupplyOrder: async (
|
||||
_: unknown,
|
||||
args: {
|
||||
@ -3488,6 +3494,7 @@ export const resolvers = {
|
||||
});
|
||||
|
||||
// Создаем расходники на основе заказанных товаров
|
||||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||||
const suppliesData = args.input.items.map((item) => {
|
||||
const product = products.find((p) => p.id === item.productId)!;
|
||||
const productWithCategory = supplyOrder.items.find(
|
||||
@ -3506,7 +3513,8 @@ export const resolvers = {
|
||||
supplier: partner.name || partner.fullName || "Не указан",
|
||||
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
|
||||
currentStock: 0, // Пока товар не пришел
|
||||
organizationId: currentUser.organization!.id,
|
||||
// Расходники создаются в организации получателя (фулфилмент-центре)
|
||||
organizationId: fulfillmentCenterId || currentUser.organization!.id,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -590,6 +590,8 @@ export const typeDefs = gql`
|
||||
|
||||
type PendingSuppliesCount {
|
||||
supplyOrders: Int!
|
||||
ourSupplyOrders: Int! # Наши расходники
|
||||
sellerSupplyOrders: Int! # Расходники селлеров
|
||||
incomingRequests: Int!
|
||||
total: Int!
|
||||
}
|
||||
|
Reference in New Issue
Block a user