Files
sfera/src/components/supplier-orders/supplier-order-card.tsx

602 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
</>
);
}