Обновлены модели и компоненты для управления поставками и расходниками. Добавлены новые поля в модели SupplyOrder и соответствующие резолверы для поддержки логистики. Реализованы компоненты уведомлений для отображения статуса логистических заявок и поставок. Оптимизирован интерфейс для улучшения пользовательского опыта, добавлены логи для диагностики запросов. Обновлены GraphQL схемы и мутации для поддержки новых функциональных возможностей.
This commit is contained in:
601
src/components/supplier-orders/supplier-order-card.tsx
Normal file
601
src/components/supplier-orders/supplier-order-card.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
178
src/components/supplier-orders/supplier-order-stats.tsx
Normal file
178
src/components/supplier-orders/supplier-order-stats.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
210
src/components/supplier-orders/supplier-orders-search.tsx
Normal file
210
src/components/supplier-orders/supplier-orders-search.tsx
Normal 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>
|
||||
);
|
||||
}
|
309
src/components/supplier-orders/supplier-orders-tabs.tsx
Normal file
309
src/components/supplier-orders/supplier-orders-tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user