chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
@@ -0,0 +1,651 @@
/* ============================================
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;
}
}
@@ -0,0 +1,385 @@
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
@@ -0,0 +1,547 @@
.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;
}
}
@@ -0,0 +1,245 @@
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;