diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go
index 980681fb..c3f0d7e6 100644
--- a/backend/gestion/db/db_init.go
+++ b/backend/gestion/db/db_init.go
@@ -563,6 +563,16 @@ func (db *Database) createTables() error {
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`,
`CREATE INDEX IF NOT EXISTS idx_ratings_livreur ON livreur_ratings(livreur_username);`,
+
+ // ============================
+ // TABLE login_history (livreur uniquement)
+ // ============================
+ `CREATE TABLE IF NOT EXISTS login_history (
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ );`,
+ `CREATE INDEX IF NOT EXISTS idx_login_history_username ON login_history(username);`,
}
for _, query := range queries {
diff --git a/backend/gestion/db/db_login_history.go b/backend/gestion/db/db_login_history.go
new file mode 100644
index 00000000..238c3483
--- /dev/null
+++ b/backend/gestion/db/db_login_history.go
@@ -0,0 +1,34 @@
+package db
+
+import (
+ "time"
+)
+
+type LoginHistoryEntry struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+// RecordLivreurLogin enregistre une connexion réussie d'un livreur (best-effort, non bloquant).
+func (d *Database) RecordLivreurLogin(username string) error {
+ return d.GDB.Exec(`
+ INSERT INTO login_history (username, created_at)
+ VALUES (?, NOW())
+ `, username).Error
+}
+
+// GetLivreurLoginHistoryByMonth retourne le détail des connexions d'un livreur pour un mois donné,
+// triées du plus récent au plus ancien (max 50 entrées).
+func (d *Database) GetLivreurLoginHistoryByMonth(username string, year, month int) ([]LoginHistoryEntry, error) {
+ var entries []LoginHistoryEntry
+ err := d.GDB.Raw(`
+ SELECT id, username, created_at FROM login_history
+ WHERE username = ?
+ AND EXTRACT(YEAR FROM created_at) = ?
+ AND EXTRACT(MONTH FROM created_at) = ?
+ ORDER BY created_at DESC
+ LIMIT 50
+ `, username, year, month).Scan(&entries).Error
+ return entries, err
+}
diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go
index 4ffc960a..55833fa7 100644
--- a/backend/gestion/handlers/auth.go
+++ b/backend/gestion/handlers/auth.go
@@ -455,6 +455,12 @@ func LoginAdmin(c *gin.Context) {
return
}
+ if user.Role == "livreur" {
+ if err := database.RecordLivreurLogin(user.Username); err != nil {
+ log.Printf("⚠️ [LOGIN_ADMIN] Erreur enregistrement historique connexion livreur: %v", err)
+ }
+ }
+
token, _ := generateAdminToken(user)
expiresAt := time.Now().Add(adminTokenDuration)
diff --git a/backend/gestion/handlers/livreur_login_history.go b/backend/gestion/handlers/livreur_login_history.go
new file mode 100644
index 00000000..adaf26fb
--- /dev/null
+++ b/backend/gestion/handlers/livreur_login_history.go
@@ -0,0 +1,80 @@
+package handlers
+
+import (
+ "gestion/db"
+ "gestion/utils"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+type loginHistoryWeek struct {
+ Week int `json:"week"`
+ Entries []db.LoginHistoryEntry `json:"entries"`
+}
+
+// GetLivreurLoginHistory retourne l'historique de connexion d'un livreur pour un mois donné,
+// regroupé par semaine ISO (détail complet, pas d'agrégation par compteur).
+func GetLivreurLoginHistory(c *gin.Context) {
+ username := c.Param("username")
+ if username == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
+ return
+ }
+
+ now := time.Now()
+ year := now.Year()
+ month := int(now.Month())
+
+ if y := c.Query("year"); y != "" {
+ parsed, err := strconv.Atoi(y)
+ if err != nil || parsed < 2000 || parsed > 2100 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Année invalide"})
+ return
+ }
+ year = parsed
+ }
+ if m := c.Query("month"); m != "" {
+ parsed, err := strconv.Atoi(m)
+ if err != nil || parsed < 1 || parsed > 12 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Mois invalide"})
+ return
+ }
+ month = parsed
+ }
+
+ database := c.MustGet("database").(*db.Database)
+ entries, err := database.GetLivreurLoginHistoryByMonth(username, year, month)
+ if err != nil {
+ utils.ServerErr(c, "Erreur récupération historique de connexion", err)
+ return
+ }
+
+ weekOrder := make([]int, 0)
+ weekMap := make(map[int]*loginHistoryWeek)
+ for _, e := range entries {
+ _, isoWeek := e.CreatedAt.ISOWeek()
+ w, ok := weekMap[isoWeek]
+ if !ok {
+ w = &loginHistoryWeek{Week: isoWeek}
+ weekMap[isoWeek] = w
+ weekOrder = append(weekOrder, isoWeek)
+ }
+ w.Entries = append(w.Entries, e)
+ }
+
+ weeks := make([]*loginHistoryWeek, 0, len(weekOrder))
+ for _, wk := range weekOrder {
+ weeks = append(weeks, weekMap[wk])
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "username": username,
+ "year": year,
+ "month": month,
+ "weeks": weeks,
+ "count": len(entries),
+ })
+}
diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go
index 79b7e799..9ca89c9b 100644
--- a/backend/gestion/routes/routes.go
+++ b/backend/gestion/routes/routes.go
@@ -278,6 +278,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
adminGroupV2.DELETE("/delivery-persons/:username/queue/:command_id", handlers.RemoveCommandFromQueue)
adminGroupV2.GET("/delivery-persons/:username/map-links", handlers.GetDeliveryPersonMapLinks)
adminGroupV2.GET("/delivery-persons/:username/ratings", handlers.GetLivreurRatings)
+ adminGroupV2.GET("/delivery-persons/:username/login-history", handlers.GetLivreurLoginHistory)
// Commandes annulées
adminGroupV2.GET("/orders/cancelled", handlers.GetAllCancelledOrders)
// ============================================
diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts
index bf078f78..3584518c 100644
--- a/frontend-admin/src/api/api_admin.ts
+++ b/frontend-admin/src/api/api_admin.ts
@@ -416,6 +416,50 @@ export const getLivreurRatings = async (
}
};
+export interface LoginHistoryEntry {
+ id: number;
+ username: string;
+ created_at: string;
+}
+export interface LoginHistoryWeek {
+ week: number;
+ entries: LoginHistoryEntry[];
+}
+
+export const getLivreurLoginHistory = async (
+ username: string,
+ year?: number,
+ month?: number,
+): Promise<{
+ success: boolean;
+ year: number;
+ month: number;
+ weeks: LoginHistoryWeek[];
+ count: number;
+}> => {
+ try {
+ const { data } = await apiClient.get(
+ `${V2}/admin/protected/delivery-persons/${username}/login-history`,
+ { params: { year, month } },
+ );
+ return {
+ success: true,
+ year: data.year,
+ month: data.month,
+ weeks: data.weeks || [],
+ count: data.count || 0,
+ };
+ } catch {
+ return {
+ success: false,
+ year: year ?? new Date().getFullYear(),
+ month: month ?? new Date().getMonth() + 1,
+ weeks: [],
+ count: 0,
+ };
+ }
+};
+
const parseStatus = (status: any): "available" | "busy" | "offline" => {
if (!status) return "offline";
if (status === "available" || status === "busy" || status === "offline")
diff --git a/frontend-admin/src/screens/admin/UsersScreen.tsx b/frontend-admin/src/screens/admin/UsersScreen.tsx
index 1a4b4ef6..f753a4a7 100644
--- a/frontend-admin/src/screens/admin/UsersScreen.tsx
+++ b/frontend-admin/src/screens/admin/UsersScreen.tsx
@@ -34,8 +34,9 @@ import {
resetClientPoints,
addClientPoints,
subtractClientPoints,
+ getLivreurLoginHistory,
} from "../../api/api_admin";
-import type { CancelledOrder } from "../../api/api_admin";
+import type { CancelledOrder, LoginHistoryWeek } from "../../api/api_admin";
import type { ClientResponse } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
@@ -225,6 +226,23 @@ export default function UsersScreen() {
loading: boolean;
}>({ visible: false, username: "", orders: [], loading: false });
+ // Login history modal (livreur)
+ const [loginHistoryModal, setLoginHistoryModal] = useState<{
+ visible: boolean;
+ username: string;
+ year: number;
+ month: number;
+ weeks: LoginHistoryWeek[];
+ loading: boolean;
+ }>({
+ visible: false,
+ username: "",
+ year: new Date().getFullYear(),
+ month: new Date().getMonth() + 1,
+ weeks: [],
+ loading: false,
+ });
+
const { alert, showError, showSuccess, showConfirm, hideAlert } =
useAlert();
@@ -708,6 +726,53 @@ export default function UsersScreen() {
}));
};
+ // --------------------------------------------------
+ // Login history (livreur)
+ // --------------------------------------------------
+ const fetchLoginHistory = async (
+ username: string,
+ year: number,
+ month: number,
+ ) => {
+ setLoginHistoryModal((prev) => ({ ...prev, loading: true }));
+ const res = await getLivreurLoginHistory(username, year, month);
+ setLoginHistoryModal((prev) => ({
+ ...prev,
+ year: res.year,
+ month: res.month,
+ weeks: res.weeks,
+ loading: false,
+ }));
+ };
+
+ const openLoginHistory = (username: string) => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = now.getMonth() + 1;
+ setLoginHistoryModal({
+ visible: true,
+ username,
+ year,
+ month,
+ weeks: [],
+ loading: true,
+ });
+ fetchLoginHistory(username, year, month);
+ };
+
+ const changeLoginHistoryMonth = (delta: number) => {
+ let year = loginHistoryModal.year;
+ let month = loginHistoryModal.month + delta;
+ if (month < 1) {
+ month = 12;
+ year -= 1;
+ } else if (month > 12) {
+ month = 1;
+ year += 1;
+ }
+ fetchLoginHistory(loginHistoryModal.username, year, month);
+ };
+
// --------------------------------------------------
// Create user
// --------------------------------------------------
@@ -1150,6 +1215,18 @@ export default function UsersScreen() {
>
)}
+ {item.role === "livreur" && (
+ openLoginHistory(item.username)}
+ >
+
+
+ )}
{(item.role === "livreur" ||
item.role === "cabine") && (
+ {/* Modal historique de connexion (livreur) */}
+
+ setLoginHistoryModal((prev) => ({
+ ...prev,
+ visible: false,
+ }))
+ }
+ title={`Connexions — ${loginHistoryModal.username}`}
+ icon="time-outline"
+ iconColor={colors.accent}
+ >
+
+ changeLoginHistoryMonth(-1)}
+ >
+
+
+
+ {new Date(
+ loginHistoryModal.year,
+ loginHistoryModal.month - 1,
+ 1,
+ )
+ .toLocaleDateString("fr-FR", {
+ month: "long",
+ year: "numeric",
+ })
+ .replace(/^./, (c) => c.toUpperCase())}
+
+ changeLoginHistoryMonth(1)}
+ >
+
+
+
+
+ {loginHistoryModal.loading ? (
+
+ ) : loginHistoryModal.weeks.length === 0 ? (
+
+ Aucune connexion ce mois-ci
+
+ ) : (
+
+ {loginHistoryModal.weeks.map((week) => (
+
+
+ Semaine {week.week}
+
+ {week.entries.map((entry, idx) => (
+
+
+ {new Date(
+ entry.created_at,
+ ).toLocaleDateString("fr-FR", {
+ weekday: "short",
+ day: "2-digit",
+ month: "2-digit",
+ })}
+
+
+ {new Date(
+ entry.created_at,
+ ).toLocaleTimeString("fr-FR", {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+ ))}
+
+ ))}
+
+ )}
+
+
{/* Modal points & amendes */}