chore: build
Backend - Build & Lint / build (push) Failing after 16m27s
Frontend Admin - EAS Build / build (push) Failing after 2h15m57s

This commit is contained in:
Nuxgrid
2026-07-15 20:55:46 +02:00
parent e5333395b9
commit 74bf9cf14c
7 changed files with 400 additions and 1 deletions
+10
View File
@@ -563,6 +563,16 @@ func (db *Database) createTables() error {
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`, );`,
`CREATE INDEX IF NOT EXISTS idx_ratings_livreur ON livreur_ratings(livreur_username);`, `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 { for _, query := range queries {
+34
View File
@@ -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
}
+6
View File
@@ -455,6 +455,12 @@ func LoginAdmin(c *gin.Context) {
return 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) token, _ := generateAdminToken(user)
expiresAt := time.Now().Add(adminTokenDuration) expiresAt := time.Now().Add(adminTokenDuration)
@@ -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),
})
}
+1
View File
@@ -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.DELETE("/delivery-persons/:username/queue/:command_id", handlers.RemoveCommandFromQueue)
adminGroupV2.GET("/delivery-persons/:username/map-links", handlers.GetDeliveryPersonMapLinks) adminGroupV2.GET("/delivery-persons/:username/map-links", handlers.GetDeliveryPersonMapLinks)
adminGroupV2.GET("/delivery-persons/:username/ratings", handlers.GetLivreurRatings) adminGroupV2.GET("/delivery-persons/:username/ratings", handlers.GetLivreurRatings)
adminGroupV2.GET("/delivery-persons/:username/login-history", handlers.GetLivreurLoginHistory)
// Commandes annulées // Commandes annulées
adminGroupV2.GET("/orders/cancelled", handlers.GetAllCancelledOrders) adminGroupV2.GET("/orders/cancelled", handlers.GetAllCancelledOrders)
// ============================================ // ============================================
+44
View File
@@ -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" => { const parseStatus = (status: any): "available" | "busy" | "offline" => {
if (!status) return "offline"; if (!status) return "offline";
if (status === "available" || status === "busy" || status === "offline") if (status === "available" || status === "busy" || status === "offline")
@@ -34,8 +34,9 @@ import {
resetClientPoints, resetClientPoints,
addClientPoints, addClientPoints,
subtractClientPoints, subtractClientPoints,
getLivreurLoginHistory,
} from "../../api/api_admin"; } 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 type { ClientResponse } from "../../api/types";
import LoadingSpinner from "../../components/ui/LoadingSpinner"; import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card"; import Card from "../../components/ui/Card";
@@ -225,6 +226,23 @@ export default function UsersScreen() {
loading: boolean; loading: boolean;
}>({ visible: false, username: "", orders: [], loading: false }); }>({ 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 } = const { alert, showError, showSuccess, showConfirm, hideAlert } =
useAlert(); 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 // Create user
// -------------------------------------------------- // --------------------------------------------------
@@ -1150,6 +1215,18 @@ export default function UsersScreen() {
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
{item.role === "livreur" && (
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openLoginHistory(item.username)}
>
<Ionicons
name="time-outline"
size={20}
color={colors.accent}
/>
</TouchableOpacity>
)}
{(item.role === "livreur" || {(item.role === "livreur" ||
item.role === "cabine") && ( item.role === "cabine") && (
<TouchableOpacity <TouchableOpacity
@@ -1799,6 +1876,153 @@ export default function UsersScreen() {
)} )}
</Modal> </Modal>
{/* Modal historique de connexion (livreur) */}
<Modal
visible={loginHistoryModal.visible}
onClose={() =>
setLoginHistoryModal((prev) => ({
...prev,
visible: false,
}))
}
title={`Connexions — ${loginHistoryModal.username}`}
icon="time-outline"
iconColor={colors.accent}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: spacing.m,
}}
>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => changeLoginHistoryMonth(-1)}
>
<Ionicons
name="chevron-back"
size={20}
color={colors.textWhite}
/>
</TouchableOpacity>
<Text
style={{
color: colors.textWhite,
fontWeight: "700",
fontSize: fontSize.md,
}}
>
{new Date(
loginHistoryModal.year,
loginHistoryModal.month - 1,
1,
)
.toLocaleDateString("fr-FR", {
month: "long",
year: "numeric",
})
.replace(/^./, (c) => c.toUpperCase())}
</Text>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => changeLoginHistoryMonth(1)}
>
<Ionicons
name="chevron-forward"
size={20}
color={colors.textWhite}
/>
</TouchableOpacity>
</View>
{loginHistoryModal.loading ? (
<ActivityIndicator
color={colors.accent}
style={{ marginVertical: spacing.xl }}
/>
) : loginHistoryModal.weeks.length === 0 ? (
<Text
style={{
color: colors.textMuted,
textAlign: "center",
paddingVertical: spacing.xl,
}}
>
Aucune connexion ce mois-ci
</Text>
) : (
<ScrollView
style={{ maxHeight: 400 }}
showsVerticalScrollIndicator={false}
>
{loginHistoryModal.weeks.map((week) => (
<View
key={week.week}
style={{ marginBottom: spacing.m }}
>
<Text
style={{
color: colors.textMuted,
fontSize: fontSize.xs,
fontWeight: "700",
textTransform: "uppercase",
marginBottom: spacing.xs,
}}
>
Semaine {week.week}
</Text>
{week.entries.map((entry, idx) => (
<View
key={entry.id}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: spacing.xs,
borderBottomWidth:
idx <
week.entries.length - 1
? 1
: 0,
borderBottomColor: colors.border,
}}
>
<Text
style={{
color: colors.textWhite,
fontSize: fontSize.sm,
}}
>
{new Date(
entry.created_at,
).toLocaleDateString("fr-FR", {
weekday: "short",
day: "2-digit",
month: "2-digit",
})}
</Text>
<Text
style={{
color: colors.textMuted,
fontSize: fontSize.sm,
}}
>
{new Date(
entry.created_at,
).toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</View>
))}
</View>
))}
</ScrollView>
)}
</Modal>
{/* Modal points & amendes */} {/* Modal points & amendes */}
<Modal <Modal
visible={sanctionModal.visible} visible={sanctionModal.visible}