This commit is contained in:
Bivekich
2025-07-29 17:44:42 +03:00
7 changed files with 344 additions and 165 deletions

View File

@ -170,15 +170,17 @@ export function FulfillmentConsumablesOrdersTab() {
// Получаем данные заказов поставок // Получаем данные заказов поставок
const supplyOrders: SupplyOrder[] = data?.supplyOrders || []; const supplyOrders: SupplyOrder[] = data?.supplyOrders || [];
// Фильтруем заказы для фулфилмента (где текущий фулфилмент является получателем) // Фильтруем заказы для фулфилмента (расходники селлеров)
const fulfillmentOrders = supplyOrders.filter((order) => { const fulfillmentOrders = supplyOrders.filter((order) => {
// Показываем только заказы где текущий фулфилмент-центр является получателем // Показываем только заказы где текущий фулфилмент-центр является получателем
const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id; const isRecipient = order.fulfillmentCenter?.id === user?.organization?.id;
// И статус не PENDING и не CANCELLED (одобренные заявки) // НО создатель заказа НЕ мы (т.е. селлер создал заказ для нас)
const isCreatedByOther = order.organization?.id !== user?.organization?.id;
// И статус не PENDING и не CANCELLED (одобренные поставщиком заявки)
const isApproved = const isApproved =
order.status !== "CANCELLED" && order.status !== "PENDING"; order.status !== "CANCELLED" && order.status !== "PENDING";
return isRecipient && isApproved; return isRecipient && isCreatedByOther && isApproved;
}); });
// Генерируем порядковые номера для заказов // Генерируем порядковые номера для заказов

View File

@ -7,12 +7,14 @@ import { Badge } from "@/components/ui/badge";
import { StatsCard } from "../../supplies/ui/stats-card"; import { StatsCard } from "../../supplies/ui/stats-card";
import { StatsGrid } from "../../supplies/ui/stats-grid"; import { StatsGrid } from "../../supplies/ui/stats-grid";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useQuery } from "@apollo/client"; import { useQuery, useMutation } from "@apollo/client";
import { import {
GET_SUPPLY_ORDERS, GET_SUPPLY_ORDERS,
GET_PENDING_SUPPLIES_COUNT, GET_PENDING_SUPPLIES_COUNT,
} from "@/graphql/queries"; } from "@/graphql/queries";
import { UPDATE_SUPPLY_ORDER_STATUS } from "@/graphql/mutations";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { toast } from "sonner";
import { import {
Calendar, Calendar,
Building2, Building2,
@ -25,6 +27,8 @@ import {
ChevronRight, ChevronRight,
Bell, Bell,
AlertTriangle, AlertTriangle,
Truck,
CheckCircle,
} from "lucide-react"; } from "lucide-react";
// Компонент уведомлений о непринятых поставках // Компонент уведомлений о непринятых поставках
@ -36,8 +40,10 @@ function PendingSuppliesAlert() {
}); });
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
const supplyOrdersCount = pendingData?.pendingSuppliesCount?.supplyOrders || 0; const supplyOrdersCount =
const incomingRequestsCount = pendingData?.pendingSuppliesCount?.incomingRequests || 0; pendingData?.pendingSuppliesCount?.supplyOrders || 0;
const incomingRequestsCount =
pendingData?.pendingSuppliesCount?.incomingRequests || 0;
if (pendingCount === 0) return null; if (pendingCount === 0) return null;
@ -52,14 +58,19 @@ function PendingSuppliesAlert() {
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
Требует вашего внимания Требует вашего внимания
</h3> </h3>
<div className="text-orange-100 text-xs mt-1 space-y-1"> <div className="text-orange-100 text-xs mt-1 space-y-1">
{supplyOrdersCount > 0 && ( {supplyOrdersCount > 0 && (
<p> {supplyOrdersCount} поставок требуют вашего действия (подтверждение/получение)</p> <p>
)} {supplyOrdersCount} поставок требуют вашего действия
{incomingRequestsCount > 0 && ( (подтверждение/получение)
<p> {incomingRequestsCount} заявок на партнерство ожидают ответа</p> </p>
)} )}
</div> {incomingRequestsCount > 0 && (
<p>
{incomingRequestsCount} заявок на партнерство ожидают ответа
</p>
)}
</div>
</div> </div>
<div className="text-right"> <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"> <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; totalItems: number;
totalAmount: number; totalAmount: number;
status: string; status: string;
fulfillmentCenterId: string;
organization: {
id: string;
name?: string;
fullName?: string;
type: string;
};
partner: {
id: string;
name?: string;
fullName?: string;
};
items: { items: {
id: string; id: string;
quantity: number; quantity: number;
@ -120,12 +143,8 @@ const getStatusBadge = (status: string) => {
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_PROGRESS: { IN_TRANSIT: {
label: "В работе", label: "В пути",
color: "bg-purple-500/20 text-purple-300 border-purple-500/30",
},
SHIPPED: {
label: "Отправлен",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30", color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
}, },
DELIVERED: { DELIVERED: {
@ -149,6 +168,15 @@ export function FulfillmentDetailedSuppliesTab() {
const { user } = useAuth(); const { user } = useAuth();
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set()); 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, { const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
fetchPolicy: "cache-and-network", // Принудительно проверяем сервер fetchPolicy: "cache-and-network", // Принудительно проверяем сервер
@ -158,11 +186,19 @@ export function FulfillmentDetailedSuppliesTab() {
// Получаем ID текущей организации (фулфилмент-центра) // Получаем ID текущей организации (фулфилмент-центра)
const currentOrganizationId = user?.organization?.id; const currentOrganizationId = user?.organization?.id;
// Фильтруем заказы созданные текущей организацией (наши расходники) // "Наши расходники" = расходники, которые МЫ (фулфилмент-центр) заказали для себя
// Критерии: создатель = мы И получатель = мы (ОБА условия)
const ourSupplyOrders: SupplyOrder[] = (data?.supplyOrders || []).filter( 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 toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders); const newExpanded = new Set(expandedOrders);
if (newExpanded.has(orderId)) { if (newExpanded.has(orderId)) {
@ -173,6 +209,31 @@ export function FulfillmentDetailedSuppliesTab() {
setExpandedOrders(newExpanded); 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) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@ -208,7 +269,7 @@ export function FulfillmentDetailedSuppliesTab() {
<div> <div>
<h2 className="text-xl font-bold text-white mb-1">Наши расходники</h2> <h2 className="text-xl font-bold text-white mb-1">Наши расходники</h2>
<p className="text-white/60 text-sm"> <p className="text-white/60 text-sm">
Управление поставками расходников фулфилмента Поставки расходников, поступающие на склад фулфилмент-центра
</p> </p>
</div> </div>
<Button <Button
@ -225,12 +286,12 @@ export function FulfillmentDetailedSuppliesTab() {
{/* Статистика наших расходников */} {/* Статистика наших расходников */}
<StatsGrid> <StatsGrid>
<StatsCard <StatsCard
title="Наши расходники" title="Наши поставки"
value={ourSupplyOrders.length} value={ourSupplyOrders.length}
icon={Package2} icon={Package2}
iconColor="text-orange-400" iconColor="text-orange-400"
iconBg="bg-orange-500/20" iconBg="bg-orange-500/20"
subtitle="Поставки расходников" subtitle="Заказано нами для склада"
/> />
<StatsCard <StatsCard
@ -244,7 +305,7 @@ export function FulfillmentDetailedSuppliesTab() {
icon={TrendingUp} icon={TrendingUp}
iconColor="text-green-400" iconColor="text-green-400"
iconBg="bg-green-500/20" iconBg="bg-green-500/20"
subtitle="Стоимость заказов" subtitle="Стоимость всех поставок"
/> />
<StatsCard <StatsCard
@ -269,7 +330,7 @@ export function FulfillmentDetailedSuppliesTab() {
icon={Calendar} icon={Calendar}
iconColor="text-purple-400" iconColor="text-purple-400"
iconBg="bg-purple-500/20" iconBg="bg-purple-500/20"
subtitle="Доставленные заказы" subtitle="Доставленные поставки"
/> />
</StatsGrid> </StatsGrid>
@ -279,11 +340,12 @@ export function FulfillmentDetailedSuppliesTab() {
<div className="text-center"> <div className="text-center">
<Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" /> <Wrench className="h-16 w-16 text-white/20 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-white mb-2">
Пока нет заказов расходников Пока нет поставок расходников
</h3> </h3>
<p className="text-white/60"> <p className="text-white/60">
Создайте первый заказ расходников через кнопку &quot;Создать Здесь будут отображаться поставки расходников, поступающие на ваш
поставку&quot; склад. Создайте заказ через кнопку &quot;Создать поставку&quot;
или ожидайте поставки от партнеров.
</p> </p>
</div> </div>
</Card> </Card>
@ -297,9 +359,6 @@ export function FulfillmentDetailedSuppliesTab() {
<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 className="text-left p-4 text-white font-semibold">
План План
</th> </th>
@ -333,11 +392,6 @@ export function FulfillmentDetailedSuppliesTab() {
> >
<td className="p-4"> <td className="p-4">
<div className="flex items-center space-x-2"> <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"> <span className="text-white font-bold text-lg">
#{order.id.slice(-8)} #{order.id.slice(-8)}
</span> </span>
@ -351,11 +405,6 @@ export function FulfillmentDetailedSuppliesTab() {
</span> </span>
</div> </div>
</td> </td>
<td className="p-4">
<span className="text-white/80">
{formatDate(order.createdAt)}
</span>
</td>
<td className="p-4"> <td className="p-4">
<span className="text-white font-semibold"> <span className="text-white font-semibold">
{order.totalItems} {order.totalItems}
@ -384,15 +433,65 @@ export function FulfillmentDetailedSuppliesTab() {
</span> </span>
</div> </div>
</td> </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> </tr>
{/* Развернутая информация о заказе */} {/* Развернутая информация о заказе */}
{isOrderExpanded && ( {isOrderExpanded && (
<tr> <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="bg-white/5 border-t border-white/10">
<div className="p-6"> <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 className="text-white font-semibold mb-4">
Состав заказа: Состав заказа:
</h4> </h4>

View File

@ -39,6 +39,10 @@ export function FulfillmentSuppliesTab() {
}); });
const pendingCount = pendingData?.pendingSuppliesCount?.total || 0; const pendingCount = pendingData?.pendingSuppliesCount?.total || 0;
const ourSupplyOrdersCount =
pendingData?.pendingSuppliesCount?.ourSupplyOrders || 0;
const sellerSupplyOrdersCount =
pendingData?.pendingSuppliesCount?.sellerSupplyOrders || 0;
// Проверяем URL параметр при загрузке // Проверяем URL параметр при загрузке
useEffect(() => { useEffect(() => {
@ -79,12 +83,13 @@ export function FulfillmentSuppliesTab() {
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="detailed-supplies" 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" /> <Building2 className="h-2.5 w-2.5 xl:h-3 xl:w-3" />
<span className="hidden md:inline">Наши расходники</span> <span className="hidden md:inline">Наши расходники</span>
<span className="md:hidden hidden sm:inline">Наши</span> <span className="md:hidden hidden sm:inline">Наши</span>
<span className="sm:hidden">Н</span> <span className="sm:hidden">Н</span>
<NotificationBadge count={ourSupplyOrdersCount} />
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="consumables" value="consumables"
@ -94,7 +99,7 @@ export function FulfillmentSuppliesTab() {
<span className="hidden md:inline">Расходники селлеров</span> <span className="hidden md:inline">Расходники селлеров</span>
<span className="md:hidden hidden sm:inline">Селлеры</span> <span className="md:hidden hidden sm:inline">Селлеры</span>
<span className="sm:hidden">С</span> <span className="sm:hidden">С</span>
<NotificationBadge count={pendingCount} /> <NotificationBadge count={sellerSupplyOrdersCount} />
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="returns" value="returns"

View File

@ -5,6 +5,9 @@ import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { useQuery } from "@apollo/client";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { useAuth } from "@/hooks/useAuth";
import { import {
Wrench, Wrench,
Plus, Plus,
@ -12,52 +15,72 @@ import {
TrendingUp, TrendingUp,
AlertCircle, AlertCircle,
Eye, Eye,
Calendar,
Package2,
Building2,
} from "lucide-react"; } from "lucide-react";
// Мок данные для расходников селлеров // Интерфейс для заказа поставки от селлера
const mockSellerMaterials = [ interface SellerSupplyOrder {
{ id: string;
id: "1", organizationId: string;
materialName: "Пакеты полиэтиленовые 30х40", deliveryDate: string;
seller: "PackStore LLC", createdAt: string;
category: "Расходники", totalItems: number;
quantity: 10000, totalAmount: number;
expectedDate: "2024-01-14", status: string;
status: "planned", fulfillmentCenterId: string;
unitPrice: 2.5, organization: {
totalValue: 25000, id: string;
purpose: "Упаковка мелких товаров", name?: string;
}, fullName?: string;
{ type: string;
id: "2", };
materialName: "Скотч упаковочный прозрачный", partner: {
seller: "Packaging Pro", id: string;
category: "Расходники", name?: string;
quantity: 500, fullName?: string;
expectedDate: "2024-01-11", };
status: "in-transit", items: {
unitPrice: 85, id: string;
totalValue: 42500, quantity: number;
purpose: "Заклейка коробок", price: number;
}, totalPrice: number;
{ product: {
id: "3", name: string;
materialName: "Этикетки штрих-код 58х40", article: string;
seller: "LabelTech", category?: {
category: "Маркировка", name: string;
quantity: 50000, };
expectedDate: "2024-01-09", };
status: "delivered", }[];
unitPrice: 0.8, }
totalValue: 40000,
purpose: "Маркировка товаров",
},
];
export function SellerMaterialsTab() { export function SellerMaterialsTab() {
const { user } = useAuth();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all"); 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) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", { return new Intl.NumberFormat("ru-RU", {
style: "currency", style: "currency",
@ -76,58 +99,83 @@ export function SellerMaterialsTab() {
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
const statusConfig = { const statusConfig = {
planned: { PENDING: {
color: "text-blue-300 border-blue-400/30", color: "text-blue-300 border-blue-400/30",
label: "Запланировано", label: "Ожидает подтверждения",
}, },
"in-transit": { CONFIRMED: {
color: "text-yellow-300 border-yellow-400/30", color: "text-yellow-300 border-yellow-400/30",
label: "Подтверждено",
},
IN_TRANSIT: {
color: "text-orange-300 border-orange-400/30",
label: "В пути", label: "В пути",
}, },
delivered: { DELIVERED: {
color: "text-green-300 border-green-400/30", color: "text-green-300 border-green-400/30",
label: "Доставлено", label: "Доставлено",
}, },
"in-processing": { CANCELLED: {
color: "text-purple-300 border-purple-400/30", color: "text-red-300 border-red-400/30",
label: "Обрабатывается", label: "Отменено",
}, },
}; };
const config = const config =
statusConfig[status as keyof typeof statusConfig] || statusConfig.planned; statusConfig[status as keyof typeof statusConfig] || statusConfig.PENDING;
return <Badge className={`${config.color}`}>{config.label}</Badge>;
return (
<Badge variant="outline" className={`glass-secondary ${config.color}`}>
{config.label}
</Badge>
);
}; };
const filteredMaterials = mockSellerMaterials.filter((material) => { // Фильтрация поставок
const filteredOrders = sellerSupplyOrders.filter((order) => {
const matchesSearch = const matchesSearch =
material.materialName.toLowerCase().includes(searchTerm.toLowerCase()) || order.organization.name
material.seller.toLowerCase().includes(searchTerm.toLowerCase()) || ?.toLowerCase()
material.category.toLowerCase().includes(searchTerm.toLowerCase()); .includes(searchTerm.toLowerCase()) ||
order.organization.fullName
?.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
order.items.some((item) =>
item.product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const matchesStatus = const matchesStatus =
statusFilter === "all" || material.status === statusFilter; statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
const getTotalValue = () => { if (loading) {
return filteredMaterials.reduce( return (
(sum, material) => sum + material.totalValue, <div className="flex items-center justify-center h-64">
0 <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 = () => { const getTotalQuantity = () => {
return filteredMaterials.reduce( return filteredOrders.reduce((sum, order) => sum + order.totalItems, 0);
(sum, material) => sum + material.quantity,
0
);
}; };
return ( return (
@ -143,7 +191,7 @@ export function SellerMaterialsTab() {
<div> <div>
<p className="text-white/60 text-xs">Поставок</p> <p className="text-white/60 text-xs">Поставок</p>
<p className="text-lg font-bold text-white"> <p className="text-lg font-bold text-white">
{filteredMaterials.length} {filteredOrders.length}
</p> </p>
</div> </div>
</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" 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="all">Все статусы</option>
<option value="planned">Запланировано</option> <option value="PENDING">Ожидает подтверждения</option>
<option value="in-transit">В пути</option> <option value="CONFIRMED">Подтверждено</option>
<option value="delivered">Доставлено</option> <option value="IN_TRANSIT">В пути</option>
<option value="in-processing">Обрабатывается</option> <option value="DELIVERED">Доставлено</option>
<option value="CANCELLED">Отменено</option>
</select> </select>
</div> </div>
{/* Список материалов */} {/* Список материалов */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto space-y-3"> {filteredOrders.length === 0 ? (
{filteredMaterials.map((material) => ( <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 <Card
key={material.id} key={order.id}
className="glass-card p-4 hover:bg-white/10 transition-colors" className="glass-card p-4 hover:bg-white/10 transition-colors"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-white font-medium"> <h3 className="text-white font-medium">
{material.materialName} {order.organization.name || order.organization.fullName}
</h3> </h3>
{getStatusBadge(material.status)} {getStatusBadge(order.status)}
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div> <div>
<p className="text-white/60">Селлер</p> <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>
<div> <div>
<p className="text-white/60">Категория</p> <p className="text-white/60">Дата доставки</p>
<p className="text-white">{material.category}</p> <p className="text-white">
{formatDate(order.deliveryDate)}
</p>
</div> </div>
<div> <div>
<p className="text-white/60">Количество</p> <p className="text-white/60">Количество</p>
<p className="text-white font-semibold"> <p className="text-white font-semibold">
{material.quantity.toLocaleString()} шт. {order.totalItems.toLocaleString()} шт.
</p> </p>
</div> </div>
<div> <div>
<p className="text-white/60">Ожидается</p> <p className="text-white/60">Общая стоимость</p>
<p className="text-white"> <p className="text-white font-semibold">
{formatDate(material.expectedDate)} {formatCurrency(order.totalAmount)}
</p> </p>
</div> </div>
</div> </div>
@ -255,15 +321,9 @@ export function SellerMaterialsTab() {
<div className="mt-3 flex items-center justify-between"> <div className="mt-3 flex items-center justify-between">
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<span className="text-white/60"> <span className="text-white/60">
Цена за ед.:{" "} Дата заказа:{" "}
<span className="text-white"> <span className="text-white">
{formatCurrency(material.unitPrice)} {formatDate(order.createdAt)}
</span>
</span>
<span className="text-white/60">
Общая стоимость:{" "}
<span className="text-green-400 font-semibold">
{formatCurrency(material.totalValue)}
</span> </span>
</span> </span>
</div> </div>
@ -271,8 +331,8 @@ export function SellerMaterialsTab() {
<div className="mt-2"> <div className="mt-2">
<p className="text-white/60 text-xs"> <p className="text-white/60 text-xs">
Назначение:{" "} Статус:{" "}
<span className="text-white/80">{material.purpose}</span> <span className="text-white/80">{order.status}</span>
</p> </p>
</div> </div>
</div> </div>
@ -289,7 +349,8 @@ export function SellerMaterialsTab() {
</div> </div>
</Card> </Card>
))} ))}
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

@ -827,7 +827,7 @@ export const GET_WILDBERRIES_CAMPAIGNS_LIST = gql`
} }
} }
} }
` `;
export const GET_EXTERNAL_ADS = gql` export const GET_EXTERNAL_ADS = gql`
query GetExternalAds($dateFrom: String!, $dateTo: String!) { query GetExternalAds($dateFrom: String!, $dateTo: String!) {
@ -948,6 +948,8 @@ export const GET_PENDING_SUPPLIES_COUNT = gql`
query GetPendingSuppliesCount { query GetPendingSuppliesCount {
pendingSuppliesCount { pendingSuppliesCount {
supplyOrders supplyOrders
ourSupplyOrders
sellerSupplyOrders
incomingRequests incomingRequests
total total
} }

View File

@ -790,28 +790,28 @@ export const resolvers = {
} }
// Считаем заказы поставок, требующие действий // Считаем заказы поставок, требующие действий
const pendingSupplyOrders = await prisma.supplyOrder.count({
// Наши расходники (созданные нами для себя) - требуют действий по статусам
const ourSupplyOrders = await prisma.supplyOrder.count({
where: { where: {
OR: [ organizationId: currentUser.organization.id, // Создали мы
// Заказы со статусом PENDING где мы - поставщик (нужно подтвердить) fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
{ status: { in: ["CONFIRMED", "IN_TRANSIT"] }, // Подтверждено или в пути
status: "PENDING",
partnerId: currentUser.organization.id,
},
// Заказы со статусом PENDING где мы - получатель ФФ (нужно подтвердить)
{
status: "PENDING",
fulfillmentCenterId: currentUser.organization.id,
},
// Заказы со статусом IN_TRANSIT где мы - получатель ФФ (нужно подтвердить получение)
{
status: "IN_TRANSIT",
fulfillmentCenterId: currentUser.organization.id,
},
],
}, },
}); });
// Расходники селлеров (созданные другими для нас) - требуют подтверждения получения
const sellerSupplyOrders = await prisma.supplyOrder.count({
where: {
fulfillmentCenterId: currentUser.organization.id, // Получатель - мы
organizationId: { not: currentUser.organization.id }, // Создали НЕ мы
status: "IN_TRANSIT", // В пути - нужно подтвердить получение
},
});
// Общий счетчик поставок
const pendingSupplyOrders = ourSupplyOrders + sellerSupplyOrders;
// Считаем входящие заявки на партнерство со статусом PENDING // Считаем входящие заявки на партнерство со статусом PENDING
const pendingIncomingRequests = await prisma.counterpartyRequest.count({ const pendingIncomingRequests = await prisma.counterpartyRequest.count({
where: { where: {
@ -822,6 +822,8 @@ export const resolvers = {
return { return {
supplyOrders: pendingSupplyOrders, supplyOrders: pendingSupplyOrders,
ourSupplyOrders: ourSupplyOrders, // Наши расходники
sellerSupplyOrders: sellerSupplyOrders, // Расходники селлеров
incomingRequests: pendingIncomingRequests, incomingRequests: pendingIncomingRequests,
total: pendingSupplyOrders + pendingIncomingRequests, total: pendingSupplyOrders + pendingIncomingRequests,
}; };
@ -3284,12 +3286,16 @@ export const resolvers = {
}, },
// Создать заказ поставки расходников // Создать заказ поставки расходников
// Процесс: Селлер → Поставщик → Логистика → Фулфилмент // Два сценария:
// 1. Селлер создает заказ у поставщика расходников // 1. Селлер → Поставщик → Фулфилмент (селлер заказывает для фулфилмент-центра)
// 2. Фулфилмент → Поставщик → Фулфилмент (фулфилмент заказывает для себя)
//
// Процесс: Заказчик → Поставщик → [Логистика] → Фулфилмент
// 1. Заказчик (селлер или фулфилмент) создает заказ у поставщика расходников
// 2. Поставщик получает заказ и готовит товары // 2. Поставщик получает заказ и готовит товары
// 3. Логистика транспортирует товары на склад фулфилмента // 3. Логистика транспортирует товары на склад фулфилмента
// 4. Фулфилмент принимает товары на склад // 4. Фулфилмент принимает товары на склад
// 5. Все участники видят информацию о поставке в своих кабинетах // 5. Расходники создаются в системе фулфилмент-центра
createSupplyOrder: async ( createSupplyOrder: async (
_: unknown, _: unknown,
args: { args: {
@ -3488,6 +3494,7 @@ export const resolvers = {
}); });
// Создаем расходники на основе заказанных товаров // Создаем расходники на основе заказанных товаров
// Расходники создаются в организации получателя (фулфилмент-центре)
const suppliesData = args.input.items.map((item) => { const suppliesData = args.input.items.map((item) => {
const product = products.find((p) => p.id === item.productId)!; const product = products.find((p) => p.id === item.productId)!;
const productWithCategory = supplyOrder.items.find( const productWithCategory = supplyOrder.items.find(
@ -3506,7 +3513,8 @@ export const resolvers = {
supplier: partner.name || partner.fullName || "Не указан", supplier: partner.name || partner.fullName || "Не указан",
minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток minStock: Math.round(item.quantity * 0.1), // 10% от заказанного как минимальный остаток
currentStock: 0, // Пока товар не пришел currentStock: 0, // Пока товар не пришел
organizationId: currentUser.organization!.id, // Расходники создаются в организации получателя (фулфилмент-центре)
organizationId: fulfillmentCenterId || currentUser.organization!.id,
}; };
}); });

View File

@ -602,6 +602,8 @@ export const typeDefs = gql`
type PendingSuppliesCount { type PendingSuppliesCount {
supplyOrders: Int! supplyOrders: Int!
ourSupplyOrders: Int! # Наши расходники
sellerSupplyOrders: Int! # Расходники селлеров
incomingRequests: Int! incomingRequests: Int!
total: Int! total: Int!
} }