chore: update

This commit is contained in:
2026-03-03 23:42:23 +01:00
parent 52b059fafa
commit 0073f80a48
95 changed files with 2720 additions and 40619 deletions
@@ -1,306 +0,0 @@
.admin-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1400px;
margin: 0 auto;
}
/* Header */
.dashboard-header {
margin-bottom: clamp(2rem, 5vw, 3rem);
text-align: center;
}
.dashboard-header h1 {
color: white;
font-size: clamp(2rem, 6vw, 3rem);
margin: 0 0 0.5rem 0;
font-weight: bold;
letter-spacing: -0.5px;
background: linear-gradient(to right, #7c3aed, #10b981);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dashboard-subtitle {
color: #888;
font-size: clamp(1rem, 3vw, 1.2rem);
margin: 0;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.stat-card {
position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
display: flex;
align-items: center;
gap: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.1), transparent);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.stat-card:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
border-color: rgba(124, 58, 237, 0.3);
box-shadow: 0 12px 32px rgba(124, 58, 237, 0.2);
transform: translateY(-4px);
}
.stat-card:hover::before {
left: 100%;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card:hover .stat-icon {
transform: scale(1.1) rotate(5deg);
}
.stat-icon.purple {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
color: #7c3aed;
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.3);
}
.stat-icon.orange {
background: linear-gradient(135deg, rgba(251, 146, 60, 0.2), rgba(249, 115, 22, 0.1));
color: #fb923c;
box-shadow: 0 8px 24px rgba(251, 146, 60, 0.3);
}
.stat-icon.green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.1));
color: #10b981;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.stat-icon.blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1));
color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
}
.stat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-label {
color: #888;
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.stat-value {
color: white;
font-size: clamp(1.8rem, 5vw, 2.2rem);
margin: 0;
font-weight: bold;
letter-spacing: -0.5px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.85rem;
font-weight: 600;
}
.stat-trend.positive {
color: #10b981;
}
.stat-trend.neutral {
color: #fb923c;
}
.stat-trend.negative {
color: #ef4444;
}
/* Quick Actions Section */
.quick-actions-section {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.quick-actions-section h2 {
color: white;
font-size: clamp(1.5rem, 4vw, 2rem);
margin: 0 0 clamp(1.5rem, 4vw, 2rem) 0;
font-weight: bold;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
}
.quick-action-card {
position: relative;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.quick-action-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.1), transparent);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.quick-action-card:hover {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(109, 40, 217, 0.05) 100%);
border-color: rgba(124, 58, 237, 0.4);
box-shadow: 0 12px 32px rgba(124, 58, 237, 0.3);
transform: translateY(-4px);
}
.quick-action-card:hover::before {
left: 100%;
}
.quick-action-card:active {
transform: scale(0.98);
}
.quick-action-card svg {
color: #7c3aed;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.quick-action-card:hover svg {
transform: scale(1.1) rotate(5deg);
}
.quick-action-card span {
color: white;
font-size: clamp(1rem, 3vw, 1.1rem);
font-weight: 600;
text-align: center;
}
/* Loading */
.loading-dashboard {
text-align: center;
padding: 4rem 2rem;
}
.loading-dashboard p {
font-size: 1.2rem;
color: #666;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* Responsive */
@media (max-width: 1024px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.actions-grid {
grid-template-columns: repeat(2, 1fr);
}
.stat-icon {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.actions-grid {
grid-template-columns: 1fr;
}
}
@media (hover: none) {
.stat-card:hover {
transform: none;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.quick-action-card:hover {
transform: none;
}
.view-all-button:hover {
transform: none;
}
}
-179
View File
@@ -1,179 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Users, Package, CheckCircle, Clock } from "lucide-react";
import AdminLayout from "../../components/AdminLayout";
import "./AdminDashboard.css";
import {
getAllClients,
getCommandCountByStatus,
getCommandCountCompleted,
getCommandCountInRoute,
isAdminAuthenticated,
} from "../../api/api_admin";
interface Stats {
pendingOrders: number;
completedOrders: number;
totalRevenue: number;
activeDeliveries: number;
totalUsers: number;
}
function Dashboard() {
const navigate = useNavigate();
const [stats, setStats] = useState<Stats>({
pendingOrders: 0,
completedOrders: 0,
totalRevenue: 0,
activeDeliveries: 0,
totalUsers: 0,
});
const [loading, setLoading] = useState(true);
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
if (!isAdminAuthenticated()) {
console.log(
"❌ [AdminDashboard] 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(
"❌ [AdminDashboard] Session admin expirée, redirection vers /login-admin/admin",
);
navigate("/login-admin/admin", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
useEffect(() => {
const fetchStats = async () => {
// ✅ Vérifier l'auth avant de charger les données
if (!isAdminAuthenticated()) {
console.log("❌ [fetchStats] Admin non authentifié");
navigate("/login-admin/admin", { replace: true });
return;
}
try {
const clients = await getAllClients(); // nombre total d'utilisateurs
const pendingOrders = await getCommandCountByStatus(); // commandes en attente
const completedOrders = await getCommandCountCompleted();
const activeDeliveries = await getCommandCountInRoute();
setStats({
pendingOrders, // récupéré via API
completedOrders, // TODO: remplacer par API réelle plus tard
totalRevenue: 45678.9, // idem
activeDeliveries, // idem
totalUsers: clients.length, // réel
});
} catch (error) {
console.error("❌ Erreur chargement stats:", error);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [AdminDashboard] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin/admin", { replace: true });
}
} finally {
setLoading(false);
}
};
fetchStats();
}, [navigate]);
if (loading) {
return (
<AdminLayout>
<div className="admin-container">
<div className="loading-dashboard">
<p>Chargement du tableau de bord...</p>
</div>
</div>
</AdminLayout>
);
}
return (
<AdminLayout>
<div className="admin-container">
{/* Header */}
<div className="dashboard-header">
<h1>Tableau de Bord Administrateur</h1>
<p className="dashboard-subtitle">
Vue d'ensemble de votre activité
</p>
</div>
{/* Stats Cards */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon orange">
<Clock size={24} />
</div>
<div className="stat-content">
<p className="stat-label">En Attente</p>
<h3 className="stat-value">
{stats.pendingOrders}
</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon green">
<CheckCircle size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Complétées</p>
<h3 className="stat-value">
{stats.completedOrders}
</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon purple">
<Package size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Livraisons Actives</p>
<h3 className="stat-value">
{stats.activeDeliveries}
</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon green">
<Users size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Utilisateurs</p>
<h3 className="stat-value">{stats.totalUsers}</h3>
</div>
</div>
</div>
</div>
</AdminLayout>
);
}
export default Dashboard;
@@ -1,165 +0,0 @@
/* Ajoutez ces styles à la fin du fichier CabineAlerts.css */
/* ============================================
BOUTON DE SUPPRESSION
============================================ */
.alert-btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
.alert-btn-danger:hover {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
transform: translateY(-2px);
}
/* ============================================
DELETE CONFIRMATION MODAL
============================================ */
.delete-confirm-modal {
background: linear-gradient(
135deg,
rgba(20, 20, 20, 0.98) 0%,
rgba(10, 10, 10, 0.98) 100%
);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 20px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(239, 68, 68, 0.4);
backdrop-filter: blur(20px);
}
.delete-modal-header {
text-align: center;
margin-bottom: 1.5rem;
}
.delete-icon {
color: #ef4444;
margin-bottom: 1rem;
animation: pulse-warning 2s ease-in-out infinite;
}
@keyframes pulse-warning {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
.delete-modal-header h3 {
color: white;
font-size: 1.5rem;
margin: 0;
font-weight: 700;
}
.delete-modal-body {
margin-bottom: 2rem;
text-align: center;
}
.delete-modal-body p {
color: #ccc;
font-size: 1rem;
margin: 0.5rem 0;
line-height: 1.6;
}
.delete-modal-body strong {
color: #ef4444;
font-weight: 700;
}
.delete-warning {
color: #fb923c !important;
font-size: 0.9rem !important;
font-weight: 600;
margin-top: 1rem !important;
}
.delete-modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.delete-btn {
flex: 1;
max-width: 200px;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.delete-btn-cancel {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
}
.delete-btn-cancel:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.delete-btn-confirm {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
.delete-btn-confirm:hover {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
transform: translateY(-2px);
}
/* Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
/* Responsive pour la modal de suppression */
@media (max-width: 640px) {
.delete-confirm-modal {
padding: 1.5rem;
}
.delete-modal-actions {
flex-direction: column;
}
.delete-btn {
max-width: 100%;
}
}
@@ -1,727 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
AlertTriangle,
Clock,
CheckCircle,
User,
Shield,
Search,
Filter,
Calendar,
X,
Eye,
Trash2,
} from "lucide-react";
import "./AdminAlerts.css";
import Sidebar from "../../components/Sidebar";
import { isAdminAuthenticated, deleteAlertAdmin } from "../../api/api_admin";
import { getAllAlerts, getAlertDetails } from "../../api/api_cabine";
import Toast from "../../components/Toast";
interface Alert {
id: number;
username: string;
status: string;
created_at: string;
updated_at: string;
}
interface AlertStats {
total: number;
active: number;
resolved: number;
}
interface ToastState {
show: boolean;
message: string;
type: "success" | "error" | "warning" | "info";
}
function AdminAlerts() {
const navigate = useNavigate();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [filteredAlerts, setFilteredAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">(
"true",
);
const [stats, setStats] = useState<AlertStats>({
total: 0,
active: 0,
resolved: 0,
});
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [alertToDelete, setAlertToDelete] = useState<number | null>(null);
// État pour le Toast
const [toast, setToast] = useState<ToastState>({
show: false,
message: "",
type: "success",
});
// Fonction pour afficher un toast
const showToast = (
message: string,
type: "success" | "error" | "warning" | "info" = "success",
) => {
setToast({ show: true, message, type });
};
// Fonction pour fermer le toast
const handleCloseToast = () => {
setToast({ ...toast, show: false });
};
// ============================================
// 🔐 VÉRIFICATION AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isAdminAuthenticated()) {
console.log("❌ [AdminAlerts] Non authentifié, redirection");
navigate("/login-admin", { replace: true });
}
};
checkAuth();
const authInterval = setInterval(() => {
if (!isAdminAuthenticated()) {
console.log("❌ [AdminAlerts] Session expirée");
navigate("/login-admin", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// ============================================
// 📊 CHARGEMENT DES ALERTES
// ============================================
const fetchAlerts = async () => {
if (!isAdminAuthenticated()) {
navigate("/login-admin", { replace: true });
return;
}
try {
console.log("🚨 [ADMIN_ALERTS] Chargement des alertes...");
const result = await getAllAlerts();
if (result.success && result.alerts) {
console.log(
"✅ [ADMIN_ALERTS] Alertes récupérées:",
result.alerts,
);
const alertsData = result.alerts.map((alert: any) => ({
id: alert.id,
username: alert.username,
status: alert.status,
created_at: alert.created_at,
updated_at: alert.updated_at,
}));
setAlerts(alertsData);
// Calculer les stats
const activeCount = alertsData.filter(
(a: Alert) => a.status === "true",
).length;
const resolvedCount = alertsData.filter(
(a: Alert) => a.status === "false",
).length;
setStats({
total: alertsData.length,
active: activeCount,
resolved: resolvedCount,
});
} else {
console.warn("⚠️ [ADMIN_ALERTS] Aucune alerte trouvée");
setAlerts([]);
setFilteredAlerts([]);
setStats({
total: 0,
active: 0,
resolved: 0,
});
}
} catch (error) {
console.error("❌ [ADMIN_ALERTS] Erreur chargement:", error);
if (error instanceof Error && error.message.includes("401")) {
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin", { replace: true });
}
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAlerts();
// Rafraîchir toutes les 30 secondes
const interval = setInterval(fetchAlerts, 30000);
return () => clearInterval(interval);
}, [navigate]);
// ============================================
// 🔍 FILTRAGE DES ALERTES
// ============================================
useEffect(() => {
let filtered = [...alerts];
// Filtre par statut
if (statusFilter !== "all") {
filtered = filtered.filter(
(alert) => alert.status === statusFilter,
);
}
// Filtre par recherche
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter((alert) =>
alert.username.toLowerCase().includes(query),
);
}
setFilteredAlerts(filtered);
}, [alerts, statusFilter, searchQuery]);
// ============================================
// 🎨 HELPERS
// ============================================
const determinePriority = (
alert: Alert,
): "critical" | "high" | "medium" | "low" => {
if (alert.status === "true") {
const createdAt = new Date(alert.created_at);
const now = new Date();
const durationMinutes =
(now.getTime() - createdAt.getTime()) / 1000 / 60;
if (durationMinutes > 30) return "critical";
if (durationMinutes > 15) return "high";
if (durationMinutes > 5) return "medium";
}
return "low";
};
const getStatusLabel = (status: string) => {
switch (status) {
case "true":
return "Active";
case "false":
return "Résolue";
default:
return "Inconnu";
}
};
const formatDuration = (createdAt: string) => {
const created = new Date(createdAt);
const now = new Date();
const duration = Math.floor((now.getTime() - created.getTime()) / 1000);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const secs = duration % 60;
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatTimeAgo = (dateString: string) => {
const seconds = Math.floor(
(Date.now() - new Date(dateString).getTime()) / 1000,
);
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}min`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 86400)}j`;
};
const handleViewDetails = async (alertId: number) => {
try {
const result = await getAlertDetails(alertId);
if (result.success && result.alert) {
setSelectedAlert(result.alert as Alert);
setShowDetailsModal(true);
} else {
showToast(
"Impossible de récupérer les détails de l'alerte",
"error",
);
}
} catch (error) {
console.error("❌ Erreur récupération détails:", error);
showToast("Erreur lors de la récupération des détails", "error");
}
};
const handleDeleteClick = (alertId: number, e: React.MouseEvent) => {
e.stopPropagation();
setAlertToDelete(alertId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!alertToDelete) return;
try {
console.log("🗑️ [DELETE_ALERT] Suppression:", alertToDelete);
const result = await deleteAlertAdmin(alertToDelete);
if (result.success) {
console.log("✅ [DELETE_ALERT] Suppression réussie");
// Rafraîchir la liste
await fetchAlerts();
// Fermer la modal de confirmation
setShowDeleteConfirm(false);
setAlertToDelete(null);
// Afficher le toast de succès
showToast(
`Alerte #${alertToDelete} supprimée avec succès`,
"success",
);
} else {
console.error("❌ [DELETE_ALERT] Erreur:", result.error);
showToast(
result.error || "Erreur lors de la suppression",
"error",
);
}
} catch (error) {
console.error("❌ [DELETE_ALERT] Erreur:", error);
showToast("Erreur lors de la suppression de l'alerte", "error");
}
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
setAlertToDelete(null);
};
// ============================================
// 🎨 RENDER
// ============================================
if (loading) {
return (
<div className="alerts-page-container">
<div className="alerts-loading">
<p>Chargement des alertes...</p>
</div>
</div>
);
}
return (
<>
<Sidebar />
<div className="alerts-page-container">
{/* Header */}
<div className="alerts-page-header">
<h1>
<AlertTriangle size={40} />
Gestion des Alertes
</h1>
<p className="alerts-page-subtitle">
Surveillance en temps réel des alertes police
</p>
</div>
{/* Stats Grid */}
<div className="alerts-stats-grid">
<div className="alert-stat-card">
<div className="alert-stat-icon red">
<AlertTriangle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Total Alertes</p>
<h3 className="alert-stat-value">{stats.total}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon orange">
<Shield size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Alertes Actives</p>
<h3 className="alert-stat-value">{stats.active}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon green">
<CheckCircle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Résolues</p>
<h3 className="alert-stat-value">
{stats.resolved}
</h3>
</div>
</div>
</div>
{/* Filters */}
<div className="alerts-filters">
<div className="alerts-search">
<Search size={20} />
<input
type="text"
placeholder="Rechercher par livreur..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="alerts-filter-buttons">
<button
className={`filter-btn-2 ${statusFilter === "all" ? "active" : ""}`}
onClick={() => setStatusFilter("all")}
>
<Filter size={16} />
Toutes ({alerts.length})
</button>
<button
className={`filter-btn-2 ${statusFilter === "true" ? "active" : ""}`}
onClick={() => setStatusFilter("true")}
>
<Shield size={16} />
Actives ({stats.active})
</button>
<button
className={`filter-btn-2 ${statusFilter === "false" ? "active" : ""}`}
onClick={() => setStatusFilter("false")}
>
<CheckCircle size={16} />
Résolues ({stats.resolved})
</button>
</div>
</div>
{/* Alerts List */}
<div className="alerts-section">
<div className="alerts-section-header">
<h2>
<Shield size={24} />
Liste des Alertes
</h2>
<div className="alerts-count-badge">
{filteredAlerts.length} alerte
{filteredAlerts.length > 1 ? "s" : ""}
</div>
</div>
{filteredAlerts.length > 0 ? (
<div className="alerts-list">
{filteredAlerts.map((alert) => {
const priority = determinePriority(alert);
return (
<div
key={alert.id}
className={`alert-card ${alert.status === "true" ? "active" : "resolved"}`}
onClick={() =>
handleViewDetails(alert.id)
}
>
<div className="alert-card-header">
<div className="alert-priority-indicator">
<div
className={`alert-priority-icon ${priority}`}
>
<AlertTriangle size={24} />
</div>
<div className="alert-info">
<p className="alert-id">
Alerte #{alert.id}
</p>
<h3 className="alert-title">
{alert.username}
</h3>
<p className="alert-subtitle">
<Clock size={14} />
Il y a{" "}
{formatTimeAgo(
alert.created_at,
)}
</p>
</div>
</div>
<div
className={`alert-status-badge ${alert.status === "true" ? "active" : "resolved"}`}
>
{alert.status === "true" ? (
<AlertTriangle size={16} />
) : (
<CheckCircle size={16} />
)}
{getStatusLabel(alert.status)}
</div>
</div>
<div className="alert-details">
<div className="alert-detail-item">
<User size={18} />
<div className="alert-detail-content">
<p className="alert-detail-label">
Livreur
</p>
<p className="alert-detail-value">
{alert.username}
</p>
</div>
</div>
<div className="alert-detail-item">
<Clock size={18} />
<div className="alert-detail-content">
<p className="alert-detail-label">
Durée
</p>
<p className="alert-detail-value">
{formatDuration(
alert.created_at,
)}
</p>
</div>
</div>
</div>
<div className="alert-actions">
<button
className="alert-btn alert-btn-outline"
onClick={(e) => {
e.stopPropagation();
handleViewDetails(alert.id);
}}
>
<Eye size={18} />
Détails
</button>
<button
className="alert-btn alert-btn-danger"
onClick={(e) =>
handleDeleteClick(
alert.id,
e,
)
}
>
<Trash2 size={18} />
Supprimer
</button>
</div>
</div>
);
})}
</div>
) : (
<div className="alerts-empty-state">
<Shield size={64} />
<h3>Aucune alerte trouvée</h3>
<p>
{searchQuery
? "Essayez de modifier vos critères de recherche"
: statusFilter === "true"
? "Aucune alerte active actuellement"
: "Aucune alerte n'est actuellement enregistrée"}
</p>
</div>
)}
</div>
{/* Details Modal */}
{showDetailsModal && selectedAlert && (
<div
className="modal-overlay"
onClick={() => setShowDetailsModal(false)}
>
<div
className="location-modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="location-modal-header">
<div className="location-header-title">
<AlertTriangle
size={24}
className="pulse-icon"
/>
<h3>
Détails de l'Alerte #{selectedAlert.id}
</h3>
</div>
<button
className="location-close-btn"
onClick={() => setShowDetailsModal(false)}
>
<X size={20} />
</button>
</div>
<div className="location-modal-body">
<div className="location-info-card">
<div className="location-info-header">
<User size={20} />
<span>Informations Livreur</span>
</div>
<div className="location-info-content">
<div className="location-info-row">
<span className="location-label">
Username:
</span>
<span className="location-value">
{selectedAlert.username}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Statut:
</span>
<span className="location-value">
{getStatusLabel(
selectedAlert.status,
)}
</span>
</div>
</div>
</div>
<div className="location-update-card">
<div className="location-update-header">
<Calendar size={20} />
<span>Informations Temporelles</span>
</div>
<div className="location-update-content">
<div className="location-info-row">
<span className="location-label">
Créée le:
</span>
<span className="location-value">
{formatDate(
selectedAlert.created_at,
)}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Durée:
</span>
<span className="location-value">
{formatDuration(
selectedAlert.created_at,
)}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Dernière mise à jour:
</span>
<span className="location-value">
{formatDate(
selectedAlert.updated_at,
)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && (
<div className="modal-overlay" onClick={handleCancelDelete}>
<div
className="delete-confirm-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="delete-modal-header">
<AlertTriangle
size={48}
className="delete-icon"
/>
<h3>Confirmer la suppression</h3>
</div>
<div className="delete-modal-body">
<p>
Êtes-vous sûr de vouloir supprimer l'alerte{" "}
<strong>#{alertToDelete}</strong> ?
</p>
<p className="delete-warning">
Cette action est irréversible.
</p>
</div>
<div className="delete-modal-actions">
<button
className="delete-btn delete-btn-cancel"
onClick={handleCancelDelete}
>
Annuler
</button>
<button
className="delete-btn delete-btn-confirm"
onClick={handleConfirmDelete}
>
<Trash2 size={18} />
Supprimer
</button>
</div>
</div>
</div>
)}
{/* Toast Notifications */}
{toast.show && (
<Toast
message={toast.message}
type={toast.type}
duration={3000}
onClose={handleCloseToast}
/>
)}
</div>
</>
);
}
export default AdminAlerts;
File diff suppressed because it is too large Load Diff
@@ -1,965 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Users,
Search,
Filter,
MapPin,
Clock,
Package,
TrendingUp,
CheckCircle,
AlertCircle,
XCircle,
ChevronDown,
RefreshCw,
Truck,
Eye,
User,
Phone,
ExternalLink,
Copy,
} from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMapLocationDot, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { faApple, faGoogle, faWaze } from "@fortawesome/free-brands-svg-icons";
import "./AdminDeliveryMen.css";
import AdminLayout from "../../components/AdminLayout";
import {
getAllDeliveryPersonsWithDetails,
getDeliveryPersonMapLinks,
isAdminAuthenticated, // ⭐ AJOUT - Vérification authentification
} from "../../api/api_admin";
// Types
interface DeliveryPerson {
id: number;
username: string;
nom: string;
prenom: string;
telephone: string;
status: "available" | "busy" | "offline";
location: {
latitude: number;
longitude: number;
last_update: string;
is_recent: boolean;
};
stats: {
total_deliveries: number;
completed_today: number;
queue_size: number;
current_command: number | null;
};
}
interface GlobalStats {
total: number;
available: number;
busy: number;
offline: number;
active_deliveries: number;
}
type FilterStatus = "all" | "available" | "busy" | "offline";
function AdminDeliverymen() {
const navigate = useNavigate();
const [deliveryPersons, setDeliveryPersons] = useState<DeliveryPerson[]>(
[],
);
const [filteredDeliveryPersons, setFilteredDeliveryPersons] = useState<
DeliveryPerson[]
>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [filterStatus, setFilterStatus] = useState<FilterStatus>("all");
const [showFilters, setShowFilters] = useState(false);
const [selectedDeliveryPerson, setSelectedDeliveryPerson] =
useState<DeliveryPerson | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [mapLinks, setMapLinks] = useState<any>(null);
const [loadingMapLinks, setLoadingMapLinks] = useState(false);
const [copiedCoords, setCopiedCoords] = useState(false);
// Stats globales
const [stats, setStats] = useState<GlobalStats>({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0,
});
// ============================================
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isAdminAuthenticated()) {
console.log(
"❌ [AdminDeliveryMen] 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(
"❌ [AdminDeliveryMen] Session admin expirée, redirection vers /login-admin/admin",
);
navigate("/login-admin/admin", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// Récupération des livreurs via API
useEffect(() => {
fetchDeliveryPersons();
}, []);
const fetchDeliveryPersons = async () => {
// ✅ Vérifier l'auth avant de charger les données
if (!isAdminAuthenticated()) {
console.log("❌ [fetchDeliveryPersons] Admin non authentifié");
navigate("/login-admin/admin", { replace: true });
return;
}
setLoading(true);
try {
console.log("[DELIVERY_PERSONS] Récupération des livreurs...");
const result = await getAllDeliveryPersonsWithDetails();
if (!result.success) {
console.error("[DELIVERY_PERSONS] Erreur:", result);
setDeliveryPersons([]);
setFilteredDeliveryPersons([]);
setStats({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0,
});
setLoading(false);
return;
}
console.log("[DELIVERY_PERSONS] Données reçues:", result);
setDeliveryPersons(result.deliveryPersons);
setFilteredDeliveryPersons(result.deliveryPersons);
setStats(result.stats);
console.log(
"[DELIVERY_PERSONS] Données chargées:",
result.count,
"livreurs",
);
} catch (error) {
console.error("[DELIVERY_PERSONS] Erreur chargement:", error);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [AdminDeliveryMen] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin/admin", { replace: true });
return;
}
setDeliveryPersons([]);
setFilteredDeliveryPersons([]);
setStats({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0,
});
} finally {
setLoading(false);
}
};
// Filtrage
useEffect(() => {
let filtered = deliveryPersons;
if (filterStatus !== "all") {
filtered = filtered.filter((d) => d.status === filterStatus);
}
if (searchTerm) {
filtered = filtered.filter(
(d) =>
d.username
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
d.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
d.prenom.toLowerCase().includes(searchTerm.toLowerCase()) ||
d.telephone.includes(searchTerm),
);
}
setFilteredDeliveryPersons(filtered);
}, [searchTerm, filterStatus, deliveryPersons]);
// Charger les liens GPS
const loadMapLinks = async (username: string) => {
setLoadingMapLinks(true);
setMapLinks(null);
try {
console.log("[MAP_LINKS] Chargement pour:", username);
const result = await getDeliveryPersonMapLinks(username);
if (result.success) {
console.log("[MAP_LINKS] Liens chargés:", result);
setMapLinks(result);
} else {
console.error("[MAP_LINKS] Erreur:", result);
}
} catch (error) {
console.error("[MAP_LINKS] Erreur:", error);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [AdminDeliveryMen] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin/admin", { replace: true });
}
} finally {
setLoadingMapLinks(false);
}
};
// Ouvrir modal avec liens GPS
const handleViewDetails = async (person: DeliveryPerson) => {
setSelectedDeliveryPerson(person);
setShowDetailsModal(true);
setMapLinks(null);
setCopiedCoords(false);
await loadMapLinks(person.username);
};
const handleRefresh = async () => {
setRefreshing(true);
await fetchDeliveryPersons();
setRefreshing(false);
};
const handleCopyCoordinates = () => {
if (mapLinks?.location) {
const coords = `${mapLinks.location.latitude}, ${mapLinks.location.longitude}`;
navigator.clipboard.writeText(coords);
setCopiedCoords(true);
setTimeout(() => setCopiedCoords(false), 2000);
}
};
const getStatusLabel = (status: string) => {
const labels = {
available: "Disponible",
busy: "Occupé",
offline: "Hors ligne",
};
return labels[status as keyof typeof labels] || status;
};
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-users">
<p>Chargement des livreurs...</p>
</div>
</div>
</AdminLayout>
);
}
return (
<AdminLayout>
<div className="admin-container">
{/* Header */}
<div className="orders-header">
<div className="header-content">
<h1>Gestion des Livreurs</h1>
</div>
<button
className="export-button"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw
size={18}
className={refreshing ? "spin-animation" : ""}
/>
<span>Actualiser</span>
</button>
</div>
{/* Stats rapides */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon purple">
<Users size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Total Livreurs</p>
<h3 className="stat-value">{stats.total}</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon green">
<CheckCircle size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Disponibles</p>
<h3 className="stat-value">{stats.available}</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon orange">
<Truck size={24} />
</div>
<div className="stat-content">
<p className="stat-label">En Livraison</p>
<h3 className="stat-value">{stats.busy}</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon blue">
<Package size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Livraisons Actives</p>
<h3 className="stat-value">
{stats.active_deliveries}
</h3>
</div>
</div>
</div>
{/* Contrôles */}
<div className="orders-controls">
<div className="search-bar">
<Search size={20} />
<input
type="text"
placeholder="Rechercher par nom, username, téléphone..."
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 */}
{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 === "available"
? "active"
: ""
}
onClick={() => setFilterStatus("available")}
>
Disponibles ({getStatusCount("available")})
</button>
<button
className={
filterStatus === "busy" ? "active" : ""
}
onClick={() => setFilterStatus("busy")}
>
Occupés ({getStatusCount("busy")})
</button>
<button
className={
filterStatus === "offline"
? "active"
: ""
}
onClick={() => setFilterStatus("offline")}
>
Hors ligne ({getStatusCount("offline")})
</button>
</div>
</div>
</div>
)}
{/* Liste des livreurs */}
<div className="orders-list">
{filteredDeliveryPersons.length === 0 ? (
<div className="empty-state">
<Users size={64} />
<h3>Aucun livreur trouvé</h3>
<p>
Aucun livreur ne correspond à vos critères de
recherche
</p>
</div>
) : (
filteredDeliveryPersons.map((person) => (
<div key={person.id} className="order-card">
<div className="order-header">
<div className="order-number">
<User size={20} />
<span>
{person.prenom} {person.nom}
</span>
</div>
<div
className={`order-status status-${person.status === "available" ? "approved" : person.status === "busy" ? "en_route" : "cancelled"}`}
>
{person.status === "available" && (
<CheckCircle size={16} />
)}
{person.status === "busy" && (
<Truck size={16} />
)}
{person.status === "offline" && (
<XCircle size={16} />
)}
<span>
{getStatusLabel(person.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">
Username
</span>
<span className="info-value">
{person.username}
</span>
</div>
</div>
<div className="info-item">
<Phone size={16} />
<div className="info-details">
<span className="info-label">
Téléphone
</span>
<span className="info-value">
{person.telephone}
</span>
</div>
</div>
<div className="info-item">
<Package size={16} />
<div className="info-details">
<span className="info-label">
Livraisons totales
</span>
<span className="info-value">
{
person.stats
.total_deliveries
}
</span>
</div>
</div>
<div className="info-item">
<TrendingUp size={16} />
<div className="info-details">
<span className="info-label">
Complétées aujourd'hui
</span>
<span className="info-value">
{
person.stats
.completed_today
}
</span>
</div>
</div>
<div className="info-item">
<Clock size={16} />
<div className="info-details">
<span className="info-label">
File d'attente
</span>
<span className="info-value">
{person.stats.queue_size}{" "}
commande(s)
</span>
</div>
</div>
<div className="info-item">
<MapPin size={16} />
<div className="info-details">
<span className="info-label">
Position GPS
</span>
<span className="info-value">
{person.location.latitude.toFixed(
4,
)}
°,{" "}
{person.location.longitude.toFixed(
4,
)}
°
</span>
</div>
</div>
</div>
{person.stats.current_command && (
<div className="order-items">
<span className="items-label">
Commande en cours:
</span>
<span className="item-tag">
CMD-
{person.stats.current_command
.toString()
.padStart(6, "0")}
</span>
</div>
)}
</div>
<div className="order-footer">
<button
className="view-details-button"
onClick={() =>
handleViewDetails(person)
}
>
<Eye size={18} />
<span>Détails</span>
</button>
</div>
</div>
))
)}
</div>
{/* Modal détails livreur */}
{showDetailsModal && selectedDeliveryPerson && (
<>
<div
className="modal-overlay"
onClick={() => setShowDetailsModal(false)}
/>
<div className="order-details-modal">
<div className="modal-header">
<h2>Détails du livreur</h2>
<button
className="close-modal"
onClick={() => setShowDetailsModal(false)}
>
<XCircle size={24} />
</button>
</div>
<div className="modal-content">
<div className="detail-section">
<h3>Informations personnelles</h3>
<div className="detail-grid">
<div className="detail-item">
<span className="detail-label">
Nom complet
</span>
<span className="detail-value">
{selectedDeliveryPerson.prenom}{" "}
{selectedDeliveryPerson.nom}
</span>
</div>
<div className="detail-item">
<span className="detail-label">
Username
</span>
<span className="detail-value">
{
selectedDeliveryPerson.username
}
</span>
</div>
<div className="detail-item">
<span className="detail-label">
Téléphone
</span>
<span className="detail-value">
{
selectedDeliveryPerson.telephone
}
</span>
</div>
<div className="detail-item">
<span className="detail-label">
Statut
</span>
<div
className={`order-status status-${selectedDeliveryPerson.status === "available" ? "approved" : selectedDeliveryPerson.status === "busy" ? "en_route" : "cancelled"}`}
>
{selectedDeliveryPerson.status ===
"available" && (
<CheckCircle size={16} />
)}
{selectedDeliveryPerson.status ===
"busy" && (
<Truck size={16} />
)}
{selectedDeliveryPerson.status ===
"offline" && (
<XCircle size={16} />
)}
<span>
{getStatusLabel(
selectedDeliveryPerson.status,
)}
</span>
</div>
</div>
</div>
</div>
<div className="detail-section">
<h3>Statistiques</h3>
<div className="detail-grid">
<div className="detail-item">
<span className="detail-label">
Total livraisons
</span>
<span className="detail-value">
{
selectedDeliveryPerson.stats
.total_deliveries
}
</span>
</div>
<div className="detail-item">
<span className="detail-label">
Complétées aujourd'hui
</span>
<span className="detail-value">
{
selectedDeliveryPerson.stats
.completed_today
}
</span>
</div>
<div className="detail-item">
<span className="detail-label">
File d'attente
</span>
<span className="detail-value">
{
selectedDeliveryPerson.stats
.queue_size
}{" "}
commande(s)
</span>
</div>
{selectedDeliveryPerson.stats
.current_command && (
<div className="detail-item">
<span className="detail-label">
Commande en cours
</span>
<span className="detail-value">
CMD-
{selectedDeliveryPerson.stats.current_command
.toString()
.padStart(6, "0")}
</span>
</div>
)}
</div>
</div>
<div className="detail-section">
<h3>Position GPS</h3>
<div
className="gps-info"
style={{ marginTop: "1rem" }}
>
<div className="gps-coordinates">
<div className="coordinate">
<span className="coord-label">
Latitude:
</span>
<span className="coord-value">
{selectedDeliveryPerson.location.latitude.toFixed(
6,
)}
</span>
</div>
<div className="coordinate">
<span className="coord-label">
Longitude:
</span>
<span className="coord-value">
{selectedDeliveryPerson.location.longitude.toFixed(
6,
)}
</span>
</div>
</div>
</div>
</div>
{/* SECTION NAVIGATION GPS */}
<div className="detail-section">
<h3>
<FontAwesomeIcon
icon={faMapLocationDot}
/>{" "}
Navigation GPS
</h3>
{loadingMapLinks ? (
<div className="map-links-loading">
<FontAwesomeIcon
icon={faSpinner}
size="2x"
className="spin-animation"
/>
<span>
Chargement des liens GPS...
</span>
</div>
) : mapLinks?.success ? (
<>
{/* Liens de navigation */}
<div className="map-links-grid">
{/* Google Maps - Vue */}
<a
href={
mapLinks.map_links
.google_maps
}
target="_blank"
rel="noopener noreferrer"
className="map-link-button google"
>
<FontAwesomeIcon
icon={faGoogle}
className="map-icon"
/>
<div className="link-content">
<span className="link-title">
Google Maps
</span>
<span className="link-subtitle">
Voir la position
</span>
</div>
<ExternalLink
size={16}
className="external-icon"
/>
</a>
{/* Waze */}
<a
href={
mapLinks.map_links.waze
}
target="_blank"
rel="noopener noreferrer"
className="map-link-button waze"
>
<FontAwesomeIcon
icon={faWaze}
className="map-icon"
/>
<div className="link-content">
<span className="link-title">
Waze
</span>
<span className="link-subtitle">
Navigation GPS
</span>
</div>
<ExternalLink
size={16}
className="external-icon"
/>
</a>
{/* Apple Maps */}
<a
href={
mapLinks.map_links
.apple_maps
}
target="_blank"
rel="noopener noreferrer"
className="map-link-button apple"
>
<FontAwesomeIcon
icon={faApple}
className="map-icon"
/>
<div className="link-content">
<span className="link-title">
Apple Maps
</span>
<span className="link-subtitle">
Ouvrir dans Plans
</span>
</div>
<ExternalLink
size={16}
className="external-icon"
/>
</a>
{/* OpenStreetMap */}
<a
href={
mapLinks.map_links
.openstreetmap
}
target="_blank"
rel="noopener noreferrer"
className="map-link-button osm"
>
<MapPin
size={24}
className="map-icon"
/>
<div className="link-content">
<span className="link-title">
OpenStreetMap
</span>
<span className="link-subtitle">
Carte open source
</span>
</div>
<ExternalLink
size={16}
className="external-icon"
/>
</a>
</div>
{/* Copie des coordonnées */}
<div className="coordinates-copy">
<button
onClick={
handleCopyCoordinates
}
className={`copy-coords-button ${copiedCoords ? "copied" : ""}`}
>
<Copy size={16} />
<span>
{copiedCoords
? "Coordonnées copiées !"
: "Copier coordonnées"}
</span>
</button>
<span className="coords-display">
{mapLinks.location.latitude.toFixed(
6,
)}
,{" "}
{mapLinks.location.longitude.toFixed(
6,
)}
</span>
</div>
</>
) : (
<div className="map-links-error">
<AlertCircle size={48} />
<h4>Position GPS non disponible</h4>
<p>
Le livreur n'a pas encore
partagé sa position GPS ou
celle-ci est trop ancienne.
</p>
</div>
)}
</div>
</div>
<div className="modal-actions">
<button
className="action-button secondary"
onClick={() => setShowDetailsModal(false)}
>
<XCircle size={18} />
<span>Fermer</span>
</button>
<button
className="action-button primary"
onClick={handleRefresh}
>
<RefreshCw size={18} />
<span>Actualiser</span>
</button>
</div>
</div>
</>
)}
</div>
</AdminLayout>
);
}
export default AdminDeliverymen;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,708 +0,0 @@
/* ============================================
ProductsPage.css - STYLE PAGE PRODUITS ADMIN
============================================ */
.admin-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1400px;
margin: 0 auto;
}
/* ============================================
HEADER
============================================ */
.products-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: clamp(1.5rem, 4vw, 2rem);
flex-wrap: wrap;
gap: 1rem;
}
.header-content {
text-align: center;
flex: 1;
}
.header-content h1 {
color: white;
font-size: clamp(2rem, 5vw, 2.5rem);
margin: 0 auto 0.3rem auto;
font-weight: bold;
letter-spacing: -0.5px;
background: linear-gradient(to right, #7c3aed, #10b981);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-subtitle {
color: #888;
font-size: clamp(0.9rem, 2.5vw, 1rem);
margin: 0;
}
.add-product-button {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.8rem 1.5rem;
background: linear-gradient(135deg, #7c3aed, #6d28d9);
border: 1px solid rgba(124, 58, 237, 0.3);
border-radius: 12px;
color: white;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.add-product-button:hover {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4);
}
/* ============================================
STATS RAPIDES
============================================ */
.products-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: clamp(1.5rem, 4vw, 2rem);
}
.stat-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-item:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-color: rgba(124, 58, 237, 0.3);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.2);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-icon.purple {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
color: #7c3aed;
}
.stat-icon.green {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.1));
color: #10b981;
}
.stat-icon.blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1));
color: #3b82f6;
}
.stat-icon.orange {
background: linear-gradient(135deg, rgba(251, 146, 60, 0.2), rgba(249, 115, 22, 0.1));
color: #fb923c;
}
.stat-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.stat-value {
color: white;
font-size: 1.8rem;
font-weight: bold;
line-height: 1;
}
.stat-label {
color: #888;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
/* ============================================
CONTRÔLES (RECHERCHE + FILTRES)
============================================ */
.products-controls {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.search-bar {
flex: 1;
min-width: 280px;
position: relative;
display: flex;
align-items: center;
gap: 0.8rem;
padding: 0 1.2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-bar:focus-within {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-color: rgba(124, 58, 237, 0.4);
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
}
.search-bar svg {
color: #888;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: white;
font-size: 0.95rem;
padding: 1rem 0;
}
.search-bar input::placeholder {
color: #666;
}
.filter-toggle {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.8rem 1.5rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
color: #888;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.filter-toggle:hover,
.filter-toggle.active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(109, 40, 217, 0.08));
border-color: rgba(124, 58, 237, 0.3);
color: #7c3aed;
}
.filter-toggle svg.rotated {
transform: rotate(180deg);
}
/* ============================================
PANEL DE FILTRES
============================================ */
.filters-panel {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.filter-group label {
display: block;
color: white;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
}
.filter-buttons button {
padding: 0.6rem 1.2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
color: #888;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.filter-buttons button:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-color: rgba(124, 58, 237, 0.2);
color: white;
}
.filter-buttons button.active {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
border-color: rgba(124, 58, 237, 0.4);
color: #7c3aed;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
}
/* ============================================
LISTE DES PRODUITS
============================================ */
.products-list {
display: grid;
gap: 1.5rem;
}
.product-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.product-card:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-color: rgba(124, 58, 237, 0.3);
box-shadow: 0 8px 32px rgba(124, 58, 237, 0.2);
transform: translateY(-2px);
}
/* ============================================
HEADER DU PRODUIT
============================================ */
.product-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.product-identity {
display: flex;
align-items: center;
gap: 1rem;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 12px;
overflow: hidden;
flex-shrink: 0;
background: rgba(0, 0, 0, 0.3);
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-image-placeholder {
width: 80px;
height: 80px;
border-radius: 12px;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
display: flex;
align-items: center;
justify-content: center;
color: #7c3aed;
flex-shrink: 0;
}
.product-name-info {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.product-name {
color: white;
font-size: 1.2rem;
font-weight: 700;
margin: 0;
}
.product-category {
color: #888;
font-size: 0.9rem;
font-weight: 500;
margin: 0;
}
.product-stock {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.product-stock.in-stock {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.1));
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.product-stock.out-of-stock {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1));
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* ============================================
CONTENT DU PRODUIT
============================================ */
.product-content {
margin-bottom: 1.5rem;
}
.product-description {
color: #aaa;
font-size: 0.95rem;
line-height: 1.6;
margin: 0 0 1.2rem 0;
}
.product-prices {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.prices-label {
display: flex;
align-items: center;
gap: 0.5rem;
color: #888;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.8rem;
}
.prices-label svg {
color: #7c3aed;
}
.prices-list {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
}
.price-tag {
padding: 0.4rem 0.8rem;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(109, 40, 217, 0.08));
border: 1px solid rgba(124, 58, 237, 0.2);
border-radius: 6px;
color: #7c3aed;
font-size: 0.85rem;
font-weight: 600;
}
.product-media-count {
display: flex;
align-items: center;
gap: 0.6rem;
color: #888;
font-size: 0.85rem;
font-weight: 500;
}
.product-media-count svg {
color: #7c3aed;
}
/* ============================================
FOOTER DU PRODUIT (ACTIONS)
============================================ */
.product-footer {
display: flex;
gap: 0.8rem;
padding-top: 1.2rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.7rem 1.3rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-btn.view {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(109, 40, 217, 0.08));
border: 1px solid rgba(124, 58, 237, 0.3);
color: #7c3aed;
}
.action-btn.view:hover {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
border-color: rgba(124, 58, 237, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.3);
}
.action-btn.edit {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(37, 99, 235, 0.08));
border: 1px solid rgba(59, 130, 246, 0.3);
color: #3b82f6;
}
.action-btn.edit:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(37, 99, 235, 0.1));
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
}
.action-btn.delete {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.08));
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
}
.action-btn.delete:hover {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1));
border-color: rgba(239, 68, 68, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.3);
}
/* ============================================
EMPTY STATE
============================================ */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #666;
}
.empty-state svg {
color: #444;
margin-bottom: 1.5rem;
}
.empty-state h3 {
color: white;
font-size: 1.5rem;
margin: 0 0 0.5rem 0;
font-weight: 600;
}
.empty-state p {
color: #888;
font-size: 1rem;
margin: 0;
}
/* ============================================
LOADING
============================================ */
.loading-products {
text-align: center;
padding: 4rem 2rem;
}
.loading-products p {
font-size: 1.2rem;
color: #666;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 1024px) {
.products-stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.products-header {
flex-direction: column;
align-items: flex-start;
}
.add-product-button {
width: 100%;
justify-content: center;
}
.products-stats {
grid-template-columns: repeat(2, 1fr);
}
.products-controls {
flex-direction: column;
}
.search-bar {
width: 100%;
}
.filter-toggle {
width: 100%;
justify-content: center;
}
.product-footer {
flex-direction: column;
}
.action-btn {
width: 100%;
}
}
@media (max-width: 480px) {
.products-stats {
grid-template-columns: 1fr;
}
.stat-item {
padding: 1rem;
}
.filter-buttons {
flex-direction: column;
}
.filter-buttons button {
width: 100%;
}
.product-card {
padding: 1.2rem;
}
.product-identity {
flex-direction: column;
align-items: flex-start;
}
}
@media (hover: none) {
.product-card:hover {
transform: none;
}
.add-product-button:hover,
.action-btn:hover {
transform: none;
}
}
@media (max-width: 768px) {
.orders-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.header-content {
width: 100%;
order: 1; /* Le titre en premier */
}
.export-button,
.add-product-button,
.add-user-button,
.header-actions {
width: 100%;
order: 2; /* Les boutons en second */
}
.export-button,
.add-product-button,
.add-user-button {
justify-content: center;
}
.header-actions {
justify-content: center;
}
}
@media (max-width: 480px) {
.header-content h1 {
font-size: 1.5rem;
}
.header-subtitle {
font-size: 0.85rem;
}
}
@@ -1,751 +0,0 @@
// ============================================
// pages/admin/AdminProduct.tsx - AVEC SIDEBAR ET PROTECTION
// ============================================
// ✅ Page principale de gestion des produits (Admin)
// ✅ CRUD complet avec multipart/form-data
// ✅ Sidebar intégrée
// ✅ Layout responsive
// ✅ Protection authentification admin
// ✅ Modales de confirmation modernes
// ✅ Système de Toast pour les notifications
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Package,
Plus,
Search,
Filter,
ChevronDown,
Eye,
Edit,
Trash2,
Image,
Video,
DollarSign,
Box,
} from "lucide-react";
import {
getAllProductsAdmin,
deleteProductAdmin,
isAdminAuthenticated,
} from "../../api/api_admin";
import type { Product } from "../../api/api_admin_types";
import ProductCreateModal from "../../components/ProductCreateModal";
import ProductDetailsModal from "../../components/ProductDetailsModal";
import ProductEditModal from "../../components/ProductEditModal";
import Sidebar from "../../components/Sidebar";
import ConfirmModal from "../../components/ConfirmModal";
import Toast from "../../components/Toast";
import "./AdminProduct.css";
const AdminProduct: React.FC = () => {
const navigate = useNavigate();
// ============================================
// 📝 STATE
// ============================================
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [showFilters, setShowFilters] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<string | null>(
null,
);
// Modals
const [showCreateModal, setShowCreateModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(
null,
);
// Confirm Modal
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [productToDelete, setProductToDelete] = useState<Product | null>(
null,
);
const [isDeleting, setIsDeleting] = useState(false);
// Toast
const [toastMessage, setToastMessage] = useState<string>("");
const [toastType, setToastType] = useState<
"success" | "error" | "warning" | "info"
>("success");
const [showToast, setShowToast] = useState(false);
// ============================================
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isAdminAuthenticated()) {
console.log(
"❌ [AdminProduct] 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(
"❌ [AdminProduct] Session admin expirée, redirection vers /login-admin/admin",
);
navigate("/login-admin/admin", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// ============================================
// 🍞 TOAST HELPER
// ============================================
const showToastMessage = (
message: string,
type: "success" | "error" | "warning" | "info" = "success",
) => {
setToastMessage(message);
setToastType(type);
setShowToast(true);
};
// ============================================
// 🔄 CHARGEMENT DES PRODUITS
// ============================================
const loadProducts = async () => {
if (!isAdminAuthenticated()) {
console.log("❌ [loadProducts] Admin non authentifié");
navigate("/login-admin/admin", { replace: true });
return;
}
try {
setLoading(true);
console.log("📦 [PRODUCTS_PAGE] Chargement des produits...");
const response = await getAllProductsAdmin();
if (response.success && response.data) {
setProducts(response.data);
console.log(
`✅ [PRODUCTS_PAGE] ${response.data.length} produits chargés`,
);
} else {
console.error("❌ [PRODUCTS_PAGE] Erreur:", response.error);
setProducts([]);
showToastMessage(
"Erreur lors du chargement des produits",
"error",
);
}
} catch (error) {
console.error("❌ [PRODUCTS_PAGE] Erreur chargement:", error);
if (error instanceof Error && error.message.includes("401")) {
console.log("🔓 [AdminProduct] Token invalide - Redirection");
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin/admin", { replace: true });
}
setProducts([]);
showToastMessage("Erreur lors du chargement des produits", "error");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadProducts();
}, []);
// ============================================
// 🔍 FILTRAGE
// ============================================
const filteredProducts = products.filter((product) => {
const matchesSearch =
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchesCategory =
!selectedCategory || product.category === selectedCategory;
return matchesSearch && matchesCategory;
});
// ============================================
// 📊 STATISTIQUES
// ============================================
const stats = {
total: products.length,
weedHash: products.filter((p) => p.category === "weed&hash").length,
zipette: products.filter((p) => p.category === "zipette&co").length,
gros: products.filter((p) => p.category === "gros&semi").length,
};
// ============================================
// ⚡ ACTIONS
// ============================================
const handleView = (product: Product) => {
setSelectedProduct(product);
setShowDetailsModal(true);
};
const handleEdit = (product: Product) => {
setSelectedProduct(product);
setShowEditModal(true);
};
const handleDeleteClick = (product: Product) => {
setProductToDelete(product);
setShowConfirmDelete(true);
};
const handleConfirmDelete = async () => {
if (!productToDelete) return;
setIsDeleting(true);
try {
console.log(
"🗑️ [PRODUCTS_PAGE] Suppression produit:",
productToDelete.id,
);
const response = await deleteProductAdmin(productToDelete.id!);
if (response.success) {
console.log("✅ [PRODUCTS_PAGE] Produit supprimé");
showToastMessage(
`Produit "${productToDelete.name}" supprimé avec succès`,
"success",
);
setShowConfirmDelete(false);
setProductToDelete(null);
await loadProducts();
} else {
showToastMessage(
"Erreur lors de la suppression: " + response.error,
"error",
);
}
} catch (error) {
console.error("❌ [PRODUCTS_PAGE] Erreur suppression:", error);
if (error instanceof Error && error.message.includes("401")) {
console.log("🔓 [AdminProduct] Token invalide - Redirection");
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-admin/admin", { replace: true });
return;
}
showToastMessage(
"Erreur lors de la suppression du produit",
"error",
);
} finally {
setIsDeleting(false);
}
};
const handleCreateSuccess = () => {
setShowCreateModal(false);
showToastMessage("Produit créé avec succès", "success");
loadProducts();
};
const handleEditSuccess = () => {
setShowEditModal(false);
showToastMessage("Produit modifié avec succès", "success");
loadProducts();
};
// ============================================
// 🎨 HELPER - CATÉGORIE LABEL
// ============================================
const getCategoryLabel = (category: string): string => {
switch (category) {
case "weed&hash":
return "Weed & Hash";
case "zipette&co":
return "Zipette & Co";
case "gros&semi":
return "Gros & Semi";
default:
return category;
}
};
// ============================================
// 🎨 RENDER
// ============================================
return (
<div className="admin-layout">
{/* ============================================
SIDEBAR
============================================ */}
<Sidebar />
{/* ============================================
MAIN CONTENT
============================================ */}
<div className="admin-main-content">
<div className="admin-container">
{loading ? (
<div className="loading-products">
<p>Chargement des produits...</p>
</div>
) : (
<>
{/* ============================================
HEADER
============================================ */}
<div className="products-header">
<div className="header-content">
<h1>Gestion des Produits</h1>
</div>
<button
className="add-product-button"
onClick={() => setShowCreateModal(true)}
>
<Plus size={20} />
Nouveau Produit
</button>
</div>
{/* ============================================
STATS RAPIDES
============================================ */}
<div className="products-stats">
<div className="stat-item">
<div className="stat-icon purple">
<Package size={24} />
</div>
<div className="stat-info">
<div className="stat-value">
{stats.total}
</div>
<div className="stat-label">
Total Produits
</div>
</div>
</div>
<div className="stat-item">
<div className="stat-icon green">
<Box size={24} />
</div>
<div className="stat-info">
<div className="stat-value">
{stats.weedHash}
</div>
<div className="stat-label">
Weed & Hash
</div>
</div>
</div>
<div className="stat-item">
<div className="stat-icon blue">
<Box size={24} />
</div>
<div className="stat-info">
<div className="stat-value">
{stats.zipette}
</div>
<div className="stat-label">
Zipette & Co
</div>
</div>
</div>
<div className="stat-item">
<div className="stat-icon orange">
<Box size={24} />
</div>
<div className="stat-info">
<div className="stat-value">
{stats.gros}
</div>
<div className="stat-label">
Gros & Semi
</div>
</div>
</div>
</div>
{/* ============================================
CONTRÔLES (RECHERCHE + FILTRES)
============================================ */}
<div className="products-controls">
<div className="search-bar">
<Search size={20} />
<input
type="text"
placeholder="Rechercher un produit..."
value={searchTerm}
onChange={(e) =>
setSearchTerm(e.target.value)
}
/>
</div>
<button
className={`filter-toggle ${showFilters ? "active" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<Filter size={20} />
Filtres
<ChevronDown
size={18}
className={showFilters ? "rotated" : ""}
/>
</button>
</div>
{/* ============================================
PANEL DE FILTRES
============================================ */}
{showFilters && (
<div className="filters-panel">
<div className="filter-group">
<label>Catégorie</label>
<div className="filter-buttons">
<button
className={
selectedCategory === null
? "active"
: ""
}
onClick={() =>
setSelectedCategory(null)
}
>
Toutes
</button>
<button
className={
selectedCategory ===
"weed&hash"
? "active"
: ""
}
onClick={() =>
setSelectedCategory(
"weed&hash",
)
}
>
Weed & Hash
</button>
<button
className={
selectedCategory ===
"zipette&co"
? "active"
: ""
}
onClick={() =>
setSelectedCategory(
"zipette&co",
)
}
>
Zipette & Co
</button>
<button
className={
selectedCategory ===
"gros&semi"
? "active"
: ""
}
onClick={() =>
setSelectedCategory(
"gros&semi",
)
}
>
Gros & Semi
</button>
</div>
</div>
</div>
)}
{/* ============================================
LISTE DES PRODUITS
============================================ */}
{filteredProducts.length === 0 ? (
<div className="empty-state">
<Package size={64} />
<h3>Aucun produit trouvé</h3>
<p>
{searchTerm || selectedCategory
? "Essayez de modifier vos filtres de recherche"
: "Commencez par créer votre premier produit"}
</p>
</div>
) : (
<div className="products-list">
{filteredProducts.map((product) => (
<div
key={product.id}
className="product-card"
>
{/* Header */}
<div className="product-header">
<div className="product-identity">
{product.media &&
product.media.length > 0 ? (
<div className="product-image">
<img
src={`${product.media[0].url}`}
alt={
product.name
}
/>
</div>
) : (
<div className="product-image-placeholder">
<Package
size={32}
/>
</div>
)}
<div className="product-name-info">
<h3 className="product-name">
{product.name}
</h3>
<p className="product-category">
{getCategoryLabel(
product.category,
)}
</p>
</div>
</div>
<div
className={`product-stock ${product.stock > 0 ? "in-stock" : "out-of-stock"}`}
>
<Box size={18} />
{product.stock}g en stock
</div>
</div>
{/* Content */}
<div className="product-content">
<p className="product-description">
{product.description
.length > 150
? product.description.substring(
0,
150,
) + "..."
: product.description}
</p>
{/* Prix */}
<div className="product-prices">
<div className="prices-label">
<DollarSign size={16} />
Tarifs
</div>
<div className="prices-list">
{product.prices?.map(
(price, idx) => (
<div
key={idx}
className="price-tag"
>
{
price.quantity
}
g {" "}
{
price.price
}
</div>
),
)}
</div>
</div>
{/* Médias */}
{product.media &&
product.media.length >
0 && (
<div className="product-media-count">
<Image size={16} />
{
product.media.filter(
(m) =>
m.type ===
"image",
).length
}{" "}
image
{product.media.filter(
(m) =>
m.type ===
"image",
).length > 1
? "s"
: ""}
{product.media.filter(
(m) =>
m.type ===
"video",
).length > 0 && (
<>
{" • "}
<Video
size={
16
}
/>
{
product.media.filter(
(
m,
) =>
m.type ===
"video",
).length
}{" "}
vidéo
{product.media.filter(
(m) =>
m.type ===
"video",
).length > 1
? "s"
: ""}
</>
)}
</div>
)}
</div>
{/* Footer - Actions */}
<div className="product-footer">
<button
className="action-btn view"
onClick={() =>
handleView(product)
}
>
<Eye size={18} />
Voir
</button>
<button
className="action-btn edit"
onClick={() =>
handleEdit(product)
}
>
<Edit size={18} />
Modifier
</button>
<button
className="action-btn delete"
onClick={() =>
handleDeleteClick(
product,
)
}
>
<Trash2 size={18} />
Supprimer
</button>
</div>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
{/* ============================================
MODALS
============================================ */}
{showCreateModal && (
<ProductCreateModal
onClose={() => setShowCreateModal(false)}
onSuccess={handleCreateSuccess}
/>
)}
{showDetailsModal && selectedProduct && (
<ProductDetailsModal
product={selectedProduct}
onClose={() => {
setShowDetailsModal(false);
setSelectedProduct(null);
}}
/>
)}
{showEditModal && selectedProduct && (
<ProductEditModal
product={selectedProduct}
onClose={() => {
setShowEditModal(false);
setSelectedProduct(null);
}}
onSuccess={handleEditSuccess}
/>
)}
{/* ============================================
CONFIRM DELETE MODAL
============================================ */}
<ConfirmModal
isOpen={showConfirmDelete}
onClose={() => {
setShowConfirmDelete(false);
setProductToDelete(null);
}}
onConfirm={handleConfirmDelete}
title="Supprimer le produit"
message={`Êtes-vous sûr de vouloir supprimer "${productToDelete?.name}" ? Cette action est irréversible.`}
type="danger"
confirmText="Supprimer"
cancelText="Annuler"
isLoading={isDeleting}
/>
{/* ============================================
TOAST NOTIFICATIONS
============================================ */}
{showToast && (
<Toast
message={toastMessage}
type={toastType}
duration={3000}
onClose={() => setShowToast(false)}
/>
)}
</div>
);
};
export default AdminProduct;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,993 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
MapPin,
Package,
Navigation,
CheckCircle,
Clock,
AlertTriangle,
Star,
User,
DollarSign,
X,
ExternalLink,
Copy,
} from "lucide-react";
import "./CabineDashboard.css";
import ItemsModal from "../../components/Items";
import SidebarCabine from "../../components/SidebarCabine";
import {
getCommandItems,
getDeliverymanLocationForCommand,
getAllDeliveryPersonsWithDetails,
getPenaltiesStats,
isCabineAuthenticated, // ⭐ AJOUT - Vérification authentification
} from "../../api/api_cabine";
import {
getAllCommands,
getCommandCountCompleted,
getCommandCountByStatus,
} from "../../api/api_admin";
interface OrderItem {
id: number;
command_id: number;
product_id: number;
produit: string;
quantite: number;
prix: number;
status: string;
created_at: string;
client_nom?: string;
client_prenom?: string;
client_telephone?: string;
client_username?: string;
command_address?: string;
delivery_address?: string;
livreur_assign?: string;
total_prix?: number;
}
interface DeliveryOrder {
id: number;
orderNumber: string;
clientName: string;
clientPhone: string;
clientPoints: number;
address: string;
status: "pending" | "in_transit" | "delivered";
amount: number;
items: number;
penalty: number;
}
interface ActiveDeliveryPerson {
id: number;
username: string;
nom?: string;
prenom?: string;
status: "available" | "busy" | "offline";
location: {
latitude: number;
longitude: number;
last_update: string;
is_recent: boolean;
};
stats: {
current_command: number | null;
queue_size: number;
};
}
interface DriverStats {
completedToday: number;
pendingDeliveries: number;
totalEarnings: number;
totalPenalties: number;
averageRating: number;
}
function DriverDashboard() {
const navigate = useNavigate();
const [stats, setStats] = useState<DriverStats>({
completedToday: 0,
pendingDeliveries: 0,
totalEarnings: 0,
totalPenalties: 0,
averageRating: 0,
});
const [orders, setOrders] = useState<DeliveryOrder[]>([]);
const [selectedOrder, setSelectedOrder] = useState<DeliveryOrder | null>(
null,
);
const [loading, setLoading] = useState(true);
const [showItemsModal, setShowItemsModal] = useState(false);
const [orderItems, setOrderItems] = useState<OrderItem[]>([]);
const [selectedOrderNumber, setSelectedOrderNumber] = useState<string>("");
const [activeDeliveryPersons, setActiveDeliveryPersons] = useState<
ActiveDeliveryPerson[]
>([]);
const [showLocationModal, setShowLocationModal] = useState(false);
const [deliverymanLocation, setDeliverymanLocation] = useState<any>(null);
// ============================================
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isCabineAuthenticated()) {
console.log(
"❌ [CabineDashboard] Cabine non authentifié, redirection vers /login-cabine/cabine",
);
navigate("/login-cabine/cabine", { replace: true });
}
};
checkAuth();
}, [navigate]);
// ============================================
// 🔐 VÉRIFICATION CONTINUE (toutes les 5 secondes)
// ============================================
useEffect(() => {
const authInterval = setInterval(() => {
if (!isCabineAuthenticated()) {
console.log(
"❌ [CabineDashboard] Session cabine expirée, redirection vers /login-cabine/cabine",
);
navigate("/login-cabines/cabine", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// Fonction pour afficher les items d'une commande
const handleViewItems = async (orderId: number, orderNumber: string) => {
try {
console.log("📦 [VIEW_ITEMS] Récupération items:", orderId);
const result = await getCommandItems(orderId);
console.log("📦 [VIEW_ITEMS] Résultat complet:", result);
console.log("📦 [VIEW_ITEMS] Items:", result.items);
console.log("📦 [VIEW_ITEMS] Nombre items:", result.items?.length);
if (result.success) {
setOrderItems(result.items || []);
setSelectedOrderNumber(orderNumber);
setShowItemsModal(true);
console.log("✅ [VIEW_ITEMS] Items récupérés:", result.items);
} else {
console.error("❌ [VIEW_ITEMS] Échec:", result);
alert("Erreur: " + "Impossible de récupérer les items");
}
} catch (error) {
console.error("❌ [VIEW_ITEMS] Erreur:", error);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [CabineDashboard] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
return;
}
alert("Erreur lors de la récupération des items");
}
};
// Fonction pour voir la position du livreur
const handleViewDeliverymanLocation = async (orderId: number) => {
try {
console.log(
"📍 [VIEW_LOCATION] Récupération position livreur:",
orderId,
);
const result = await getDeliverymanLocationForCommand(orderId);
if (result.success && result.data) {
setDeliverymanLocation(result.data);
setShowLocationModal(true);
} else {
alert(result.message || "Position non disponible");
}
} catch (error) {
console.error("❌ [VIEW_LOCATION] Erreur:", error);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [CabineDashboard] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
return;
}
alert("Erreur lors de la récupération de la position");
}
};
const copyCoordinates = () => {
if (deliverymanLocation) {
const coords = `${deliverymanLocation.deliveryman.location.latitude}, ${deliverymanLocation.deliveryman.location.longitude}`;
navigator.clipboard.writeText(coords);
alert("Coordonnées copiées !");
}
};
const openInGoogleMaps = () => {
if (deliverymanLocation) {
const { latitude, longitude } =
deliverymanLocation.deliveryman.location;
window.open(
`https://www.google.com/maps?q=${latitude},${longitude}`,
"_blank",
);
}
};
const openInWaze = () => {
if (deliverymanLocation) {
const { latitude, longitude } =
deliverymanLocation.deliveryman.location;
window.open(
`https://waze.com/ul?ll=${latitude},${longitude}&navigate=yes`,
"_blank",
);
}
};
useEffect(() => {
const fetchDriverData = async () => {
// ✅ Vérifier l'auth avant de charger les données
if (!isCabineAuthenticated()) {
console.log("❌ [fetchDriverData] Cabine non authentifié");
navigate("/login-cabine/cabine", { replace: true });
return;
}
try {
console.log("📊 [CABINE_DASHBOARD] Chargement des données...");
// ✅ 1. Récupérer les COUNTS directement
const [completedCount, pendingCount] = await Promise.all([
getCommandCountCompleted(),
getCommandCountByStatus(),
]);
console.log("📊 [CABINE_DASHBOARD] Counts récupérés:", {
completedCount,
pendingCount,
});
// ✅ 2. Récupérer les statistiques de pénalités
let totalPenaltiesFromAPI = 0;
try {
const penaltiesResult = await getPenaltiesStats();
console.log(
"⚠️ [CABINE_DASHBOARD] Statistiques pénalités:",
penaltiesResult,
);
if (penaltiesResult.success && penaltiesResult.data) {
totalPenaltiesFromAPI =
penaltiesResult.data.total_penalties || 0;
console.log(
"✅ [CABINE_DASHBOARD] Total pénalités récupéré:",
totalPenaltiesFromAPI,
);
}
} catch (error) {
console.error(
"❌ [CABINE_DASHBOARD] Erreur récupération stats pénalités:",
error,
);
}
// ✅ 3. Récupérer les commandes détaillées
const commandsResult = await getAllCommands();
console.log(
"📦 [CABINE_DASHBOARD] Commandes récupérées:",
commandsResult,
);
let transformedOrders: DeliveryOrder[] = [];
let totalEarnings = 0;
if (
commandsResult.success &&
commandsResult.commands &&
Array.isArray(commandsResult.commands)
) {
transformedOrders = await Promise.all(
commandsResult.commands.map(async (cmd: any) => {
let itemsCount = 0;
let clientInfo: any = null;
try {
const itemsResult = await getCommandItems(
cmd.id,
);
itemsCount = itemsResult.count || 0;
clientInfo = itemsResult.client_info;
} catch (error) {
console.warn(
`⚠️ Impossible de récupérer items pour CMD ${cmd.id}`,
);
}
return {
id: cmd.id,
orderNumber: `CMD-${String(cmd.id).padStart(3, "0")}`,
clientName:
clientInfo?.username || cmd.username,
clientPhone: clientInfo?.telephone || "N/A",
clientPoints: clientInfo?.point || 0,
address: cmd.adresse,
status:
cmd.status === "en_route"
? ("in_transit" as const)
: cmd.status === "livre" ||
cmd.status === "approved"
? ("delivered" as const)
: ("pending" as const),
amount: cmd.total_prix,
items: itemsCount,
penalty: clientInfo?.amende || 0,
};
}),
);
setOrders(transformedOrders);
totalEarnings = transformedOrders
.filter((o) => o.status === "delivered")
.reduce((sum, o) => sum + o.amount, 0);
}
// ✅ 4. Utiliser les COUNTS récupérés
const totalPenalties = totalPenaltiesFromAPI;
setStats({
completedToday: completedCount,
pendingDeliveries: pendingCount,
totalEarnings,
totalPenalties,
averageRating: 4.8,
});
console.log("✅ [CABINE_DASHBOARD] Stats finales:", {
completedToday: completedCount,
pendingDeliveries: pendingCount,
totalEarnings,
totalPenalties,
});
// ✅ 5. Récupérer les livreurs
let deliveryPersonsResult;
try {
deliveryPersonsResult =
await getAllDeliveryPersonsWithDetails();
console.log(
"👥 [CABINE_DASHBOARD] Livreurs récupérés:",
deliveryPersonsResult,
);
} catch (error) {
console.error(
"❌ [CABINE_DASHBOARD] Erreur récupération livreurs:",
error,
);
deliveryPersonsResult = {
success: false,
deliveryPersons: [],
count: 0,
};
}
if (
deliveryPersonsResult.success &&
deliveryPersonsResult.deliveryPersons &&
deliveryPersonsResult.deliveryPersons.length > 0
) {
const allPersons =
deliveryPersonsResult.deliveryPersons.map(
(person: any) => ({
id: person.id,
username: person.username,
nom: person.nom || "",
prenom: person.prenom || "",
status: person.status,
location: person.location,
stats: {
current_command:
person.stats?.current_command || null,
queue_size: person.stats?.queue_size || 0,
},
}),
);
console.log(
"✅ [CABINE_DASHBOARD] Tous les livreurs:",
allPersons,
);
setActiveDeliveryPersons(allPersons);
} else {
setActiveDeliveryPersons([]);
}
} catch (error) {
console.error(
"❌ [CABINE_DASHBOARD] Erreur chargement données:",
error,
);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log(
"🔓 [CabineDashboard] Token invalide - Redirection",
);
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
}
} finally {
setLoading(false);
}
};
fetchDriverData();
}, [navigate]);
const getStatusColor = (status: string) => {
switch (status) {
case "delivered":
return "green";
case "in_transit":
return "blue";
case "pending":
return "orange";
case "available":
return "green";
case "busy":
return "blue";
case "offline":
return "gray";
default:
return "gray";
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case "delivered":
return "Livrée";
case "in_transit":
return "En cours";
case "pending":
return "En attente";
case "available":
return "En ligne";
case "busy":
return "Occupé";
case "offline":
return "Hors ligne";
default:
return "Inconnu";
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "available":
return <CheckCircle size={16} />;
case "busy":
return <Navigation size={16} />;
case "offline":
return <Clock size={16} />;
default:
return <AlertTriangle size={16} />;
}
};
const formatTimeAgo = (dateString: string) => {
const seconds = Math.floor(
(Date.now() - new Date(dateString).getTime()) / 1000,
);
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}min`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 86400)}j`;
};
if (loading) {
return (
<div className="driver-dashboard-container">
<div className="loading-dashboard">
<p>Chargement de votre cabine...</p>
</div>
</div>
);
}
return (
<>
<SidebarCabine />
<div className="driver-dashboard-container">
<div className="dashboard-header">
<h1>Dashboard Cabine</h1>
<p className="dashboard-subtitle">
Gestion de vos livraisons en temps réel
</p>
</div>
{/* Stats Cards */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon red">
<AlertTriangle size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Amendes</p>
<h3 className="stat-value">
{stats.totalPenalties.toFixed(2)}
</h3>
</div>
</div>
<div className="stat-card">
<div className="stat-icon purple">
<CheckCircle size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Total Complétées</p>
<h3 className="stat-value">
{stats.completedToday}
</h3>
</div>
</div>
</div>
{/* Orders List */}
<div className="orders-section">
<h2>
<Package size={24} />
Mes Commandes ({orders.length})
</h2>
<div className="orders-list">
{orders.map((order) => (
<div
key={order.id}
className={`order-card ${selectedOrder?.id === order.id ? "selected" : ""}`}
onClick={() => setSelectedOrder(order)}
>
<div className="order-header">
<div className="order-number2">
<Package size={20} />
<span>{order.orderNumber}</span>
</div>
<div
className={`order-status ${getStatusColor(order.status)}`}
>
{order.status === "delivered" && (
<CheckCircle size={16} />
)}
{order.status === "in_transit" && (
<Navigation size={16} />
)}
{order.status === "pending" && (
<Clock size={16} />
)}
<span>
{getStatusLabel(order.status)}
</span>
</div>
</div>
<div className="order-client">
<div className="client-info">
<User size={18} />
<div>
<strong>{order.clientName}</strong>
<span className="client-points">
<Star size={14} />
{order.clientPoints} points
</span>
</div>
</div>
<a
href={`tel:${order.clientPhone}`}
className="phone-number"
>
{order.clientPhone}
</a>
</div>
<div className="order-address">
<MapPin size={18} />
<div>
<p>{order.address}</p>
</div>
</div>
<div className="order-details">
<div className="order-amount">
<DollarSign size={18} />
<span>
{order.amount.toFixed(2)} {" "}
{order.items} articles
</span>
</div>
{order.penalty > 0 && (
<div className="order-penalty">
<AlertTriangle size={18} />
<span>
Amende:{" "}
{order.penalty.toFixed(2)}
</span>
</div>
)}
</div>
<div className="order-actions">
<button
className="btn-secondary"
onClick={(e) => {
e.stopPropagation();
handleViewItems(
order.id,
order.orderNumber,
);
}}
>
<Package size={18} />
Voir items ({order.items})
</button>
{order.status === "in_transit" && (
<button
className="btn-secondary"
onClick={(e) => {
e.stopPropagation();
handleViewDeliverymanLocation(
order.id,
);
}}
>
<MapPin size={18} />
Position livreur
</button>
)}
</div>
</div>
))}
</div>
</div>
{/* Items Modal */}
<ItemsModal
isOpen={showItemsModal}
onClose={() => setShowItemsModal(false)}
items={orderItems}
commandNumber={selectedOrderNumber}
/>
{/* Location Modal */}
{showLocationModal && deliverymanLocation && (
<div
className="modal-overlay"
onClick={() => setShowLocationModal(false)}
>
<div
className="location-modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="location-modal-header">
<div className="location-header-title">
<Navigation
size={24}
className="pulse-icon"
/>
<h3>Position du Livreur</h3>
</div>
<button
className="location-close-btn"
onClick={() => setShowLocationModal(false)}
>
<X size={20} />
</button>
</div>
<div className="location-modal-body">
<div className="location-info-card">
<div className="location-info-header">
<User size={20} />
<span>Informations Livreur</span>
</div>
<div className="location-info-content">
<div className="location-info-row">
<span className="location-label">
Nom:
</span>
<span className="location-value">
{
deliverymanLocation
.deliveryman.prenom
}{" "}
{
deliverymanLocation
.deliveryman.nom
}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Username:
</span>
<span className="location-value">
{
deliverymanLocation
.deliveryman.username
}
</span>
</div>
</div>
</div>
<div className="location-coords-card">
<div className="location-coords-header">
<MapPin size={20} />
<span>Coordonnées GPS</span>
</div>
<div className="location-coords-content">
<div className="location-coord-row">
<span className="location-coord-label">
Latitude:
</span>
<span className="location-coord-value">
{deliverymanLocation.deliveryman.location.latitude.toFixed(
6,
)}
°
</span>
</div>
<div className="location-coord-row">
<span className="location-coord-label">
Longitude:
</span>
<span className="location-coord-value">
{deliverymanLocation.deliveryman.location.longitude.toFixed(
6,
)}
°
</span>
</div>
</div>
</div>
<div className="location-update-card">
<div className="location-update-header">
<Clock size={20} />
<span>Dernière Mise à Jour</span>
</div>
<div className="location-update-content">
<div className="location-update-badge">
<CheckCircle size={16} />
<span>
Il y a{" "}
{
deliverymanLocation
.deliveryman.location
.last_update_ago
}
s
</span>
</div>
<div className="location-update-time">
{new Date(
deliverymanLocation.deliveryman
.location.last_update,
).toLocaleString("fr-FR")}
</div>
</div>
</div>
<div className="location-modal-actions">
<button
className="location-btn location-btn-primary"
onClick={openInGoogleMaps}
>
<ExternalLink size={18} />
Google Maps
</button>
<button
className="location-btn location-btn-waze"
onClick={openInWaze}
>
<Navigation size={18} />
Waze
</button>
<button
className="location-btn location-btn-secondary"
onClick={copyCoordinates}
>
<Copy size={18} />
Copier
</button>
</div>
</div>
</div>
</div>
)}
{/* Delivery Persons Section */}
<div className="orders-section">
<h2>
<Navigation size={24} />
Livreurs ({activeDeliveryPersons.length})
</h2>
{activeDeliveryPersons.length > 0 ? (
<div className="orders-list">
{activeDeliveryPersons.map((person) => (
<div key={person.id} className="order-card">
<div className="order-header">
<div className="order-number2">
<User size={20} />
<span>
{person.prenom && person.nom
? `${person.prenom} ${person.nom}`
: person.username}
</span>
</div>
<div
className={`order-status ${getStatusColor(person.status)}`}
>
{getStatusIcon(person.status)}
<span>
{getStatusLabel(person.status)}
</span>
</div>
</div>
<div className="order-client">
<div className="client-info">
<MapPin size={18} />
<div>
<strong>Position GPS</strong>
<span className="client-points">
{person.location.latitude.toFixed(
6,
)}
°,{" "}
{person.location.longitude.toFixed(
6,
)}
°
</span>
</div>
</div>
<div
className={`update-status ${person.location.is_recent ? "recent" : "old"}`}
style={{
padding: "0.5rem",
borderRadius: "8px",
background: person.location
.is_recent
? "rgba(34, 197, 94, 0.1)"
: "rgba(239, 68, 68, 0.1)",
color: person.location.is_recent
? "#22c55e"
: "#ef4444",
fontSize: "0.875rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
}}
>
{person.location.is_recent ? (
<CheckCircle size={16} />
) : (
<Clock size={16} />
)}
<span>
{formatTimeAgo(
person.location.last_update,
)}
</span>
</div>
</div>
{person.stats.current_command && (
<div className="order-details">
<div className="order-amount">
<Package size={18} />
<span>
Commande en cours: CMD-
{String(
person.stats
.current_command,
).padStart(3, "0")}
</span>
</div>
{person.stats.queue_size > 0 && (
<div
className="order-penalty"
style={{
color: "#3b82f6",
borderColor: "#3b82f6",
}}
>
<Clock size={18} />
<span>
File d'attente:{" "}
{
person.stats
.queue_size
}
</span>
</div>
)}
</div>
)}
<div className="order-actions">
<button
className="btn-secondary"
onClick={() =>
window.open(
`https://www.google.com/maps?q=${person.location.latitude},${person.location.longitude}`,
"_blank",
)
}
>
<MapPin size={18} />
Voir sur carte
</button>
<button
className="btn-secondary"
onClick={() => {
navigator.clipboard.writeText(
`${person.location.latitude}, ${person.location.longitude}`,
);
alert("Coordonnées copiées !");
}}
>
<Copy size={18} />
Copier coordonnées
</button>
</div>
</div>
))}
</div>
) : (
<div
className="empty-state"
style={{ padding: "2rem", textAlign: "center" }}
>
<Navigation
size={64}
style={{ opacity: 0.3, margin: "0 auto 1rem" }}
/>
<h3>Aucun livreur enregistré</h3>
<p>
Aucun livreur n'est actuellement dans le système
</p>
</div>
)}
</div>
</div>
</>
);
}
export default DriverDashboard;
@@ -1,887 +0,0 @@
/* ============================================
CABINE ALERTS PAGE - MODERN DARK THEME
============================================ */
.alerts-page-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1600px;
margin: 0 auto;
background: #0a0a0a;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (min-width: 769px) {
.alerts-page-container {
padding-top: 1rem;
}
}
/* ============================================
HEADER SECTION
============================================ */
.alerts-page-header {
margin-bottom: clamp(1.5rem, 4vw, 2rem);
text-align: center;
padding-top: 3rem;
}
.alerts-page-header h1 {
color: white;
font-size: clamp(2rem, 6vw, 3rem);
margin: 0 0 0.5rem 0;
font-weight: bold;
letter-spacing: -0.5px;
background: linear-gradient(to right, #ef4444, #dc2626);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.alerts-page-subtitle {
color: #888;
font-size: clamp(1rem, 3vw, 1.2rem);
margin: 0;
}
/* ============================================
STATS OVERVIEW
============================================ */
.alerts-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.alert-stat-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
display: flex;
align-items: center;
gap: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.alert-stat-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(239, 68, 68, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.alert-stat-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(239, 68, 68, 0.3);
box-shadow: 0 12px 32px rgba(239, 68, 68, 0.2);
transform: translateY(-4px);
}
.alert-stat-card:hover::before {
left: 100%;
}
.alert-stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-stat-card:hover .alert-stat-icon {
transform: scale(1.1) rotate(5deg);
}
.alert-stat-icon.red {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.2),
rgba(220, 38, 38, 0.1)
);
color: #ef4444;
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.3);
}
.alert-stat-icon.orange {
background: linear-gradient(
135deg,
rgba(251, 146, 60, 0.2),
rgba(249, 115, 22, 0.1)
);
color: #fb923c;
box-shadow: 0 8px 24px rgba(251, 146, 60, 0.3);
}
.alert-stat-icon.green {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
color: #10b981;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.alert-stat-icon.blue {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
}
.alert-stat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alert-stat-label {
color: #888;
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.alert-stat-value {
color: white;
font-size: clamp(1.8rem, 5vw, 2.2rem);
margin: 0;
font-weight: bold;
letter-spacing: -0.5px;
}
/* ============================================
FILTERS SECTION
============================================ */
.alerts-filters {
display: flex;
gap: 1rem;
margin-bottom: clamp(1.5rem, 4vw, 2rem);
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
.alerts-search {
flex: 1;
min-width: 250px;
position: relative;
}
.alerts-search input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 3rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.alerts-search input:focus {
outline: none;
border-color: rgba(239, 68, 68, 0.5);
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.03) 100%
);
}
.alerts-search input::placeholder {
color: #666;
}
.alerts-search svg {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #888;
pointer-events: none;
}
.alerts-filter-buttons {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.filter-btn-2 {
padding: 0.75rem 1.5rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #888;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.filter-btn-2:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.03) 100%
);
border-color: rgba(239, 68, 68, 0.3);
color: white;
}
.filter-btn-2.active {
background: linear-gradient(135deg, #ef4444, #dc2626);
border-color: rgba(239, 68, 68, 0.5);
color: white;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
/* ============================================
ALERTS LIST
============================================ */
.alerts-section {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.alerts-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: clamp(1rem, 3vw, 1.5rem);
flex-wrap: wrap;
gap: 1rem;
}
.alerts-section-header h2 {
color: white;
font-size: clamp(1.5rem, 4vw, 2rem);
margin: 0;
font-weight: bold;
display: flex;
align-items: center;
gap: 0.75rem;
}
.alerts-section-header h2 svg {
color: #ef4444;
}
.alerts-count-badge {
padding: 0.5rem 1rem;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 8px;
color: #ef4444;
font-weight: 600;
font-size: 0.9rem;
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* ============================================
ALERT CARD
============================================ */
.alert-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.alert-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(239, 68, 68, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.alert-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(239, 68, 68, 0.3);
box-shadow: 0 12px 32px rgba(239, 68, 68, 0.2);
transform: translateY(-2px);
}
.alert-card:hover::before {
left: 100%;
}
.alert-card.active {
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);
}
.alert-card.resolved {
opacity: 0.6;
border-color: rgba(16, 185, 129, 0.3);
}
.alert-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
gap: 1rem;
}
.alert-priority-indicator {
display: flex;
align-items: center;
gap: 0.75rem;
}
.alert-priority-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.alert-priority-icon.critical {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.3),
rgba(220, 38, 38, 0.2)
);
color: #ef4444;
animation: pulse-critical 2s ease-in-out infinite;
}
.alert-priority-icon.high {
background: linear-gradient(
135deg,
rgba(251, 146, 60, 0.2),
rgba(249, 115, 22, 0.1)
);
color: #fb923c;
}
.alert-priority-icon.medium {
background: linear-gradient(
135deg,
rgba(234, 179, 8, 0.2),
rgba(202, 138, 4, 0.1)
);
color: #eab308;
}
.alert-priority-icon.low {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
}
@keyframes pulse-critical {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
.alert-info {
flex: 1;
}
.alert-id {
color: #888;
font-size: 0.85rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
}
.alert-title {
color: white;
font-size: 1.2rem;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.alert-subtitle {
color: #888;
font-size: 0.9rem;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.alert-status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.alert-status-badge.active {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.alert-status-badge.resolved {
background: rgba(16, 185, 129, 0.15);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.alert-status-badge.investigating {
background: rgba(234, 179, 8, 0.15);
color: #eab308;
border: 1px solid rgba(234, 179, 8, 0.3);
}
/* ============================================
ALERT DETAILS
============================================ */
.alert-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.alert-detail-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.02);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.alert-detail-item svg {
color: #3b82f6;
flex-shrink: 0;
}
.alert-detail-content {
flex: 1;
min-width: 0;
}
.alert-detail-label {
color: #888;
font-size: 0.8rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alert-detail-value {
color: white;
font-size: 0.95rem;
margin: 0;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ============================================
ALERT LOCATION
============================================ */
.alert-location {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(59, 130, 246, 0.05);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 12px;
margin-bottom: 1rem;
}
.alert-location svg {
color: #3b82f6;
flex-shrink: 0;
margin-top: 0.2rem;
}
.alert-location-content {
flex: 1;
}
.alert-location-content p {
color: white;
margin: 0 0 0.5rem 0;
font-weight: 500;
}
.alert-coordinates {
display: flex;
align-items: center;
gap: 0.5rem;
color: #888;
font-size: 0.85rem;
font-family: monospace;
}
.alert-coordinates svg {
color: #3b82f6;
}
/* ============================================
ALERT ACTIONS
============================================ */
.alert-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.alert-btn {
flex: 1;
min-width: fit-content;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.alert-btn-primary {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
.alert-btn-primary:hover {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
transform: translateY(-2px);
}
.alert-btn-success {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
}
.alert-btn-success:hover {
background: linear-gradient(135deg, #059669, #047857);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
transform: translateY(-2px);
}
.alert-btn-secondary {
background: rgba(59, 130, 246, 0.15);
border: 1px solid rgba(59, 130, 246, 0.3);
color: #3b82f6;
}
.alert-btn-secondary:hover {
background: rgba(59, 130, 246, 0.25);
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
}
.alert-btn-outline {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #888;
}
.alert-btn-outline:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.3);
color: white;
}
/* ============================================
EMPTY STATE
============================================ */
.alerts-empty-state {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
backdrop-filter: blur(8px);
}
.alerts-empty-state svg {
color: #3b82f6;
opacity: 0.3;
margin: 0 auto 1.5rem;
}
.alerts-empty-state h3 {
color: white;
font-size: 1.5rem;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.alerts-empty-state p {
color: #888;
font-size: 1rem;
margin: 0;
}
/* ============================================
LOADING STATE
============================================ */
.alerts-loading {
text-align: center;
padding: 4rem 2rem;
}
.alerts-loading p {
font-size: 1.2rem;
color: #666;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* ============================================
RESPONSIVE DESIGN
============================================ */
@media (max-width: 1024px) {
.alerts-stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.alerts-filters {
flex-direction: column;
align-items: stretch;
}
.alerts-search {
width: 100%;
}
.alerts-filter-buttons {
width: 100%;
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.alerts-page-container {
padding: 1rem;
}
.alerts-stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.alert-stat-card {
padding: 1rem;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.alert-stat-icon {
width: 48px;
height: 48px;
}
.alert-stat-value {
font-size: 1.5rem;
}
.alert-card-header {
flex-direction: column;
align-items: flex-start;
}
.alert-details {
grid-template-columns: 1fr;
}
.alert-actions {
flex-direction: column;
width: 100%;
}
.alert-btn {
width: 100%;
}
}
@media (max-width: 640px) {
.alerts-page-header h1 {
font-size: 1.8rem;
flex-direction: column;
}
.alerts-stats-grid {
grid-template-columns: 1fr;
}
.alert-stat-card {
flex-direction: row;
align-items: center;
}
.alert-card {
padding: 1rem;
}
.alert-priority-icon {
width: 40px;
height: 40px;
}
.alert-title {
font-size: 1.1rem;
}
}
@media (max-width: 480px) {
.alerts-page-container {
padding: 0.75rem;
}
.filter-btn-2 {
padding: 0.65rem 1rem;
font-size: 0.85rem;
}
.alert-btn {
font-size: 0.9rem;
padding: 0.65rem 1rem;
}
}
@media (hover: none) {
.alert-stat-card:hover,
.alert-card:hover {
transform: none;
}
.alert-btn-primary:hover,
.alert-btn-success:hover,
.alert-btn-secondary:hover {
transform: none;
}
}
@@ -1,586 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
AlertTriangle,
Clock,
CheckCircle,
User,
Shield,
Search,
Filter,
Calendar,
X,
Eye,
} from "lucide-react";
import "./CabineAlerts.css";
import SidebarCabine from "../../components/SidebarCabine";
import {
isCabineAuthenticated,
getAllAlerts,
getAlertDetails,
} from "../../api/api_cabine";
interface Alert {
id: number;
username: string;
status: string;
created_at: string;
updated_at: string;
}
interface AlertStats {
total: number;
active: number;
resolved: number;
}
function CabineAlerts() {
const navigate = useNavigate();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [filteredAlerts, setFilteredAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">(
"true",
);
const [stats, setStats] = useState<AlertStats>({
total: 0,
active: 0,
resolved: 0,
});
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
// ============================================
// 🔐 VÉRIFICATION AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isCabineAuthenticated()) {
console.log("❌ [CabineAlerts] Non authentifié, redirection");
navigate("/login-cabine/cabine", { replace: true });
}
};
checkAuth();
const authInterval = setInterval(() => {
if (!isCabineAuthenticated()) {
console.log("❌ [CabineAlerts] Session expirée");
navigate("/login-cabine/cabine", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// ============================================
// 📊 CHARGEMENT DES ALERTES
// ============================================
useEffect(() => {
const fetchAlerts = async () => {
if (!isCabineAuthenticated()) {
navigate("/login-cabine/cabine", { replace: true });
return;
}
try {
console.log("🚨 [CABINE_ALERTS] Chargement des alertes...");
// ✅ CHANGEMENT: Utiliser getAllAlerts au lieu de getActiveAlerts
const result = await getAllAlerts();
if (result.success && result.alerts) {
console.log(
"✅ [CABINE_ALERTS] Alertes récupérées:",
result.alerts,
);
const alertsData = result.alerts.map((alert: any) => ({
id: alert.id,
username: alert.username,
status: alert.status,
created_at: alert.created_at,
updated_at: alert.updated_at,
}));
setAlerts(alertsData);
// Calculer les stats
const activeCount = alertsData.filter(
(a: Alert) => a.status === "true",
).length;
const resolvedCount = alertsData.filter(
(a: Alert) => a.status === "false",
).length;
setStats({
total: alertsData.length,
active: activeCount,
resolved: resolvedCount,
});
} else {
console.warn("⚠️ [CABINE_ALERTS] Aucune alerte trouvée");
setAlerts([]);
setFilteredAlerts([]);
setStats({
total: 0,
active: 0,
resolved: 0,
});
}
} catch (error) {
console.error("❌ [CABINE_ALERTS] Erreur chargement:", error);
if (error instanceof Error && error.message.includes("401")) {
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
}
} finally {
setLoading(false);
}
};
fetchAlerts();
// Rafraîchir toutes les 30 secondes
const interval = setInterval(fetchAlerts, 30000);
return () => clearInterval(interval);
}, [navigate]);
// ============================================
// 🔍 FILTRAGE DES ALERTES
// ============================================
useEffect(() => {
let filtered = [...alerts];
// Filtre par statut
if (statusFilter !== "all") {
filtered = filtered.filter(
(alert) => alert.status === statusFilter,
);
}
// Filtre par recherche
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter((alert) =>
alert.username.toLowerCase().includes(query),
);
}
setFilteredAlerts(filtered);
}, [alerts, statusFilter, searchQuery]);
// ============================================
// 🎨 HELPERS
// ============================================
const determinePriority = (
alert: Alert,
): "critical" | "high" | "medium" | "low" => {
if (alert.status === "true") {
const createdAt = new Date(alert.created_at);
const now = new Date();
const durationMinutes =
(now.getTime() - createdAt.getTime()) / 1000 / 60;
if (durationMinutes > 30) return "critical";
if (durationMinutes > 15) return "high";
if (durationMinutes > 5) return "medium";
}
return "low";
};
const getStatusLabel = (status: string) => {
switch (status) {
case "true":
return "Active";
case "false":
return "Résolue";
default:
return "Inconnu";
}
};
const formatDuration = (createdAt: string) => {
const created = new Date(createdAt);
const now = new Date();
const duration = Math.floor((now.getTime() - created.getTime()) / 1000);
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const secs = duration % 60;
if (hours > 0) return `${hours}h ${minutes}m`;
if (minutes > 0) return `${minutes}m ${secs}s`;
return `${secs}s`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const formatTimeAgo = (dateString: string) => {
const seconds = Math.floor(
(Date.now() - new Date(dateString).getTime()) / 1000,
);
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}min`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
return `${Math.floor(seconds / 86400)}j`;
};
const handleViewDetails = async (alertId: number) => {
try {
const result = await getAlertDetails(alertId);
if (result.success && result.alert) {
setSelectedAlert(result.alert as Alert);
setShowDetailsModal(true);
} else {
alert("Impossible de récupérer les détails");
}
} catch (error) {
console.error("❌ Erreur récupération détails:", error);
alert("Erreur lors de la récupération des détails");
}
};
// ============================================
// 🎨 RENDER
// ============================================
if (loading) {
return (
<div className="alerts-page-container">
<div className="alerts-loading">
<p>Chargement des alertes...</p>
</div>
</div>
);
}
return (
<>
<SidebarCabine />
<div className="alerts-page-container">
{/* Header */}
<div className="alerts-page-header">
<h1>
<AlertTriangle size={40} />
Gestion des Alertes
</h1>
<p className="alerts-page-subtitle">
Surveillance en temps réel des alertes police
</p>
</div>
{/* Stats Grid */}
<div className="alerts-stats-grid">
<div className="alert-stat-card">
<div className="alert-stat-icon red">
<AlertTriangle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Total Alertes</p>
<h3 className="alert-stat-value">{stats.total}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon orange">
<Shield size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Alertes Actives</p>
<h3 className="alert-stat-value">{stats.active}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon green">
<CheckCircle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Résolues</p>
<h3 className="alert-stat-value">
{stats.resolved}
</h3>
</div>
</div>
</div>
{/* Filters */}
<div className="alerts-filters">
<div className="alerts-search">
<Search size={20} />
<input
type="text"
placeholder="Rechercher par livreur..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="alerts-filter-buttons">
<button
className={`filter-btn-2 ${statusFilter === "all" ? "active" : ""}`}
onClick={() => setStatusFilter("all")}
>
<Filter size={16} />
Toutes ({alerts.length})
</button>
<button
className={`filter-btn-2 ${statusFilter === "true" ? "active" : ""}`}
onClick={() => setStatusFilter("true")}
>
<Shield size={16} />
Actives ({stats.active})
</button>
<button
className={`filter-btn-2 ${statusFilter === "false" ? "active" : ""}`}
onClick={() => setStatusFilter("false")}
>
<CheckCircle size={16} />
Résolues ({stats.resolved})
</button>
</div>
</div>
{/* Alerts List */}
<div className="alerts-section">
<div className="alerts-section-header">
<h2>
<Shield size={24} />
Liste des Alertes
</h2>
<div className="alerts-count-badge">
{filteredAlerts.length} alerte
{filteredAlerts.length > 1 ? "s" : ""}
</div>
</div>
{filteredAlerts.length > 0 ? (
<div className="alerts-list">
{filteredAlerts.map((alert) => {
const priority = determinePriority(alert);
return (
<div
key={alert.id}
className={`alert-card ${alert.status === "true" ? "active" : "resolved"}`}
onClick={() =>
handleViewDetails(alert.id)
}
>
<div className="alert-card-header">
<div className="alert-priority-indicator">
<div
className={`alert-priority-icon ${priority}`}
>
<AlertTriangle size={24} />
</div>
<div className="alert-info">
<p className="alert-id">
Alerte #{alert.id}
</p>
<h3 className="alert-title">
{alert.username}
</h3>
<p className="alert-subtitle">
<Clock size={14} />
Il y a{" "}
{formatTimeAgo(
alert.created_at,
)}
</p>
</div>
</div>
<div
className={`alert-status-badge ${alert.status === "true" ? "active" : "resolved"}`}
>
{alert.status === "true" ? (
<AlertTriangle size={16} />
) : (
<CheckCircle size={16} />
)}
{getStatusLabel(alert.status)}
</div>
</div>
<div className="alert-details">
<div className="alert-detail-item">
<User size={18} />
<div className="alert-detail-content">
<p className="alert-detail-label">
Livreur
</p>
<p className="alert-detail-value">
{alert.username}
</p>
</div>
</div>
<div className="alert-detail-item">
<Clock size={18} />
<div className="alert-detail-content">
<p className="alert-detail-label">
Durée
</p>
<p className="alert-detail-value">
{formatDuration(
alert.created_at,
)}
</p>
</div>
</div>
</div>
<div className="alert-actions">
<button
className="alert-btn alert-btn-outline"
onClick={(e) => {
e.stopPropagation();
handleViewDetails(alert.id);
}}
>
<Eye size={18} />
Détails
</button>
</div>
</div>
);
})}
</div>
) : (
<div className="alerts-empty-state">
<Shield size={64} />
<h3>Aucune alerte trouvée</h3>
<p>
{searchQuery
? "Essayez de modifier vos critères de recherche"
: statusFilter === "true"
? "Aucune alerte active actuellement"
: "Aucune alerte n'est actuellement enregistrée"}
</p>
</div>
)}
</div>
{/* Details Modal */}
{showDetailsModal && selectedAlert && (
<div
className="modal-overlay"
onClick={() => setShowDetailsModal(false)}
>
<div
className="location-modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="location-modal-header">
<div className="location-header-title">
<AlertTriangle
size={24}
className="pulse-icon"
/>
<h3>
Détails de l'Alerte #{selectedAlert.id}
</h3>
</div>
<button
className="location-close-btn"
onClick={() => setShowDetailsModal(false)}
>
<X size={20} />
</button>
</div>
<div className="location-modal-body">
<div className="location-info-card">
<div className="location-info-header">
<User size={20} />
<span>Informations Livreur</span>
</div>
<div className="location-info-content">
<div className="location-info-row">
<span className="location-label">
Username:
</span>
<span className="location-value">
{selectedAlert.username}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Statut:
</span>
<span className="location-value">
{getStatusLabel(
selectedAlert.status,
)}
</span>
</div>
</div>
</div>
<div className="location-update-card">
<div className="location-update-header">
<Calendar size={20} />
<span>Informations Temporelles</span>
</div>
<div className="location-update-content">
<div className="location-info-row">
<span className="location-label">
Créée le:
</span>
<span className="location-value">
{formatDate(
selectedAlert.created_at,
)}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Durée:
</span>
<span className="location-value">
{formatDuration(
selectedAlert.created_at,
)}
</span>
</div>
<div className="location-info-row">
<span className="location-label">
Dernière mise à jour:
</span>
<span className="location-value">
{formatDate(
selectedAlert.updated_at,
)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</>
);
}
export default CabineAlerts;
@@ -1,714 +0,0 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Users,
Search,
Filter,
MapPin,
Clock,
Package,
TrendingUp,
CheckCircle,
AlertCircle,
XCircle,
ChevronDown,
RefreshCw,
Truck,
Eye,
User,
ExternalLink,
Copy,
X
} from 'lucide-react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faMapLocationDot,
faSpinner
} from '@fortawesome/free-solid-svg-icons';
import {
faApple,
faGoogle,
faWaze
} from '@fortawesome/free-brands-svg-icons';
import './CabineDeliverymen.css';
import SidebarCabine from '../../components/SidebarCabine';
import {
getAllDeliveryPersonsWithDetails,
isCabineAuthenticated // ⭐ AJOUT
} from '../../api/api_cabine';
import { getDeliveryPersonDetails, getDeliveryPersonMapLinks } from '../../api/api_admin';
import type {
DeliveryPerson,
DeliveryPersonsStats,
DeliveryPersonDetails,
MapLinksResponse
} from '../../api/api_admin_types';
type FilterStatus = 'all' | 'available' | 'busy' | 'offline';
function CabineDeliverymen() {
const navigate = useNavigate();
const [deliveryPersons, setDeliveryPersons] = useState<DeliveryPerson[]>([]);
const [filteredDeliveryPersons, setFilteredDeliveryPersons] = useState<DeliveryPerson[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState<FilterStatus>('all');
const [showFilters, setShowFilters] = useState(false);
const [selectedDeliveryPerson, setSelectedDeliveryPerson] = useState<DeliveryPerson | null>(null);
const [selectedDeliveryPersonDetails, setSelectedDeliveryPersonDetails] = useState<DeliveryPersonDetails | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [mapLinks, setMapLinks] = useState<MapLinksResponse | null>(null);
const [loadingMapLinks, setLoadingMapLinks] = useState(false);
const [loadingDetails, setLoadingDetails] = useState(false);
const [copiedCoords, setCopiedCoords] = useState(false);
const [stats, setStats] = useState<DeliveryPersonsStats>({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0
});
// ============================================
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isCabineAuthenticated()) {
console.log('❌ [CabineDeliverymen] Cabine non authentifié, redirection vers /login-cabine/cabine');
navigate('/login-cabine/cabine', { replace: true });
}
};
checkAuth();
}, [navigate]);
// ============================================
// 🔐 VÉRIFICATION CONTINUE (toutes les 5 secondes)
// ============================================
useEffect(() => {
const authInterval = setInterval(() => {
if (!isCabineAuthenticated()) {
console.log('❌ [CabineDeliverymen] Session cabine expirée, redirection vers /login-cabine/cabine');
navigate('/login-cabine/cabine', { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// Récupération des livreurs
useEffect(() => {
fetchDeliveryPersons();
}, []);
const fetchDeliveryPersons = async () => {
// ✅ Vérifier l'auth avant de charger
if (!isCabineAuthenticated()) {
console.log('❌ [fetchDeliveryPersons] Cabine non authentifié');
navigate('/login-cabine/cabine', { replace: true });
return;
}
setLoading(true);
try {
console.log('[DELIVERY_PERSONS] Récupération des livreurs...');
const result = await getAllDeliveryPersonsWithDetails();
if (!result.success) {
console.error('[DELIVERY_PERSONS] Erreur:', result);
setDeliveryPersons([]);
setFilteredDeliveryPersons([]);
setStats({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0
});
setLoading(false);
return;
}
console.log('[DELIVERY_PERSONS] Données reçues:', result);
setDeliveryPersons(result.deliveryPersons);
setFilteredDeliveryPersons(result.deliveryPersons);
setStats(result.stats);
console.log('[DELIVERY_PERSONS] Données chargées:', result.count, 'livreurs');
} catch (error) {
console.error('[DELIVERY_PERSONS] Erreur chargement:', error);
if (error instanceof Error && error.message.includes('401')) {
console.log('🔓 [CabineDeliverymen] Token invalide');
sessionStorage.removeItem('admin_token');
sessionStorage.removeItem('admin_username');
navigate('/login-cabine/cabine', { replace: true });
return;
}
setDeliveryPersons([]);
setFilteredDeliveryPersons([]);
setStats({
total: 0,
available: 0,
busy: 0,
offline: 0,
active_deliveries: 0
});
} finally {
setLoading(false);
}
};
// Filtrage
useEffect(() => {
let filtered = deliveryPersons;
if (filterStatus !== 'all') {
filtered = filtered.filter(d => d.status === filterStatus);
}
if (searchTerm) {
filtered = filtered.filter(d =>
d.username.toLowerCase().includes(searchTerm.toLowerCase())
);
}
setFilteredDeliveryPersons(filtered);
}, [searchTerm, filterStatus, deliveryPersons]);
// Charger les détails complets
const loadDeliveryPersonDetails = async (username: string) => {
setLoadingDetails(true);
setSelectedDeliveryPersonDetails(null);
try {
console.log('[DELIVERY_DETAILS] Chargement détails pour:', username);
const details = await getDeliveryPersonDetails(username);
console.log('[DELIVERY_DETAILS] Détails reçus:', details);
setSelectedDeliveryPersonDetails(details);
} catch (error) {
console.error('[DELIVERY_DETAILS] Erreur:', error);
if (error instanceof Error && error.message.includes('401')) {
console.log('🔓 [CabineDeliverymen] Token invalide');
sessionStorage.removeItem('admin_token');
sessionStorage.removeItem('admin_username');
navigate('/login-cabine/cabine', { replace: true });
}
} finally {
setLoadingDetails(false);
}
};
// Charger les liens GPS
const loadMapLinks = async (username: string) => {
setLoadingMapLinks(true);
setMapLinks(null);
try {
console.log('[MAP_LINKS] Chargement pour:', username);
const result = await getDeliveryPersonMapLinks(username);
if (result.success) {
console.log('[MAP_LINKS] Liens chargés:', result);
setMapLinks(result);
} else {
console.error('[MAP_LINKS] Erreur:', result);
setMapLinks(result);
}
} catch (error) {
console.error('[MAP_LINKS] Erreur:', error);
if (error instanceof Error && error.message.includes('401')) {
console.log('🔓 [CabineDeliverymen] Token invalide');
sessionStorage.removeItem('admin_token');
sessionStorage.removeItem('admin_username');
navigate('/login-cabine/cabine', { replace: true });
}
} finally {
setLoadingMapLinks(false);
}
};
// Ouvrir modal avec détails
const handleViewDetails = async (person: DeliveryPerson) => {
setSelectedDeliveryPerson(person);
setShowDetailsModal(true);
setMapLinks(null);
setSelectedDeliveryPersonDetails(null);
setCopiedCoords(false);
await Promise.all([
loadDeliveryPersonDetails(person.username),
loadMapLinks(person.username)
]);
};
const handleRefresh = async () => {
setRefreshing(true);
await fetchDeliveryPersons();
setRefreshing(false);
};
const handleCopyCoordinates = () => {
if (mapLinks?.location) {
const coords = `${mapLinks.location.latitude}, ${mapLinks.location.longitude}`;
navigator.clipboard.writeText(coords);
setCopiedCoords(true);
setTimeout(() => setCopiedCoords(false), 2000);
}
};
const getStatusLabel = (status: string) => {
const labels = {
available: 'En ligne',
busy: 'Occupé',
offline: 'Hors ligne'
};
return labels[status as keyof typeof labels] || status;
};
const getStatusCount = (status: FilterStatus) => {
if (status === 'all') return stats.total;
return stats[status as keyof typeof stats] || 0;
};
const hasValidMapData = mapLinks?.success && mapLinks.map_links && mapLinks.location;
if (loading) {
return (
<>
<SidebarCabine />
<div className="cabine-deliverymen-container">
<div className="loading-deliverymen">
<p>Chargement des livreurs...</p>
</div>
</div>
</>
);
}
return (
<>
<SidebarCabine />
<div className="cabine-deliverymen-container">
{/* Header */}
<div className="orders-header">
<div className="header-content">
<h1>Gestion des Livreurs</h1>
<p className="header-subtitle">Suivi et supervision des livreurs</p>
</div>
<button
className="export-button"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw size={18} className={refreshing ? 'spin-animation' : ''} />
<span>Actualiser</span>
</button>
</div>
{/* Stats rapides */}
<div className="orders-stats">
<div className="stat-item">
<div className="stat-icon purple">
<Users size={20} />
</div>
<div className="stat-info">
<span className="stat-value">{stats.total}</span>
<span className="stat-label">Total Livreurs</span>
</div>
</div>
<div className="stat-item">
<div className="stat-icon green">
<CheckCircle size={20} />
</div>
<div className="stat-info">
<span className="stat-value">{stats.available}</span>
<span className="stat-label">Disponibles</span>
</div>
</div>
<div className="stat-item">
<div className="stat-icon orange">
<Truck size={20} />
</div>
<div className="stat-info">
<span className="stat-value">{stats.busy}</span>
<span className="stat-label">Occupés</span>
</div>
</div>
<div className="stat-item">
<div className="stat-icon red">
<XCircle size={20} />
</div>
<div className="stat-info">
<span className="stat-value">{stats.offline}</span>
<span className="stat-label">Hors Ligne</span>
</div>
</div>
</div>
{/* Contrôles */}
<div className="orders-controls">
<div className="search-bar">
<Search size={20} />
<input
type="text"
placeholder="Rechercher username ..."
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 */}
{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 === 'available' ? 'active' : ''}
onClick={() => setFilterStatus('available')}
>
Disponibles ({getStatusCount('available')})
</button>
<button
className={filterStatus === 'busy' ? 'active' : ''}
onClick={() => setFilterStatus('busy')}
>
Occupés ({getStatusCount('busy')})
</button>
<button
className={filterStatus === 'offline' ? 'active' : ''}
onClick={() => setFilterStatus('offline')}
>
Hors ligne ({getStatusCount('offline')})
</button>
</div>
</div>
</div>
)}
{/* Liste des livreurs */}
<div className="orders-list">
{filteredDeliveryPersons.length === 0 ? (
<div className="empty-state">
<Users size={64} />
<h3>Aucun livreur trouvé</h3>
<p>Aucun livreur ne correspond à vos critères de recherche</p>
</div>
) : (
filteredDeliveryPersons.map(person => (
<div key={person.id} className="order-card">
<div className="order-header">
<div className={`order-status status-${person.status === 'available' ? 'livre' : person.status === 'busy' ? 'en_route' : 'cancelled'}`}>
{person.status === 'available' && <CheckCircle size={16} />}
{person.status === 'busy' && <Truck size={16} />}
{person.status === 'offline' && <XCircle size={16} />}
<span>{getStatusLabel(person.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">Username</span>
<span className="info-value">{person.username}</span>
</div>
</div>
<div className="info-item">
<Package size={16} />
<div className="info-details">
<span className="info-label">Livraisons totales</span>
<span className="info-value">{person.stats.total_deliveries}</span>
</div>
</div>
<div className="info-item">
<TrendingUp size={16} />
<div className="info-details">
<span className="info-label">Complétées aujourd'hui</span>
<span className="info-value">{person.stats.completed_today}</span>
</div>
</div>
<div className="info-item">
<Clock size={16} />
<div className="info-details">
<span className="info-label">File d'attente</span>
<span className="info-value">{person.stats.queue_size} commande(s)</span>
</div>
</div>
<div className="info-item">
<MapPin size={16} />
<div className="info-details">
<span className="info-label">Position GPS</span>
<span className="info-value">
{person.location.latitude.toFixed(4)}°, {person.location.longitude.toFixed(4)}°
</span>
</div>
</div>
</div>
{person.stats.current_command && (
<div className="order-items">
<span className="items-label">Commande en cours:</span>
<span className="item-tag">
CMD-{person.stats.current_command.toString().padStart(6, '0')}
</span>
</div>
)}
</div>
<div className="order-footer">
<button
className="view-details-button"
onClick={() => handleViewDetails(person)}
>
<Eye size={18} />
<span>Détails</span>
</button>
</div>
</div>
))
)}
</div>
{/* Modal détails - Code identique avec gestion erreurs 401 */}
{showDetailsModal && selectedDeliveryPerson && (
<>
<div className="modal-overlay" onClick={() => setShowDetailsModal(false)} />
<div className="order-details-modal">
<div className="modal-header">
<h2>Détails du livreur</h2>
<button
className="close-modal"
onClick={() => setShowDetailsModal(false)}
>
<X size={24} />
</button>
</div>
<div className="modal-content">
{loadingDetails ? (
<div className="loading-section">
<FontAwesomeIcon icon={faSpinner} size="2x" className="spin-animation" />
<span>Chargement des détails...</span>
</div>
) : selectedDeliveryPersonDetails ? (
<>
<div className="detail-section">
<h3>Informations personnelles</h3>
<div className="detail-grid">
<div className="detail-item">
<span className="detail-label">Username</span>
<span className="detail-value">{selectedDeliveryPersonDetails.username}</span>
</div>
<div className="detail-item">
<span className="detail-label">Rôle</span>
<span className="detail-value">{selectedDeliveryPersonDetails.role}</span>
</div>
{selectedDeliveryPersonDetails.location?.last_update && (
<div className="detail-item">
<span className="detail-label">Dernière mise à jour</span>
<span className="detail-value">
{new Date(selectedDeliveryPersonDetails.location.last_update * 1000).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</span>
</div>
)}
</div>
</div>
<div className="detail-section">
<h3>Statistiques détaillées</h3>
<div className="detail-grid">
<div className="detail-item">
<span className="detail-label">Total livraisons</span>
<span className="detail-value">{selectedDeliveryPersonDetails.total_deliveries}</span>
</div>
<div className="detail-item">
<span className="detail-label">Livraisons complétées</span>
<span className="detail-value">{selectedDeliveryPersonDetails.completed_deliveries}</span>
</div>
<div className="detail-item">
<span className="detail-label">Livraisons en attente</span>
<span className="detail-value">{selectedDeliveryPersonDetails.pending_deliveries}</span>
</div>
<div className="detail-item">
<span className="detail-label">File d'attente</span>
<span className="detail-value">{selectedDeliveryPersonDetails.queue_size} commande(s)</span>
</div>
{selectedDeliveryPersonDetails.current_command && (
<div className="detail-item">
<span className="detail-label">Commande en cours</span>
<span className="detail-value">
CMD-{selectedDeliveryPersonDetails.current_command.toString().padStart(6, '0')}
</span>
</div>
)}
</div>
</div>
{selectedDeliveryPersonDetails.location && (
<div className="detail-section">
<h3>Position GPS</h3>
<div className="gps-info">
<div className="gps-coordinates">
<div className="coordinate">
<span className="coord-label">Latitude:</span>
<span className="coord-value">
{selectedDeliveryPersonDetails.location.latitude.toFixed(6)}°
</span>
</div>
<div className="coordinate">
<span className="coord-label">Longitude:</span>
<span className="coord-value">
{selectedDeliveryPersonDetails.location.longitude.toFixed(6)}°
</span>
</div>
</div>
</div>
</div>
)}
</>
) : (
<div className="detail-section">
<div className="detail-item">
<span className="detail-label">Username</span>
<span className="detail-value">{selectedDeliveryPerson.username}</span>
</div>
</div>
)}
{/* Section Navigation GPS */}
<div className="detail-section">
<h3>
<FontAwesomeIcon icon={faMapLocationDot} /> Navigation GPS
</h3>
{loadingMapLinks ? (
<div className="map-links-loading">
<FontAwesomeIcon icon={faSpinner} size="2x" className="spin-animation" />
<span>Chargement des liens GPS...</span>
</div>
) : hasValidMapData && mapLinks.map_links && mapLinks.location ? (
(() => {
const links = mapLinks.map_links;
const location = mapLinks.location;
return (
<>
<div className="map-links-grid">
<a href={links.google_maps} target="_blank" rel="noopener noreferrer" className="map-link-button google">
<FontAwesomeIcon icon={faGoogle} className="map-icon" />
<div className="link-content">
<span className="link-title">Google Maps</span>
<span className="link-subtitle">Voir la position</span>
</div>
<ExternalLink size={16} className="external-icon" />
</a>
<a href={links.waze} target="_blank" rel="noopener noreferrer" className="map-link-button waze">
<FontAwesomeIcon icon={faWaze} className="map-icon" />
<div className="link-content">
<span className="link-title">Waze</span>
<span className="link-subtitle">Navigation GPS</span>
</div>
<ExternalLink size={16} className="external-icon" />
</a>
<a href={links.apple_maps} target="_blank" rel="noopener noreferrer" className="map-link-button apple">
<FontAwesomeIcon icon={faApple} className="map-icon" />
<div className="link-content">
<span className="link-title">Apple Maps</span>
<span className="link-subtitle">Ouvrir dans Plans</span>
</div>
<ExternalLink size={16} className="external-icon" />
</a>
<a href={links.openstreetmap} target="_blank" rel="noopener noreferrer" className="map-link-button osm">
<MapPin size={24} className="map-icon" />
<div className="link-content">
<span className="link-title">OpenStreetMap</span>
<span className="link-subtitle">Carte open source</span>
</div>
<ExternalLink size={16} className="external-icon" />
</a>
</div>
<div className="coordinates-copy">
<button onClick={handleCopyCoordinates} className={`copy-coords-button ${copiedCoords ? 'copied' : ''}`}>
<Copy size={16} />
<span>{copiedCoords ? 'Coordonnées copiées !' : 'Copier coordonnées'}</span>
</button>
<span className="coords-display">
{location.latitude.toFixed(6)}, {location.longitude.toFixed(6)}
</span>
</div>
</>
);
})()
) : (
<div className="map-links-error">
<AlertCircle size={48} />
<h4>Position GPS non disponible</h4>
<p>
{mapLinks?.message || "Le livreur n'a pas encore partagé sa position GPS ou celle-ci est trop ancienne."}
</p>
</div>
)}
</div>
</div>
<div className="modal-actions">
<button
className="action-button secondary"
onClick={() => setShowDetailsModal(false)}
>
<X size={18} />
<span>Fermer</span>
</button>
</div>
</div>
</>
)}
</div>
</>
);
}
export default CabineDeliverymen;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,939 +0,0 @@
/* ============================================ */
/* USERS MANAGEMENT - STYLE MODERNE CABINE */
/* ============================================ */
.users-management-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1600px;
margin: 0 auto;
background: #0a0a0a;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (min-width: 769px) {
.users-management-container {
padding-top: 1rem;
}
}
/* ============================================ */
/* FILTERS SECTION */
/* ============================================ */
.filters-section {
display: flex;
gap: 1rem;
margin-bottom: clamp(2rem, 5vw, 3rem);
flex-wrap: wrap;
}
.search-bar {
flex: 1;
min-width: 280px;
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
backdrop-filter: blur(8px);
transition: all 0.3s ease;
}
.search-bar:focus-within {
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
}
.search-bar svg {
color: #3b82f6;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: white;
font-size: 1rem;
font-weight: 500;
}
.search-bar input::placeholder {
color: #888;
}
.role-filters {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.filter-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: #888;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.filter-btn:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(59, 130, 246, 0.3);
color: white;
transform: translateY(-2px);
}
.filter-btn.active {
background: #5b21b6;
border-color: #4c1d95;
color: white;
box-shadow: 0 4px 16px #7c3aed;
}
.filter-btn svg {
flex-shrink: 0;
}
/* ============================================ */
/* USERS SECTION */
/* ============================================ */
.users-section {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.users-section h2 {
color: white;
font-size: clamp(1.5rem, 4vw, 2rem);
margin: 0 0 clamp(1rem, 3vw, 1.5rem) 0;
font-weight: bold;
display: flex;
align-items: center;
gap: 0.75rem;
}
.users-section h2 svg {
color: #3b82f6;
}
.users-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* ============================================ */
/* USER CARD */
/* ============================================ */
.user-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.user-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(59, 130, 246, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.user-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 32px rgba(59, 130, 246, 0.2);
transform: translateY(-2px);
}
.user-card:hover::before {
left: 100%;
}
.user-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
flex-wrap: wrap;
}
.user-info-main {
display: flex;
align-items: center;
gap: 1rem;
}
.user-avatar {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
transition: transform 0.3s ease;
}
.user-card:hover .user-avatar {
transform: scale(1.1) rotate(5deg);
}
.user-details {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.user-username {
color: white;
font-size: 1.1rem;
font-weight: 700;
}
.user-id {
color: #888;
font-size: 0.85rem;
font-family: monospace;
}
.user-role-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
}
.user-role-badge.red {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.user-role-badge.purple {
background: rgba(124, 58, 237, 0.15);
color: #7c3aed;
border: 1px solid rgba(124, 58, 237, 0.3);
}
.user-role-badge.blue {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.user-role-badge.gray {
background: rgba(156, 163, 175, 0.15);
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
}
/* ============================================ */
/* USER ACTIONS */
/* ============================================ */
.user-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-edit,
.btn-delete {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
min-width: fit-content;
}
.btn-edit {
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
}
.btn-edit:hover {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
transform: translateY(-2px);
}
.btn-delete {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
.btn-delete:hover {
background: linear-gradient(135deg, #dc2626, #b91c1c);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
transform: translateY(-2px);
}
/* ============================================ */
/* EDIT MODAL */
/* ============================================ */
.edit-modal-content {
background: linear-gradient(
135deg,
rgba(20, 20, 20, 0.98) 0%,
rgba(15, 15, 15, 0.95) 100%
);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 24px;
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow:
0 24px 48px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(59, 130, 246, 0.2);
animation: slideUp 0.3s ease;
}
.edit-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.1) 0%,
transparent 100%
);
}
.modal-header-title {
display: flex;
align-items: center;
gap: 1rem;
}
.modal-header-title svg {
color: #3b82f6;
}
.modal-header-title h3 {
color: white;
font-size: 1.5rem;
margin: 0;
font-weight: 700;
}
.modal-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #888;
cursor: pointer;
transition: all 0.3s ease;
}
.modal-close-btn:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
transform: scale(1.1);
}
.edit-modal-body {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* ============================================ */
/* FORM GROUPS */
/* ============================================ */
.form-group2 {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.form-group2 label {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-weight: 600;
font-size: 0.95rem;
}
.form-group2 label svg {
color: #3b82f6;
}
.form-group2 input,
.form-group2 select {
width: 100%;
padding: 0.875rem 1.25rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: white;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
}
.form-group2 input:focus,
.form-group2 select:focus {
outline: none;
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
background: rgba(255, 255, 255, 0.05);
}
.form-group2 input::placeholder {
color: #666;
}
.form-group2 select {
cursor: pointer;
}
.form-group2 select option {
background: #1a1a1a;
color: white;
}
.password-input-group {
position: relative;
display: flex;
align-items: center;
}
.password-input-group input {
flex: 1;
padding-right: 3rem;
}
.password-toggle {
position: absolute;
right: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
background: transparent;
border: none;
color: #888;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 6px;
}
.password-toggle:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
/* ============================================ */
/* MODAL ACTIONS */
/* ============================================ */
.modal-actions {
display: flex;
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-cancel,
.btn-save {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1rem 1.5rem;
border: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-cancel {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
margin-left: -2px;
}
.btn-cancel:hover {
background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.5);
transform: translateY(-2px);
}
.btn-save {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.btn-save:hover {
background: linear-gradient(135deg, #059669, #047857);
box-shadow: 0 12px 32px rgba(16, 185, 129, 0.4);
transform: translateY(-2px);
}
/* ============================================ */
/* EMPTY STATE */
/* ============================================ */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
color: #888;
gap: 1rem;
}
.empty-state h3 {
color: white;
font-size: 1.5rem;
margin: 0;
}
.empty-state p {
margin: 0;
font-size: 1rem;
}
/* ============================================ */
/* LOADING */
/* ============================================ */
.loading-users {
text-align: center;
padding: 4rem 2rem;
}
.loading-users p {
font-size: 1.2rem;
color: #666;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* ============================================ */
/* RESPONSIVE */
/* ============================================ */
@media (max-width: 1024px) {
.filters-section {
flex-direction: column;
}
.search-bar {
min-width: 100%;
}
.role-filters {
flex-wrap: wrap;
}
}
@media (max-width: 768px) {
.users-management-container {
padding: 1rem;
}
.user-header {
flex-direction: column;
align-items: flex-start;
}
.user-role-badge {
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
}
.user-actions {
flex-direction: column;
width: 100%;
}
.btn-edit,
.btn-delete {
width: 100%;
}
.edit-modal-content {
max-width: 95%;
}
.edit-modal-header,
.edit-modal-body {
padding: 1.5rem;
}
.modal-header-title h3 {
font-size: 1.25rem;
}
.modal-actions {
flex-direction: column;
}
.btn-cancel,
.btn-save {
width: 100%;
}
}
@media (max-width: 480px) {
.filter-btn {
padding: 10px 13px;
font-size: 0.85rem;
}
.user-avatar {
width: 48px;
height: 48px;
}
.user-username {
font-size: 1rem;
}
.btn-edit,
.btn-delete {
padding: 0.65rem 1rem;
font-size: 0.9rem;
}
.edit-modal-header,
.edit-modal-body {
padding: 1rem;
}
.modal-header-title h3 {
font-size: 1.1rem;
}
.form-group2 input,
.form-group2 select {
padding: 0.75rem 1rem;
font-size: 0.95rem;
}
}
@media (hover: none) {
.user-card:hover {
transform: none;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.btn-edit:hover,
.btn-delete:hover,
.btn-cancel:hover,
.btn-save:hover {
transform: none;
}
}
/* Styles supplémentaires pour les clients */
.clients-section {
margin-top: 3rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.search-bar.compact {
max-width: 400px;
margin: 0;
}
.client-card {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
}
.user-avatar.client {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
}
.user-role-badge.green {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(16, 185, 129, 0.1)
);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.client-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
padding: 1.5rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
margin: 1rem 0;
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
color: rgba(255, 255, 255, 0.9);
font-size: 0.9rem;
}
.info-item.full-width {
grid-column: 1 / -1;
}
.info-item svg {
color: #10b981;
flex-shrink: 0;
}
.info-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-view {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.btn-view:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
background: linear-gradient(135deg, #059669, #047857);
}
.btn-view:active {
transform: translateY(0);
}
/* Client Details Modal */
.client-modal {
max-width: 700px;
}
.client-detail-section {
margin-bottom: 2rem;
}
.client-detail-section h4 {
color: #10b981;
font-size: 1.1rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(16, 185, 129, 0.3);
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-item.full {
grid-column: 1 / -1;
}
.detail-item label {
display: flex;
align-items: center;
gap: 0.5rem;
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-item label svg {
color: #10b981;
}
.detail-item p {
color: rgba(255, 255, 255, 0.95);
font-size: 1rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
border-left: 3px solid #10b981;
margin: 0;
word-break: break-word;
}
/* Stat card green variant */
.stat-icon.green {
background: linear-gradient(135deg, #10b981, #059669);
}
/* Responsive */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
align-items: stretch;
}
.search-bar.compact {
max-width: 100%;
}
.client-info-grid {
grid-template-columns: 1fr;
}
.detail-grid {
grid-template-columns: 1fr;
}
}
/* Animation pour les cartes clients */
@keyframes slideInClient {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.client-card {
animation: slideInClient 0.4s ease-out;
}
/* Hover effect pour les info items */
.info-item:hover svg {
transform: scale(1.2);
transition: transform 0.2s ease;
}
@@ -1,876 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import {
User,
Shield,
Truck,
Store,
Search,
XCircle,
Phone,
TrendingUp,
AlertCircle,
Filter,
Eye,
Leaf,
Wind,
} from "lucide-react";
import "./UserManagement.css";
import SidebarCabine from "../../components/SidebarCabine";
import { getAllUsers, getAllClients } from "../../api/api_admin";
import { isCabineAuthenticated } from "../../api/api_cabine"; // ⭐ AJOUT
import type { AdminResponse } from "../../api/api_admin_types";
import type { ClientResponse } from "../../api/api_admin";
// ============================================
// TYPES & INTERFACES
// ============================================
interface UserStats {
totalUsers: number;
totalCabines: number;
totalLivreurs: number;
}
type UserRole = "admin" | "cabine" | "livreur" | "client";
type RoleFilter = "all" | "cabine" | "livreur";
interface RoleConfig {
color: string;
label: string;
icon: React.ComponentType<{ size: number }>;
}
// ============================================
// CONSTANTS
// ============================================
const ROLE_CONFIG: Record<UserRole, RoleConfig> = {
admin: { color: "red", label: "Administrateur", icon: Shield },
cabine: { color: "purple", label: "Cabine", icon: Store },
livreur: { color: "blue", label: "Livreur", icon: Truck },
client: { color: "green", label: "Client", icon: User },
};
// ============================================
// MAIN COMPONENT
// ============================================
function UsersManagement() {
const navigate = useNavigate();
// État des utilisateurs
const [users, setUsers] = useState<AdminResponse[]>([]);
const [filteredUsers, setFilteredUsers] = useState<AdminResponse[]>([]);
const [stats, setStats] = useState<UserStats>({
totalUsers: 0,
totalCabines: 0,
totalLivreurs: 0,
});
// État des clients
const [clients, setClients] = useState<ClientResponse[]>([]);
const [filteredClients, setFilteredClients] = useState<ClientResponse[]>(
[],
);
const [totalClients, setTotalClients] = useState(0);
// État des filtres et recherche
const [searchTerm, setSearchTerm] = useState("");
const [clientSearchTerm, setClientSearchTerm] = useState("");
const [roleFilter, setRoleFilter] = useState<RoleFilter>("all");
// État du modal
const [selectedClient, setSelectedClient] = useState<ClientResponse | null>(
null,
);
const [showClientModal, setShowClientModal] = useState(false);
// État de chargement
const [loading, setLoading] = useState(true);
// ============================================
// 🔐 VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
// ============================================
useEffect(() => {
const checkAuth = () => {
if (!isCabineAuthenticated()) {
console.log(
"❌ [UserManagement] Cabine non authentifié, redirection vers /login-cabine/cabine",
);
navigate("/login-cabine/cabine", { replace: true });
}
};
checkAuth();
}, [navigate]);
// ============================================
// 🔐 VÉRIFICATION CONTINUE (toutes les 5 secondes)
// ============================================
useEffect(() => {
const authInterval = setInterval(() => {
if (!isCabineAuthenticated()) {
console.log(
"❌ [UserManagement] Session cabine expirée, redirection vers /login-cabine/cabine",
);
navigate("/login-cabine/cabine", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
// ============================================
// FETCH DATA
// ============================================
const fetchUsers = useCallback(async () => {
// ✅ Vérifier l'auth avant de charger
if (!isCabineAuthenticated()) {
console.log("❌ [fetchUsers] Cabine non authentifié");
navigate("/login-cabine/cabine", { replace: true });
return;
}
try {
console.log("👥 [USERS_MANAGEMENT] Chargement des utilisateurs...");
setLoading(true);
const usersData = await getAllUsers();
console.log(
"✅ [USERS_MANAGEMENT] Utilisateurs récupérés:",
usersData,
);
// Exclure les admins de la liste
const filteredUsersData = usersData.filter(
(u) => u.role !== "admin",
);
setUsers(filteredUsersData);
// Calculer les statistiques
const newStats: UserStats = {
totalUsers: filteredUsersData.length,
totalCabines: filteredUsersData.filter(
(u) => u.role === "cabine",
).length,
totalLivreurs: filteredUsersData.filter(
(u) => u.role === "livreur",
).length,
};
setStats(newStats);
console.log("📊 [USERS_MANAGEMENT] Stats:", newStats);
} catch (error) {
console.error("❌ [USERS_MANAGEMENT] Erreur chargement:", error);
if (error instanceof Error && error.message.includes("401")) {
console.log("🔓 [UserManagement] Token invalide");
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
return;
}
alert("Erreur lors du chargement des utilisateurs");
} finally {
setLoading(false);
}
}, [navigate]);
const fetchClients = useCallback(async () => {
// ✅ Vérifier l'auth avant de charger
if (!isCabineAuthenticated()) {
console.log("❌ [fetchClients] Cabine non authentifié");
navigate("/login-cabine/cabine", { replace: true });
return;
}
try {
console.log("👥 [CLIENTS_MANAGEMENT] Chargement des clients...");
setLoading(true);
const clientData = await getAllClients();
console.log(
"✅ [CLIENTS_MANAGEMENT] Clients récupérés:",
clientData,
);
if (clientData && clientData.length > 0) {
const firstClient = clientData[0];
console.log("🔍 [DEBUG] Premier client:", {
username: firstClient.username,
points_weed: firstClient.point,
points_zipette: firstClient.points_zipette,
});
}
setClients(clientData);
setTotalClients(clientData.length);
} catch (error) {
console.error("❌ [CLIENTS_MANAGEMENT] Erreur:", error);
if (error instanceof Error && error.message.includes("401")) {
console.log("🔓 [UserManagement] Token invalide");
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-cabine/cabine", { replace: true });
return;
}
alert("Erreur lors du chargement des clients");
} finally {
setLoading(false);
}
}, [navigate]);
// ============================================
// FILTERS
// ============================================
const filterUsers = useCallback(() => {
let filtered = [...users];
if (searchTerm) {
filtered = filtered.filter((user) =>
user.username.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
if (roleFilter !== "all") {
filtered = filtered.filter((user) => user.role === roleFilter);
}
setFilteredUsers(filtered);
}, [users, searchTerm, roleFilter]);
const filterClients = useCallback(() => {
let filtered = [...clients];
if (clientSearchTerm) {
const searchLower = clientSearchTerm.toLowerCase();
filtered = filtered.filter(
(client) =>
client.nom.toLowerCase().includes(searchLower) ||
client.prenom.toLowerCase().includes(searchLower) ||
client.username.toLowerCase().includes(searchLower) ||
client.telephone.includes(clientSearchTerm),
);
}
setFilteredClients(filtered);
}, [clients, clientSearchTerm]);
// ============================================
// HANDLERS
// ============================================
const handleViewClient = useCallback((client: ClientResponse) => {
setSelectedClient(client);
setShowClientModal(true);
}, []);
const handleCloseModal = useCallback(() => {
setShowClientModal(false);
setSelectedClient(null);
}, []);
const handleRoleFilterChange = useCallback((filter: RoleFilter) => {
setRoleFilter(filter);
}, []);
// ============================================
// EFFECTS
// ============================================
useEffect(() => {
fetchUsers();
fetchClients();
}, [fetchUsers, fetchClients]);
useEffect(() => {
filterUsers();
}, [filterUsers]);
useEffect(() => {
filterClients();
}, [filterClients]);
// ============================================
// RENDER HELPERS
// ============================================
const getRoleConfig = (role: string): RoleConfig => {
return ROLE_CONFIG[role as UserRole] || ROLE_CONFIG.client;
};
const getRoleIcon = (role: string): React.ReactElement => {
const config = getRoleConfig(role);
const IconComponent = config.icon;
return <IconComponent size={18} />;
};
// ============================================
// LOADING STATE
// ============================================
if (loading) {
return (
<div className="users-management-container">
<div className="loading-users">
<p>Chargement des utilisateurs...</p>
</div>
</div>
);
}
// ============================================
// MAIN RENDER
// ============================================
return (
<>
<SidebarCabine />
<div className="users-management-container">
<Header />
<StatsGrid stats={stats} totalClients={totalClients} />
<FiltersSection
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
roleFilter={roleFilter}
onRoleFilterChange={handleRoleFilterChange}
stats={stats}
usersCount={users.length}
/>
<UsersList
filteredUsers={filteredUsers}
getRoleIcon={getRoleIcon}
getRoleConfig={getRoleConfig}
/>
<ClientsSection
clients={clients}
filteredClients={filteredClients}
clientSearchTerm={clientSearchTerm}
setClientSearchTerm={setClientSearchTerm}
onViewClient={handleViewClient}
/>
{showClientModal && selectedClient && (
<ClientModal
client={selectedClient}
onClose={handleCloseModal}
/>
)}
</div>
</>
);
}
// ============================================
// SUB-COMPONENTS (identiques à l'original)
// ============================================
function Header() {
return (
<div className="dashboard-header">
<h1>Gestion des Utilisateurs</h1>
<p className="dashboard-subtitle">
Visualiser et gérer tous les comptes système
</p>
</div>
);
}
interface StatsGridProps {
stats: UserStats;
totalClients: number;
}
function StatsGrid({ stats, totalClients }: StatsGridProps) {
const statsCards = [
{
icon: User,
color: "blue",
label: "Total Utilisateurs",
value: stats.totalUsers,
},
{
icon: Store,
color: "purple",
label: "Cabines",
value: stats.totalCabines,
},
{
icon: Truck,
color: "blue",
label: "Livreurs",
value: stats.totalLivreurs,
},
{
icon: User,
color: "green",
label: "Clients",
value: totalClients,
},
];
return (
<div className="stats-grid">
{statsCards.map((card, index) => (
<StatCard key={index} {...card} />
))}
</div>
);
}
interface StatCardProps {
icon: React.ComponentType<{ size: number }>;
color: string;
label: string;
value: number;
}
function StatCard({ icon: Icon, color, label, value }: StatCardProps) {
return (
<div className="stat-card">
<div className={`stat-icon ${color}`}>
<Icon size={24} />
</div>
<div className="stat-content">
<p className="stat-label">{label}</p>
<h3 className="stat-value">{value}</h3>
</div>
</div>
);
}
interface FiltersSectionProps {
searchTerm: string;
setSearchTerm: (term: string) => void;
roleFilter: RoleFilter;
onRoleFilterChange: (filter: RoleFilter) => void;
stats: UserStats;
usersCount: number;
}
function FiltersSection({
searchTerm,
setSearchTerm,
roleFilter,
onRoleFilterChange,
stats,
usersCount,
}: FiltersSectionProps) {
const filters = [
{
value: "all" as RoleFilter,
icon: Filter,
label: "Tous",
count: usersCount,
},
{
value: "cabine" as RoleFilter,
icon: Store,
label: "Cabines",
count: stats.totalCabines,
},
{
value: "livreur" as RoleFilter,
icon: Truck,
label: "Livreurs",
count: stats.totalLivreurs,
},
];
return (
<div className="filters-section">
<div className="search-bar">
<Search size={20} />
<input
type="text"
placeholder="Rechercher par username..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="role-filters">
{filters.map((filter) => {
const IconComponent = filter.icon;
return (
<button
key={filter.value}
className={`filter-btn ${roleFilter === filter.value ? "active" : ""}`}
onClick={() => onRoleFilterChange(filter.value)}
>
<IconComponent size={18} />
{filter.label} ({filter.count})
</button>
);
})}
</div>
</div>
);
}
interface UsersListProps {
filteredUsers: AdminResponse[];
getRoleIcon: (role: string) => React.ReactElement;
getRoleConfig: (role: string) => RoleConfig;
}
function UsersList({
filteredUsers,
getRoleIcon,
getRoleConfig,
}: UsersListProps) {
return (
<div className="users-section">
<h2>
<User size={24} />
Utilisateurs Système ({filteredUsers.length})
</h2>
<div className="users-list">
{filteredUsers.length === 0 ? (
<EmptyState
icon={User}
title="Aucun utilisateur trouvé"
message="Essayez de modifier vos filtres de recherche"
/>
) : (
filteredUsers.map((user) => (
<UserCard
key={user.id}
user={user}
getRoleIcon={getRoleIcon}
getRoleConfig={getRoleConfig}
/>
))
)}
</div>
</div>
);
}
interface UserCardProps {
user: AdminResponse;
getRoleIcon: (role: string) => React.ReactElement;
getRoleConfig: (role: string) => RoleConfig;
}
function UserCard({ user, getRoleIcon, getRoleConfig }: UserCardProps) {
const roleConfig = getRoleConfig(user.role);
return (
<div className="user-card">
<div className="user-header">
<div className="user-info-main">
<div className="user-avatar">{getRoleIcon(user.role)}</div>
<div className="user-details">
<strong className="user-username">
{user.username}
</strong>
<span className="user-id">ID: {user.id}</span>
</div>
</div>
<div className={`user-role-badge ${roleConfig.color}`}>
{getRoleIcon(user.role)}
<span>{roleConfig.label}</span>
</div>
</div>
</div>
);
}
interface ClientsSectionProps {
clients: ClientResponse[];
filteredClients: ClientResponse[];
clientSearchTerm: string;
setClientSearchTerm: (term: string) => void;
onViewClient: (client: ClientResponse) => void;
}
function ClientsSection({
clients,
filteredClients,
clientSearchTerm,
setClientSearchTerm,
onViewClient,
}: ClientsSectionProps) {
return (
<div className="users-section clients-section">
<div className="section-header">
<h2>
<User size={24} />
Clients Enregistrés ({clients.length})
</h2>
<div className="search-bar compact">
<Search size={18} />
<input
type="text"
placeholder="Rechercher un client..."
value={clientSearchTerm}
onChange={(e) => setClientSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="users-list">
{filteredClients.length === 0 ? (
<EmptyState
icon={User}
title="Aucun client trouvé"
message={
clientSearchTerm
? "Essayez de modifier votre recherche"
: "Les clients s'afficheront ici une fois inscrits"
}
/>
) : (
filteredClients.map((client) => (
<ClientCard
key={client.id}
client={client}
onViewClient={onViewClient}
/>
))
)}
</div>
</div>
);
}
interface ClientCardProps {
client: ClientResponse;
onViewClient: (client: ClientResponse) => void;
}
function ClientCard({ client, onViewClient }: ClientCardProps) {
return (
<div className="user-card client-card">
<div className="user-header">
<div className="user-info-main">
<div className="user-avatar client">
<User size={18} />
</div>
<div className="user-details">
<strong className="user-username">
{client.prenom} {client.nom}
</strong>
<span className="user-id">@{client.username}</span>
</div>
</div>
<div className="user-role-badge green">
<User size={18} />
<span>Client</span>
</div>
</div>
<div className="client-info-grid">
<InfoItem icon={Phone} text={client.telephone} />
<InfoItem
icon={TrendingUp}
text={`${client.command} commande${client.command > 1 ? "s" : ""}`}
/>
<InfoItem icon={Leaf} text={`${client.point || 0} pts Weed`} />
<InfoItem
icon={Wind}
text={`${client.points_zipette || 0} pts Zipette`}
/>
<InfoItem
icon={AlertCircle}
text={`${client.amende}€ d'amendes`}
/>
</div>
<div className="user-actions">
<button
className="btn-view"
onClick={() => onViewClient(client)}
>
<Eye size={18} />
Voir détails
</button>
</div>
</div>
);
}
interface InfoItemProps {
icon: React.ComponentType<{ size: number }>;
text: string;
}
function InfoItem({ icon: Icon, text }: InfoItemProps) {
return (
<div className="info-item">
<Icon size={16} />
<span>{text}</span>
</div>
);
}
interface ClientModalProps {
client: ClientResponse;
onClose: () => void;
}
function ClientModal({ client, onClose }: ClientModalProps) {
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div className="modal-overlay" onClick={handleBackdropClick}>
<div
className="edit-modal-content client-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="edit-modal-header">
<div className="modal-header-title">
<User size={24} />
<h3>Détails du Client</h3>
</div>
<button className="modal-close-btn" onClick={onClose}>
<XCircle size={20} />
</button>
</div>
<div className="edit-modal-body">
<DetailSection title="Informations personnelles">
<DetailItem
icon={User}
label="Prénom"
value={client.prenom}
/>
<DetailItem
icon={User}
label="Nom"
value={client.nom}
/>
<DetailItem
icon={Shield}
label="Username"
value={`@${client.username}`}
/>
<DetailItem
icon={Phone}
label="Téléphone"
value={client.telephone}
/>
</DetailSection>
<DetailSection title="Statistiques">
<DetailItem
icon={TrendingUp}
label="Commandes"
value={`${client.command} commande${client.command > 1 ? "s" : ""}`}
/>
<DetailItem
icon={Leaf}
label="Points Weed/Hash"
value={`${client.point || 0} points`}
/>
<DetailItem
icon={Wind}
label="Points Zipette"
value={`${client.points_zipette || 0} points`}
/>
<DetailItem
icon={AlertCircle}
label="Amendes"
value={`${client.amende}`}
/>
<DetailItem
icon={XCircle}
label="Annulations"
value={String(client.cancellations_count)}
/>
{client.last_penalty_reason && (
<DetailItem
icon={AlertCircle}
label="Dernière pénalité"
value={client.last_penalty_reason}
fullWidth
/>
)}
</DetailSection>
<DetailSection title="Informations système">
<DetailItem
icon={Shield}
label="ID Client"
value={`#${client.id}`}
/>
</DetailSection>
<div className="modal-actions">
<button className="btn-cancel" onClick={onClose}>
<XCircle size={18} />
Fermer
</button>
</div>
</div>
</div>
</div>
);
}
interface DetailSectionProps {
title: string;
children: React.ReactNode;
}
function DetailSection({ title, children }: DetailSectionProps) {
return (
<div className="client-detail-section">
<h4>{title}</h4>
<div className="detail-grid">{children}</div>
</div>
);
}
interface DetailItemProps {
icon: React.ComponentType<{ size: number }>;
label: string;
value: string;
fullWidth?: boolean;
}
function DetailItem({ icon: Icon, label, value, fullWidth }: DetailItemProps) {
return (
<div className={`detail-item ${fullWidth ? "full" : ""}`}>
<label>
<Icon size={16} />
{label}
</label>
<p>{value}</p>
</div>
);
}
interface EmptyStateProps {
icon: React.ComponentType<{ size: number }>;
title: string;
message: string;
}
function EmptyState({ icon: Icon, title, message }: EmptyStateProps) {
return (
<div className="empty-state">
<div style={{ opacity: 0.3 }}>
<Icon size={64} />
</div>
<h3>{title}</h3>
<p>{message}</p>
</div>
);
}
export default UsersManagement;
@@ -0,0 +1,218 @@
.cp-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #09090b;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow-y: auto;
}
.cp-content {
width: 100%;
max-width: 28rem;
}
.cp-header {
text-align: center;
margin-bottom: 2rem;
}
.cp-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
background: linear-gradient(135deg, #7c3aed, #6d28d9);
border-radius: 1rem;
margin-bottom: 1rem;
box-shadow: 0 10px 24px rgba(109, 40, 217, 0.45);
}
.cp-title {
font-size: 1.6rem;
font-weight: 700;
color: #f4f4f5;
margin-bottom: 0.5rem;
}
.cp-subtitle {
color: #71717a;
font-size: 0.9rem;
line-height: 1.5;
padding: 0 0.5rem;
}
.cp-card {
background-color: #111115;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.07);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
padding: 2rem;
}
.cp-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.cp-form-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cp-label {
font-size: 0.875rem;
font-weight: 500;
color: #d1d5db;
}
.cp-input-wrapper {
position: relative;
}
.cp-input-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
pointer-events: none;
color: #52525b;
}
.cp-input {
width: 100%;
padding: 0.75rem 3rem 0.75rem 3rem;
background-color: #09090b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
color: #f4f4f5;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.cp-input::placeholder {
color: #52525b;
}
.cp-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.18);
}
.cp-input-error {
border-color: #ef4444;
}
.cp-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
background: transparent;
border: none;
color: #52525b;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s;
}
.cp-toggle:hover {
color: #a1a1aa;
}
.cp-hint {
font-size: 0.78rem;
color: #71717a;
margin: 0;
}
.cp-error-msg {
font-size: 0.78rem;
color: #f87171;
margin: 0;
}
.cp-error-banner {
padding: 0.75rem 1rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.25);
border-radius: 0.5rem;
color: #f87171;
font-size: 0.875rem;
}
.cp-submit {
width: 100%;
padding: 0.8rem 1rem;
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: #fff;
font-weight: 600;
font-size: 0.95rem;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
box-shadow: 0 4px 16px rgba(109, 40, 217, 0.4);
margin-top: 0.25rem;
}
.cp-submit:hover:not(:disabled) {
opacity: 0.9;
}
.cp-submit:active:not(:disabled) {
transform: scale(0.98);
}
.cp-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Success state */
.cp-success {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 1.5rem 0;
text-align: center;
color: #f4f4f5;
}
.cp-success-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
background: rgba(34, 197, 94, 0.15);
border: 2px solid rgba(34, 197, 94, 0.4);
border-radius: 50%;
font-size: 1.5rem;
color: #4ade80;
}
.cp-success p {
margin: 0;
font-weight: 600;
font-size: 1rem;
}
.cp-success-sub {
color: #71717a;
font-size: 0.85rem;
font-weight: 400 !important;
}
@@ -0,0 +1,190 @@
import { useState } from "react";
import { Lock, Eye, EyeOff, ShieldCheck } from "lucide-react";
import "./ChangePassword.css";
import { changePassword } from "../../api/api";
import { useNavigate } from "react-router-dom";
const ChangePasswordPage = () => {
const navigate = useNavigate();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const validate = (): string | null => {
if (!currentPassword) return "Le mot de passe actuel est requis";
if (!newPassword) return "Le nouveau mot de passe est requis";
if (newPassword.length < 8) return "Le nouveau mot de passe doit contenir au moins 8 caractères";
if (newPassword === currentPassword) return "Le nouveau mot de passe doit être différent de l'ancien";
if (newPassword !== confirmPassword) return "Les mots de passe ne correspondent pas";
return null;
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
setIsLoading(true);
try {
const result = await changePassword(currentPassword, newPassword);
if (result.success) {
setSuccess(true);
setTimeout(() => navigate("/user/accueil"), 1800);
} else {
setError(result.message);
}
} finally {
setIsLoading(false);
}
};
return (
<div className="cp-container">
<div className="cp-content">
<div className="cp-header">
<div className="cp-logo">
<ShieldCheck className="w-8 h-8 text-white" />
</div>
<h1 className="cp-title">Changement de mot de passe</h1>
<p className="cp-subtitle">
Pour votre sécurité, veuillez définir un nouveau mot de passe
avant de continuer.
</p>
</div>
<div className="cp-card">
{success ? (
<div className="cp-success">
<span className="cp-success-icon"></span>
<p>Mot de passe mis à jour avec succès !</p>
<p className="cp-success-sub">Redirection en cours</p>
</div>
) : (
<form className="cp-form" onSubmit={handleSubmit}>
{error && (
<div className="cp-error-banner">
{error}
</div>
)}
{/* Mot de passe actuel */}
<div className="cp-form-group">
<label className="cp-label">Mot de passe actuel</label>
<div className="cp-input-wrapper">
<Lock className="cp-input-icon" style={{ width: "18px", height: "18px" }} />
<input
type={showCurrent ? "text" : "password"}
value={currentPassword}
onChange={(e) => { setCurrentPassword(e.target.value); setError(""); }}
className="cp-input"
placeholder="••••••••"
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
className="cp-toggle"
onClick={() => setShowCurrent((v) => !v)}
disabled={isLoading}
>
{showCurrent ? (
<EyeOff style={{ width: "18px", height: "18px" }} />
) : (
<Eye style={{ width: "18px", height: "18px" }} />
)}
</button>
</div>
</div>
{/* Nouveau mot de passe */}
<div className="cp-form-group">
<label className="cp-label">Nouveau mot de passe</label>
<div className="cp-input-wrapper">
<Lock className="cp-input-icon" style={{ width: "18px", height: "18px" }} />
<input
type={showNew ? "text" : "password"}
value={newPassword}
onChange={(e) => { setNewPassword(e.target.value); setError(""); }}
className="cp-input"
placeholder="Minimum 8 caractères"
disabled={isLoading}
autoComplete="new-password"
/>
<button
type="button"
className="cp-toggle"
onClick={() => setShowNew((v) => !v)}
disabled={isLoading}
>
{showNew ? (
<EyeOff style={{ width: "18px", height: "18px" }} />
) : (
<Eye style={{ width: "18px", height: "18px" }} />
)}
</button>
</div>
{newPassword.length > 0 && newPassword.length < 8 && (
<p className="cp-hint">Encore {8 - newPassword.length} caractère{8 - newPassword.length > 1 ? "s" : ""} minimum</p>
)}
</div>
{/* Confirmer */}
<div className="cp-form-group">
<label className="cp-label">Confirmer le nouveau mot de passe</label>
<div className="cp-input-wrapper">
<Lock className="cp-input-icon" style={{ width: "18px", height: "18px" }} />
<input
type={showConfirm ? "text" : "password"}
value={confirmPassword}
onChange={(e) => { setConfirmPassword(e.target.value); setError(""); }}
className={`cp-input ${confirmPassword && confirmPassword !== newPassword ? "cp-input-error" : ""}`}
placeholder="••••••••"
disabled={isLoading}
autoComplete="new-password"
/>
<button
type="button"
className="cp-toggle"
onClick={() => setShowConfirm((v) => !v)}
disabled={isLoading}
>
{showConfirm ? (
<EyeOff style={{ width: "18px", height: "18px" }} />
) : (
<Eye style={{ width: "18px", height: "18px" }} />
)}
</button>
</div>
{confirmPassword && confirmPassword !== newPassword && (
<p className="cp-error-msg">Les mots de passe ne correspondent pas</p>
)}
</div>
<button
type="submit"
className="cp-submit"
disabled={isLoading}
>
{isLoading ? "Mise à jour…" : "Confirmer le nouveau mot de passe"}
</button>
</form>
)}
</div>
</div>
</div>
);
};
export default ChangePasswordPage;
@@ -1,651 +0,0 @@
/* ============================================
ALERT HISTORY - CONTAINER
============================================ */
.alert-history-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1400px;
margin: 0 auto;
}
/* ============================================
HEADER
============================================ */
.alert-history-header {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: #9ca3af;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 2rem;
}
.back-button:hover {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.1) 0%,
rgba(5, 150, 105, 0.05) 100%
);
border-color: rgba(16, 185, 129, 0.3);
color: #10b981;
transform: translateX(-4px);
}
.header-content {
display: flex;
align-items: center;
gap: 1.5rem;
}
.header-icon {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.alert-history-header h1 {
color: white;
font-size: clamp(2rem, 6vw, 2.5rem);
margin: 0 0 0.5rem 0;
font-weight: bold;
letter-spacing: -0.5px;
}
.header-subtitle {
color: #888;
font-size: clamp(1rem, 3vw, 1.1rem);
margin: 0;
}
/* ============================================
STATS CARDS
============================================ */
.alert-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.alert-stat-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
display: flex;
align-items: center;
gap: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.alert-stat-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(16, 185, 129, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.alert-stat-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(16, 185, 129, 0.3);
box-shadow: 0 12px 32px rgba(16, 185, 129, 0.2);
transform: translateY(-4px);
}
.alert-stat-card:hover::before {
left: 100%;
}
.alert-stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.alert-stat-card:hover .alert-stat-icon {
transform: scale(1.1) rotate(5deg);
}
.alert-stat-icon.total {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
}
.alert-stat-icon.active {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.2),
rgba(220, 38, 38, 0.1)
);
color: #ef4444;
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.3);
}
.alert-stat-icon.resolved {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
color: #10b981;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.alert-stat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alert-stat-label {
color: #888;
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.alert-stat-value {
color: white;
font-size: clamp(1.8rem, 5vw, 2.2rem);
margin: 0;
font-weight: bold;
letter-spacing: -0.5px;
}
/* ============================================
FILTERS
============================================ */
.alert-filters {
display: flex;
gap: 1rem;
margin-bottom: clamp(2rem, 5vw, 3rem);
flex-wrap: wrap;
}
.filter-button {
padding: 0.75rem 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
color: #9ca3af;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
}
.filter-button:hover {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.1) 0%,
rgba(5, 150, 105, 0.05) 100%
);
border-color: rgba(16, 185, 129, 0.3);
color: #10b981;
transform: translateY(-2px);
}
.filter-button.active {
background: linear-gradient(135deg, #10b981, #059669);
border-color: #10b981;
color: white;
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
}
.filter-button.active:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4);
}
/* ============================================
ALERTS LIST
============================================ */
.alerts-section {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.alerts-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.alert-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.alert-card.active {
border-color: rgba(239, 68, 68, 0.3);
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.05) 0%,
rgba(220, 38, 38, 0.02) 100%
);
}
.alert-card.resolved {
border-color: rgba(16, 185, 129, 0.2);
}
.alert-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
}
.alert-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.alert-card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.alert-card.active .alert-card-icon {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.2),
rgba(220, 38, 38, 0.1)
);
color: #ef4444;
}
.alert-card.resolved .alert-card-icon {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
color: #10b981;
}
.alert-card-info {
flex: 1;
}
.alert-card-info h3 {
color: white;
font-size: 1.2rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
}
.alert-card-meta {
display: flex;
align-items: center;
gap: 0.5rem;
color: #9ca3af;
font-size: 0.9rem;
}
.alert-status-badge {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alert-status-badge.active {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.2),
rgba(220, 38, 38, 0.1)
);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.4);
animation: pulse-badge 2s infinite;
}
.alert-status-badge.resolved {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.4);
}
@keyframes pulse-badge {
0%,
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
}
}
.alert-card-details {
display: flex;
flex-direction: column;
gap: 1rem;
}
.alert-detail-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.02) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
}
.alert-detail-item svg {
color: #10b981;
flex-shrink: 0;
margin-top: 2px;
}
.alert-detail-item > div {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
color: #888;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.detail-value {
color: white;
font-size: 1rem;
font-weight: 500;
}
.alert-active-indicator {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.1) 0%,
rgba(220, 38, 38, 0.05) 100%
);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
color: #ef4444;
font-weight: 600;
}
.pulse-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ef4444;
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%,
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
}
50% {
box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
}
}
/* ============================================
NO ALERTS CARD
============================================ */
.no-alerts-card {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
backdrop-filter: blur(8px);
}
.no-alerts-card p {
color: #9ca3af;
font-size: 1.2rem;
font-weight: 600;
margin: 1rem 0 0.5rem 0;
}
.no-alerts-text {
color: #6b7280;
font-size: 0.95rem;
}
/* ============================================
INFO BOX
============================================ */
.info-box {
display: flex;
gap: 1rem;
padding: 1.5rem;
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.1) 0%,
rgba(37, 99, 235, 0.05) 100%
);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 16px;
color: #3b82f6;
}
.info-box svg {
flex-shrink: 0;
margin-top: 2px;
}
.info-box h4 {
color: white;
font-size: 1rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
}
.info-box p {
color: #9ca3af;
font-size: 0.95rem;
line-height: 1.6;
margin: 0;
}
/* ============================================
LOADING
============================================ */
.loading-alerts {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
padding: 4rem 2rem;
text-align: center;
}
.loading-alerts p {
color: #9ca3af;
font-size: 1.2rem;
font-weight: 600;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: center;
}
.header-icon {
width: 56px;
height: 56px;
}
.alert-stats-grid {
grid-template-columns: 1fr;
}
.alert-filters {
flex-direction: column;
}
.filter-button {
width: 100%;
}
.alert-card-header {
flex-wrap: wrap;
}
.alert-status-badge {
width: 100%;
text-align: center;
}
}
@media (max-width: 480px) {
.alert-card-header {
flex-direction: column;
align-items: flex-start;
}
.alert-card-icon {
width: 40px;
height: 40px;
}
.alert-card-info h3 {
font-size: 1.1rem;
}
}
@media (hover: none) {
.back-button:hover,
.alert-stat-card:hover,
.filter-button:hover,
.alert-card:hover {
transform: none;
}
}
@@ -1,385 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Shield,
ArrowLeft,
Clock,
CheckCircle,
AlertTriangle,
Calendar,
Info,
} from "lucide-react";
import "./AlertHistory.css";
import { getMyAlerts, isDeliveryAuthenticated } from "../../api/api_delivery";
import { extractAdminUsernameFromToken } from "../../api/api_admin";
interface Alert {
id: number;
username: string;
status: string;
created_at: string;
updated_at: string;
}
interface AlertStats {
total: number;
active: number;
resolved: number;
}
function AlertHistory() {
const navigate = useNavigate();
const [alerts, setAlerts] = useState<Alert[]>([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState<AlertStats>({
total: 0,
active: 0,
resolved: 0,
});
const [filter, setFilter] = useState<"all" | "active" | "resolved">("all");
const [deliveryPersonName, setDeliveryPersonName] = useState("");
useEffect(() => {
const checkAuth = () => {
if (!isDeliveryAuthenticated()) {
console.log(
"❌ [ALERT_HISTORY] Non authentifié, redirection vers /login-delivery/delivery",
);
navigate("/login-delivery/delivery", { replace: true });
}
};
checkAuth();
}, [navigate]);
useEffect(() => {
const fetchAlerts = async () => {
try {
const username = extractAdminUsernameFromToken();
if (!username) {
console.error("❌ [ALERT_HISTORY] Username non trouvé");
setLoading(false);
return;
}
setDeliveryPersonName(username);
console.log(
"🔍 [ALERT_HISTORY] Chargement alertes pour:",
username,
);
const result = await getMyAlerts();
if (result.success && result.alerts) {
const alertsData = result.alerts;
console.log("✅ [ALERT_HISTORY] Alertes:", alertsData);
setAlerts(alertsData);
// Calculer les statistiques
const total = alertsData.length;
const active = alertsData.filter(
(alert) => alert.status === "true",
).length;
const resolved = alertsData.filter(
(alert) => alert.status === "false",
).length;
setStats({ total, active, resolved });
} else {
console.error("❌ [ALERT_HISTORY] Erreur:", result.error);
}
} catch (error) {
console.error(
"❌ [ALERT_HISTORY] Erreur chargement alertes:",
error,
);
} finally {
setLoading(false);
}
};
fetchAlerts();
}, []);
const getFilteredAlerts = () => {
switch (filter) {
case "active":
return alerts.filter((alert) => alert.status === "true");
case "resolved":
return alerts.filter((alert) => alert.status === "false");
default:
return alerts;
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now.getTime() - date.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return `Aujourd'hui à ${date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
})}`;
} else if (diffDays === 1) {
return `Hier à ${date.toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
})}`;
} else if (diffDays < 7) {
return `Il y a ${diffDays} jours`;
} else {
return date.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
};
const getDuration = (createdAt: string, updatedAt: string) => {
const start = new Date(createdAt);
const end = new Date(updatedAt);
const diffMs = end.getTime() - start.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return "< 1 min";
if (diffMins < 60) return `${diffMins} min`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`;
};
if (loading) {
return (
<div className="alert-history-container">
<div className="loading-alerts">
<Shield size={48} color="#10b981" />
<p>Chargement de l'historique...</p>
</div>
</div>
);
}
const filteredAlerts = getFilteredAlerts();
return (
<div className="alert-history-container">
{/* Header */}
<div className="alert-history-header">
<button
className="back-button"
onClick={() => navigate("/delivery/dashboard")}
>
<ArrowLeft size={20} />
<span>Retour</span>
</button>
<div className="header-content">
<div className="header-icon">
<Shield size={32} color="#10b981" />
</div>
<div>
<h1>Historique des Alertes</h1>
<p className="header-subtitle">
Toutes vos alertes police - {deliveryPersonName}
</p>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="alert-stats-grid">
<div className="alert-stat-card">
<div className="alert-stat-icon total">
<Shield size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Total Alertes</p>
<h3 className="alert-stat-value">{stats.total}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon active">
<AlertTriangle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Alertes Actives</p>
<h3 className="alert-stat-value">{stats.active}</h3>
</div>
</div>
<div className="alert-stat-card">
<div className="alert-stat-icon resolved">
<CheckCircle size={24} />
</div>
<div className="alert-stat-content">
<p className="alert-stat-label">Résolues</p>
<h3 className="alert-stat-value">{stats.resolved}</h3>
</div>
</div>
</div>
{/* Filters */}
<div className="alert-filters">
<button
className={`filter-button ${filter === "all" ? "active" : ""}`}
onClick={() => setFilter("all")}
>
Toutes ({stats.total})
</button>
<button
className={`filter-button ${filter === "active" ? "active" : ""}`}
onClick={() => setFilter("active")}
>
Actives ({stats.active})
</button>
<button
className={`filter-button ${filter === "resolved" ? "active" : ""}`}
onClick={() => setFilter("resolved")}
>
Résolues ({stats.resolved})
</button>
</div>
{/* Alerts List */}
<div className="alerts-section">
{filteredAlerts.length === 0 ? (
<div className="no-alerts-card">
<Info size={48} color="#888" />
<p>Aucune alerte trouvée</p>
<span className="no-alerts-text">
{filter === "active"
? "Vous n'avez aucune alerte active en ce moment."
: filter === "resolved"
? "Vous n'avez aucune alerte résolue."
: "Vous n'avez pas encore déclenché d'alerte."}
</span>
</div>
) : (
<div className="alerts-list">
{filteredAlerts.map((alert) => (
<div
key={alert.id}
className={`alert-card ${alert.status === "true" ? "active" : "resolved"}`}
>
<div className="alert-card-header">
<div className="alert-card-icon">
{alert.status === "true" ? (
<AlertTriangle size={24} />
) : (
<CheckCircle size={24} />
)}
</div>
<div className="alert-card-info">
<h3>Alerte #{alert.id}</h3>
<div className="alert-card-meta">
<Calendar size={14} />
<span>
{formatDate(alert.created_at)}
</span>
</div>
</div>
<div
className={`alert-status-badge ${alert.status === "true" ? "active" : "resolved"}`}
>
{alert.status === "true"
? "Active"
: "Résolue"}
</div>
</div>
<div className="alert-card-details">
<div className="alert-detail-item">
<Clock size={16} />
<div>
<span className="detail-label">
Déclenchée
</span>
<span className="detail-value">
{new Date(
alert.created_at,
).toLocaleString("fr-FR")}
</span>
</div>
</div>
{alert.status === "false" && (
<>
<div className="alert-detail-item">
<CheckCircle size={16} />
<div>
<span className="detail-label">
Terminée
</span>
<span className="detail-value">
{new Date(
alert.updated_at,
).toLocaleString(
"fr-FR",
)}
</span>
</div>
</div>
<div className="alert-detail-item">
<Clock size={16} />
<div>
<span className="detail-label">
Durée
</span>
<span className="detail-value">
{getDuration(
alert.created_at,
alert.updated_at,
)}
</span>
</div>
</div>
</>
)}
{alert.status === "true" && (
<div className="alert-active-indicator">
<div className="pulse-indicator"></div>
<span>Alerte en cours...</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Info Box */}
{alerts.length > 0 && (
<div className="info-box">
<Info size={20} />
<div>
<h4>À propos des alertes police</h4>
<p>
Les alertes police sont déclenchées lorsque vous
vous sentez en danger. Elles notifient immédiatement
votre équipe et les autorités. Une fois la situation
résolue, n'oubliez pas de terminer l'alerte depuis
votre tableau de bord.
</p>
</div>
</div>
)}
</div>
);
}
export default AlertHistory;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,547 +0,0 @@
.stats-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 2rem);
max-width: 1400px;
margin: 0 auto;
}
/* Back Button */
.back-button-container {
margin-bottom: clamp(1rem, 3vw, 1.5rem);
}
.back-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
}
.back-button:hover {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.3),
rgba(37, 99, 235, 0.2)
);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
}
.back-button:active {
transform: translateY(0);
}
/* Header */
.stats-header {
margin-bottom: clamp(2rem, 5vw, 3rem);
text-align: center;
}
.stats-header h1 {
color: white;
font-size: clamp(2rem, 6vw, 3rem);
margin: 0 0 0.5rem 0;
font-weight: bold;
letter-spacing: -0.5px;
background: linear-gradient(to right, #10b981, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stats-subtitle {
color: #888;
font-size: clamp(1rem, 3vw, 1.2rem);
margin: 0 0 1rem 0;
}
.stats-welcome {
color: #10b981;
font-size: clamp(1rem, 3vw, 1.1rem);
font-weight: 600;
margin: 0;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.stat-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
display: flex;
align-items: center;
gap: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.stat-card::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(16, 185, 129, 0.1),
transparent
);
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.stat-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(16, 185, 129, 0.3);
box-shadow: 0 12px 32px rgba(16, 185, 129, 0.2);
transform: translateY(-4px);
}
.stat-card:hover::before {
left: 100%;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card:hover .stat-icon {
transform: scale(1.1) rotate(5deg);
}
.stat-icon.green {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
color: #10b981;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
}
.stat-icon.blue {
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.2),
rgba(37, 99, 235, 0.1)
);
color: #3b82f6;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.3);
}
.stat-icon.orange {
background: linear-gradient(
135deg,
rgba(251, 146, 60, 0.2),
rgba(249, 115, 22, 0.1)
);
color: #fb923c;
box-shadow: 0 8px 24px rgba(251, 146, 60, 0.3);
}
.stat-icon.purple {
background: linear-gradient(
135deg,
rgba(124, 58, 237, 0.2),
rgba(109, 40, 217, 0.1)
);
color: #7c3aed;
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.3);
}
.stat-icon.yellow {
background: linear-gradient(
135deg,
rgba(234, 179, 8, 0.2),
rgba(202, 138, 4, 0.1)
);
color: #eab308;
box-shadow: 0 8px 24px rgba(234, 179, 8, 0.3);
}
.stat-icon.teal {
background: linear-gradient(
135deg,
rgba(20, 184, 166, 0.2),
rgba(13, 148, 136, 0.1)
);
color: #14b8a6;
box-shadow: 0 8px 24px rgba(20, 184, 166, 0.3);
}
.stat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-label {
color: #888;
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.stat-value {
color: white;
font-size: clamp(1.8rem, 5vw, 2.2rem);
margin: 0;
font-weight: bold;
letter-spacing: -0.5px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.85rem;
font-weight: 600;
}
.stat-trend.positive {
color: #10b981;
}
.stat-trend.neutral {
color: #fb923c;
}
/* Performance Section */
.performance-section {
margin-bottom: clamp(2rem, 5vw, 3rem);
}
.performance-section h2 {
color: white;
font-size: clamp(1.5rem, 4vw, 2rem);
margin: 0 0 clamp(1.5rem, 4vw, 2rem) 0;
font-weight: bold;
}
.performance-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: clamp(1rem, 3vw, 1.5rem);
}
.performance-card {
position: relative;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.performance-card:hover {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.02) 100%
);
border-color: rgba(16, 185, 129, 0.3);
box-shadow: 0 12px 32px rgba(16, 185, 129, 0.2);
transform: translateY(-2px);
}
.performance-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.performance-header h3 {
color: white;
font-size: clamp(1.1rem, 3vw, 1.3rem);
margin: 0;
font-weight: 600;
}
.performance-percentage {
color: #10b981;
font-size: clamp(1.5rem, 4vw, 1.8rem);
font-weight: bold;
}
.performance-bar {
width: 100%;
height: 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
overflow: hidden;
margin-bottom: 1rem;
}
.performance-fill {
height: 100%;
background: linear-gradient(135deg, #10b981, #059669);
border-radius: 6px;
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.performance-fill.approval {
background: linear-gradient(135deg, #14b8a6, #0f766e);
}
.performance-fill::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.performance-description {
color: #9ca3af;
font-size: clamp(0.9rem, 2.5vw, 1rem);
margin: 0;
line-height: 1.5;
}
/* Quick Summary */
.quick-summary {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.03) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: clamp(1.5rem, 4vw, 2rem);
backdrop-filter: blur(8px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.quick-summary h3 {
color: white;
font-size: clamp(1.3rem, 3.5vw, 1.5rem);
margin: 0 0 1.5rem 0;
font-weight: 600;
}
.summary-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.02) 0%,
rgba(255, 255, 255, 0.01) 100%
);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
transition: all 0.3s ease;
}
.summary-item:hover {
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.05) 0%,
rgba(5, 150, 105, 0.02) 100%
);
border-color: rgba(16, 185, 129, 0.2);
transform: translateX(4px);
}
.summary-label {
color: #9ca3af;
font-size: clamp(0.9rem, 2.5vw, 1rem);
font-weight: 500;
}
.summary-value {
color: white;
font-size: clamp(0.9rem, 2.5vw, 1rem);
font-weight: 600;
}
.summary-value.status-active {
color: #10b981;
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.2),
rgba(5, 150, 105, 0.1)
);
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
}
/* Loading */
.loading-stats {
text-align: center;
padding: 4rem 2rem;
}
.loading-stats p {
font-size: 1.2rem;
color: #666;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* Responsive */
@media (max-width: 1024px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.performance-cards {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.stat-icon {
width: 50px;
height: 50px;
}
.performance-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.summary-items {
grid-template-columns: 1fr;
}
.summary-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.back-button {
font-size: 0.9rem;
padding: 0.7rem 1.2rem;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.stat-card {
flex-direction: column;
text-align: center;
}
.stat-icon {
width: 60px;
height: 60px;
}
.back-button {
width: 100%;
justify-content: center;
font-size: 0.85rem;
padding: 0.6rem 1rem;
}
}
@media (hover: none) {
.stat-card:hover {
transform: none;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.performance-card:hover {
transform: none;
}
.summary-item:hover {
transform: none;
}
}
@@ -1,245 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Package,
CheckCircle,
Activity,
TrendingUp,
ArrowLeft,
} from "lucide-react";
import "./StatsPage.css";
import { extractAdminUsernameFromToken } from "../../api/api_admin";
import {
getMyDeliveries,
isDeliveryAuthenticated,
} from "../../api/api_delivery";
interface DeliveryStats {
totalDeliveries: number;
completedDeliveries: number;
todayDeliveries: number;
thisWeekDeliveries: number;
thisMonthDeliveries: number;
approvedDeliveries: number;
}
function StatsPage() {
const navigate = useNavigate();
const [stats, setStats] = useState<DeliveryStats>({
totalDeliveries: 0,
completedDeliveries: 0,
todayDeliveries: 0,
thisWeekDeliveries: 0,
thisMonthDeliveries: 0,
approvedDeliveries: 0,
});
const [loading, setLoading] = useState(true);
const [deliveryPersonName, setDeliveryPersonName] = useState("");
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
if (!isDeliveryAuthenticated()) {
console.log(
"❌ [StatsPage] Livreur non authentifié, redirection vers /login-delivery/delivery",
);
navigate("/login-delivery/delivery", { replace: true });
}
};
checkAuth();
}, [navigate]);
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
useEffect(() => {
const authInterval = setInterval(() => {
if (!isDeliveryAuthenticated()) {
console.log(
"❌ [StatsPage] Session livreur expirée, redirection vers /login-delivery/delivery",
);
navigate("/login-delivery/delivery", { replace: true });
}
}, 5000);
return () => clearInterval(authInterval);
}, [navigate]);
useEffect(() => {
const fetchStats = async () => {
// ✅ Vérifier l'auth avant de charger les données
if (!isDeliveryAuthenticated()) {
console.log("❌ [fetchStats] Livreur non authentifié");
navigate("/login-delivery/delivery", { replace: true });
return;
}
try {
const username = extractAdminUsernameFromToken();
setDeliveryPersonName(username || "Livreur");
console.log("📊 [STATS_PAGE] Chargement des statistiques...");
const deliveriesResult = await getMyDeliveries();
if (deliveriesResult.success && deliveriesResult.deliveries) {
const deliveries = deliveriesResult.deliveries;
console.log(
"✅ [STATS_PAGE] Livraisons récupérées:",
deliveries,
);
const now = new Date();
const today = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
);
const thisWeekStart = new Date(today);
thisWeekStart.setDate(today.getDate() - today.getDay());
const thisMonthStart = new Date(
now.getFullYear(),
now.getMonth(),
1,
);
// Calcul des statistiques
const totalDeliveries = deliveries.length;
const completedDeliveries = deliveries.filter(
(d: any) => d.status === "livre",
).length;
const todayDeliveries = deliveries.filter((d: any) => {
const deliveryDate = new Date(d.updated_at);
return d.status === "livre" && deliveryDate >= today;
}).length;
const thisWeekDeliveries = deliveries.filter((d: any) => {
const deliveryDate = new Date(d.updated_at);
return (
d.status === "livre" &&
deliveryDate >= thisWeekStart
);
}).length;
const thisMonthDeliveries = deliveries.filter((d: any) => {
const deliveryDate = new Date(d.updated_at);
return (
d.status === "livre" &&
deliveryDate >= thisMonthStart
);
}).length;
const approvedDeliveries = deliveries.filter(
(d: any) => d.status === "approved",
).length;
setStats({
totalDeliveries,
completedDeliveries,
todayDeliveries,
thisWeekDeliveries,
thisMonthDeliveries,
approvedDeliveries,
});
console.log("✅ [STATS_PAGE] Statistiques calculées:", {
totalDeliveries,
completedDeliveries,
todayDeliveries,
thisWeekDeliveries,
thisMonthDeliveries,
approvedDeliveries,
});
}
} catch (error) {
console.error(
"❌ [STATS_PAGE] Erreur chargement stats:",
error,
);
// Si erreur 401, rediriger vers login
if (error instanceof Error && error.message.includes("401")) {
console.log("🔓 [StatsPage] Token invalide - Redirection");
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login-delivery/delivery", { replace: true });
}
} finally {
setLoading(false);
}
};
fetchStats();
}, [navigate]);
if (loading) {
return (
<div className="stats-container">
<div className="loading-stats">
<p>Chargement des statistiques...</p>
</div>
</div>
);
}
return (
<div className="stats-container">
{/* Back Button */}
<div className="back-button-container">
<button
className="back-button"
onClick={() => navigate("/delivery/dashboard")}
>
<ArrowLeft size={20} />
Retour au Dashboard
</button>
</div>
{/* Header */}
<div className="stats-header">
<h1>Mes Statistiques</h1>
<p className="stats-subtitle">
Tableau de bord de vos performances de livraison
</p>
<p className="stats-welcome">Bonjour, {deliveryPersonName}!</p>
</div>
{/* Stats Cards */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon green">
<Package size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Total Livraisons</p>
<h3 className="stat-value">{stats.totalDeliveries}</h3>
<span className="stat-trend neutral">
<Activity size={14} /> Toutes les livraisons
</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon teal">
<CheckCircle size={24} />
</div>
<div className="stat-content">
<p className="stat-label">Approuvées</p>
<h3 className="stat-value">
{stats.approvedDeliveries}
</h3>
<span className="stat-trend positive">
<TrendingUp size={14} /> Validées par clients
</span>
</div>
</div>
</div>
{/* Quick Stats Summary */}
</div>
);
}
export default StatsPage;
@@ -1,262 +0,0 @@
.login-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow-y: auto;
}
.login-content {
width: 100%;
max-width: 28rem;
position: relative;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
background: linear-gradient(to bottom right, #7c3aed, #6d28d9);
border-radius: 1rem;
margin-bottom: 1rem;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.login-title {
font-size: 1.875rem;
font-weight: bold;
color: white;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: #9ca3af;
}
.login-card {
background-color: #000000;
backdrop-filter: blur(16px);
border-radius: 1rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(75, 85, 99, 0.5);
padding: 2rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #d1d5db;
margin-bottom: 0.5rem;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
pointer-events: none;
color: #6b7280;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 3rem;
background-color: #000000;
border: 1px solid #4b5563;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
transition: all 0.3s;
}
.form-input::placeholder {
color: #6b7280;
}
.form-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 10px rgba(124, 58, 237, 0.5),
0 0 20px rgba(124, 58, 237, 0.3),
0 0 30px rgba(124, 58, 237, 0.1);
}
.form-input.error {
border-color: #ef4444;
}
.password-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s;
}
.password-toggle:hover {
color: #d1d5db;
}
.error-message {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #f87171;
}
.form-options {
display: flex;
align-items: center;
justify-content: space-between;
}
.remember-me {
display: flex;
align-items: center;
cursor: pointer;
}
.remember-me input {
width: 1rem;
height: 1rem;
background-color: #111827;
border: 1px solid #4b5563;
border-radius: 0.25rem;
cursor: pointer;
accent-color: #7c3aed;
}
.remember-me span {
margin-left: 0.5rem;
font-size: 0.875rem;
color: #9ca3af;
}
.forgot-password {
background: transparent;
border: none;
font-size: 0.875rem;
color: #a78bfa;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.forgot-password:hover {
color: #c4b5fd;
}
.submit-button {
width: 100%;
background: linear-gradient(to right, #7c3aed, #6d28d9);
color: white;
font-weight: 600;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.submit-button:hover:not(:disabled) {
background: linear-gradient(to right, #6d28d9, #5b21b6);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submit-button:active:not(:disabled) {
transform: scale(0.98);
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 1s linear infinite;
margin-right: 0.75rem;
height: 1.25rem;
width: 1.25rem;
color: white;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.signup-section {
margin-top: 1.5rem;
text-align: center;
}
.signup-text {
color: #9ca3af;
font-size: 0.875rem;
}
.signup-link {
background: transparent;
border: none;
color: #a78bfa;
font-weight: 500;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.signup-link:hover {
color: #c4b5fd;
}
.login-footer {
margin-top: 2rem;
text-align: center;
color: #6b7280;
font-size: 0.875rem;
}
@@ -1,261 +0,0 @@
import { useState } from 'react';
import { Lock, User, Eye, EyeOff, Shield } from 'lucide-react';
import './Login.css';
import { loginAdmin, syncAdminUsernameFromJWT } from '../../api/api_admin';
import { useNavigate } from 'react-router-dom';
interface AdminLoginRequest {
username: string;
password: string;
}
const LoginAdmin = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<AdminLoginRequest>({
username: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>('');
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: { username?: string; password?: string } = {};
console.log('🔍 [ADMIN_LOGIN] Validation - Username:', `"${formData.username}"`);
console.log('🔍 [ADMIN_LOGIN] Après trim:', `"${formData.username.trim()}"`);
if (!formData.username.trim()) {
newErrors.username = 'L\'username admin est requis';
} else if (formData.username.trim().length < 3) {
newErrors.username = 'Username invalide (minimum 3 caractères)';
}
if (!formData.password) {
newErrors.password = 'Le mot de passe est requis';
} else if (formData.password.length < 6) {
newErrors.password = 'Le mot de passe doit contenir au moins 6 caractères';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setApiError('');
try {
console.log('🔐 [ADMIN_LOGIN] Appel API avec:', {
username: formData.username,
password: '***'
});
// ✅ Appeler la fonction de login admin sécurisée
const result = await loginAdmin(formData.username, formData.password);
console.log('📋 [ADMIN_LOGIN] Résultat:', {
success: result.success,
message: result.message,
hasToken: !!result.access_token,
role: result.user?.role
});
if (result.success && result.access_token) {
console.log('✅ [ADMIN_LOGIN] Connexion admin réussie!');
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginAdmin()
// Pas besoin de le faire ici
console.log('✅ [ADMIN_LOGIN] Token et username admin synchronisés');
// ✅ Vérifier la synchronisation
const syncedUsername = syncAdminUsernameFromJWT();
console.log('✅ [ADMIN_LOGIN] Username admin synchronisé:', syncedUsername);
// ✅ Redirection vers dashboard admin
console.log('✅ [ADMIN_LOGIN] Redirection vers /admin/dashboard');
navigate('/admin/dashboard');
} else {
// ❌ Erreur API
const errorMessage = result.message || 'Identifiants admin incorrects';
console.error('❌ [ADMIN_LOGIN] Erreur API:', errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error('💥 [ADMIN_LOGIN] Erreur catch:', err);
const errorMessage = err instanceof Error ? err.message : 'Erreur de connexion admin';
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError('');
}
};
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<Shield className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Connexion du staff</h1>
<p className="login-subtitle">Accédez au panneau d'administration</p>
</div>
<div className="login-card">
<form className="login-form" onSubmit={handleSubmit}>
{/* ✅ Erreur API globale */}
{apiError && (
<div className="error-banner" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: '#fee',
borderLeft: '4px solid #f44',
borderRadius: '4px',
color: '#c33'
}}>
{apiError}
</div>
)}
{/* Username Admin */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Username Administrateur
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? 'error' : ''}`}
placeholder="Username admin"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && (
<p className="error-message">{errors.username}</p>
)}
</div>
{/* Password */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="••••••••"
style={{ paddingLeft: '3rem', paddingRight: '3rem' }}
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff style={{ width: '20px', height: '20px' }} />
) : (
<Eye style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
{errors.password && (
<p className="error-message">{errors.password}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? (
<span className="loading-spinner">
Connexion en cours...
</span>
) : (
'Se connecter'
)}
</button>
</form>
{/* Info sécurité */}
<div className="signup-section">
<p className="signup-text" style={{ color: '#9ca3af', fontSize: '0.8rem' }}>
Accès réservé aux administrateurs
</p>
</div>
</div>
{/* Footer */}
<div className="login-footer">
<p>
Besoin d'aide ? Contactez le support technique
</p>
</div>
</div>
</div>
);
};
export default LoginAdmin;
+258 -226
View File
@@ -1,256 +1,288 @@
import { useState } from 'react';
import { Lock, Mail, Eye, EyeOff, User } from 'lucide-react';
import './Login.css';
import { loginUser, syncUsernameFromJWT } from '../../api/api';
import type { LoginRequest } from '../../api/api_types';
import { useNavigate } from 'react-router-dom';
import { useState } from "react";
import { Lock, Mail, Eye, EyeOff, User } from "lucide-react";
import "./Login.css";
import { loginUser, syncUsernameFromJWT } from "../../api/api";
import type { LoginRequest } from "../../api/api_types";
import { useNavigate } from "react-router-dom";
const LoginClient = () => {
const navigate = useNavigate();
const navigate = useNavigate();
const [formData, setFormData] = useState<LoginRequest>({
username: '',
password: ''
});
const [formData, setFormData] = useState<LoginRequest>({
username: "",
password: "",
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>('');
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{
username?: string;
password?: string;
}>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>("");
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: { username?: string; password?: string } = {};
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: { username?: string; password?: string } = {};
console.log('🔍 [LOGIN] Validation - Username:', `"${formData.username}"`);
console.log('🔍 [LOGIN] Après trim:', `"${formData.username.trim()}"`);
console.log(
"🔍 [LOGIN] Validation - Username:",
`"${formData.username}"`,
);
console.log("🔍 [LOGIN] Après trim:", `"${formData.username.trim()}"`);
if (!formData.username.trim()) {
newErrors.username = 'L\'username est requis';
} else if (formData.username.trim().length < 3) {
newErrors.username = 'Username invalide (minimum 3 caractères)';
}
if (!formData.username.trim()) {
newErrors.username = "L'username est requis";
} else if (formData.username.trim().length < 3) {
newErrors.username = "Username invalide (minimum 3 caractères)";
}
if (!formData.password) {
newErrors.password = 'Le mot de passe est requis';
} else if (formData.password.length < 6) {
newErrors.password = 'Le mot de passe doit contenir au moins 6 caractères';
}
if (!formData.password) {
newErrors.password = "Le mot de passe est requis";
} else if (formData.password.length < 6) {
newErrors.password =
"Le mot de passe doit contenir au moins 6 caractères";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) return;
if (!validateForm()) return;
setIsLoading(true);
setApiError('');
setIsLoading(true);
setApiError("");
try {
console.log('🔐 [LOGIN] Appel API avec:', {
username: formData.username,
password: '***'
});
try {
console.log("🔐 [LOGIN] Appel API avec:", {
username: formData.username,
password: "***",
});
// ✅ Appeler la fonction de login sécurisée
const result = await loginUser(formData.username, formData.password);
// ✅ Appeler la fonction de login sécurisée
const result = await loginUser(
formData.username,
formData.password,
);
console.log('📋 [LOGIN] Résultat:', {
success: result.success,
message: result.message,
hasToken: !!result.access_token
});
console.log("📋 [LOGIN] Résultat:", {
success: result.success,
message: result.message,
hasToken: !!result.access_token,
});
if (result.success && result.access_token) {
console.log('✅ [LOGIN] Connexion réussie!');
if (result.success && result.access_token) {
console.log("✅ [LOGIN] Connexion réussie!");
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginUser()
// Pas besoin de le faire ici
console.log('✅ [LOGIN] Token et username synchronisés');
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginUser()
// Pas besoin de le faire ici
console.log("✅ [LOGIN] Token et username synchronisés");
// ✅ Vérifier la synchronisation
const syncedUsername = syncUsernameFromJWT();
console.log('✅ [LOGIN] Username synchronisé:', syncedUsername);
// ✅ Vérifier la synchronisation
const syncedUsername = syncUsernameFromJWT();
console.log("✅ [LOGIN] Username synchronisé:", syncedUsername);
// ✅ Redirection
console.log('✅ [LOGIN] Redirection vers /user/accueil');
navigate('/user/accueil');
} else {
// ❌ Erreur API
const errorMessage = result.message || 'Identifiants incorrects';
console.error('❌ [LOGIN] Erreur API:', errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error('💥 [LOGIN] Erreur catch:', err);
const errorMessage = err instanceof Error ? err.message : 'Erreur de connexion';
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
// ✅ Redirection
if (result.user?.must_change_password) {
console.log("✅ [LOGIN] Première connexion - changement de mot de passe requis");
navigate("/user/change-password");
} else {
console.log("✅ [LOGIN] Redirection vers /user/accueil");
navigate("/user/accueil");
}
} else {
// ❌ Erreur API
const errorMessage =
result.message || "Identifiants incorrects";
console.error("❌ [LOGIN] Erreur API:", errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error("💥 [LOGIN] Erreur catch:", err);
const errorMessage =
err instanceof Error ? err.message : "Erreur de connexion";
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors((prev) => ({
...prev,
[name]: undefined,
}));
}
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError('');
}
};
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError("");
}
};
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<User className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Connexion Client</h1>
<p className="login-subtitle">Accédez à votre espace personnel</p>
</div>
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<User className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Connexion Client</h1>
<p className="login-subtitle">
Accédez à votre espace personnel
</p>
</div>
<div className="login-card">
<form className="login-form" onSubmit={handleSubmit}>
<div className="login-card">
<form className="login-form" onSubmit={handleSubmit}>
{/* ✅ Erreur API globale */}
{apiError && (
<div
className="error-banner"
style={{
padding: "12px",
marginBottom: "16px",
backgroundColor: "#fee",
borderLeft: "4px solid #f44",
borderRadius: "4px",
color: "#c33",
}}
>
{apiError}
</div>
)}
{/* ✅ Erreur API globale */}
{apiError && (
<div className="error-banner" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: '#fee',
borderLeft: '4px solid #f44',
borderRadius: '4px',
color: '#c33'
}}>
{apiError}
</div>
)}
{/* Username / Email */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Username
</label>
<div className="input-wrapper">
<Mail
className="input-icon"
style={{ width: "20px", height: "20px" }}
/>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? "error" : ""}`}
placeholder="Votre username"
style={{ paddingLeft: "3rem" }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && (
<p className="error-message">
{errors.username}
</p>
)}
</div>
{/* Username / Email */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Username
</label>
<div className="input-wrapper">
<Mail className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? 'error' : ''}`}
placeholder="Votre username"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && (
<p className="error-message">{errors.username}</p>
)}
{/* Password */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock
className="input-icon"
style={{ width: "20px", height: "20px" }}
/>
<input
type={showPassword ? "text" : "password"}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? "error" : ""}`}
placeholder="••••••••"
style={{
paddingLeft: "3rem",
paddingRight: "3rem",
}}
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() =>
setShowPassword(!showPassword)
}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff
style={{
width: "20px",
height: "20px",
}}
/>
) : (
<Eye
style={{
width: "20px",
height: "20px",
}}
/>
)}
</button>
</div>
{errors.password && (
<p className="error-message">
{errors.password}
</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? "not-allowed" : "pointer",
}}
>
{isLoading ? (
<span className="loading-spinner">
Connexion en cours...
</span>
) : (
"Se connecter"
)}
</button>
</form>
</div>
</div>
{/* Password */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="••••••••"
style={{ paddingLeft: '3rem', paddingRight: '3rem' }}
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff style={{ width: '20px', height: '20px' }} />
) : (
<Eye style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
{errors.password && (
<p className="error-message">{errors.password}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? (
<span className="loading-spinner">
Connexion en cours...
</span>
) : (
'Se connecter'
)}
</button>
</form>
{/* Signup Link */}
<div className="signup-section">
<p className="signup-text">
Pas encore de compte ?{' '}
<button
type="button"
onClick={() => navigate('/register/client')}
className="signup-link"
>
Créer un compte
</button>
</p>
</div>
</div>
</div>
</div>
);
);
};
export default LoginClient;
export default LoginClient;
@@ -1,262 +0,0 @@
.login-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow-y: auto;
}
.login-content {
width: 100%;
max-width: 28rem;
position: relative;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
background: linear-gradient(to bottom right, #7c3aed, #6d28d9);
border-radius: 1rem;
margin-bottom: 1rem;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.login-title {
font-size: 1.875rem;
font-weight: bold;
color: white;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: #9ca3af;
}
.login-card {
background-color: #000000;
backdrop-filter: blur(16px);
border-radius: 1rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(75, 85, 99, 0.5);
padding: 2rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #d1d5db;
margin-bottom: 0.5rem;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
pointer-events: none;
color: #6b7280;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 3rem;
background-color: #000000;
border: 1px solid #4b5563;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
transition: all 0.3s;
}
.form-input::placeholder {
color: #6b7280;
}
.form-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 10px rgba(124, 58, 237, 0.5),
0 0 20px rgba(124, 58, 237, 0.3),
0 0 30px rgba(124, 58, 237, 0.1);
}
.form-input.error {
border-color: #ef4444;
}
.password-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s;
}
.password-toggle:hover {
color: #d1d5db;
}
.error-message {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #f87171;
}
.form-options {
display: flex;
align-items: center;
justify-content: space-between;
}
.remember-me {
display: flex;
align-items: center;
cursor: pointer;
}
.remember-me input {
width: 1rem;
height: 1rem;
background-color: #111827;
border: 1px solid #4b5563;
border-radius: 0.25rem;
cursor: pointer;
accent-color: #7c3aed;
}
.remember-me span {
margin-left: 0.5rem;
font-size: 0.875rem;
color: #9ca3af;
}
.forgot-password {
background: transparent;
border: none;
font-size: 0.875rem;
color: #a78bfa;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.forgot-password:hover {
color: #c4b5fd;
}
.submit-button {
width: 100%;
background: linear-gradient(to right, #7c3aed, #6d28d9);
color: white;
font-weight: 600;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.submit-button:hover:not(:disabled) {
background: linear-gradient(to right, #6d28d9, #5b21b6);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submit-button:active:not(:disabled) {
transform: scale(0.98);
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 1s linear infinite;
margin-right: 0.75rem;
height: 1.25rem;
width: 1.25rem;
color: white;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.signup-section {
margin-top: 1.5rem;
text-align: center;
}
.signup-text {
color: #9ca3af;
font-size: 0.875rem;
}
.signup-link {
background: transparent;
border: none;
color: #a78bfa;
font-weight: 500;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.signup-link:hover {
color: #c4b5fd;
}
.login-footer {
margin-top: 2rem;
text-align: center;
color: #6b7280;
font-size: 0.875rem;
}
@@ -1,269 +0,0 @@
import { useState } from 'react';
import { Lock, User, Eye, EyeOff, Truck } from 'lucide-react';
import './LoginLivreur.css';
import { loginAdmin, syncAdminUsernameFromJWT, extractAdminRoleFromToken } from '../../api/api_admin';
import { useNavigate } from 'react-router-dom';
interface LoginRequest {
username: string;
password: string;
}
const LoginLivreur = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<LoginRequest>({
username: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>('');
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: { username?: string; password?: string } = {};
console.log('🔍 [LIVREUR_LOGIN] Validation - Username:', `"${formData.username}"`);
console.log('🔍 [LIVREUR_LOGIN] Après trim:', `"${formData.username.trim()}"`);
if (!formData.username.trim()) {
newErrors.username = 'L\'username est requis';
} else if (formData.username.trim().length < 3) {
newErrors.username = 'Username invalide (minimum 3 caractères)';
}
if (!formData.password) {
newErrors.password = 'Le mot de passe est requis';
} else if (formData.password.length < 6) {
newErrors.password = 'Le mot de passe doit contenir au moins 6 caractères';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setApiError('');
try {
console.log('🔐 [LIVREUR_LOGIN] Appel API avec:', {
username: formData.username,
password: '***'
});
// ✅ Appeler la fonction de login (utilise le même endpoint que admin)
const result = await loginAdmin(formData.username, formData.password);
console.log('📋 [LIVREUR_LOGIN] Résultat:', {
success: result.success,
message: result.message,
hasToken: !!result.access_token,
role: result.user?.role
});
if (result.success && result.access_token) {
console.log('✅ [LIVREUR_LOGIN] Connexion réussie!');
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginAdmin()
console.log('✅ [LIVREUR_LOGIN] Token et username synchronisés');
// ✅ Vérifier la synchronisation
const syncedUsername = syncAdminUsernameFromJWT();
console.log('✅ [LIVREUR_LOGIN] Username synchronisé:', syncedUsername);
// ✅ Extraire le rôle pour rediriger correctement
const role = extractAdminRoleFromToken();
console.log('✅ [LIVREUR_LOGIN] Rôle détecté:', role);
// ✅ Redirection selon le rôle
if (role === 'livreur') {
console.log('✅ [LIVREUR_LOGIN] Redirection vers /livreur/dashboard');
navigate('/delivery/dashboard');
} else if (role === 'admin') {
console.log('✅ [LIVREUR_LOGIN] Redirection vers /admin/dashboard');
navigate('/admin/dashboard');
} else if (role === 'cabine') {
console.log('✅ [LIVREUR_LOGIN] Redirection vers /cabine/dashboard');
navigate('/cabine/dashboard');
} else {
console.warn('⚠️ [LIVREUR_LOGIN] Rôle inconnu, redirection par défaut');
navigate('/delivery/dashboard');
}
} else {
// ❌ Erreur API
const errorMessage = result.message || 'Identifiants incorrects';
console.error('❌ [LIVREUR_LOGIN] Erreur API:', errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error('💥 [LIVREUR_LOGIN] Erreur catch:', err);
const errorMessage = err instanceof Error ? err.message : 'Erreur de connexion';
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError('');
}
};
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<Truck className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Connexion Livreur</h1>
<p className="login-subtitle">Accédez à votre espace de livraison</p>
</div>
<div className="login-card">
<form className="login-form" onSubmit={handleSubmit}>
{/* ✅ Erreur API globale */}
{apiError && (
<div className="error-banner" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: '#fee',
borderLeft: '4px solid #f44',
borderRadius: '4px',
color: '#c33'
}}>
{apiError}
</div>
)}
{/* Username */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Nom d'utilisateur
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? 'error' : ''}`}
placeholder="Votre username"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && (
<p className="error-message">{errors.username}</p>
)}
</div>
{/* Password */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="••••••••"
style={{ paddingLeft: '3rem', paddingRight: '3rem' }}
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff style={{ width: '20px', height: '20px' }} />
) : (
<Eye style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
{errors.password && (
<p className="error-message">{errors.password}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? (
<span className="loading-spinner">
Connexion en cours...
</span>
) : (
'Se connecter'
)}
</button>
</form>
{/* Info */}
<div className="signup-section">
<p className="signup-text" style={{ color: '#9ca3af', fontSize: '0.8rem' }}>
Espace réservé aux livreurs
</p>
</div>
</div>
</div>
</div>
);
};
export default LoginLivreur;
@@ -1,262 +0,0 @@
.login-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow-y: auto;
}
.login-content {
width: 100%;
max-width: 28rem;
position: relative;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
background: linear-gradient(to bottom right, #7c3aed, #6d28d9);
border-radius: 1rem;
margin-bottom: 1rem;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.login-title {
font-size: 1.875rem;
font-weight: bold;
color: white;
margin-bottom: 0.5rem;
}
.login-subtitle {
color: #9ca3af;
}
.login-card {
background-color: #000000;
backdrop-filter: blur(16px);
border-radius: 1rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(75, 85, 99, 0.5);
padding: 2rem;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #d1d5db;
margin-bottom: 0.5rem;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
pointer-events: none;
color: #6b7280;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 3rem;
background-color: #000000;
border: 1px solid #4b5563;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
transition: all 0.3s;
}
.form-input::placeholder {
color: #6b7280;
}
.form-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 10px rgba(124, 58, 237, 0.5),
0 0 20px rgba(124, 58, 237, 0.3),
0 0 30px rgba(124, 58, 237, 0.1);
}
.form-input.error {
border-color: #ef4444;
}
.password-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s;
}
.password-toggle:hover {
color: #d1d5db;
}
.error-message {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #f87171;
}
.form-options {
display: flex;
align-items: center;
justify-content: space-between;
}
.remember-me {
display: flex;
align-items: center;
cursor: pointer;
}
.remember-me input {
width: 1rem;
height: 1rem;
background-color: #111827;
border: 1px solid #4b5563;
border-radius: 0.25rem;
cursor: pointer;
accent-color: #7c3aed;
}
.remember-me span {
margin-left: 0.5rem;
font-size: 0.875rem;
color: #9ca3af;
}
.forgot-password {
background: transparent;
border: none;
font-size: 0.875rem;
color: #a78bfa;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.forgot-password:hover {
color: #c4b5fd;
}
.submit-button {
width: 100%;
background: linear-gradient(to right, #7c3aed, #6d28d9);
color: white;
font-weight: 600;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.submit-button:hover:not(:disabled) {
background: linear-gradient(to right, #6d28d9, #5b21b6);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submit-button:active:not(:disabled) {
transform: scale(0.98);
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 1s linear infinite;
margin-right: 0.75rem;
height: 1.25rem;
width: 1.25rem;
color: white;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.signup-section {
margin-top: 1.5rem;
text-align: center;
}
.signup-text {
color: #9ca3af;
font-size: 0.875rem;
}
.signup-link {
background: transparent;
border: none;
color: #a78bfa;
font-weight: 500;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.signup-link:hover {
color: #c4b5fd;
}
.login-footer {
margin-top: 2rem;
text-align: center;
color: #6b7280;
font-size: 0.875rem;
}
@@ -1,261 +0,0 @@
import { useState } from 'react';
import { Lock, User, Eye, EyeOff, MessageCircle } from 'lucide-react';
import './LoginPage.css';
import { loginAdmin, syncAdminUsernameFromJWT } from '../../api/api_admin';
import { useNavigate } from 'react-router-dom';
interface AdminLoginRequest {
username: string;
password: string;
}
const LoginCabine = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<AdminLoginRequest>({
username: '',
password: ''
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<{ username?: string; password?: string }>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>('');
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: { username?: string; password?: string } = {};
console.log('🔍 [ADMIN_LOGIN] Validation - Username:', `"${formData.username}"`);
console.log('🔍 [ADMIN_LOGIN] Après trim:', `"${formData.username.trim()}"`);
if (!formData.username.trim()) {
newErrors.username = 'L\'username admin est requis';
} else if (formData.username.trim().length < 3) {
newErrors.username = 'Username invalide (minimum 3 caractères)';
}
if (!formData.password) {
newErrors.password = 'Le mot de passe est requis';
} else if (formData.password.length < 6) {
newErrors.password = 'Le mot de passe doit contenir au moins 6 caractères';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setApiError('');
try {
console.log('🔐 [ADMIN_LOGIN] Appel API avec:', {
username: formData.username,
password: '***'
});
// ✅ Appeler la fonction de login admin sécurisée
const result = await loginAdmin(formData.username, formData.password);
console.log('📋 [ADMIN_LOGIN] Résultat:', {
success: result.success,
message: result.message,
hasToken: !!result.access_token,
role: result.user?.role
});
if (result.success && result.access_token) {
console.log('✅ [ADMIN_LOGIN] Connexion admin réussie!');
// ✅ La synchronisation JWT est AUTOMATIQUE dans loginAdmin()
// Pas besoin de le faire ici
console.log('✅ [ADMIN_LOGIN] Token et username admin synchronisés');
// ✅ Vérifier la synchronisation
const syncedUsername = syncAdminUsernameFromJWT();
console.log('✅ [ADMIN_LOGIN] Username admin synchronisé:', syncedUsername);
// ✅ Redirection vers dashboard admin
console.log('✅ [ADMIN_LOGIN] Redirection vers /admin/dashboard');
navigate('/cabine/dashboard');
} else {
// ❌ Erreur API
const errorMessage = result.message || 'Identifiants admin incorrects';
console.error('❌ [ADMIN_LOGIN] Erreur API:', errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error('💥 [ADMIN_LOGIN] Erreur catch:', err);
const errorMessage = err instanceof Error ? err.message : 'Erreur de connexion admin';
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError('');
}
};
return (
<div className="login-container">
<div className="login-content">
<div className="login-header">
<div className="login-logo">
<MessageCircle className="w-8 h-8 text-white" />
</div>
<h1 className="login-title">Connexion cabine</h1>
<p className="login-subtitle">Accédez au panneau cabine</p>
</div>
<div className="login-card">
<form className="login-form" onSubmit={handleSubmit}>
{/* ✅ Erreur API globale */}
{apiError && (
<div className="error-banner" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: '#fee',
borderLeft: '4px solid #f44',
borderRadius: '4px',
color: '#c33'
}}>
{apiError}
</div>
)}
{/* Username Admin */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Username Administrateur
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? 'error' : ''}`}
placeholder="Username admin"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && (
<p className="error-message">{errors.username}</p>
)}
</div>
{/* Password */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="••••••••"
style={{ paddingLeft: '3rem', paddingRight: '3rem' }}
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff style={{ width: '20px', height: '20px' }} />
) : (
<Eye style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
{errors.password && (
<p className="error-message">{errors.password}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? (
<span className="loading-spinner">
Connexion en cours...
</span>
) : (
'Se connecter'
)}
</button>
</form>
{/* Info sécurité */}
<div className="signup-section">
<p className="signup-text" style={{ color: '#9ca3af', fontSize: '0.8rem' }}>
Accès réservé aux administrateurs
</p>
</div>
</div>
{/* Footer */}
<div className="login-footer">
<p>
Besoin d'aide ? Contactez le support technique
</p>
</div>
</div>
</div>
);
};
export default LoginCabine;
@@ -1,221 +0,0 @@
.register-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #1a1a1a;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.register-content {
width: 100%;
max-width: 28rem;
position: relative;
padding: 2rem 0;
margin: auto 0;
}
.register-header {
text-align: center;
margin-bottom: 1.5rem;
}
.register-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
background: linear-gradient(to bottom right, #7c3aed, #6d28d9);
border-radius: 1rem;
margin-bottom: 1rem;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
}
.register-title {
font-size: 1.875rem;
font-weight: bold;
color: white;
margin-bottom: 0.5rem;
}
.register-subtitle {
color: #9ca3af;
}
.register-card {
background-color: #000000;
backdrop-filter: blur(16px);
border-radius: 1rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(75, 85, 99, 0.5);
padding: 2rem;
}
.register-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #d1d5db;
margin-bottom: 0.5rem;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
pointer-events: none;
color: #6b7280;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 3rem;
background-color: #000000;
border: 1px solid #4b5563;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
transition: all 0.3s;
}
.form-input::placeholder {
color: #6b7280;
}
.form-input:focus {
outline: none;
border-color: #7c3aed;
box-shadow: 0 0 10px rgba(124, 58, 237, 0.5),
0 0 20px rgba(124, 58, 237, 0.3),
0 0 30px rgba(124, 58, 237, 0.1);
}
.form-input.error {
border-color: #ef4444;
}
.password-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
transition: color 0.2s;
}
.password-toggle:hover {
color: #d1d5db;
}
.error-message {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #f87171;
}
.submit-button {
width: 100%;
background: linear-gradient(to right, #7c3aed, #6d28d9);
color: white;
font-weight: 600;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(109, 40, 217, 0.5);
margin-top: 0.5rem;
}
.submit-button:hover:not(:disabled) {
background: linear-gradient(to right, #6d28d9, #5b21b6);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submit-button:active:not(:disabled) {
transform: scale(0.98);
}
.loading-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 1s linear infinite;
margin-right: 0.75rem;
height: 1.25rem;
width: 1.25rem;
color: white;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.signup-section {
margin-top: 1.5rem;
text-align: center;
}
.signup-text {
color: #9ca3af;
font-size: 0.875rem;
}
.signup-link {
background: transparent;
border: none;
color: #a78bfa;
font-weight: 500;
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.signup-link:hover {
color: #c4b5fd;
}
/* Media queries pour mobile */
@media (max-height: 800px) {
.register-content {
padding: 1rem 0;
}
.register-header {
margin-bottom: 1rem;
}
.register-form {
gap: 0.875rem;
}
.register-card {
padding: 1.5rem;
}
}
@media (max-height: 700px) {
.register-logo {
width: 3rem;
height: 3rem;
}
.register-title {
font-size: 1.5rem;
}
.form-input {
padding: 0.625rem 1rem 0.625rem 3rem;
}
}
@@ -1,379 +0,0 @@
import { useState } from 'react';
import { Lock, Eye, EyeOff, ShoppingBag, User, Phone } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import './Register.css';
import { registerUser, syncUsernameFromJWT } from '../../api/api';
import type { RegisterData, RegisterFormValidation } from '../../api/api_types';
const Register = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState<RegisterData>({
nom: '',
prenom: '',
telephone: '',
username: '',
password: '',
});
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState<RegisterFormValidation['errors']>({});
const [isLoading, setIsLoading] = useState(false);
const [apiError, setApiError] = useState<string>('');
/**
* Valider le formulaire
*/
const validateForm = (): boolean => {
const newErrors: RegisterFormValidation['errors'] = {};
// Valider nom
if (!formData.nom.trim()) {
newErrors.nom = 'Le nom est requis';
} else if (formData.nom.trim().length < 2) {
newErrors.nom = 'Le nom doit contenir au moins 2 caractères';
}
// Valider prénom
if (!formData.prenom.trim()) {
newErrors.prenom = 'Le prénom est requis';
} else if (formData.prenom.trim().length < 2) {
newErrors.prenom = 'Le prénom doit contenir au moins 2 caractères';
}
// Valider téléphone
if (!formData.telephone.trim()) {
newErrors.telephone = 'Le numéro de téléphone est requis';
} else {
const cleanPhone = formData.telephone.replace(/\s/g, '');
if (!/^[0-9+]{10,15}$/.test(cleanPhone)) {
newErrors.telephone = 'Numéro de téléphone invalide (10-15 chiffres)';
}
}
// Valider username
if (!formData.username.trim()) {
newErrors.username = 'Le nom d\'utilisateur est requis';
} else if (formData.username.trim().length < 3) {
newErrors.username = 'Le nom d\'utilisateur doit contenir au moins 3 caractères';
} else if (formData.username.trim().length > 50) {
newErrors.username = 'Le nom d\'utilisateur ne doit pas dépasser 50 caractères';
}
// Valider mot de passe
if (!formData.password) {
newErrors.password = 'Le mot de passe est requis';
} else if (formData.password.length < 8) {
newErrors.password = 'Le mot de passe doit contenir au moins 8 caractères';
} else if (formData.password.length > 128) {
newErrors.password = 'Le mot de passe ne doit pas dépasser 128 caractères';
}
console.log('🔍 [REGISTER] Erreurs validation:', newErrors);
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
/**
* Soumettre le formulaire
*/
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setApiError('');
try {
console.log('📝 [REGISTER] Inscription avec:', {
username: formData.username,
nom: formData.nom,
prenom: formData.prenom,
telephone: formData.telephone,
password: '***'
});
// ✅ Appeler la fonction d'enregistrement sécurisée
const result = await registerUser(
formData.username,
formData.password,
formData.nom,
formData.prenom,
formData.telephone
);
console.log('📋 [REGISTER] Résultat:', {
success: result.success,
message: result.message,
hasToken: !!result.access_token
});
if (result.success && result.access_token) {
console.log('✅ [REGISTER] Inscription réussie!');
// ✅ La synchronisation JWT est AUTOMATIQUE dans registerUser()
// Pas besoin de le faire ici
console.log('✅ [REGISTER] Token et username synchronisés');
// ✅ Vérifier la synchronisation
const syncedUsername = syncUsernameFromJWT();
console.log('✅ [REGISTER] Username synchronisé:', syncedUsername);
// ✅ Redirection vers accueil (utilisateur déjà connecté)
console.log('✅ [REGISTER] Redirection vers /user/accueil');
navigate('/user/accueil');
} else {
// ❌ Erreur API
const errorMessage = result.message || 'Erreur lors de l\'inscription';
console.error('❌ [REGISTER] Erreur API:', errorMessage);
setApiError(errorMessage);
setErrors({ username: errorMessage });
}
} catch (err) {
console.error('💥 [REGISTER] Erreur catch:', err);
const errorMessage = err instanceof Error ? err.message : 'Erreur serveur';
setApiError(errorMessage);
setErrors({ username: errorMessage });
} finally {
setIsLoading(false);
}
};
/**
* Gérer les changements d'input
*/
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Effacer l'erreur de ce champ
if (errors[name as keyof typeof errors]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
// Effacer l'erreur API si l'utilisateur recommence à saisir
if (apiError) {
setApiError('');
}
};
return (
<div className="register-container">
<div className="register-content">
<div className="register-header">
<div className="register-logo">
<ShoppingBag className="w-8 h-8 text-white" />
</div>
<h1 className="register-title">Créer un compte</h1>
<p className="register-subtitle">Rejoignez-nous dès maintenant</p>
</div>
<div className="register-card">
<form className="register-form" onSubmit={handleSubmit}>
{/* ✅ Erreur API globale */}
{apiError && (
<div className="error-banner" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: '#fee',
borderLeft: '4px solid #f44',
borderRadius: '4px',
color: '#c33'
}}>
{apiError}
</div>
)}
{/* Nom */}
<div className="form-group">
<label htmlFor="nom" className="form-label">
Nom
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="nom"
name="nom"
value={formData.nom}
onChange={handleChange}
className={`form-input ${errors.nom ? 'error' : ''}`}
placeholder="Votre nom"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
/>
</div>
{errors.nom && <p className="error-message">{errors.nom}</p>}
</div>
{/* Prénom */}
<div className="form-group">
<label htmlFor="prenom" className="form-label">
Prénom
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="prenom"
name="prenom"
value={formData.prenom}
onChange={handleChange}
className={`form-input ${errors.prenom ? 'error' : ''}`}
placeholder="Votre prénom"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
/>
</div>
{errors.prenom && <p className="error-message">{errors.prenom}</p>}
</div>
{/* Téléphone */}
<div className="form-group">
<label htmlFor="telephone" className="form-label">
Numéro de téléphone
</label>
<div className="input-wrapper">
<Phone className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="tel"
id="telephone"
name="telephone"
value={formData.telephone}
onChange={handleChange}
className={`form-input ${errors.telephone ? 'error' : ''}`}
placeholder="0123456789 ou +33123456789"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
/>
</div>
{errors.telephone && <p className="error-message">{errors.telephone}</p>}
</div>
{/* Username */}
<div className="form-group">
<label htmlFor="username" className="form-label">
Nom d'utilisateur
</label>
<div className="input-wrapper">
<User className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={`form-input ${errors.username ? 'error' : ''}`}
placeholder="Votre nom d'utilisateur"
style={{ paddingLeft: '3rem' }}
disabled={isLoading}
autoComplete="username"
/>
</div>
{errors.username && <p className="error-message">{errors.username}</p>}
</div>
{/* Mot de passe */}
<div className="form-group">
<label htmlFor="password" className="form-label">
Mot de passe
</label>
<div className="input-wrapper">
<Lock className="input-icon" style={{ width: '20px', height: '20px' }} />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={`form-input ${errors.password ? 'error' : ''}`}
placeholder="••••••••"
style={{ paddingLeft: '3rem', paddingRight: '3rem' }}
disabled={isLoading}
autoComplete="new-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
disabled={isLoading}
>
{showPassword ? (
<EyeOff style={{ width: '20px', height: '20px' }} />
) : (
<Eye style={{ width: '20px', height: '20px' }} />
)}
</button>
</div>
{errors.password && <p className="error-message">{errors.password}</p>}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading}
className="submit-button"
style={{
opacity: isLoading ? 0.6 : 1,
cursor: isLoading ? 'not-allowed' : 'pointer'
}}
>
{isLoading ? (
<span className="loading-spinner">
<svg
className="spinner"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
style={{ width: '16px', height: '16px', marginRight: '8px' }}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Création en cours...
</span>
) : (
'Créer mon compte'
)}
</button>
</form>
{/* Login Link */}
<div className="signup-section">
<p className="signup-text">
Vous avez déjà un compte ?{' '}
<button
type="button"
onClick={() => navigate('/login/client')}
className="signup-link"
>
Se connecter
</button>
</p>
</div>
</div>
</div>
</div>
);
};
export default Register;