1386 lines
66 KiB
TypeScript
1386 lines
66 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
Search,
|
|
Filter,
|
|
Eye,
|
|
MapPin,
|
|
Clock,
|
|
User,
|
|
Package,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertCircle,
|
|
Truck,
|
|
ChevronDown,
|
|
Navigation,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
import AdminLayout from "../../components/AdminLayout";
|
|
import "./AdminOrders.css";
|
|
import {
|
|
getAllCommands,
|
|
getDeliverymanLocationForCommand,
|
|
isAdminAuthenticated,
|
|
} from "../../api/api_admin";
|
|
import { getCommandItems } from "../../api/api_cabine";
|
|
import type { DeliverymanLocationResponse } from "../../api/api_admin_types";
|
|
|
|
// Interface correspondant au backend
|
|
interface CommandResponse {
|
|
id: number;
|
|
username: string;
|
|
status: string;
|
|
adresse: string;
|
|
total_prix: number;
|
|
livreur_assign?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
interface CommandItem {
|
|
id: number;
|
|
command_id: number;
|
|
produit: string;
|
|
product_id: number;
|
|
quantite: number;
|
|
prix: number;
|
|
client_username: string;
|
|
client_nom: string;
|
|
client_prenom: string;
|
|
client_telephone: string;
|
|
delivery_address: string;
|
|
status: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
command_status: string;
|
|
command_address: string;
|
|
total_prix: number;
|
|
livreur_assign: string;
|
|
command_created_at: string;
|
|
}
|
|
|
|
interface Order {
|
|
id: number;
|
|
orderNumber: string;
|
|
client: {
|
|
name: string;
|
|
phone: string;
|
|
username: string;
|
|
};
|
|
deliveryPerson?: {
|
|
name: string;
|
|
username: string;
|
|
};
|
|
items: Array<{
|
|
name: string;
|
|
quantity: number;
|
|
price: number;
|
|
}>;
|
|
total: number;
|
|
status:
|
|
| "pending"
|
|
| "assigned"
|
|
| "en_route"
|
|
| "livre"
|
|
| "approved"
|
|
| "cancelled";
|
|
createdAt: string;
|
|
deliveryAddress: string;
|
|
}
|
|
|
|
type FilterStatus =
|
|
| "all"
|
|
| "pending"
|
|
| "assigned"
|
|
| "en_route"
|
|
| "livre"
|
|
| "approved"
|
|
| "cancelled";
|
|
|
|
function AdminOrders() {
|
|
const navigate = useNavigate();
|
|
const [orders, setOrders] = useState<Order[]>([]);
|
|
const [filteredOrders, setFilteredOrders] = useState<Order[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [filterStatus, setFilterStatus] = useState<FilterStatus>("all");
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
|
const [showOrderDetails, setShowOrderDetails] = useState(false);
|
|
|
|
const [deliverymanLocation, setDeliverymanLocation] =
|
|
useState<DeliverymanLocationResponse | null>(null);
|
|
const [loadingLocation, setLoadingLocation] = useState(false);
|
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
|
|
|
// ✅ AJOUT du compteur cancelled dans les stats
|
|
const [stats, setStats] = useState({
|
|
pending: 0,
|
|
assigned: 0,
|
|
en_route: 0,
|
|
livre: 0,
|
|
approved: 0,
|
|
cancelled: 0, // ✅ NOUVEAU
|
|
total: 0,
|
|
});
|
|
|
|
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
|
useEffect(() => {
|
|
const checkAuth = () => {
|
|
if (!isAdminAuthenticated()) {
|
|
console.log(
|
|
"❌ [AdminOrders] Admin non authentifié, redirection vers /login-admin/admin",
|
|
);
|
|
navigate("/login-admin/admin", { replace: true });
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, [navigate]);
|
|
|
|
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
|
useEffect(() => {
|
|
const authInterval = setInterval(() => {
|
|
if (!isAdminAuthenticated()) {
|
|
console.log(
|
|
"❌ [AdminOrders] Session admin expirée, redirection vers /login-admin/admin",
|
|
);
|
|
navigate("/login-admin/admin", { replace: true });
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearInterval(authInterval);
|
|
}, [navigate]);
|
|
|
|
// ✅ Récupération des statistiques depuis le backend - AVEC CANCELLED
|
|
useEffect(() => {
|
|
const fetchStats = async () => {
|
|
if (!isAdminAuthenticated()) {
|
|
console.log("❌ [fetchStats] Admin non authentifié");
|
|
navigate("/login-admin/admin", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("📊 [STATS] Récupération des statistiques...");
|
|
|
|
// ✅ MODIFICATION: Récupérer AUSSI les commandes cancelled explicitement
|
|
const [completedResponse, cancelledResponse] =
|
|
await Promise.all([
|
|
getAllCommands("approved", ""),
|
|
getAllCommands("cancelled", ""), // ✅ AJOUT CRITIQUE
|
|
]);
|
|
|
|
// Récupérer toutes les commandes actives (sans cancelled ni approved)
|
|
const allCommandsResponse = await getAllCommands("", "");
|
|
|
|
let pending = 0;
|
|
let assigned = 0;
|
|
let en_route = 0;
|
|
let livre = 0;
|
|
|
|
if (
|
|
allCommandsResponse.success &&
|
|
allCommandsResponse.commands
|
|
) {
|
|
allCommandsResponse.commands.forEach(
|
|
(cmd: CommandResponse) => {
|
|
switch (cmd.status) {
|
|
case "pending":
|
|
pending++;
|
|
break;
|
|
case "assigned":
|
|
assigned++;
|
|
break;
|
|
case "en_route":
|
|
en_route++;
|
|
break;
|
|
case "livre":
|
|
livre++;
|
|
break;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
// ✅ MODIFICATION: Compter les cancelled depuis la requête dédiée
|
|
const cancelledCount = cancelledResponse.success
|
|
? cancelledResponse.count || 0
|
|
: 0;
|
|
|
|
const completedCount = completedResponse.success
|
|
? completedResponse.count || 0
|
|
: 0;
|
|
|
|
setStats({
|
|
pending,
|
|
assigned,
|
|
en_route,
|
|
livre,
|
|
approved: completedCount,
|
|
cancelled: cancelledCount, // ✅ UTILISÉ ICI
|
|
total:
|
|
pending +
|
|
assigned +
|
|
en_route +
|
|
livre +
|
|
completedCount +
|
|
cancelledCount, // ✅ INCLUS dans le total
|
|
});
|
|
|
|
console.log("✅ [STATS] Statistiques chargées:", {
|
|
pending,
|
|
assigned,
|
|
en_route,
|
|
livre,
|
|
approved: completedCount,
|
|
cancelled: cancelledCount, // ✅ LOG
|
|
total:
|
|
pending +
|
|
assigned +
|
|
en_route +
|
|
livre +
|
|
completedCount +
|
|
cancelledCount,
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
"❌ [STATS] Erreur chargement statistiques:",
|
|
error,
|
|
);
|
|
|
|
if (error instanceof Error && error.message.includes("401")) {
|
|
console.log(
|
|
"🔓 [AdminOrders] Token invalide - Redirection",
|
|
);
|
|
sessionStorage.removeItem("admin_token");
|
|
sessionStorage.removeItem("admin_username");
|
|
navigate("/login-admin/admin", { replace: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchStats();
|
|
}, [navigate]);
|
|
|
|
// Récupération des commandes depuis le backend
|
|
useEffect(() => {
|
|
const fetchOrders = async () => {
|
|
if (!isAdminAuthenticated()) {
|
|
console.log("❌ [fetchOrders] Admin non authentifié");
|
|
navigate("/login-admin/admin", { replace: true });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const statusParam = filterStatus !== "all" ? filterStatus : "";
|
|
const response = await getAllCommands(statusParam, "");
|
|
|
|
if (!response.success || !response.commands) {
|
|
console.error(
|
|
"❌ Erreur récupération commandes:",
|
|
response,
|
|
);
|
|
setOrders([]);
|
|
setFilteredOrders([]);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
"✅ Commandes récupérées:",
|
|
response.commands.length,
|
|
);
|
|
|
|
const transformedOrders: Order[] = await Promise.all(
|
|
response.commands.map(async (cmd: CommandResponse) => {
|
|
let items: Array<{
|
|
name: string;
|
|
quantity: number;
|
|
price: number;
|
|
}> = [];
|
|
try {
|
|
const itemsResponse = await getCommandItems(cmd.id);
|
|
if (itemsResponse.success && itemsResponse.items) {
|
|
items = itemsResponse.items.map(
|
|
(item: CommandItem) => ({
|
|
name: item.produit,
|
|
quantity: item.quantite,
|
|
price: item.prix,
|
|
}),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
`⚠️ Erreur items pour commande ${cmd.id}:`,
|
|
error,
|
|
);
|
|
}
|
|
|
|
let clientInfo = {
|
|
name: cmd.username,
|
|
phone: "",
|
|
username: cmd.username,
|
|
};
|
|
|
|
if (items.length > 0) {
|
|
try {
|
|
const itemsResponse = await getCommandItems(
|
|
cmd.id,
|
|
);
|
|
if (
|
|
itemsResponse.success &&
|
|
itemsResponse.items &&
|
|
itemsResponse.items.length > 0
|
|
) {
|
|
const firstItem = itemsResponse
|
|
.items[0] as CommandItem;
|
|
clientInfo = {
|
|
name:
|
|
`${firstItem.client_prenom} ${firstItem.client_nom}`.trim() ||
|
|
cmd.username,
|
|
phone: firstItem.client_telephone || "",
|
|
username: cmd.username,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
`⚠️ Erreur infos client pour commande ${cmd.id}:`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: cmd.id,
|
|
orderNumber: `CMD-${cmd.id.toString().padStart(6, "0")}`,
|
|
client: clientInfo,
|
|
deliveryPerson: cmd.livreur_assign
|
|
? {
|
|
name: cmd.livreur_assign,
|
|
username: cmd.livreur_assign,
|
|
}
|
|
: undefined,
|
|
items: items,
|
|
total: cmd.total_prix,
|
|
status: cmd.status as Order["status"],
|
|
createdAt: cmd.created_at,
|
|
deliveryAddress: cmd.adresse,
|
|
};
|
|
}),
|
|
);
|
|
|
|
setOrders(transformedOrders);
|
|
setFilteredOrders(transformedOrders);
|
|
} catch (error) {
|
|
console.error("❌ Erreur chargement commandes:", error);
|
|
|
|
if (error instanceof Error && error.message.includes("401")) {
|
|
console.log(
|
|
"🔓 [AdminOrders] Token invalide - Redirection",
|
|
);
|
|
sessionStorage.removeItem("admin_token");
|
|
sessionStorage.removeItem("admin_username");
|
|
navigate("/login-admin/admin", { replace: true });
|
|
}
|
|
|
|
setOrders([]);
|
|
setFilteredOrders([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchOrders();
|
|
}, [filterStatus, navigate]);
|
|
|
|
// Filtrage et recherche
|
|
useEffect(() => {
|
|
let filtered = orders;
|
|
|
|
if (searchTerm) {
|
|
filtered = filtered.filter(
|
|
(order) =>
|
|
order.orderNumber
|
|
.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()) ||
|
|
order.client.name
|
|
.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()) ||
|
|
order.deliveryAddress
|
|
.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
setFilteredOrders(filtered);
|
|
}, [searchTerm, orders]);
|
|
|
|
const handleViewDeliverymanLocation = async (order: Order) => {
|
|
if (!order.deliveryPerson) {
|
|
alert("Aucun livreur assigné à cette commande");
|
|
return;
|
|
}
|
|
|
|
setLoadingLocation(true);
|
|
setShowLocationModal(true);
|
|
setDeliverymanLocation(null);
|
|
|
|
try {
|
|
console.log(
|
|
"📍 [LOCATION] Récupération position pour commande:",
|
|
order.id,
|
|
);
|
|
|
|
const result = await getDeliverymanLocationForCommand(order.id);
|
|
|
|
console.log("✅ [LOCATION] Réponse:", result);
|
|
|
|
if (!result.success) {
|
|
console.warn(
|
|
"⚠️ [LOCATION] Position non disponible:",
|
|
result.message,
|
|
);
|
|
}
|
|
|
|
setDeliverymanLocation(result);
|
|
} catch (error) {
|
|
console.error("❌ [LOCATION] Erreur récupération position:", error);
|
|
setDeliverymanLocation({
|
|
success: false,
|
|
error: "Erreur lors de la récupération de la position",
|
|
message:
|
|
error instanceof Error ? error.message : "Erreur inconnue",
|
|
});
|
|
} finally {
|
|
setLoadingLocation(false);
|
|
}
|
|
};
|
|
|
|
const handleRefreshLocation = async () => {
|
|
if (!selectedOrder) return;
|
|
await handleViewDeliverymanLocation(selectedOrder);
|
|
};
|
|
|
|
const formatTimeAgo = (seconds: number): string => {
|
|
if (seconds < 60) return `${seconds}s`;
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}min`;
|
|
return `${Math.floor(seconds / 3600)}h`;
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const labels: { [key: string]: string } = {
|
|
pending: "En attente",
|
|
assigned: "Assignée",
|
|
en_route: "En livraison",
|
|
livre: "Livrée",
|
|
approved: "Approuvée",
|
|
cancelled: "Annulée",
|
|
};
|
|
return labels[status] || status;
|
|
};
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case "pending":
|
|
return <Clock size={16} />;
|
|
case "assigned":
|
|
return <User size={16} />;
|
|
case "en_route":
|
|
return <Truck size={16} />;
|
|
case "livre":
|
|
return <Package size={16} />;
|
|
case "approved":
|
|
return <CheckCircle size={16} />;
|
|
case "cancelled":
|
|
return <XCircle size={16} />;
|
|
default:
|
|
return <AlertCircle size={16} />;
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return new Intl.DateTimeFormat("fr-FR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}).format(date);
|
|
};
|
|
|
|
const handleViewOrder = (order: Order) => {
|
|
setSelectedOrder(order);
|
|
setShowOrderDetails(true);
|
|
};
|
|
|
|
const getStatusCount = (status: FilterStatus) => {
|
|
if (status === "all") return stats.total;
|
|
return stats[status as keyof typeof stats] || 0;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<AdminLayout>
|
|
<div className="admin-container">
|
|
<div className="loading-orders">
|
|
<p>Chargement des commandes...</p>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AdminLayout>
|
|
<div className="admin-container">
|
|
{/* Header */}
|
|
<div className="orders-header">
|
|
<div className="header-content">
|
|
<h1>Gestion des Commandes</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats rapides - ✅ AJOUT de la carte Cancelled */}
|
|
<div className="orders-stats">
|
|
<div className="stat-item">
|
|
<div className="stat-icon orange">
|
|
<Clock size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("pending")}
|
|
</span>
|
|
<span className="stat-label">En attente</span>
|
|
</div>
|
|
</div>
|
|
<div className="stat-item">
|
|
<div className="stat-icon blue">
|
|
<User size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("assigned")}
|
|
</span>
|
|
<span className="stat-label">Assignées</span>
|
|
</div>
|
|
</div>
|
|
<div className="stat-item">
|
|
<div className="stat-icon purple">
|
|
<Truck size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("en_route")}
|
|
</span>
|
|
<span className="stat-label">En livraison</span>
|
|
</div>
|
|
</div>
|
|
<div className="stat-item">
|
|
<div className="stat-icon green">
|
|
<CheckCircle size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("livre")}
|
|
</span>
|
|
<span className="stat-label">Livrées</span>
|
|
</div>
|
|
</div>
|
|
{/* ✅ NOUVELLE CARTE - Commandes annulées */}
|
|
<div className="stat-item">
|
|
<div className="stat-icon red">
|
|
<XCircle size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("cancelled")}
|
|
</span>
|
|
<span className="stat-label">Annulées</span>
|
|
</div>
|
|
</div>
|
|
<div className="stat-item">
|
|
<div className="stat-icon success">
|
|
<CheckCircle size={20} />
|
|
</div>
|
|
<div className="stat-info">
|
|
<span className="stat-value">
|
|
{getStatusCount("approved")}
|
|
</span>
|
|
<span className="stat-label">Approuvées</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Barre de recherche et filtres */}
|
|
<div className="orders-controls">
|
|
<div className="search-bar">
|
|
<Search size={20} />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher par numéro, client, adresse..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<button
|
|
className={`filter-toggle ${showFilters ? "active" : ""}`}
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
>
|
|
<Filter size={20} />
|
|
<span>Filtres</span>
|
|
<ChevronDown
|
|
size={16}
|
|
className={showFilters ? "rotated" : ""}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filtres étendus - ✅ AJOUT du bouton Annulées */}
|
|
{showFilters && (
|
|
<div className="filters-panel">
|
|
<div className="filter-group">
|
|
<label>Statut</label>
|
|
<div className="filter-buttons">
|
|
<button
|
|
className={
|
|
filterStatus === "all" ? "active" : ""
|
|
}
|
|
onClick={() => setFilterStatus("all")}
|
|
>
|
|
Tous ({getStatusCount("all")})
|
|
</button>
|
|
<button
|
|
className={
|
|
filterStatus === "pending"
|
|
? "active"
|
|
: ""
|
|
}
|
|
onClick={() => setFilterStatus("pending")}
|
|
>
|
|
En attente ({getStatusCount("pending")})
|
|
</button>
|
|
<button
|
|
className={
|
|
filterStatus === "assigned"
|
|
? "active"
|
|
: ""
|
|
}
|
|
onClick={() => setFilterStatus("assigned")}
|
|
>
|
|
Assignées ({getStatusCount("assigned")})
|
|
</button>
|
|
<button
|
|
className={
|
|
filterStatus === "en_route"
|
|
? "active"
|
|
: ""
|
|
}
|
|
onClick={() => setFilterStatus("en_route")}
|
|
>
|
|
En livraison ({getStatusCount("en_route")})
|
|
</button>
|
|
<button
|
|
className={
|
|
filterStatus === "livre" ? "active" : ""
|
|
}
|
|
onClick={() => setFilterStatus("livre")}
|
|
>
|
|
Livrées ({getStatusCount("livre")})
|
|
</button>
|
|
{/* ✅ NOUVEAU BOUTON - Filtre Annulées */}
|
|
<button
|
|
className={
|
|
filterStatus === "cancelled"
|
|
? "active"
|
|
: ""
|
|
}
|
|
onClick={() => setFilterStatus("cancelled")}
|
|
>
|
|
Annulées ({getStatusCount("cancelled")})
|
|
</button>
|
|
{/* Bouton Approuvées */}
|
|
<button
|
|
className={
|
|
filterStatus === "approved"
|
|
? "active"
|
|
: ""
|
|
}
|
|
onClick={() => setFilterStatus("approved")}
|
|
>
|
|
Approuvées ({getStatusCount("approved")})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Liste des commandes */}
|
|
<div className="orders-list">
|
|
{filteredOrders.length === 0 ? (
|
|
<div className="empty-state">
|
|
<Package size={64} />
|
|
<h3>Aucune commande trouvée</h3>
|
|
<p>
|
|
Aucune commande ne correspond à vos critères de
|
|
recherche
|
|
</p>
|
|
</div>
|
|
) : (
|
|
filteredOrders.map((order) => (
|
|
<div key={order.id} className="order-card">
|
|
<div className="order-header">
|
|
<div className="order-number">
|
|
<Package size={20} />
|
|
<span>{order.orderNumber}</span>
|
|
</div>
|
|
<div
|
|
className={`order-status status-${order.status}`}
|
|
>
|
|
{getStatusIcon(order.status)}
|
|
<span>
|
|
{getStatusLabel(order.status)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="order-content">
|
|
<div className="order-info-grid">
|
|
<div className="info-item">
|
|
<User size={16} />
|
|
<div className="info-details">
|
|
<span className="info-label">
|
|
Client
|
|
</span>
|
|
<span className="info-value">
|
|
{order.client.username}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{order.deliveryPerson && (
|
|
<div className="info-item">
|
|
<Truck size={16} />
|
|
<div className="info-details">
|
|
<span className="info-label">
|
|
Livreur
|
|
</span>
|
|
<span className="info-value">
|
|
{
|
|
order.deliveryPerson
|
|
.name
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="info-item">
|
|
<MapPin size={16} />
|
|
<div className="info-details">
|
|
<span className="info-label">
|
|
Adresse
|
|
</span>
|
|
<span className="info-value">
|
|
{order.deliveryAddress}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="info-item">
|
|
<Clock size={16} />
|
|
<div className="info-details">
|
|
<span className="info-label">
|
|
Date
|
|
</span>
|
|
<span className="info-value">
|
|
{formatDate(
|
|
order.createdAt,
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="order-items">
|
|
<span className="items-label">
|
|
Articles:
|
|
</span>
|
|
{order.items.map((item, index) => (
|
|
<span
|
|
key={index}
|
|
className="item-tag"
|
|
>
|
|
{item.quantity}x {item.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="order-footer">
|
|
<div className="order-total">
|
|
<span>{order.total.toFixed(2)} €</span>
|
|
</div>
|
|
<button
|
|
className="view-details-button"
|
|
onClick={() => handleViewOrder(order)}
|
|
>
|
|
<Eye size={18} />
|
|
<span>Détails</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* ============================================
|
|
MODAL DÉTAILS DE COMMANDE
|
|
============================================ */}
|
|
{showOrderDetails && selectedOrder && (
|
|
<>
|
|
<div
|
|
className="modal-overlay"
|
|
onClick={() => {
|
|
setShowOrderDetails(false);
|
|
setSelectedOrder(null);
|
|
}}
|
|
/>
|
|
<div className="order-details-modal">
|
|
<div className="modal-header">
|
|
<h2>Détails de la commande</h2>
|
|
<button
|
|
className="close-modal"
|
|
onClick={() => {
|
|
setShowOrderDetails(false);
|
|
setSelectedOrder(null);
|
|
}}
|
|
>
|
|
<XCircle size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="modal-content">
|
|
{/* Informations générales */}
|
|
<div className="detail-section">
|
|
<h3>Informations générales</h3>
|
|
<div className="detail-grid">
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Numéro de commande
|
|
</span>
|
|
<span className="detail-value">
|
|
{selectedOrder.orderNumber}
|
|
</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Statut
|
|
</span>
|
|
<span className="detail-value">
|
|
<div
|
|
className={`order-status status-${selectedOrder.status}`}
|
|
>
|
|
{getStatusIcon(
|
|
selectedOrder.status,
|
|
)}
|
|
<span>
|
|
{getStatusLabel(
|
|
selectedOrder.status,
|
|
)}
|
|
</span>
|
|
</div>
|
|
</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Date de création
|
|
</span>
|
|
<span className="detail-value">
|
|
{formatDate(
|
|
selectedOrder.createdAt,
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations client */}
|
|
<div className="detail-section">
|
|
<h3>Client</h3>
|
|
<div className="detail-grid">
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Nom
|
|
</span>
|
|
<span className="detail-value">
|
|
{selectedOrder.client.name}
|
|
</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Username
|
|
</span>
|
|
<span className="detail-value">
|
|
{selectedOrder.client.username}
|
|
</span>
|
|
</div>
|
|
{selectedOrder.client.phone && (
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Téléphone
|
|
</span>
|
|
<span className="detail-value">
|
|
{selectedOrder.client.phone}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="detail-item full-width">
|
|
<span className="detail-label">
|
|
Adresse de livraison
|
|
</span>
|
|
<span className="detail-value">
|
|
{selectedOrder.deliveryAddress}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations livreur */}
|
|
{selectedOrder.deliveryPerson && (
|
|
<div className="detail-section">
|
|
<h3>Livreur</h3>
|
|
<div className="detail-grid">
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Nom
|
|
</span>
|
|
<span className="detail-value">
|
|
{
|
|
selectedOrder
|
|
.deliveryPerson.name
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="detail-item">
|
|
<span className="detail-label">
|
|
Username
|
|
</span>
|
|
<span className="detail-value">
|
|
{
|
|
selectedOrder
|
|
.deliveryPerson
|
|
.username
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Articles */}
|
|
<div className="detail-section">
|
|
<h3>Articles commandés</h3>
|
|
<div className="items-list">
|
|
{selectedOrder.items.map(
|
|
(item, index) => (
|
|
<div
|
|
key={index}
|
|
className="item-row"
|
|
>
|
|
<span className="item-name">
|
|
{item.name}
|
|
</span>
|
|
<span className="item-quantity">
|
|
x{item.quantity}
|
|
</span>
|
|
<span className="item-price">
|
|
{item.price.toFixed(2)}{" "}
|
|
€
|
|
</span>
|
|
</div>
|
|
),
|
|
)}
|
|
<div className="items-total">
|
|
<span>Total</span>
|
|
<span className="total-amount">
|
|
{selectedOrder.total.toFixed(2)}{" "}
|
|
€
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="modal-actions">
|
|
<button
|
|
className="action-button secondary"
|
|
onClick={() => {
|
|
setShowOrderDetails(false);
|
|
setSelectedOrder(null);
|
|
}}
|
|
>
|
|
<XCircle size={18} />
|
|
<span>Fermer</span>
|
|
</button>
|
|
{selectedOrder.deliveryPerson &&
|
|
(selectedOrder.status === "en_route" ||
|
|
selectedOrder.status ===
|
|
"assigned") && (
|
|
<button
|
|
className="action-button primary"
|
|
onClick={() => {
|
|
handleViewDeliverymanLocation(
|
|
selectedOrder,
|
|
);
|
|
}}
|
|
>
|
|
<Navigation size={18} />
|
|
<span>Voir position livreur</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ============================================
|
|
MODAL POSITION LIVREUR
|
|
============================================ */}
|
|
{showLocationModal && (
|
|
<>
|
|
<div
|
|
className="location-modal-overlay"
|
|
onClick={() => {
|
|
setShowLocationModal(false);
|
|
setDeliverymanLocation(null);
|
|
}}
|
|
/>
|
|
<div className="location-modal">
|
|
<div className="modal-header">
|
|
<h2>Position du livreur</h2>
|
|
<button
|
|
className="close-modal"
|
|
onClick={() => {
|
|
setShowLocationModal(false);
|
|
setDeliverymanLocation(null);
|
|
}}
|
|
>
|
|
<XCircle size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="modal-content">
|
|
{loadingLocation ? (
|
|
<div className="loading-location">
|
|
<RefreshCw
|
|
size={48}
|
|
className="spin-animation"
|
|
/>
|
|
<p>Récupération de la position...</p>
|
|
</div>
|
|
) : deliverymanLocation &&
|
|
!deliverymanLocation.success ? (
|
|
<div className="location-error">
|
|
<AlertCircle
|
|
size={64}
|
|
color="#ef4444"
|
|
/>
|
|
<h3>Position non disponible</h3>
|
|
<p>
|
|
{deliverymanLocation.message ||
|
|
"Impossible de récupérer la position du livreur"}
|
|
</p>
|
|
{deliverymanLocation.command_info && (
|
|
<div className="error-details">
|
|
<p>
|
|
<strong>Commande:</strong>{" "}
|
|
{
|
|
deliverymanLocation
|
|
.command_info
|
|
.command_id
|
|
}
|
|
</p>
|
|
<p>
|
|
<strong>Client:</strong>{" "}
|
|
{
|
|
deliverymanLocation
|
|
.command_info.client
|
|
}
|
|
</p>
|
|
<p>
|
|
<strong>Statut:</strong>{" "}
|
|
{
|
|
deliverymanLocation
|
|
.command_info.status
|
|
}
|
|
</p>
|
|
{deliverymanLocation
|
|
.command_info
|
|
.deliveryman ? (
|
|
<p>
|
|
<strong>
|
|
Livreur:
|
|
</strong>{" "}
|
|
{
|
|
deliverymanLocation
|
|
.command_info
|
|
.deliveryman
|
|
}
|
|
</p>
|
|
) : (
|
|
<p>
|
|
<strong>
|
|
Livreur:
|
|
</strong>{" "}
|
|
Aucun livreur assigné
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
deliverymanLocation &&
|
|
deliverymanLocation.success &&
|
|
deliverymanLocation.data && (
|
|
<div className="location-content">
|
|
{/* Informations commande */}
|
|
<div className="location-section">
|
|
<h3>
|
|
<Package size={20} />
|
|
Informations commande
|
|
</h3>
|
|
<div className="info-grid">
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Commande
|
|
</span>
|
|
<span className="value">
|
|
#
|
|
{
|
|
deliverymanLocation
|
|
.data
|
|
.command_id
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Client
|
|
</span>
|
|
<span className="value">
|
|
{
|
|
deliverymanLocation
|
|
.data.client
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Statut
|
|
</span>
|
|
<span className="value">
|
|
<div
|
|
className={`order-status status-${deliverymanLocation.data.command_status}`}
|
|
>
|
|
{getStatusIcon(
|
|
deliverymanLocation
|
|
.data
|
|
.command_status,
|
|
)}
|
|
<span>
|
|
{getStatusLabel(
|
|
deliverymanLocation
|
|
.data
|
|
.command_status,
|
|
)}
|
|
</span>
|
|
</div>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Informations livreur */}
|
|
<div className="location-section">
|
|
<h3>
|
|
<Truck size={20} />
|
|
Livreur
|
|
</h3>
|
|
<div className="info-grid">
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Username
|
|
</span>
|
|
<span className="value">
|
|
{
|
|
deliverymanLocation
|
|
.data
|
|
.deliveryman
|
|
.username
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Statut
|
|
</span>
|
|
<span className="value">
|
|
{
|
|
deliverymanLocation
|
|
.data
|
|
.deliveryman
|
|
.status
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="info-row">
|
|
<span className="label">
|
|
Commande en cours
|
|
</span>
|
|
<span className="value">
|
|
#
|
|
{
|
|
deliverymanLocation
|
|
.data
|
|
.deliveryman
|
|
.current_command
|
|
}
|
|
</span>
|
|
</div>
|
|
<div className="info-row">
|
|
<span className="label">
|
|
File d'attente
|
|
</span>
|
|
<span className="value">
|
|
{
|
|
deliverymanLocation
|
|
.data
|
|
.deliveryman
|
|
.queue_size
|
|
}{" "}
|
|
commande(s)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Position GPS */}
|
|
<div className="location-section">
|
|
<h3>
|
|
<MapPin size={20} />
|
|
Position GPS
|
|
</h3>
|
|
<div className="gps-info">
|
|
<div className="gps-coordinates">
|
|
<div className="coordinate">
|
|
<span className="coord-label">
|
|
Latitude
|
|
</span>
|
|
<span className="coord-value">
|
|
{deliverymanLocation.data.deliveryman.location.latitude.toFixed(
|
|
6,
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="coordinate">
|
|
<span className="coord-label">
|
|
Longitude
|
|
</span>
|
|
<span className="coord-value">
|
|
{deliverymanLocation.data.deliveryman.location.longitude.toFixed(
|
|
6,
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`update-status ${deliverymanLocation.data.deliveryman.location.is_recent ? "recent" : "old"}`}
|
|
>
|
|
<Clock size={20} />
|
|
<span>
|
|
Dernière mise à
|
|
jour:{" "}
|
|
{formatTimeAgo(
|
|
deliverymanLocation
|
|
.data
|
|
.deliveryman
|
|
.location
|
|
.last_update_ago,
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="map-placeholder">
|
|
<MapPin size={48} />
|
|
<p className="map-coords">
|
|
{deliverymanLocation.data.deliveryman.location.latitude.toFixed(
|
|
6,
|
|
)}
|
|
,{" "}
|
|
{deliverymanLocation.data.deliveryman.location.longitude.toFixed(
|
|
6,
|
|
)}
|
|
</p>
|
|
<p className="map-hint">
|
|
Position actuelle du
|
|
livreur
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ETA */}
|
|
{deliverymanLocation.data.eta
|
|
.has_eta && (
|
|
<div className="location-section eta-section">
|
|
<h3>
|
|
<Clock size={20} />
|
|
Temps estimé d'arrivée
|
|
</h3>
|
|
<div className="eta-value">
|
|
~
|
|
{
|
|
deliverymanLocation
|
|
.data.eta
|
|
.minutes
|
|
}{" "}
|
|
min
|
|
</div>
|
|
<p className="eta-note">
|
|
Estimation basée sur la
|
|
position actuelle
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
|
|
{!loadingLocation &&
|
|
deliverymanLocation &&
|
|
deliverymanLocation.success && (
|
|
<button
|
|
className="refresh-location-btn"
|
|
onClick={handleRefreshLocation}
|
|
disabled={loadingLocation}
|
|
>
|
|
<RefreshCw size={20} />
|
|
<span>Actualiser la position</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|
|
|
|
export default AdminOrders;
|