From f8473b9a54250103a715ff0b821bfd97afa4aeec Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sun, 3 May 2026 19:38:29 +0200 Subject: [PATCH] chore: fix --- backend/gestion/db/db_basket.go | 5 + backend/gestion/db/db_commands.go | 17 +- backend/gestion/db/db_referral.go | 11 + backend/gestion/handlers/cabine.go | 2 +- backend/gestion/handlers/deleviry.go | 2 +- backend/gestion/handlers/panier.go | 4 +- backend/gestion/handlers/referral.go | 17 + backend/gestion/routes/routes.go | 1 + frontend-admin/src/api/api_admin.ts | 16 + frontend-admin/src/api/types.ts | 5 + .../src/screens/admin/OrdersScreen.tsx | 480 ++++++++++++++++-- .../src/screens/admin/UsersScreen.tsx | 27 + .../src/screens/delivery/DashboardScreen.tsx | 2 +- frontend-prep/src/api/api.ts | 10 +- frontend-prep/src/api/api_types.ts | 11 +- frontend-prep/src/components/Navbar.tsx | 4 +- frontend-prep/src/context/CartContext.tsx | 3 +- frontend-prep/src/context/ThemeContext.tsx | 1 + frontend-prep/src/pages/User/Accueil.tsx | 6 +- frontend-prep/src/pages/User/Cart.tsx | 6 +- frontend-prep/src/pages/User/Checkout.tsx | 12 +- .../src/pages/User/ConsultationHistorique.tsx | 2 +- frontend-prep/src/pages/User/ModalSuccess.tsx | 17 +- frontend-prep/src/pages/User/OrderDetails.tsx | 4 +- .../src/pages/User/ProductDetail.tsx | 6 +- frontend-prep/src/pages/User/ProfilePage.tsx | 13 +- .../src/pages/User/SuiviLivraison.tsx | 39 +- 27 files changed, 608 insertions(+), 115 deletions(-) diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index a6587614..a970a73e 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -256,6 +256,11 @@ func (d *Database) ClearBasket(username string) error { }) } +// ClearBasketOnCheckout vide le panier après commande validée SANS restituer le stock. +func (d *Database) ClearBasketOnCheckout(username string) error { + return d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error +} + // GetBasketTotal calcule le montant total du panier d'un utilisateur func (d *Database) GetBasketTotal(username string) (float64, error) { var result struct { diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go index cffe6017..f1e05617 100644 --- a/backend/gestion/db/db_commands.go +++ b/backend/gestion/db/db_commands.go @@ -231,14 +231,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (* return nil, fmt.Errorf("erreur insertion items: %w", err) } - result := d.GDB.Model(&models.Product{}).Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity)) - if result.Error != nil { - log.Printf("⚠️ Erreur décrémentation stock produit %d: %v", item.ProductID, result.Error) - return nil, fmt.Errorf("erreur mise à jour stock: %w", result.Error) - } - if result.RowsAffected == 0 { - log.Printf("⚠️ [CHECKOUT] Stock déjà réservé pour produit %d (double réservation panier/checkout)", item.ProductID) - } + // Stock déjà déduit à l'ajout au panier — ne pas déduire une seconde fois ici. } if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil { @@ -296,13 +289,17 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er ProposedAddress *string `gorm:"column:proposed_address"` AddressProposalStatus string `gorm:"column:address_proposal_status"` ClientOrderNumber int `gorm:"column:client_order_number"` + ReferralUsed float64 `gorm:"column:referral_used"` + CancelReason string `gorm:"column:cancel_reason"` } gdb := d.GDB.Table("commandes c"). Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status, - c.client_order_id AS client_order_number`) + c.client_order_id AS client_order_number, + COALESCE(c.referral_used, 0) AS referral_used, + COALESCE(c.cancel_reason, '') AS cancel_reason`) if status == "" { gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"}) @@ -330,6 +327,8 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er "updated_at": row.UpdatedAt, "address_proposal_status": row.AddressProposalStatus, "client_order_number": row.ClientOrderNumber, + "referral_used": row.ReferralUsed, + "cancel_reason": row.CancelReason, } if row.LivreurAssign != nil { diff --git a/backend/gestion/db/db_referral.go b/backend/gestion/db/db_referral.go index e34c250a..5a276d20 100644 --- a/backend/gestion/db/db_referral.go +++ b/backend/gestion/db/db_referral.go @@ -48,6 +48,17 @@ func (d *Database) DebitReferralBalance(username string, amount float64) error { }) } +func (d *Database) ResetClientReferralBalance(username string) error { + result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("referral_balance", 0) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("client non trouvé") + } + return nil +} + func (d *Database) UseClientReferralBalance(tx *gorm.DB, username string, amount float64) error { if amount <= 0 { return nil diff --git a/backend/gestion/handlers/cabine.go b/backend/gestion/handlers/cabine.go index dc3b2794..6a57d4ac 100644 --- a/backend/gestion/handlers/cabine.go +++ b/backend/gestion/handlers/cabine.go @@ -631,7 +631,7 @@ func ForceValidateDelivery(c *gin.Context) { livreurAssign, _ := command["livreur_assign"].(string) if clientUsername != "" { - clientMsg := fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID)) + clientMsg := fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊\n\n⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️", database.GetClientOrderID(commandID)) database.NotifyClient(clientUsername, commandID, "livre", clientMsg) } diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 69c70621..04e12972 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -375,7 +375,7 @@ func UpdateDeliveryStatus(c *gin.Context) { case "arrived": clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID)) case "livre": - clientMsg = fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID)) + clientMsg = fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊\n\n⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️", database.GetClientOrderID(commandID)) case "cancelled": clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(commandID)) } diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index 370c5182..38ddc1db 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -483,9 +483,9 @@ func ValidateBasket(c *gin.Context) { go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress) // ============================================ - // 3️⃣ Vider le panier + // 3️⃣ Vider le panier (sans restituer le stock — déjà déduit à l'ajout) // ============================================ - err = database.ClearBasket(usernameStr) + err = database.ClearBasketOnCheckout(usernameStr) if err != nil { utils.ServerErr(c, "Impossible de vider le panier", err) return diff --git a/backend/gestion/handlers/referral.go b/backend/gestion/handlers/referral.go index 7840b42b..85c797e2 100644 --- a/backend/gestion/handlers/referral.go +++ b/backend/gestion/handlers/referral.go @@ -61,6 +61,23 @@ func CreditClientReferralAdmin(c *gin.Context) { }) } +// ResetClientReferralAdmin — DELETE /api/v2/admin/protected/client/:username/referral/reset (admin) +func ResetClientReferralAdmin(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + targetUsername := c.Param("username") + + if err := database.ResetClientReferralBalance(targetUsername); err != nil { + utils.ServerErr(c, "Impossible de réinitialiser le solde", err) + return + } + + log.Printf("✅ [REFERRAL] Solde parrainage remis à zéro pour %s", targetUsername) + c.JSON(http.StatusOK, gin.H{ + "message": "Solde parrainage réinitialisé", + "balance": 0, + }) +} + // GetClientReferralAdmin — GET /api/v2/admin/protected/client/:username/referral (admin) func GetClientReferralAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index 4f4f32fd..3a06b49f 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -265,6 +265,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services // 🎁 PARRAINAGE ADMIN adminGroupV2.GET("/client/:username/referral", handlers.GetClientReferralAdmin) adminGroupV2.POST("/client/:username/referral/credit", handlers.CreditClientReferralAdmin) + adminGroupV2.DELETE("/client/:username/referral/reset", handlers.ResetClientReferralAdmin) adminGroupV2.POST("/client/:username/parrain/set", handlers.SetClientParrainAdmin) adminGroupV2.GET("/client/:username/parrain", handlers.GetClientParrainAdmin) adminGroupV2.GET("/parrain/stats", handlers.GetParrainStatsAdmin) diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index ef14ef06..bd9d77ed 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -531,6 +531,22 @@ export const getClientReferralAdmin = async ( } }; +export const resetClientReferral = async ( + username: string, +): Promise<{ success: boolean; message?: string }> => { + try { + const { data } = await apiClient.delete( + `${V2}/admin/protected/client/${username}/referral/reset`, + ); + return { success: true, message: data.message }; + } catch (error: any) { + return { + success: false, + message: error.response?.data?.error || "Erreur reset parrainage", + }; + } +}; + export interface CancelledOrder { id: number; username: string; diff --git a/frontend-admin/src/api/types.ts b/frontend-admin/src/api/types.ts index fb29548a..bd82bb33 100644 --- a/frontend-admin/src/api/types.ts +++ b/frontend-admin/src/api/types.ts @@ -35,11 +35,16 @@ export interface ClientResponse { export interface CommandResponse { id: number; + client_order_number?: number; username: string; status: string; adresse: string; total_prix: number; livreur_assign?: string | null; + proposed_address?: string | null; + address_proposal_status?: string; + referral_used?: number; + cancel_reason?: string; created_at: string; updated_at: string; } diff --git a/frontend-admin/src/screens/admin/OrdersScreen.tsx b/frontend-admin/src/screens/admin/OrdersScreen.tsx index 94e28af3..d0b660d1 100644 --- a/frontend-admin/src/screens/admin/OrdersScreen.tsx +++ b/frontend-admin/src/screens/admin/OrdersScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { View, Text, @@ -9,6 +9,7 @@ import { ScrollView, TextInput, Share, + ActivityIndicator, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useNavigation } from "@react-navigation/native"; @@ -38,6 +39,7 @@ import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; type Nav = NativeStackNavigationProp; +type ArchiveTab = "approved" | "cancelled"; export default function OrdersScreen() { const { colors } = useTheme(); @@ -77,6 +79,48 @@ export default function OrdersScreen() { const [exporting, setExporting] = useState(false); + // --- Archive (approved / cancelled) --- + const [archiveTab, setArchiveTab] = useState(null); + const [archiveSearch, setArchiveSearch] = useState(""); + const [archiveCommands, setArchiveCommands] = useState([]); + const [archiveLoading, setArchiveLoading] = useState(false); + const archiveDebounce = useRef | null>(null); + + const loadArchive = useCallback( + async (tab: ArchiveTab, search: string) => { + setArchiveLoading(true); + try { + const result = await getAllCommands(tab, search.trim() || undefined); + setArchiveCommands(result.commands); + } catch { + setArchiveCommands([]); + } + setArchiveLoading(false); + }, + [], + ); + + const handleArchiveTab = (tab: ArchiveTab) => { + if (archiveTab === tab) { + setArchiveTab(null); + setArchiveCommands([]); + setArchiveSearch(""); + return; + } + setArchiveTab(tab); + setArchiveSearch(""); + loadArchive(tab, ""); + }; + + const handleArchiveSearch = (text: string) => { + setArchiveSearch(text); + if (archiveDebounce.current) clearTimeout(archiveDebounce.current); + archiveDebounce.current = setTimeout(() => { + if (archiveTab) loadArchive(archiveTab, text); + }, 400); + }; + + // --- Active orders --- const loadData = useCallback(async () => { try { const result = await getAllCommands(undefined); @@ -106,6 +150,7 @@ export default function OrdersScreen() { const onRefresh = async () => { setRefreshing(true); await loadData(); + if (archiveTab) loadArchive(archiveTab, archiveSearch); setRefreshing(false); }; @@ -258,6 +303,7 @@ export default function OrdersScreen() { gap: spacing.s, paddingBottom: spacing.m, alignItems: "center", + flexWrap: "wrap", }, refreshBtn: { flexDirection: "row", @@ -274,11 +320,79 @@ export default function OrdersScreen() { color: colors.textSecondary, fontSize: fontSize.sm, }, + archiveRow: { + flexDirection: "row", + gap: spacing.s, + marginBottom: spacing.m, + flexWrap: "wrap", + }, + archiveTabBtn: { + flexDirection: "row", + alignItems: "center", + gap: spacing.xs, + paddingHorizontal: spacing.m, + paddingVertical: spacing.s, + borderRadius: borderRadius.sm, + borderWidth: 1, + }, + archiveTabBtnText: { + fontSize: fontSize.sm, + fontWeight: "600", + }, + searchBar: { + flexDirection: "row", + alignItems: "center", + gap: spacing.s, + backgroundColor: colors.bgCard, + borderRadius: borderRadius.sm, + borderWidth: 1, + borderColor: colors.border, + paddingHorizontal: spacing.m, + paddingVertical: spacing.s, + marginBottom: spacing.m, + }, + searchInput: { + flex: 1, + color: colors.textWhite, + fontSize: fontSize.sm, + padding: 0, + }, + archiveHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: spacing.s, + }, + archiveHeaderText: { + color: colors.textMuted, + fontSize: fontSize.xs, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.8, + }, + divider: { + height: 1, + backgroundColor: colors.border, + marginVertical: spacing.m, + }, + activeSectionTitle: { + color: colors.textMuted, + fontSize: fontSize.xs, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.8, + marginBottom: spacing.m, + }, cardText: { color: colors.textSecondary, fontSize: fontSize.sm, marginTop: 2, }, + cardTextMuted: { + color: colors.textMuted, + fontSize: fontSize.xs, + marginTop: 2, + }, cardDate: { color: colors.textMuted, fontSize: fontSize.xs, @@ -289,12 +403,54 @@ export default function OrdersScreen() { fontWeight: "bold", color: colors.textWhite, }, + orderIdSub: { + fontSize: fontSize.xs, + color: colors.textMuted, + marginLeft: spacing.xs, + }, row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: spacing.s, }, + idRow: { + flexDirection: "row", + alignItems: "baseline", + gap: spacing.xs, + marginBottom: spacing.s, + }, + chip: { + paddingHorizontal: spacing.s, + paddingVertical: 2, + borderRadius: borderRadius.full ?? 99, + alignSelf: "flex-start", + marginTop: spacing.xs, + }, + chipText: { + fontSize: fontSize.xs, + fontWeight: "600", + }, + cancelReasonBox: { + backgroundColor: colors.bgPrimary, + borderRadius: borderRadius.sm, + padding: spacing.s, + marginTop: spacing.s, + borderLeftWidth: 3, + borderLeftColor: colors.danger, + }, + cancelReasonText: { + color: colors.textSecondary, + fontSize: fontSize.xs, + }, + proposedAddressBox: { + backgroundColor: colors.bgPrimary, + borderRadius: borderRadius.sm, + padding: spacing.s, + marginTop: spacing.s, + borderLeftWidth: 3, + borderLeftColor: colors.warning, + }, selectBtn: { flexDirection: "row", alignItems: "center", @@ -343,6 +499,12 @@ export default function OrdersScreen() { marginTop: spacing.xxl, fontSize: fontSize.md, }, + archiveEmpty: { + color: colors.textMuted, + textAlign: "center", + marginVertical: spacing.l, + fontSize: fontSize.sm, + }, livreurItem: { flexDirection: "row", alignItems: "center", @@ -353,8 +515,6 @@ export default function OrdersScreen() { gap: spacing.m, }, livreurName: { color: colors.textWhite, fontSize: fontSize.md }, - - // Modal items modalSummary: { backgroundColor: colors.bgCard, borderRadius: borderRadius.sm, @@ -432,6 +592,103 @@ export default function OrdersScreen() { [colors], ); + const renderArchiveCard = (item: CommandResponse) => { + const isCancelled = item.status === "cancelled"; + const accentColor = isCancelled ? colors.danger : colors.success; + + return ( + + navigation.navigate("OrderDetail", { orderId: item.id })} + activeOpacity={0.7} + > + {/* ID row */} + + #{item.id} + {item.client_order_number != null && item.client_order_number > 0 && ( + + (commande #{item.client_order_number} du client) + + )} + + + + {/* Client */} + + Client : + {item.username} + + + {/* Adresse */} + {!!item.adresse && ( + + Adresse : + {item.adresse} + + )} + + {/* Adresse proposée */} + {!!item.proposed_address && ( + + + Adresse proposée ({item.address_proposal_status ?? "—"}) :{" "} + {item.proposed_address} + + + )} + + {/* Total + parrainage */} + + Total : + {item.total_prix.toFixed(2)} € + {(item.referral_used ?? 0) > 0 && ( + + {" "}(parrainage -{item.referral_used!.toFixed(2)} €) + + )} + + + {/* Livreur */} + {!!item.livreur_assign && ( + + Livreur : + {item.livreur_assign} + + )} + + {/* Raison annulation */} + {isCancelled && !!item.cancel_reason && ( + + + Raison : {item.cancel_reason} + + + )} + + {/* Dates */} + + Créée : {new Date(item.created_at).toLocaleString("fr-FR")} + + + Modifiée : {new Date(item.updated_at).toLocaleString("fr-FR")} + + + + {/* Action rapide items */} + openItems(item.id)} + > + Voir les articles + + + + ); + }; + const renderOrder = ({ item }: { item: CommandResponse }) => { const isOpen = openMenuId === item.id; const isDone = ["approved", "cancelled"].includes(item.status); @@ -592,6 +849,189 @@ export default function OrdersScreen() { ); }; + const ListHeader = ( + + {/* Boutons actions */} + + + + Actualiser + + + + + {exporting ? "Export..." : "Export CSV"} + + + + + {/* Boutons archives */} + + handleArchiveTab("approved")} + > + + + Approuvées + + + + handleArchiveTab("cancelled")} + > + + + Annulées + + + + + {/* Section archive */} + {archiveTab !== null && ( + + {/* Barre de recherche */} + + + + {archiveSearch.length > 0 && ( + { + setArchiveSearch(""); + if (archiveTab) loadArchive(archiveTab, ""); + }} + > + + + )} + + + {/* Header résultats */} + + + {archiveTab === "approved" ? "Commandes approuvées" : "Commandes annulées"} + + {!archiveLoading && ( + + {archiveCommands.length} résultat{archiveCommands.length !== 1 ? "s" : ""} + + )} + + + {archiveLoading ? ( + + + + ) : archiveCommands.length === 0 ? ( + Aucune commande trouvée + ) : ( + archiveCommands.map((item) => renderArchiveCard(item)) + )} + + + + )} + + {/* Titre section commandes actives */} + + Commandes actives ({commands.length}) + + + ); + if (loading) return ; return ( @@ -608,42 +1048,12 @@ export default function OrdersScreen() { /> } contentContainerStyle={{ padding: spacing.l }} - ListHeaderComponent={ - - - - Actualiser - - - - - {exporting ? "Export..." : "Export CSV"} - - - - } + ListHeaderComponent={ListHeader} ListEmptyComponent={ - Aucune commande + Aucune commande active } /> - {/* Modal items */} { + try { + const res = await resetClientReferral(username); + if (res.success) { + showSuccess("Parrainage", "Solde remis à zéro"); + await loadData(); + } else { + showError("Erreur", res.message || "Echec du reset"); + } + } catch (e: any) { + showError("Erreur", e.message); + } + }; + // -------------------------------------------------- // Points & amende // -------------------------------------------------- @@ -934,6 +949,18 @@ export default function UsersScreen() { /> )} + {referralEnabled && (item.clientData!.referral_balance ?? 0) > 0 && ( + handleResetReferral(item.clientData!.username)} + > + + + )} openCancelledOrders(item.clientData!)} diff --git a/frontend-admin/src/screens/delivery/DashboardScreen.tsx b/frontend-admin/src/screens/delivery/DashboardScreen.tsx index e638a61a..2c3d96c8 100644 --- a/frontend-admin/src/screens/delivery/DashboardScreen.tsx +++ b/frontend-admin/src/screens/delivery/DashboardScreen.tsx @@ -58,7 +58,7 @@ import TomTomMap, { } from "../../components/TomTomMap"; import DetailsModal from "../../components/ui/Modal"; -const LOCATION_INTERVAL_MS = 15000; +const LOCATION_INTERVAL_MS = 5000; const STATUS_LABELS: Record = { available: "Disponible", diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index 01a395f4..6e92afb0 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -330,8 +330,8 @@ export const changePassword = async ( export interface BasketResponse { success: boolean; message?: string; - panier?: any[]; - data?: { panier?: any[] }; + panier?: Record[]; + data?: { panier?: Record[] }; } /** @@ -569,7 +569,7 @@ export const clearCart = async (username: string) => { success: false, message: errorData.error || "Erreur vidage", }; - } catch (parseError) { + } catch { // Si le parsing JSON échoue (HTML retourné) console.error("❌ [CLEAR] Réponse non-JSON du serveur"); return { @@ -1206,7 +1206,7 @@ export const getOrderTotal = async (commandId: number): Promise => { } }; -export const calculateOrderTotal = (order: any): number => { +export const calculateOrderTotal = (order: Record): number => { // Préférer total (colonne calculée par le backend) if (typeof order.total === "number" && order.total > 0) { return order.total; @@ -1219,7 +1219,7 @@ export const calculateOrderTotal = (order: any): number => { // Fallback: calculer depuis les items si présents if (Array.isArray(order.items) && order.items.length > 0) { - return order.items.reduce((sum: number, item: any) => { + return (order.items as Record[]).reduce((sum: number, item) => { const price = item.prix || item.price || 0; const quantity = item.quantite || item.quantity || 1; return sum + price * quantity; diff --git a/frontend-prep/src/api/api_types.ts b/frontend-prep/src/api/api_types.ts index 7c7d9dff..2e1602af 100644 --- a/frontend-prep/src/api/api_types.ts +++ b/frontend-prep/src/api/api_types.ts @@ -19,6 +19,7 @@ export interface ApiResponse { token_type?: string; expires_in?: number; user?: UserResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; // Pour les champs additionnels } @@ -258,6 +259,7 @@ export interface OrderDetail { // Métadonnées payment_method?: string; notes?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -352,6 +354,7 @@ export interface TrackingResponse { updated_at?: number; created_at?: string; message?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -379,6 +382,7 @@ export interface Product { }>; created_at?: string; updated_at?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -388,6 +392,7 @@ export interface ProductsResponse { data?: Product[]; products?: Product[]; count?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -429,6 +434,7 @@ export interface JWTPayload { iat: number; exp: number; iss: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -445,6 +451,7 @@ export interface ErrorResponse { message?: string; details?: string; status_code?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -607,6 +614,7 @@ export interface ETAResponse { eta_available?: boolean; livreur_distance?: number; message?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -767,6 +775,7 @@ export interface ConfirmReceptionResponse { data?: { category?: string; points_earned?: number; - [key: string]: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; }; } diff --git a/frontend-prep/src/components/Navbar.tsx b/frontend-prep/src/components/Navbar.tsx index 21e8676c..c7b39a04 100644 --- a/frontend-prep/src/components/Navbar.tsx +++ b/frontend-prep/src/components/Navbar.tsx @@ -128,12 +128,12 @@ function Navbar() { Authorization: `Bearer ${token}`, }, }); - } catch (_) {} + } catch { /* noop */ } } sessionStorage.removeItem("admin_token"); sessionStorage.removeItem("admin_username"); navigate("/login/client", { replace: true }); - } catch (_) { + } catch { sessionStorage.removeItem("admin_token"); sessionStorage.removeItem("admin_username"); navigate("/login/client", { replace: true }); diff --git a/frontend-prep/src/context/CartContext.tsx b/frontend-prep/src/context/CartContext.tsx index 84201d43..42dd4aac 100644 --- a/frontend-prep/src/context/CartContext.tsx +++ b/frontend-prep/src/context/CartContext.tsx @@ -54,6 +54,7 @@ interface ToastMessage { type: "success" | "error" | "warning" | "info"; } +// eslint-disable-next-line react-refresh/only-export-components export const CartContext = createContext(undefined); export function CartProvider({ children }: { children: ReactNode }) { @@ -168,7 +169,7 @@ export function CartProvider({ children }: { children: ReactNode }) { return () => { console.log("🔄 [CART] CartProvider unmount"); }; - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps /** * ✅ Ajouter au panier diff --git a/frontend-prep/src/context/ThemeContext.tsx b/frontend-prep/src/context/ThemeContext.tsx index 253f454e..f11edfae 100644 --- a/frontend-prep/src/context/ThemeContext.tsx +++ b/frontend-prep/src/context/ThemeContext.tsx @@ -36,6 +36,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) { ); } +// eslint-disable-next-line react-refresh/only-export-components export function useTheme(): ThemeContextValue { return useContext(ThemeContext); } diff --git a/frontend-prep/src/pages/User/Accueil.tsx b/frontend-prep/src/pages/User/Accueil.tsx index 9cd40c60..6be4e40b 100644 --- a/frontend-prep/src/pages/User/Accueil.tsx +++ b/frontend-prep/src/pages/User/Accueil.tsx @@ -38,7 +38,7 @@ function UserAccueil() { return; } loadProducts(); - }, [selectedCategory, categories]); + }, [selectedCategory, categories]); // eslint-disable-line react-hooks/exhaustive-deps const loadProducts = async () => { setLoading(true); @@ -58,8 +58,8 @@ function UserAccueil() { } else { setProducts([]); } - } catch (err: any) { - setError(err.message || "Erreur lors du chargement des produits"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Erreur lors du chargement des produits"); } finally { setLoading(false); } diff --git a/frontend-prep/src/pages/User/Cart.tsx b/frontend-prep/src/pages/User/Cart.tsx index dc599d98..d749d087 100644 --- a/frontend-prep/src/pages/User/Cart.tsx +++ b/frontend-prep/src/pages/User/Cart.tsx @@ -42,7 +42,7 @@ function Cart() { useEffect(() => { if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } refreshCart(); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const total = cartItems.reduce((sum, item) => sum + item.price, 0); @@ -71,7 +71,7 @@ function Cart() { const res = await getProductById(item.product_id); if (res.success && res.data) { const p = res.data; - const videoMedia = p.media?.find((m: any) => m && m.type === "video"); + const videoMedia = p.media?.find((m) => m && m.type === "video"); return { ...item, image: getProductImage(p), @@ -89,7 +89,7 @@ function Cart() { } }; enrich(); - }, [cartItems]); + }, [cartItems]); // eslint-disable-line react-hooks/exhaustive-deps const handleClearCart = async () => { if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } diff --git a/frontend-prep/src/pages/User/Checkout.tsx b/frontend-prep/src/pages/User/Checkout.tsx index 24ea2afe..5546653f 100644 --- a/frontend-prep/src/pages/User/Checkout.tsx +++ b/frontend-prep/src/pages/User/Checkout.tsx @@ -66,7 +66,7 @@ function Checkout() { const res = await getProductById(item.product_id); if (res.success && res.data) { const p: Product = res.data; - const img = p.media?.find((m: any) => m && m.type === 'image'); + const img = p.media?.find((m) => m && m.type === 'image'); if (img?.url) { setItemImages((prev) => ({ ...prev, [item.id]: getMediaUrl(img.url) })); } @@ -289,7 +289,7 @@ function Checkout() { if (response.success && response.payment_method === 'crypto') { setCryptoPaymentData({ command_id: response.command_id!, - client_order_number: (response as any).client_order_number, + client_order_number: (response as Record).client_order_number as number | undefined, payment_status: response.payment_status!, pay_address: response.pay_address!, pay_amount: response.pay_amount!, @@ -348,13 +348,13 @@ function Checkout() { // ✅ Préparer les données pour le modal setConfirmationData({ command_id, - client_order_number: (response as any).client_order_number, + client_order_number: (response as Record).client_order_number as number | undefined, assigned_to, queue_info, delivery_address: delivery_address || address, arrivalTime, total: frontendTotal, - referral_used: (response as any).referral_used, + referral_used: (response as Record).referral_used as number | undefined, clientInfo: { first_name: firstName, last_name: lastName, @@ -369,9 +369,9 @@ function Checkout() { } else { setError(response.message || '❌ Erreur lors de la validation de la commande'); } - } catch (err: any) { + } catch (err: unknown) { console.error('❌ Erreur checkout:', err); - setError(err.message || '❌ Erreur serveur. Veuillez réessayer.'); + setError(err instanceof Error ? err.message : '❌ Erreur serveur. Veuillez réessayer.'); } finally { setLoading(false); } diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.tsx b/frontend-prep/src/pages/User/ConsultationHistorique.tsx index b561e231..09b26bb6 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.tsx +++ b/frontend-prep/src/pages/User/ConsultationHistorique.tsx @@ -65,7 +65,7 @@ function ConsultationHistorique() { getReferralBalance().then((r) => { if (r.success) setReferralBalance(r.balance); }); } }); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const fetchHistory = async () => { if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; } diff --git a/frontend-prep/src/pages/User/ModalSuccess.tsx b/frontend-prep/src/pages/User/ModalSuccess.tsx index c9405a9c..154959e3 100644 --- a/frontend-prep/src/pages/User/ModalSuccess.tsx +++ b/frontend-prep/src/pages/User/ModalSuccess.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import './ModalSuccess.css'; interface ModalSuccessProps { @@ -10,20 +10,13 @@ interface ModalSuccessProps { } export function ModalSuccess({ isOpen, productName, quantity, price, onClose }: ModalSuccessProps) { - const [isVisible, setIsVisible] = useState(isOpen); - useEffect(() => { - setIsVisible(isOpen); - if (isOpen) { - const timer = setTimeout(() => { - setIsVisible(false); - onClose(); - }, 2500); - return () => clearTimeout(timer); - } + if (!isOpen) return; + const timer = setTimeout(() => { onClose(); }, 2500); + return () => clearTimeout(timer); }, [isOpen, onClose]); - if (!isVisible) return null; + if (!isOpen) return null; return (
diff --git a/frontend-prep/src/pages/User/OrderDetails.tsx b/frontend-prep/src/pages/User/OrderDetails.tsx index 5e881e63..ca780c7f 100644 --- a/frontend-prep/src/pages/User/OrderDetails.tsx +++ b/frontend-prep/src/pages/User/OrderDetails.tsx @@ -104,7 +104,7 @@ function OrderDetails() { if (commandId) { fetchOrderDetails(commandId); } - }, [orderId]); + }, [orderId]); // eslint-disable-line react-hooks/exhaustive-deps // ============================================ // FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD) @@ -299,7 +299,7 @@ function OrderDetails() { // ✅ Produits depuis items (command_items) products: - apiData.items?.map((item: any) => ({ + apiData.items?.map((item: Record) => ({ id: item.id, product_id: item.product_id, name_product: item.produit, diff --git a/frontend-prep/src/pages/User/ProductDetail.tsx b/frontend-prep/src/pages/User/ProductDetail.tsx index a58404a9..328faec7 100644 --- a/frontend-prep/src/pages/User/ProductDetail.tsx +++ b/frontend-prep/src/pages/User/ProductDetail.tsx @@ -63,7 +63,7 @@ function ProductDetail() { useEffect(() => { if (id) loadProduct(Number(id)); - }, [id]); + }, [id]); // eslint-disable-line react-hooks/exhaustive-deps const loadProduct = async (productId: number) => { // ✅ Vérifier l'auth avant de charger le produit @@ -110,8 +110,8 @@ function ProductDetail() { } else { setError(response.message || "Produit non trouvé"); } - } catch (err: any) { - setError(err.message || "Erreur lors du chargement du produit"); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Erreur lors du chargement du produit"); } finally { setLoading(false); } diff --git a/frontend-prep/src/pages/User/ProfilePage.tsx b/frontend-prep/src/pages/User/ProfilePage.tsx index 6df65bbd..e3728785 100644 --- a/frontend-prep/src/pages/User/ProfilePage.tsx +++ b/frontend-prep/src/pages/User/ProfilePage.tsx @@ -23,9 +23,9 @@ export default function ProfilePage() { const [loadingProfile, setLoadingProfile] = useState(true); // Données locales (localStorage) - const [defaultAddress, setDefaultAddress] = useState(''); - const [defaultPhone, setDefaultPhone] = useState(''); - const [signalPseudo, setSignalPseudo] = useState(''); + const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? ''); + const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? ''); + const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? ''); // Feedback const [savingContact, setSavingContact] = useState(false); @@ -47,11 +47,6 @@ export default function ProfilePage() { navigate('/login/client', { replace: true }); return; } - // Charger depuis localStorage - setDefaultAddress(localStorage.getItem(STORAGE_ADDRESS) ?? ''); - setDefaultPhone(localStorage.getItem(STORAGE_PHONE) ?? ''); - setSignalPseudo(localStorage.getItem(STORAGE_SIGNAL) || username); - // Statut Telegram getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); }); @@ -68,7 +63,7 @@ export default function ProfilePage() { } setLoadingProfile(false); }); - }, [navigate]); + }, [navigate]); // eslint-disable-line react-hooks/exhaustive-deps const showSuccess = (msg: string) => { setSuccessMsg(msg); diff --git a/frontend-prep/src/pages/User/SuiviLivraison.tsx b/frontend-prep/src/pages/User/SuiviLivraison.tsx index c74e0a29..bb2ce401 100644 --- a/frontend-prep/src/pages/User/SuiviLivraison.tsx +++ b/frontend-prep/src/pages/User/SuiviLivraison.tsx @@ -27,6 +27,7 @@ import Navbar from "../../components/Navbar"; import Toast from "../../components/Toast"; import "./SuiviLivraison.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { faHourglassHalf, faTruck, @@ -109,7 +110,7 @@ const getDeliveryAddress = (order: OrderWithTracking): string => { return order.delivery_address || order.adresse || "Non disponible"; }; -const formatOrderItem = (item: any) => { +const formatOrderItem = (item: Record) => { return { name: item.produit || item.product_name || item.name_product || "Produit", @@ -153,8 +154,8 @@ const getStatusLabel = (status: string): string => { return statusMap[status?.toLowerCase()] || "Statut inconnu"; }; -const getStatusIcon = (status: string): any => { - const iconMap: Record = { +const getStatusIcon = (status: string): IconDefinition => { + const iconMap: Record = { pending: faHourglassHalf, assigned: faBiking, en_route: faTruck, @@ -178,7 +179,7 @@ const calculateOrderPoints = ( points: number; category: string; categoryDisplay: string; - categoryIcon: any; + categoryIcon: IconDefinition; categoryColor: string; } => { // Totaux indexés par pool (+ index spécial pour "gros&semi" exclu des points) @@ -186,7 +187,7 @@ const calculateOrderPoints = ( let excludedTotal = 0; if (order.items && order.items.length > 0) { - order.items.forEach((item: any) => { + order.items.forEach((item: Record) => { const cat = (item.category || "").toLowerCase(); const itemPrice = item.prix || item.price || 0; @@ -332,7 +333,7 @@ function SuiviLivraison() { loadOrders(); const interval = setInterval(loadOrders, 10000); return () => clearInterval(interval); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const loadOrders = async () => { // ✅ Vérifier l'auth avant de charger les commandes @@ -359,7 +360,7 @@ function SuiviLivraison() { try { tracking = await getOrderTracking(order.id); - } catch (err) { + } catch { console.warn( `Tracking non disponible pour commande ${order.id}`, ); @@ -368,7 +369,7 @@ function SuiviLivraison() { try { eta = await getOrderETA(order.id); - } catch (err) { + } catch { console.warn( `ETA non disponible pour commande ${order.id}`, ); @@ -389,10 +390,11 @@ function SuiviLivraison() { setError("Impossible de charger les commandes"); showToast("Impossible de charger les commandes", "error"); } - } catch (err: any) { + } catch (err: unknown) { console.error("Erreur loadOrders:", err); - setError(err.message || "Erreur lors du chargement"); - showToast(err.message || "Erreur lors du chargement", "error"); + const msg = err instanceof Error ? err.message : "Erreur lors du chargement"; + setError(msg); + showToast(msg, "error"); } finally { setLoading(false); } @@ -462,7 +464,7 @@ function SuiviLivraison() { const pointsEarned = response.points_earned || selectedOrderPoints; const apiCategory = - response.category || (response as any).data?.category || ""; + response.category || (response as Record & { data?: { category?: string } }).data?.category || ""; const displayCategory = apiCategory && apiCategory !== "total" @@ -483,9 +485,10 @@ function SuiviLivraison() { ); setConfirming(null); } - } catch (err: any) { - setError(err.message || "Erreur serveur"); - showToast(err.message || "Erreur serveur", "error"); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : "Erreur serveur"; + setError(msg); + showToast(msg, "error"); setConfirming(null); } }; @@ -567,9 +570,9 @@ function SuiviLivraison() { "error", ); } - } catch (error: any) { + } catch (error: unknown) { console.error("❌ [CANCEL] Erreur:", error); - showToast(error.message || "Erreur lors de l'annulation", "error"); + showToast(error instanceof Error ? error.message : "Erreur lors de l'annulation", "error"); } finally { setCancellingOrder(null); } @@ -895,7 +898,7 @@ function SuiviLivraison() {
{order.items.map( ( - item: any, + item: Record, idx: number, ) => { const formatted =