Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.

This commit is contained in:
Veronika Smirnova
2025-08-03 17:04:29 +03:00
parent a33adda9d7
commit 8407ca397c
34 changed files with 5382 additions and 1795 deletions

View File

@ -0,0 +1,601 @@
"use client";
import { useState } from "react";
import { useMutation } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import {
SUPPLIER_APPROVE_ORDER,
SUPPLIER_REJECT_ORDER,
SUPPLIER_SHIP_ORDER,
} from "@/graphql/mutations";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { toast } from "sonner";
import {
Calendar,
Package,
Truck,
User,
CheckCircle,
Clock,
XCircle,
MapPin,
Phone,
Mail,
Building,
Hash,
ChevronDown,
ChevronUp,
MessageCircle,
Loader2,
} from "lucide-react";
interface SupplierOrderCardProps {
order: {
id: string;
organizationId: string;
partnerId: string;
deliveryDate: string;
status:
| "PENDING"
| "SUPPLIER_APPROVED"
| "CONFIRMED"
| "LOGISTICS_CONFIRMED"
| "SHIPPED"
| "IN_TRANSIT"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
organization: {
id: string;
name?: string;
fullName?: string;
type: string;
inn?: string;
};
fulfillmentCenter?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
logisticsPartner?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
items: Array<{
id: string;
quantity: number;
price: number;
totalPrice: number;
product: {
id: string;
name: string;
article: string;
description?: string;
category?: {
id: string;
name: string;
};
};
}>;
};
}
export function SupplierOrderCard({ order }: SupplierOrderCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectReason, setRejectReason] = useState("");
// Мутации для действий поставщика
const [supplierApproveOrder, { loading: approving }] = useMutation(
SUPPLIER_APPROVE_ORDER,
{
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierApproveOrder.success) {
toast.success(data.supplierApproveOrder.message);
} else {
toast.error(data.supplierApproveOrder.message);
}
},
onError: (error) => {
console.error("Error approving order:", error);
toast.error("Ошибка при одобрении заказа");
},
}
);
const [supplierRejectOrder, { loading: rejecting }] = useMutation(
SUPPLIER_REJECT_ORDER,
{
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierRejectOrder.success) {
toast.success(data.supplierRejectOrder.message);
} else {
toast.error(data.supplierRejectOrder.message);
}
setShowRejectModal(false);
setRejectReason("");
},
onError: (error) => {
console.error("Error rejecting order:", error);
toast.error("Ошибка при отклонении заказа");
},
}
);
const [supplierShipOrder, { loading: shipping }] = useMutation(
SUPPLIER_SHIP_ORDER,
{
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierShipOrder.success) {
toast.success(data.supplierShipOrder.message);
} else {
toast.error(data.supplierShipOrder.message);
}
},
onError: (error) => {
console.error("Error shipping order:", error);
toast.error("Ошибка при отправке заказа");
},
}
);
const handleApproveOrder = async () => {
try {
await supplierApproveOrder({
variables: { id: order.id },
});
} catch (error) {
console.error("Error in handleApproveOrder:", error);
}
};
const handleRejectOrder = async () => {
if (!rejectReason.trim()) {
toast.error("Укажите причину отклонения заявки");
return;
}
try {
await supplierRejectOrder({
variables: {
id: order.id,
reason: rejectReason,
},
});
} catch (error) {
console.error("Error in handleRejectOrder:", error);
}
};
const handleShipOrder = async () => {
try {
await supplierShipOrder({
variables: { id: order.id },
});
} catch (error) {
console.error("Error in handleShipOrder:", error);
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case "PENDING":
return (
<Badge className="bg-yellow-500/20 text-yellow-300 border-yellow-400/30">
🟡 ОЖИДАЕТ
</Badge>
);
case "SUPPLIER_APPROVED":
return (
<Badge className="bg-green-500/20 text-green-300 border-green-400/30">
🟢 ОДОБРЕНО
</Badge>
);
case "CONFIRMED":
case "LOGISTICS_CONFIRMED":
return (
<Badge className="bg-blue-500/20 text-blue-300 border-blue-400/30">
🔵 В РАБОТЕ
</Badge>
);
case "SHIPPED":
case "IN_TRANSIT":
return (
<Badge className="bg-orange-500/20 text-orange-300 border-orange-400/30">
🟠 В ПУТИ
</Badge>
);
case "DELIVERED":
return (
<Badge className="bg-emerald-500/20 text-emerald-300 border-emerald-400/30">
ДОСТАВЛЕНО
</Badge>
);
default:
return (
<Badge className="bg-white/20 text-white/70 border-white/30">
{status}
</Badge>
);
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const getInitials = (name: string) => {
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
const calculateVolume = () => {
// Примерный расчет объема - можно улучшить на основе реальных данных о товарах
return (order.totalItems * 0.02).toFixed(1); // 0.02 м³ на единицу товара
};
return (
<>
<Card className="glass-card border-white/10 hover:border-white/20 transition-all">
{/* Основная информация - структура согласно правилам */}
<div className="p-4">
{/* Шапка заявки */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Hash className="h-4 w-4 text-white/60" />
<span className="text-white font-semibold">
СФ-{order.id.slice(-8)}
</span>
</div>
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-blue-400" />
<span className="text-white/70 text-sm">
{formatDate(order.createdAt)}
</span>
</div>
{getStatusBadge(order.status)}
</div>
</div>
{/* Информация об участниках */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{/* Заказчик */}
<div>
<div className="flex items-center space-x-2 mb-2">
<User className="h-4 w-4 text-white/60" />
<span className="text-white/60 text-sm">Заказчик:</span>
</div>
<div className="flex items-center space-x-3">
<Avatar className="w-8 h-8">
<AvatarFallback className="bg-blue-500 text-white text-sm">
{getInitials(
order.organization.name ||
order.organization.fullName ||
"ОРГ"
)}
</AvatarFallback>
</Avatar>
<div>
<p className="text-white font-medium text-sm">
{order.organization.name || order.organization.fullName}
</p>
{order.organization.inn && (
<p className="text-white/60 text-xs">
ИНН: {order.organization.inn}
</p>
)}
</div>
</div>
</div>
{/* Фулфилмент */}
{order.fulfillmentCenter && (
<div>
<div className="flex items-center space-x-2 mb-2">
<Building className="h-4 w-4 text-white/60" />
<span className="text-white/60 text-sm">Фулфилмент:</span>
</div>
<p className="text-white font-medium text-sm">
{order.fulfillmentCenter.name ||
order.fulfillmentCenter.fullName}
</p>
</div>
)}
{/* Логистика */}
{order.logisticsPartner && (
<div>
<div className="flex items-center space-x-2 mb-2">
<Truck className="h-4 w-4 text-white/60" />
<span className="text-white/60 text-sm">Логистика:</span>
</div>
<p className="text-white font-medium text-sm">
{order.logisticsPartner.name ||
order.logisticsPartner.fullName}
</p>
</div>
)}
</div>
{/* Краткая информация о заказе */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-6">
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-green-400" />
<span className="text-white text-sm">
{order.items.length} вид
{order.items.length === 1
? ""
: order.items.length < 5
? "а"
: "ов"}{" "}
товаров
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-white text-sm">
{order.totalItems} единиц
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-white text-sm">
📏 {calculateVolume()} м³
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-white font-semibold">
💰 {order.totalAmount.toLocaleString()}
</span>
</div>
</div>
{/* Кнопки действий */}
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
className="text-white/70 hover:text-white"
>
Подробности
{isExpanded ? (
<ChevronUp className="h-4 w-4 ml-1" />
) : (
<ChevronDown className="h-4 w-4 ml-1" />
)}
</Button>
{/* Действия для PENDING */}
{order.status === "PENDING" && (
<>
<Button
size="sm"
onClick={handleApproveOrder}
disabled={approving}
className="glass-button bg-green-500/20 hover:bg-green-500/30 text-green-300 border border-green-500/30"
>
{approving ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<CheckCircle className="h-3 w-3 mr-1" />
)}
Одобрить
</Button>
<Button
size="sm"
onClick={() => setShowRejectModal(true)}
disabled={rejecting}
className="glass-secondary bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
>
<XCircle className="h-3 w-3 mr-1" />
Отклонить
</Button>
</>
)}
{/* Действие для LOGISTICS_CONFIRMED */}
{order.status === "LOGISTICS_CONFIRMED" && (
<Button
size="sm"
onClick={handleShipOrder}
disabled={shipping}
className="glass-button bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border border-orange-500/30"
>
{shipping ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<Truck className="h-3 w-3 mr-1" />
)}
Отгрузить
</Button>
)}
{/* Кнопка связаться всегда доступна */}
<Button
size="sm"
variant="ghost"
className="glass-secondary text-blue-300 hover:text-blue-200"
>
<MessageCircle className="h-3 w-3 mr-1" />
Связаться
</Button>
</div>
</div>
{/* Срок доставки */}
<div className="mt-3 pt-3 border-t border-white/10">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<MapPin className="h-4 w-4 text-white/60" />
<span className="text-white/60 text-sm">Доставка:</span>
<span className="text-white text-sm">Склад фулфилмента</span>
</div>
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-white/60" />
<span className="text-white/60 text-sm">Срок:</span>
<span className="text-white text-sm">
{formatDate(order.deliveryDate)}
</span>
</div>
</div>
</div>
</div>
{/* Расширенная детализация */}
{isExpanded && (
<div className="border-t border-white/10 p-4">
<h4 className="text-white font-semibold mb-3">
📋 ДЕТАЛИ ЗАЯВКИ #{order.id.slice(-8)}
</h4>
{/* Товары в заявке */}
<div className="mb-4">
<h5 className="text-white/80 font-medium mb-2">
📦 ТОВАРЫ В ЗАЯВКЕ:
</h5>
<div className="space-y-2">
{order.items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-2 bg-white/5 rounded"
>
<div className="flex-1">
<span className="text-white text-sm">
{item.product.name} {item.quantity} шт {item.price}
/шт = {item.totalPrice.toLocaleString()}
</span>
<div className="text-white/60 text-xs">
Артикул: {item.product.article}
{item.product.category &&
`${item.product.category.name}`}
</div>
</div>
</div>
))}
<div className="pt-2 border-t border-white/10">
<span className="text-white font-semibold">
Общая стоимость: {order.totalAmount.toLocaleString()}
</span>
</div>
</div>
</div>
{/* Логистическая информация */}
<div className="mb-4">
<h5 className="text-white/80 font-medium mb-2">
📍 ЛОГИСТИЧЕСКАЯ ИНФОРМАЦИЯ:
</h5>
<div className="space-y-1 text-sm">
<div className="text-white/70">
Объем груза: {calculateVolume()} м³
</div>
<div className="text-white/70">
Предварительная стоимость доставки: ~
{Math.round(
parseFloat(calculateVolume()) * 3500
).toLocaleString()}
</div>
<div className="text-white/70">
Маршрут: Склад поставщика {" "}
{order.fulfillmentCenter?.name || "Фулфилмент-центр"}
</div>
</div>
</div>
{/* Контактная информация */}
<div>
<h5 className="text-white/80 font-medium mb-2">📞 КОНТАКТЫ:</h5>
<div className="space-y-1 text-sm">
<div className="text-white/70">
Заказчик:{" "}
{order.organization.name || order.organization.fullName}
{order.organization.inn &&
` (ИНН: ${order.organization.inn})`}
</div>
{order.fulfillmentCenter && (
<div className="text-white/70">
Фулфилмент:{" "}
{order.fulfillmentCenter.name ||
order.fulfillmentCenter.fullName}
</div>
)}
</div>
</div>
</div>
)}
</Card>
{/* Модал отклонения заявки */}
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
<DialogContent className="glass-card border-white/20">
<DialogHeader>
<DialogTitle className="text-white">Отклонить заявку</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-white/90 text-sm mb-2 block">
Причина отклонения заявки:
</label>
<Textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Укажите причину отклонения..."
className="glass-input text-white placeholder:text-white/50"
rows={3}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
variant="ghost"
onClick={() => setShowRejectModal(false)}
className="text-white/70 hover:text-white"
>
Отмена
</Button>
<Button
onClick={handleRejectOrder}
disabled={rejecting || !rejectReason.trim()}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
>
{rejecting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<XCircle className="h-4 w-4 mr-2" />
)}
Отклонить заявку
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,178 @@
"use client";
import { useMemo } from "react";
import { Card } from "@/components/ui/card";
import {
Clock,
CheckCircle,
Settings,
Truck,
Package,
TrendingUp,
Calendar,
DollarSign,
} from "lucide-react";
interface SupplierOrderStatsProps {
orders: Array<{
id: string;
status: string;
totalAmount: number;
totalItems: number;
createdAt: string;
}>;
}
export function SupplierOrderStats({ orders }: SupplierOrderStatsProps) {
const stats = useMemo(() => {
const pending = orders.filter((order) => order.status === "PENDING").length;
const approved = orders.filter(
(order) => order.status === "SUPPLIER_APPROVED"
).length;
const inProgress = orders.filter((order) =>
["CONFIRMED", "LOGISTICS_CONFIRMED"].includes(order.status)
).length;
const shipping = orders.filter((order) =>
["SHIPPED", "IN_TRANSIT"].includes(order.status)
).length;
const completed = orders.filter(
(order) => order.status === "DELIVERED"
).length;
const totalRevenue = orders
.filter((order) => order.status === "DELIVERED")
.reduce((sum, order) => sum + order.totalAmount, 0);
const totalItems = orders.reduce((sum, order) => sum + order.totalItems, 0);
// Заявки за сегодня
const today = new Date().toDateString();
const todayOrders = orders.filter(
(order) => new Date(order.createdAt).toDateString() === today
).length;
return {
pending,
approved,
inProgress,
shipping,
completed,
totalRevenue,
totalItems,
todayOrders,
total: orders.length,
};
}, [orders]);
return (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{/* Ожидают одобрения */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-yellow-500/20 rounded-lg">
<Clock className="h-5 w-5 text-yellow-400" />
</div>
<div>
<p className="text-white/60 text-sm">Ожидают одобрения</p>
<p className="text-xl font-bold text-white">{stats.pending}</p>
</div>
</div>
</Card>
{/* Одобренные */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-green-500/20 rounded-lg">
<CheckCircle className="h-5 w-5 text-green-400" />
</div>
<div>
<p className="text-white/60 text-sm">Одобренные</p>
<p className="text-xl font-bold text-white">{stats.approved}</p>
</div>
</div>
</Card>
{/* В работе */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Settings className="h-5 w-5 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-sm">В работе</p>
<p className="text-xl font-bold text-white">{stats.inProgress}</p>
</div>
</div>
</Card>
{/* Готово к отправке / В пути */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-orange-500/20 rounded-lg">
<Truck className="h-5 w-5 text-orange-400" />
</div>
<div>
<p className="text-white/60 text-sm">Отгрузка/В пути</p>
<p className="text-xl font-bold text-white">{stats.shipping}</p>
</div>
</div>
</Card>
{/* Доставлено */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-emerald-500/20 rounded-lg">
<Package className="h-5 w-5 text-emerald-400" />
</div>
<div>
<p className="text-white/60 text-sm">Доставлено</p>
<p className="text-xl font-bold text-white">{stats.completed}</p>
</div>
</div>
</Card>
{/* Заявки за сегодня */}
<Card className="glass-card border-white/10 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-purple-500/20 rounded-lg">
<Calendar className="h-5 w-5 text-purple-400" />
</div>
<div>
<p className="text-white/60 text-sm">За сегодня</p>
<p className="text-xl font-bold text-white">{stats.todayOrders}</p>
</div>
</div>
</Card>
{/* Общая выручка */}
<Card className="glass-card border-white/10 p-4 md:col-span-2">
<div className="flex items-center space-x-3">
<div className="p-2 bg-green-500/20 rounded-lg">
<DollarSign className="h-5 w-5 text-green-400" />
</div>
<div>
<p className="text-white/60 text-sm">Выручка (завершенные)</p>
<p className="text-xl font-bold text-white">
{stats.totalRevenue.toLocaleString()}
</p>
</div>
</div>
</Card>
{/* Всего товаров */}
<Card className="glass-card border-white/10 p-4 md:col-span-2">
<div className="flex items-center space-x-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<TrendingUp className="h-5 w-5 text-blue-400" />
</div>
<div>
<p className="text-white/60 text-sm">Всего товаров в заявках</p>
<p className="text-xl font-bold text-white">
{stats.totalItems.toLocaleString()} шт.
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@ -1,591 +1,35 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation } from "@apollo/client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Sidebar } from "@/components/dashboard/sidebar";
import { useSidebar } from "@/hooks/useSidebar";
import { useAuth } from "@/hooks/useAuth";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import {
SUPPLIER_APPROVE_ORDER,
SUPPLIER_REJECT_ORDER,
SUPPLIER_SHIP_ORDER
} from "@/graphql/mutations";
import { toast } from "sonner";
import {
Calendar,
Package,
Truck,
User,
CheckCircle,
Clock,
XCircle,
MapPin,
Phone,
Mail,
Building,
Hash,
AlertTriangle,
} from "lucide-react";
interface SupplyOrder {
id: string;
organizationId: string;
partnerId: string;
deliveryDate: string;
status: "PENDING" | "SUPPLIER_APPROVED" | "LOGISTICS_CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
organization: {
id: string;
name?: string;
fullName?: string;
type: string;
};
fulfillmentCenter?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
logisticsPartner?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
items: Array<{
id: string;
quantity: number;
price: number;
totalPrice: number;
product: {
id: string;
name: string;
article: string;
description?: string;
category?: {
id: string;
name: string;
};
};
}>;
}
import { SupplierOrdersTabs } from "./supplier-orders-tabs";
import { Package, AlertTriangle } from "lucide-react";
export function SupplierOrdersDashboard() {
const { getSidebarMargin } = useSidebar();
const { user } = useAuth();
const [expandedOrders, setExpandedOrders] = useState<Set<string>>(new Set());
const [rejectReason, setRejectReason] = useState<string>("");
const [showRejectModal, setShowRejectModal] = useState<string | null>(null);
// Загружаем заказы поставок
const { data, loading, error, refetch } = useQuery(GET_SUPPLY_ORDERS, {
fetchPolicy: "cache-and-network",
});
// Мутации для действий поставщика
const [supplierApproveOrder] = useMutation(SUPPLIER_APPROVE_ORDER, {
refetchQueries: [
{ query: GET_SUPPLY_ORDERS },
"GetMyProducts", // Обновляем товары поставщика
"GetWarehouseProducts", // Обновляем склад фулфилмента (если нужно)
],
awaitRefetchQueries: true,
onCompleted: (data) => {
if (data.supplierApproveOrder.success) {
toast.success(data.supplierApproveOrder.message);
} else {
toast.error(data.supplierApproveOrder.message);
}
},
onError: (error) => {
console.error("Error approving order:", error);
toast.error("Ошибка при одобрении заказа");
},
});
const [supplierRejectOrder] = useMutation(SUPPLIER_REJECT_ORDER, {
refetchQueries: [{ query: GET_SUPPLY_ORDERS }],
onCompleted: (data) => {
if (data.supplierRejectOrder.success) {
toast.success(data.supplierRejectOrder.message);
} else {
toast.error(data.supplierRejectOrder.message);
}
setShowRejectModal(null);
setRejectReason("");
},
onError: (error) => {
console.error("Error rejecting order:", error);
toast.error("Ошибка при отклонении заказа");
},
});
const [supplierShipOrder] = useMutation(SUPPLIER_SHIP_ORDER, {
refetchQueries: [
{ query: GET_SUPPLY_ORDERS },
"GetMyProducts", // Обновляем товары поставщика для актуальных остатков
],
onCompleted: (data) => {
if (data.supplierShipOrder.success) {
toast.success(data.supplierShipOrder.message);
} else {
toast.error(data.supplierShipOrder.message);
}
},
onError: (error) => {
console.error("Error shipping order:", error);
toast.error("Ошибка при отправке заказа");
},
});
const toggleOrderExpansion = (orderId: string) => {
const newExpanded = new Set(expandedOrders);
if (newExpanded.has(orderId)) {
newExpanded.delete(orderId);
} else {
newExpanded.add(orderId);
}
setExpandedOrders(newExpanded);
};
// Фильтруем заказы где текущая организация является поставщиком
// В GraphQL partnerId - это ID поставщика, а organizationId - это ID создателя заказа
const supplierOrders: SupplyOrder[] = (data?.supplyOrders || []).filter(
(order: SupplyOrder) => {
// Нужно найти поле partner или использовать partnerId
// Проверяем через partnerId из схемы
const isSupplier = order.partnerId === user?.organization?.id;
return isSupplier;
}
);
const getStatusBadge = (status: SupplyOrder["status"]) => {
const statusMap = {
PENDING: {
label: "Ожидает одобрения",
color: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
icon: Clock,
},
SUPPLIER_APPROVED: {
label: "Ожидает подтверждения логистики",
color: "bg-blue-500/20 text-blue-300 border-blue-500/30",
icon: Clock,
},
LOGISTICS_CONFIRMED: {
label: "Готов к отправке",
color: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
icon: CheckCircle,
},
SHIPPED: {
label: "Отправлено",
color: "bg-orange-500/20 text-orange-300 border-orange-500/30",
icon: Truck,
},
DELIVERED: {
label: "Доставлено",
color: "bg-green-500/20 text-green-300 border-green-500/30",
icon: Package,
},
CANCELLED: {
label: "Отменено",
color: "bg-red-500/20 text-red-300 border-red-500/30",
icon: XCircle,
},
};
const { label, color, icon: Icon } = statusMap[status];
return (
<Badge className={`${color} border flex items-center gap-1 text-xs`}>
<Icon className="h-3 w-3" />
{label}
</Badge>
);
};
const handleApproveOrder = async (orderId: string) => {
await supplierApproveOrder({ variables: { id: orderId } });
};
const handleRejectOrder = async (orderId: string) => {
await supplierRejectOrder({
variables: { id: orderId, reason: rejectReason || undefined },
});
};
const handleShipOrder = async (orderId: string) => {
await supplierShipOrder({ variables: { id: orderId } });
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ru-RU", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
}).format(amount);
};
const getInitials = (name: string): string => {
return name
.split(" ")
.map((word) => word.charAt(0))
.join("")
.toUpperCase()
.slice(0, 2);
};
if (loading) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}>
<div className="flex-1 overflow-y-auto flex items-center justify-center">
<div className="text-white">Загрузка заказов...</div>
</div>
</main>
</div>
);
}
if (error) {
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}>
<div className="flex-1 overflow-y-auto flex items-center justify-center">
<div className="text-red-300">Ошибка загрузки заказов: {error.message}</div>
</div>
</main>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden">
<Sidebar />
<main className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}>
<main
className={`flex-1 ${getSidebarMargin()} px-4 py-3 flex flex-col transition-all duration-300 overflow-hidden`}
>
<div className="flex-1 overflow-y-auto space-y-6">
{/* Заголовок */}
{/* Заголовок страницы */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white mb-2">
Заказы поставок
</h1>
<h1 className="text-2xl font-bold text-white mb-2">Заявки</h1>
<p className="text-white/60">
Управление входящими заказами от фулфилмент-центров
Управление входящими заявками от заказчиков согласно правилам
системы
</p>
</div>
</div>
{/* Статистика */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-yellow-500/20 rounded">
<Clock className="h-5 w-5 text-yellow-400" />
</div>
<div>
<p className="text-white/60 text-sm">Ожидают одобрения</p>
<p className="text-xl font-bold text-white">
{supplierOrders.filter(order => order.status === "PENDING").length}
</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-cyan-500/20 rounded">
<CheckCircle className="h-5 w-5 text-cyan-400" />
</div>
<div>
<p className="text-white/60 text-sm">Готово к отправке</p>
<p className="text-xl font-bold text-white">
{supplierOrders.filter(order => order.status === "LOGISTICS_CONFIRMED").length}
</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-orange-500/20 rounded">
<Truck className="h-5 w-5 text-orange-400" />
</div>
<div>
<p className="text-white/60 text-sm">В пути</p>
<p className="text-xl font-bold text-white">
{supplierOrders.filter(order => order.status === "SHIPPED").length}
</p>
</div>
</div>
</Card>
<Card className="bg-white/10 backdrop-blur border-white/20 p-4">
<div className="flex items-center space-x-3">
<div className="p-2 bg-green-500/20 rounded">
<Package className="h-5 w-5 text-green-400" />
</div>
<div>
<p className="text-white/60 text-sm">Доставлено</p>
<p className="text-xl font-bold text-white">
{supplierOrders.filter(order => order.status === "DELIVERED").length}
</p>
</div>
</div>
</Card>
</div>
{/* Список заказов */}
<div className="space-y-4">
{supplierOrders.length === 0 ? (
<Card className="bg-white/10 backdrop-blur border-white/20 p-8">
<div className="text-center">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
Нет заказов поставок
</h3>
<p className="text-white/60">
Входящие заказы от фулфилмент-центров будут отображаться здесь
</p>
</div>
</Card>
) : (
supplierOrders.map((order) => (
<Card
key={order.id}
className="bg-white/10 backdrop-blur border-white/20 overflow-hidden hover:bg-white/15 transition-colors cursor-pointer"
onClick={() => toggleOrderExpansion(order.id)}
>
{/* Основная информация о заказе */}
<div className="p-4">
<div className="flex items-center justify-between">
{/* Левая часть */}
<div className="flex items-center space-x-4 flex-1 min-w-0">
{/* Номер заказа */}
<div className="flex items-center space-x-2">
<Hash className="h-4 w-4 text-white/60" />
<span className="text-white font-semibold">
{order.id.slice(-8)}
</span>
</div>
{/* Заказчик */}
<div className="flex items-center space-x-3 min-w-0">
<Avatar className="w-8 h-8">
<AvatarFallback className="bg-blue-500 text-white text-sm">
{getInitials(order.organization.name || order.organization.fullName || "ФФ")}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<h3 className="text-white font-medium text-sm truncate">
{order.organization.name || order.organization.fullName}
</h3>
<p className="text-white/60 text-xs">
{order.organization.type === "FULFILLMENT" ? "Фулфилмент" : "Организация"}
</p>
</div>
</div>
{/* Краткая информация */}
<div className="hidden lg:flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Calendar className="h-4 w-4 text-blue-400" />
<span className="text-white text-sm">
{formatDate(order.deliveryDate)}
</span>
</div>
<div className="flex items-center space-x-1">
<Package className="h-4 w-4 text-green-400" />
<span className="text-white text-sm">
{order.totalItems} шт.
</span>
</div>
</div>
</div>
{/* Правая часть - статус и действия */}
<div className="flex items-center space-x-3 flex-shrink-0">
{getStatusBadge(order.status)}
{/* Кнопки действий для поставщика */}
{order.status === "PENDING" && (
<div className="flex items-center space-x-2">
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleApproveOrder(order.id);
}}
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>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
setShowRejectModal(order.id);
}}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30 text-xs px-3 py-1 h-7"
>
<XCircle className="h-3 w-3 mr-1" />
Отклонить
</Button>
</div>
)}
{order.status === "LOGISTICS_CONFIRMED" && (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
handleShipOrder(order.id);
}}
className="bg-orange-500/20 hover:bg-orange-500/30 text-orange-300 border border-orange-500/30 text-xs px-3 py-1 h-7"
>
<Truck className="h-3 w-3 mr-1" />
Отправить
</Button>
)}
</div>
</div>
{/* Развернутые детали */}
{expandedOrders.has(order.id) && (
<>
<Separator className="my-4 bg-white/10" />
{/* Сумма заказа */}
<div className="mb-4 p-3 bg-white/5 rounded">
<div className="flex items-center justify-between">
<span className="text-white/60">Общая сумма:</span>
<span className="text-white font-semibold text-lg">
{formatCurrency(order.totalAmount)}
</span>
</div>
</div>
{/* Информация о логистике */}
{order.logisticsPartner && (
<div className="mb-4">
<h4 className="text-white font-semibold mb-2 flex items-center text-sm">
<Truck className="h-4 w-4 mr-2 text-purple-400" />
Логистическая компания
</h4>
<div className="bg-white/5 rounded p-3">
<p className="text-white">
{order.logisticsPartner.name || order.logisticsPartner.fullName}
</p>
</div>
</div>
)}
{/* Список товаров */}
<div>
<h4 className="text-white font-semibold mb-3 flex items-center text-sm">
<Package className="h-4 w-4 mr-2 text-green-400" />
Товары ({order.items.length})
</h4>
<div className="space-y-2">
{order.items.map((item) => (
<div
key={item.id}
className="bg-white/5 rounded p-3 flex items-center justify-between"
>
<div className="flex-1 min-w-0">
<h5 className="text-white font-medium text-sm">
{item.product.name}
</h5>
<p className="text-white/60 text-xs">
Артикул: {item.product.article}
</p>
{item.product.category && (
<Badge
variant="secondary"
className="bg-blue-500/20 text-blue-300 text-xs mt-1"
>
{item.product.category.name}
</Badge>
)}
</div>
<div className="text-right flex-shrink-0 ml-4">
<p className="text-white font-semibold">
{item.quantity} шт.
</p>
<p className="text-white/60 text-xs">
{formatCurrency(item.price)}
</p>
<p className="text-green-400 font-semibold text-sm">
{formatCurrency(item.totalPrice)}
</p>
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
</Card>
))
)}
</div>
{/* Основной интерфейс заявок */}
<SupplierOrdersTabs />
</div>
{/* Модальное окно для отклонения заказа */}
{showRejectModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="bg-gray-900 border-white/20 p-6 max-w-md w-full mx-4">
<h3 className="text-white font-semibold text-lg mb-4">
Отклонить заказ
</h3>
<p className="text-white/60 text-sm mb-4">
Укажите причину отклонения заказа (необязательно):
</p>
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Причина отклонения..."
className="w-full bg-white/10 border border-white/20 rounded-md px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-transparent mb-4"
rows={3}
/>
<div className="flex items-center space-x-3">
<Button
onClick={() => handleRejectOrder(showRejectModal)}
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border border-red-500/30"
>
Отклонить заказ
</Button>
<Button
onClick={() => {
setShowRejectModal(null);
setRejectReason("");
}}
variant="outline"
className="border-white/20 text-white hover:bg-white/10"
>
Отмена
</Button>
</div>
</Card>
</div>
)}
</main>
</div>
);
}
}

View File

@ -0,0 +1,210 @@
"use client";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Search,
Filter,
Calendar,
DollarSign,
Package,
Building,
X,
} from "lucide-react";
interface SupplierOrdersSearchProps {
searchQuery: string;
onSearchChange: (value: string) => void;
priceRange: { min: string; max: string };
onPriceRangeChange: (range: { min: string; max: string }) => void;
dateFilter: string;
onDateFilterChange: (value: string) => void;
}
export function SupplierOrdersSearch({
searchQuery,
onSearchChange,
priceRange,
onPriceRangeChange,
dateFilter,
onDateFilterChange,
}: SupplierOrdersSearchProps) {
const hasActiveFilters = priceRange.min || priceRange.max || dateFilter;
const clearFilters = () => {
onPriceRangeChange({ min: "", max: "" });
onDateFilterChange("");
};
return (
<Card className="glass-card border-white/10 p-4">
<div className="flex flex-col md:flex-row gap-4">
{/* Поисковая строка */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-white/60" />
<Input
placeholder="Поиск по номеру заявки, заказчику, товарам, ИНН..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="glass-input text-white placeholder:text-white/50 pl-10"
/>
</div>
{/* Фильтры */}
<div className="flex items-center space-x-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={`glass-secondary border-white/20 ${
hasActiveFilters ? "border-blue-400/50 bg-blue-500/10" : ""
}`}
>
<Filter className="h-4 w-4 mr-2" />
Фильтры
{hasActiveFilters && (
<span className="ml-2 bg-blue-500/20 text-blue-300 px-2 py-1 rounded text-xs">
Активны
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="glass-card border-white/20 w-80">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-white font-semibold">Фильтры поиска</h4>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-white/60 hover:text-white"
>
<X className="h-4 w-4 mr-1" />
Очистить
</Button>
)}
</div>
{/* Фильтр по дате */}
<div className="space-y-2">
<Label className="text-white/90 flex items-center">
<Calendar className="h-4 w-4 mr-2" />
Период создания заявки
</Label>
<Input
type="date"
value={dateFilter}
onChange={(e) => onDateFilterChange(e.target.value)}
className="glass-input text-white"
/>
</div>
{/* Фильтр по стоимости */}
<div className="space-y-2">
<Label className="text-white/90 flex items-center">
<DollarSign className="h-4 w-4 mr-2" />
Диапазон стоимости ()
</Label>
<div className="flex space-x-2">
<Input
type="number"
placeholder="От"
value={priceRange.min}
onChange={(e) =>
onPriceRangeChange({
...priceRange,
min: e.target.value,
})
}
className="glass-input text-white placeholder:text-white/50 w-24"
/>
<span className="text-white/60 self-center"></span>
<Input
type="number"
placeholder="До"
value={priceRange.max}
onChange={(e) =>
onPriceRangeChange({
...priceRange,
max: e.target.value,
})
}
className="glass-input text-white placeholder:text-white/50 w-24"
/>
</div>
</div>
{/* Информация о поиске */}
<div className="pt-2 border-t border-white/10">
<p className="text-white/60 text-xs">
💡 <strong>Поиск работает по:</strong>
</p>
<ul className="text-white/60 text-xs mt-1 space-y-1">
<li> Номеру заявки (СФ-2024-XXX)</li>
<li> Названию заказчика</li>
<li> Названию товаров</li>
<li> ИНН заказчика</li>
</ul>
</div>
</div>
</PopoverContent>
</Popover>
{/* Быстрые фильтры */}
<div className="hidden lg:flex items-center space-x-2 text-white/60 text-sm">
<span>Быстро:</span>
<Button
variant="ghost"
size="sm"
onClick={() =>
onDateFilterChange(new Date().toISOString().split("T")[0])
}
className="text-xs h-7 px-2"
>
Сегодня
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
onDateFilterChange(weekAgo.toISOString().split("T")[0]);
}}
className="text-xs h-7 px-2"
>
Неделя
</Button>
</div>
</div>
</div>
{/* Активные фильтры */}
{hasActiveFilters && (
<div className="mt-3 pt-3 border-t border-white/10">
<div className="flex items-center space-x-2 text-sm">
<span className="text-white/60">Активные фильтры:</span>
{dateFilter && (
<span className="bg-blue-500/20 text-blue-300 px-2 py-1 rounded border border-blue-400/30">
📅 {new Date(dateFilter).toLocaleDateString("ru-RU")}
</span>
)}
{(priceRange.min || priceRange.max) && (
<span className="bg-green-500/20 text-green-300 px-2 py-1 rounded border border-green-400/30">
💰 {priceRange.min || "0"} {priceRange.max || "∞"}
</span>
)}
</div>
</div>
)}
</Card>
);
}

View File

@ -0,0 +1,309 @@
"use client";
import { useState, useMemo } from "react";
import { useQuery } from "@apollo/client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { GET_SUPPLY_ORDERS } from "@/graphql/queries";
import { useAuth } from "@/hooks/useAuth";
import { SupplierOrderCard } from "./supplier-order-card";
import { SupplierOrderStats } from "./supplier-order-stats";
import { SupplierOrdersSearch } from "./supplier-orders-search";
import {
Clock,
CheckCircle,
Settings,
Truck,
Package,
Calendar,
Search,
} from "lucide-react";
interface SupplyOrder {
id: string;
organizationId: string;
partnerId: string;
deliveryDate: string;
status:
| "PENDING"
| "SUPPLIER_APPROVED"
| "CONFIRMED"
| "LOGISTICS_CONFIRMED"
| "SHIPPED"
| "IN_TRANSIT"
| "DELIVERED"
| "CANCELLED";
totalAmount: number;
totalItems: number;
createdAt: string;
organization: {
id: string;
name?: string;
fullName?: string;
type: string;
inn?: string;
};
partner?: {
id: string;
name?: string;
fullName?: string;
inn?: string;
address?: string;
phones?: string[];
emails?: string[];
};
fulfillmentCenter?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
logisticsPartner?: {
id: string;
name?: string;
fullName?: string;
type: string;
};
items: Array<{
id: string;
quantity: number;
price: number;
totalPrice: number;
product: {
id: string;
name: string;
article: string;
description?: string;
category?: {
id: string;
name: string;
};
};
}>;
}
export function SupplierOrdersTabs() {
const { user } = useAuth();
const [activeTab, setActiveTab] = useState("new");
const [searchQuery, setSearchQuery] = useState("");
const [dateFilter, setDateFilter] = useState("");
const [priceRange, setPriceRange] = useState({ min: "", max: "" });
// Загружаем заказы поставок
const { data, loading, error } = useQuery(GET_SUPPLY_ORDERS, {
fetchPolicy: "cache-and-network",
});
// Фильтруем заказы где текущая организация является поставщиком
const supplierOrders: SupplyOrder[] = useMemo(() => {
return (data?.supplyOrders || []).filter(
(order: SupplyOrder) => order.partnerId === user?.organization?.id
);
}, [data?.supplyOrders, user?.organization?.id]);
// Фильтрация заказов по поисковому запросу
const filteredOrders = useMemo(() => {
let filtered = supplierOrders;
// Поиск по номеру заявки, заказчику, товарам, ИНН
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(
(order) =>
order.id.toLowerCase().includes(query) ||
(order.organization.name || "").toLowerCase().includes(query) ||
(order.organization.fullName || "").toLowerCase().includes(query) ||
(order.organization.inn || "").toLowerCase().includes(query) ||
order.items.some((item) =>
item.product.name.toLowerCase().includes(query)
)
);
}
// Фильтр по диапазону цены
if (priceRange.min || priceRange.max) {
filtered = filtered.filter((order) => {
if (priceRange.min && order.totalAmount < parseFloat(priceRange.min))
return false;
if (priceRange.max && order.totalAmount > parseFloat(priceRange.max))
return false;
return true;
});
}
return filtered;
}, [supplierOrders, searchQuery, priceRange]);
// Разделение заказов по статусам согласно правилам
const ordersByStatus = useMemo(() => {
return {
new: filteredOrders.filter((order) => order.status === "PENDING"),
approved: filteredOrders.filter(
(order) => order.status === "SUPPLIER_APPROVED"
),
inProgress: filteredOrders.filter((order) =>
["CONFIRMED", "LOGISTICS_CONFIRMED"].includes(order.status)
),
shipping: filteredOrders.filter((order) =>
["SHIPPED", "IN_TRANSIT"].includes(order.status)
),
completed: filteredOrders.filter((order) => order.status === "DELIVERED"),
all: filteredOrders,
};
}, [filteredOrders]);
const getTabBadgeCount = (tabKey: string) => {
return ordersByStatus[tabKey as keyof typeof ordersByStatus]?.length || 0;
};
const getCurrentOrders = () => {
return ordersByStatus[activeTab as keyof typeof ordersByStatus] || [];
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-white/60">Загрузка заявок...</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-red-400">
Ошибка загрузки заявок: {error.message}
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Статистика - Модуль 2 согласно правилам */}
<SupplierOrderStats orders={supplierOrders} />
{/* Блок табов - отдельный блок согласно visual-design-rules.md */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl p-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-transparent p-0 space-x-2">
{/* Уровень 2: Фильтрация по статусам */}
<TabsTrigger
value="new"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-0 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
<Clock className="h-4 w-4 mr-2" />
Новые
{getTabBadgeCount("new") > 0 && (
<Badge className="ml-2 bg-red-500/20 text-red-300 border-red-400/30">
{getTabBadgeCount("new")}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="approved"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
<CheckCircle className="h-4 w-4 mr-2" />
Одобренные
{getTabBadgeCount("approved") > 0 && (
<Badge className="ml-2 bg-green-500/20 text-green-300 border-green-400/30">
{getTabBadgeCount("approved")}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="inProgress"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
<Settings className="h-4 w-4 mr-2" />В работе
{getTabBadgeCount("inProgress") > 0 && (
<Badge className="ml-2 bg-blue-500/20 text-blue-300 border-blue-400/30">
{getTabBadgeCount("inProgress")}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="shipping"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
<Truck className="h-4 w-4 mr-2" />
Отгрузка
{getTabBadgeCount("shipping") > 0 && (
<Badge className="ml-2 bg-orange-500/20 text-orange-300 border-orange-400/30">
{getTabBadgeCount("shipping")}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="completed"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
<Package className="h-4 w-4 mr-2" />
Завершенные
{getTabBadgeCount("completed") > 0 && (
<Badge className="ml-2 bg-emerald-500/20 text-emerald-300 border-emerald-400/30">
{getTabBadgeCount("completed")}
</Badge>
)}
</TabsTrigger>
<TabsTrigger
value="all"
className="h-9 bg-white/8 border-white/20 rounded-lg font-medium ml-4 data-[state=active]:bg-white/15 data-[state=active]:text-white"
>
Все заявки
{getTabBadgeCount("all") > 0 && (
<Badge className="ml-2 bg-white/20 text-white/70 border-white/30">
{getTabBadgeCount("all")}
</Badge>
)}
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Поиск и фильтры */}
<SupplierOrdersSearch
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
priceRange={priceRange}
onPriceRangeChange={setPriceRange}
dateFilter={dateFilter}
onDateFilterChange={setDateFilter}
/>
{/* Рабочее пространство - отдельный блок */}
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl">
<div className="p-6">
{getCurrentOrders().length === 0 ? (
<div className="text-center py-12">
<Package className="h-12 w-12 text-white/40 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">
{activeTab === "new" ? "Нет новых заявок" : "Заявки не найдены"}
</h3>
<p className="text-white/60">
{activeTab === "new"
? "Новые заявки от заказчиков будут отображаться здесь"
: "Попробуйте изменить фильтры поиска"}
</p>
</div>
) : (
<div className="space-y-4">
{getCurrentOrders().map((order) => (
<SupplierOrderCard key={order.id} order={order} />
))}
</div>
)}
</div>
</div>
</div>
);
}