chore: fix

This commit is contained in:
2026-05-03 19:38:29 +02:00
parent 4075ae2568
commit f8473b9a54
27 changed files with 608 additions and 115 deletions
+5
View File
@@ -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 // GetBasketTotal calcule le montant total du panier d'un utilisateur
func (d *Database) GetBasketTotal(username string) (float64, error) { func (d *Database) GetBasketTotal(username string) (float64, error) {
var result struct { var result struct {
+8 -9
View File
@@ -231,14 +231,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
return nil, fmt.Errorf("erreur insertion items: %w", err) 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)) // Stock déjà déduit à l'ajout au panier — ne pas déduire une seconde fois ici.
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)
}
} }
if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil { 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"` ProposedAddress *string `gorm:"column:proposed_address"`
AddressProposalStatus string `gorm:"column:address_proposal_status"` AddressProposalStatus string `gorm:"column:address_proposal_status"`
ClientOrderNumber int `gorm:"column:client_order_number"` 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"). gdb := d.GDB.Table("commandes c").
Select(`c.id, c.username, c.status, c.adresse, c.total_prix, Select(`c.id, c.username, c.status, c.adresse, c.total_prix,
c.livreur_assign, c.created_at, c.updated_at, c.livreur_assign, c.created_at, c.updated_at,
c.proposed_address, c.address_proposal_status, 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 == "" { if status == "" {
gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"}) 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, "updated_at": row.UpdatedAt,
"address_proposal_status": row.AddressProposalStatus, "address_proposal_status": row.AddressProposalStatus,
"client_order_number": row.ClientOrderNumber, "client_order_number": row.ClientOrderNumber,
"referral_used": row.ReferralUsed,
"cancel_reason": row.CancelReason,
} }
if row.LivreurAssign != nil { if row.LivreurAssign != nil {
+11
View File
@@ -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 { func (d *Database) UseClientReferralBalance(tx *gorm.DB, username string, amount float64) error {
if amount <= 0 { if amount <= 0 {
return nil return nil
+1 -1
View File
@@ -631,7 +631,7 @@ func ForceValidateDelivery(c *gin.Context) {
livreurAssign, _ := command["livreur_assign"].(string) livreurAssign, _ := command["livreur_assign"].(string)
if clientUsername != "" { 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<b>⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️</b>", database.GetClientOrderID(commandID))
database.NotifyClient(clientUsername, commandID, "livre", clientMsg) database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
} }
+1 -1
View File
@@ -375,7 +375,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
case "arrived": case "arrived":
clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID)) clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID))
case "livre": 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<b>⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️</b>", database.GetClientOrderID(commandID))
case "cancelled": case "cancelled":
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(commandID)) clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(commandID))
} }
+2 -2
View File
@@ -483,9 +483,9 @@ func ValidateBasket(c *gin.Context) {
go database.NotifyAllAdminCabine(commandID, usernameStr, req.DeliveryAddress) 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 { if err != nil {
utils.ServerErr(c, "Impossible de vider le panier", err) utils.ServerErr(c, "Impossible de vider le panier", err)
return return
+17
View File
@@ -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) // GetClientReferralAdmin — GET /api/v2/admin/protected/client/:username/referral (admin)
func GetClientReferralAdmin(c *gin.Context) { func GetClientReferralAdmin(c *gin.Context) {
database := c.MustGet("database").(*db.Database) database := c.MustGet("database").(*db.Database)
+1
View File
@@ -265,6 +265,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// 🎁 PARRAINAGE ADMIN // 🎁 PARRAINAGE ADMIN
adminGroupV2.GET("/client/:username/referral", handlers.GetClientReferralAdmin) adminGroupV2.GET("/client/:username/referral", handlers.GetClientReferralAdmin)
adminGroupV2.POST("/client/:username/referral/credit", handlers.CreditClientReferralAdmin) 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.POST("/client/:username/parrain/set", handlers.SetClientParrainAdmin)
adminGroupV2.GET("/client/:username/parrain", handlers.GetClientParrainAdmin) adminGroupV2.GET("/client/:username/parrain", handlers.GetClientParrainAdmin)
adminGroupV2.GET("/parrain/stats", handlers.GetParrainStatsAdmin) adminGroupV2.GET("/parrain/stats", handlers.GetParrainStatsAdmin)
+16
View File
@@ -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 { export interface CancelledOrder {
id: number; id: number;
username: string; username: string;
+5
View File
@@ -35,11 +35,16 @@ export interface ClientResponse {
export interface CommandResponse { export interface CommandResponse {
id: number; id: number;
client_order_number?: number;
username: string; username: string;
status: string; status: string;
adresse: string; adresse: string;
total_prix: number; total_prix: number;
livreur_assign?: string | null; livreur_assign?: string | null;
proposed_address?: string | null;
address_proposal_status?: string;
referral_used?: number;
cancel_reason?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
+445 -35
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"; import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { import {
View, View,
Text, Text,
@@ -9,6 +9,7 @@ import {
ScrollView, ScrollView,
TextInput, TextInput,
Share, Share,
ActivityIndicator,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
@@ -38,6 +39,7 @@ import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert"; import { useAlert } from "../../hooks/useAlert";
type Nav = NativeStackNavigationProp<AdminStackParamList>; type Nav = NativeStackNavigationProp<AdminStackParamList>;
type ArchiveTab = "approved" | "cancelled";
export default function OrdersScreen() { export default function OrdersScreen() {
const { colors } = useTheme(); const { colors } = useTheme();
@@ -77,6 +79,48 @@ export default function OrdersScreen() {
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
// --- Archive (approved / cancelled) ---
const [archiveTab, setArchiveTab] = useState<ArchiveTab | null>(null);
const [archiveSearch, setArchiveSearch] = useState("");
const [archiveCommands, setArchiveCommands] = useState<CommandResponse[]>([]);
const [archiveLoading, setArchiveLoading] = useState(false);
const archiveDebounce = useRef<ReturnType<typeof setTimeout> | 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 () => { const loadData = useCallback(async () => {
try { try {
const result = await getAllCommands(undefined); const result = await getAllCommands(undefined);
@@ -106,6 +150,7 @@ export default function OrdersScreen() {
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
await loadData(); await loadData();
if (archiveTab) loadArchive(archiveTab, archiveSearch);
setRefreshing(false); setRefreshing(false);
}; };
@@ -258,6 +303,7 @@ export default function OrdersScreen() {
gap: spacing.s, gap: spacing.s,
paddingBottom: spacing.m, paddingBottom: spacing.m,
alignItems: "center", alignItems: "center",
flexWrap: "wrap",
}, },
refreshBtn: { refreshBtn: {
flexDirection: "row", flexDirection: "row",
@@ -274,11 +320,79 @@ export default function OrdersScreen() {
color: colors.textSecondary, color: colors.textSecondary,
fontSize: fontSize.sm, 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: { cardText: {
color: colors.textSecondary, color: colors.textSecondary,
fontSize: fontSize.sm, fontSize: fontSize.sm,
marginTop: 2, marginTop: 2,
}, },
cardTextMuted: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginTop: 2,
},
cardDate: { cardDate: {
color: colors.textMuted, color: colors.textMuted,
fontSize: fontSize.xs, fontSize: fontSize.xs,
@@ -289,12 +403,54 @@ export default function OrdersScreen() {
fontWeight: "bold", fontWeight: "bold",
color: colors.textWhite, color: colors.textWhite,
}, },
orderIdSub: {
fontSize: fontSize.xs,
color: colors.textMuted,
marginLeft: spacing.xs,
},
row: { row: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: spacing.s, 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: { selectBtn: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -343,6 +499,12 @@ export default function OrdersScreen() {
marginTop: spacing.xxl, marginTop: spacing.xxl,
fontSize: fontSize.md, fontSize: fontSize.md,
}, },
archiveEmpty: {
color: colors.textMuted,
textAlign: "center",
marginVertical: spacing.l,
fontSize: fontSize.sm,
},
livreurItem: { livreurItem: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -353,8 +515,6 @@ export default function OrdersScreen() {
gap: spacing.m, gap: spacing.m,
}, },
livreurName: { color: colors.textWhite, fontSize: fontSize.md }, livreurName: { color: colors.textWhite, fontSize: fontSize.md },
// Modal items
modalSummary: { modalSummary: {
backgroundColor: colors.bgCard, backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm, borderRadius: borderRadius.sm,
@@ -432,6 +592,103 @@ export default function OrdersScreen() {
[colors], [colors],
); );
const renderArchiveCard = (item: CommandResponse) => {
const isCancelled = item.status === "cancelled";
const accentColor = isCancelled ? colors.danger : colors.success;
return (
<Card
key={item.id}
style={{ marginBottom: spacing.s, borderLeftWidth: 3, borderLeftColor: accentColor }}
>
<TouchableOpacity
onPress={() => navigation.navigate("OrderDetail", { orderId: item.id })}
activeOpacity={0.7}
>
{/* ID row */}
<View style={styles.idRow}>
<Text style={styles.orderId}>#{item.id}</Text>
{item.client_order_number != null && item.client_order_number > 0 && (
<Text style={styles.orderIdSub}>
(commande #{item.client_order_number} du client)
</Text>
)}
<StatusBadge status={item.status} />
</View>
{/* Client */}
<Text style={styles.cardText}>
<Text style={{ color: colors.textMuted }}>Client : </Text>
{item.username}
</Text>
{/* Adresse */}
{!!item.adresse && (
<Text style={styles.cardText}>
<Text style={{ color: colors.textMuted }}>Adresse : </Text>
{item.adresse}
</Text>
)}
{/* Adresse proposée */}
{!!item.proposed_address && (
<View style={styles.proposedAddressBox}>
<Text style={[styles.cancelReasonText, { color: colors.warning }]}>
Adresse proposée ({item.address_proposal_status ?? "—"}) :{" "}
{item.proposed_address}
</Text>
</View>
)}
{/* Total + parrainage */}
<Text style={styles.cardText}>
<Text style={{ color: colors.textMuted }}>Total : </Text>
{item.total_prix.toFixed(2)}
{(item.referral_used ?? 0) > 0 && (
<Text style={{ color: colors.accent }}>
{" "}(parrainage -{item.referral_used!.toFixed(2)} )
</Text>
)}
</Text>
{/* Livreur */}
{!!item.livreur_assign && (
<Text style={styles.cardText}>
<Text style={{ color: colors.textMuted }}>Livreur : </Text>
{item.livreur_assign}
</Text>
)}
{/* Raison annulation */}
{isCancelled && !!item.cancel_reason && (
<View style={styles.cancelReasonBox}>
<Text style={styles.cancelReasonText}>
Raison : {item.cancel_reason}
</Text>
</View>
)}
{/* Dates */}
<Text style={styles.cardDate}>
Créée : {new Date(item.created_at).toLocaleString("fr-FR")}
</Text>
<Text style={styles.cardDate}>
Modifiée : {new Date(item.updated_at).toLocaleString("fr-FR")}
</Text>
</TouchableOpacity>
{/* Action rapide items */}
<TouchableOpacity
style={[styles.selectBtn, { marginTop: spacing.s }]}
onPress={() => openItems(item.id)}
>
<Text style={styles.selectBtnText}>Voir les articles</Text>
<Ionicons name="receipt-outline" size={14} color={colors.accent} />
</TouchableOpacity>
</Card>
);
};
const renderOrder = ({ item }: { item: CommandResponse }) => { const renderOrder = ({ item }: { item: CommandResponse }) => {
const isOpen = openMenuId === item.id; const isOpen = openMenuId === item.id;
const isDone = ["approved", "cancelled"].includes(item.status); const isDone = ["approved", "cancelled"].includes(item.status);
@@ -592,6 +849,189 @@ export default function OrdersScreen() {
); );
}; };
const ListHeader = (
<View>
{/* Boutons actions */}
<View style={styles.refreshRow}>
<TouchableOpacity
style={styles.refreshBtn}
onPress={onRefresh}
disabled={refreshing}
>
<Ionicons
name="refresh-outline"
size={16}
color={refreshing ? colors.textMuted : colors.accent}
/>
<Text style={styles.refreshBtnText}>Actualiser</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.refreshBtn}
onPress={handleExportCSV}
disabled={exporting}
>
<Ionicons
name="download-outline"
size={16}
color={exporting ? colors.textMuted : colors.accent}
/>
<Text style={styles.refreshBtnText}>
{exporting ? "Export..." : "Export CSV"}
</Text>
</TouchableOpacity>
</View>
{/* Boutons archives */}
<View style={styles.archiveRow}>
<TouchableOpacity
style={[
styles.archiveTabBtn,
{
backgroundColor:
archiveTab === "approved"
? colors.success + "22"
: colors.bgCard,
borderColor:
archiveTab === "approved"
? colors.success
: colors.border,
},
]}
onPress={() => handleArchiveTab("approved")}
>
<Ionicons
name="checkmark-circle-outline"
size={16}
color={
archiveTab === "approved"
? colors.success
: colors.textMuted
}
/>
<Text
style={[
styles.archiveTabBtnText,
{
color:
archiveTab === "approved"
? colors.success
: colors.textMuted,
},
]}
>
Approuvées
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.archiveTabBtn,
{
backgroundColor:
archiveTab === "cancelled"
? colors.danger + "22"
: colors.bgCard,
borderColor:
archiveTab === "cancelled"
? colors.danger
: colors.border,
},
]}
onPress={() => handleArchiveTab("cancelled")}
>
<Ionicons
name="close-circle-outline"
size={16}
color={
archiveTab === "cancelled"
? colors.danger
: colors.textMuted
}
/>
<Text
style={[
styles.archiveTabBtnText,
{
color:
archiveTab === "cancelled"
? colors.danger
: colors.textMuted,
},
]}
>
Annulées
</Text>
</TouchableOpacity>
</View>
{/* Section archive */}
{archiveTab !== null && (
<View>
{/* Barre de recherche */}
<View style={styles.searchBar}>
<Ionicons
name="search-outline"
size={16}
color={colors.textMuted}
/>
<TextInput
style={styles.searchInput}
placeholder="Rechercher par username..."
placeholderTextColor={colors.textMuted}
value={archiveSearch}
onChangeText={handleArchiveSearch}
autoCapitalize="none"
autoCorrect={false}
/>
{archiveSearch.length > 0 && (
<TouchableOpacity
onPress={() => {
setArchiveSearch("");
if (archiveTab) loadArchive(archiveTab, "");
}}
>
<Ionicons
name="close-outline"
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
)}
</View>
{/* Header résultats */}
<View style={styles.archiveHeader}>
<Text style={styles.archiveHeaderText}>
{archiveTab === "approved" ? "Commandes approuvées" : "Commandes annulées"}
</Text>
{!archiveLoading && (
<Text style={styles.archiveHeaderText}>
{archiveCommands.length} résultat{archiveCommands.length !== 1 ? "s" : ""}
</Text>
)}
</View>
{archiveLoading ? (
<View style={{ paddingVertical: spacing.l, alignItems: "center" }}>
<ActivityIndicator size="small" color={colors.accent} />
</View>
) : archiveCommands.length === 0 ? (
<Text style={styles.archiveEmpty}>Aucune commande trouvée</Text>
) : (
archiveCommands.map((item) => renderArchiveCard(item))
)}
<View style={styles.divider} />
</View>
)}
{/* Titre section commandes actives */}
<Text style={styles.activeSectionTitle}>
Commandes actives ({commands.length})
</Text>
</View>
);
if (loading) return <LoadingSpinner message="Chargement commandes..." />; if (loading) return <LoadingSpinner message="Chargement commandes..." />;
return ( return (
@@ -608,42 +1048,12 @@ export default function OrdersScreen() {
/> />
} }
contentContainerStyle={{ padding: spacing.l }} contentContainerStyle={{ padding: spacing.l }}
ListHeaderComponent={ ListHeaderComponent={ListHeader}
<View style={styles.refreshRow}>
<TouchableOpacity
style={styles.refreshBtn}
onPress={onRefresh}
disabled={refreshing}
>
<Ionicons
name="refresh-outline"
size={16}
color={refreshing ? colors.textMuted : colors.accent}
/>
<Text style={styles.refreshBtnText}>Actualiser</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.refreshBtn}
onPress={handleExportCSV}
disabled={exporting}
>
<Ionicons
name="download-outline"
size={16}
color={exporting ? colors.textMuted : colors.accent}
/>
<Text style={styles.refreshBtnText}>
{exporting ? "Export..." : "Export CSV"}
</Text>
</TouchableOpacity>
</View>
}
ListEmptyComponent={ ListEmptyComponent={
<Text style={styles.empty}>Aucune commande</Text> <Text style={styles.empty}>Aucune commande active</Text>
} }
/> />
{/* Modal items */} {/* Modal items */}
<Modal <Modal
visible={itemsModal.visible} visible={itemsModal.visible}
@@ -25,6 +25,7 @@ import {
createUserByAdmin, createUserByAdmin,
setClientParrain, setClientParrain,
creditClientReferral, creditClientReferral,
resetClientReferral,
getClientCancelledOrders, getClientCancelledOrders,
getSettings, getSettings,
applyClientPenalty, applyClientPenalty,
@@ -452,6 +453,20 @@ export default function UsersScreen() {
} }
}; };
const handleResetReferral = async (username: string) => {
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 // Points & amende
// -------------------------------------------------- // --------------------------------------------------
@@ -934,6 +949,18 @@ export default function UsersScreen() {
/> />
</TouchableOpacity> </TouchableOpacity>
)} )}
{referralEnabled && (item.clientData!.referral_balance ?? 0) > 0 && (
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => handleResetReferral(item.clientData!.username)}
>
<Ionicons
name="refresh-outline"
size={20}
color={colors.danger}
/>
</TouchableOpacity>
)}
<TouchableOpacity <TouchableOpacity
style={styles.editIconBtn} style={styles.editIconBtn}
onPress={() => openCancelledOrders(item.clientData!)} onPress={() => openCancelledOrders(item.clientData!)}
@@ -58,7 +58,7 @@ import TomTomMap, {
} from "../../components/TomTomMap"; } from "../../components/TomTomMap";
import DetailsModal from "../../components/ui/Modal"; import DetailsModal from "../../components/ui/Modal";
const LOCATION_INTERVAL_MS = 15000; const LOCATION_INTERVAL_MS = 5000;
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
available: "Disponible", available: "Disponible",
+5 -5
View File
@@ -330,8 +330,8 @@ export const changePassword = async (
export interface BasketResponse { export interface BasketResponse {
success: boolean; success: boolean;
message?: string; message?: string;
panier?: any[]; panier?: Record<string, unknown>[];
data?: { panier?: any[] }; data?: { panier?: Record<string, unknown>[] };
} }
/** /**
@@ -569,7 +569,7 @@ export const clearCart = async (username: string) => {
success: false, success: false,
message: errorData.error || "Erreur vidage", message: errorData.error || "Erreur vidage",
}; };
} catch (parseError) { } catch {
// Si le parsing JSON échoue (HTML retourné) // Si le parsing JSON échoue (HTML retourné)
console.error("❌ [CLEAR] Réponse non-JSON du serveur"); console.error("❌ [CLEAR] Réponse non-JSON du serveur");
return { return {
@@ -1206,7 +1206,7 @@ export const getOrderTotal = async (commandId: number): Promise<number> => {
} }
}; };
export const calculateOrderTotal = (order: any): number => { export const calculateOrderTotal = (order: Record<string, unknown>): number => {
// Préférer total (colonne calculée par le backend) // Préférer total (colonne calculée par le backend)
if (typeof order.total === "number" && order.total > 0) { if (typeof order.total === "number" && order.total > 0) {
return order.total; return order.total;
@@ -1219,7 +1219,7 @@ export const calculateOrderTotal = (order: any): number => {
// Fallback: calculer depuis les items si présents // Fallback: calculer depuis les items si présents
if (Array.isArray(order.items) && order.items.length > 0) { if (Array.isArray(order.items) && order.items.length > 0) {
return order.items.reduce((sum: number, item: any) => { return (order.items as Record<string, unknown>[]).reduce((sum: number, item) => {
const price = item.prix || item.price || 0; const price = item.prix || item.price || 0;
const quantity = item.quantite || item.quantity || 1; const quantity = item.quantite || item.quantity || 1;
return sum + price * quantity; return sum + price * quantity;
+10 -1
View File
@@ -19,6 +19,7 @@ export interface ApiResponse {
token_type?: string; token_type?: string;
expires_in?: number; expires_in?: number;
user?: UserResponse; user?: UserResponse;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; // Pour les champs additionnels [key: string]: any; // Pour les champs additionnels
} }
@@ -258,6 +259,7 @@ export interface OrderDetail {
// Métadonnées // Métadonnées
payment_method?: string; payment_method?: string;
notes?: string; notes?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -352,6 +354,7 @@ export interface TrackingResponse {
updated_at?: number; updated_at?: number;
created_at?: string; created_at?: string;
message?: string; message?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -379,6 +382,7 @@ export interface Product {
}>; }>;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -388,6 +392,7 @@ export interface ProductsResponse {
data?: Product[]; data?: Product[];
products?: Product[]; products?: Product[];
count?: number; count?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -429,6 +434,7 @@ export interface JWTPayload {
iat: number; iat: number;
exp: number; exp: number;
iss: string; iss: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -445,6 +451,7 @@ export interface ErrorResponse {
message?: string; message?: string;
details?: string; details?: string;
status_code?: number; status_code?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -607,6 +614,7 @@ export interface ETAResponse {
eta_available?: boolean; eta_available?: boolean;
livreur_distance?: number; livreur_distance?: number;
message?: string; message?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any;
} }
@@ -767,6 +775,7 @@ export interface ConfirmReceptionResponse {
data?: { data?: {
category?: string; category?: string;
points_earned?: number; points_earned?: number;
[key: string]: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}; };
} }
+2 -2
View File
@@ -128,12 +128,12 @@ function Navbar() {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
} catch (_) {} } catch { /* noop */ }
} }
sessionStorage.removeItem("admin_token"); sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username"); sessionStorage.removeItem("admin_username");
navigate("/login/client", { replace: true }); navigate("/login/client", { replace: true });
} catch (_) { } catch {
sessionStorage.removeItem("admin_token"); sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username"); sessionStorage.removeItem("admin_username");
navigate("/login/client", { replace: true }); navigate("/login/client", { replace: true });
+2 -1
View File
@@ -54,6 +54,7 @@ interface ToastMessage {
type: "success" | "error" | "warning" | "info"; type: "success" | "error" | "warning" | "info";
} }
// eslint-disable-next-line react-refresh/only-export-components
export const CartContext = createContext<CartContextType | undefined>(undefined); export const CartContext = createContext<CartContextType | undefined>(undefined);
export function CartProvider({ children }: { children: ReactNode }) { export function CartProvider({ children }: { children: ReactNode }) {
@@ -168,7 +169,7 @@ export function CartProvider({ children }: { children: ReactNode }) {
return () => { return () => {
console.log("🔄 [CART] CartProvider unmount"); console.log("🔄 [CART] CartProvider unmount");
}; };
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
/** /**
* Ajouter au panier * Ajouter au panier
@@ -36,6 +36,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
export function useTheme(): ThemeContextValue { export function useTheme(): ThemeContextValue {
return useContext(ThemeContext); return useContext(ThemeContext);
} }
+3 -3
View File
@@ -38,7 +38,7 @@ function UserAccueil() {
return; return;
} }
loadProducts(); loadProducts();
}, [selectedCategory, categories]); }, [selectedCategory, categories]); // eslint-disable-line react-hooks/exhaustive-deps
const loadProducts = async () => { const loadProducts = async () => {
setLoading(true); setLoading(true);
@@ -58,8 +58,8 @@ function UserAccueil() {
} else { } else {
setProducts([]); setProducts([]);
} }
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Erreur lors du chargement des produits"); setError(err instanceof Error ? err.message : "Erreur lors du chargement des produits");
} finally { } finally {
setLoading(false); setLoading(false);
} }
+3 -3
View File
@@ -42,7 +42,7 @@ function Cart() {
useEffect(() => { useEffect(() => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
refreshCart(); refreshCart();
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const total = cartItems.reduce((sum, item) => sum + item.price, 0); const total = cartItems.reduce((sum, item) => sum + item.price, 0);
@@ -71,7 +71,7 @@ function Cart() {
const res = await getProductById(item.product_id); const res = await getProductById(item.product_id);
if (res.success && res.data) { if (res.success && res.data) {
const p = 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 { return {
...item, ...item,
image: getProductImage(p), image: getProductImage(p),
@@ -89,7 +89,7 @@ function Cart() {
} }
}; };
enrich(); enrich();
}, [cartItems]); }, [cartItems]); // eslint-disable-line react-hooks/exhaustive-deps
const handleClearCart = async () => { const handleClearCart = async () => {
if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; }
+6 -6
View File
@@ -66,7 +66,7 @@ function Checkout() {
const res = await getProductById(item.product_id); const res = await getProductById(item.product_id);
if (res.success && res.data) { if (res.success && res.data) {
const p: Product = 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) { if (img?.url) {
setItemImages((prev) => ({ ...prev, [item.id]: getMediaUrl(img.url) })); setItemImages((prev) => ({ ...prev, [item.id]: getMediaUrl(img.url) }));
} }
@@ -289,7 +289,7 @@ function Checkout() {
if (response.success && response.payment_method === 'crypto') { if (response.success && response.payment_method === 'crypto') {
setCryptoPaymentData({ setCryptoPaymentData({
command_id: response.command_id!, command_id: response.command_id!,
client_order_number: (response as any).client_order_number, client_order_number: (response as Record<string, unknown>).client_order_number as number | undefined,
payment_status: response.payment_status!, payment_status: response.payment_status!,
pay_address: response.pay_address!, pay_address: response.pay_address!,
pay_amount: response.pay_amount!, pay_amount: response.pay_amount!,
@@ -348,13 +348,13 @@ function Checkout() {
// ✅ Préparer les données pour le modal // ✅ Préparer les données pour le modal
setConfirmationData({ setConfirmationData({
command_id, command_id,
client_order_number: (response as any).client_order_number, client_order_number: (response as Record<string, unknown>).client_order_number as number | undefined,
assigned_to, assigned_to,
queue_info, queue_info,
delivery_address: delivery_address || address, delivery_address: delivery_address || address,
arrivalTime, arrivalTime,
total: frontendTotal, total: frontendTotal,
referral_used: (response as any).referral_used, referral_used: (response as Record<string, unknown>).referral_used as number | undefined,
clientInfo: { clientInfo: {
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
@@ -369,9 +369,9 @@ function Checkout() {
} else { } else {
setError(response.message || '❌ Erreur lors de la validation de la commande'); setError(response.message || '❌ Erreur lors de la validation de la commande');
} }
} catch (err: any) { } catch (err: unknown) {
console.error('❌ Erreur checkout:', err); 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -65,7 +65,7 @@ function ConsultationHistorique() {
getReferralBalance().then((r) => { if (r.success) setReferralBalance(r.balance); }); getReferralBalance().then((r) => { if (r.success) setReferralBalance(r.balance); });
} }
}); });
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchHistory = async () => { const fetchHistory = async () => {
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; } if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; }
+5 -12
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import './ModalSuccess.css'; import './ModalSuccess.css';
interface ModalSuccessProps { interface ModalSuccessProps {
@@ -10,20 +10,13 @@ interface ModalSuccessProps {
} }
export function ModalSuccess({ isOpen, productName, quantity, price, onClose }: ModalSuccessProps) { export function ModalSuccess({ isOpen, productName, quantity, price, onClose }: ModalSuccessProps) {
const [isVisible, setIsVisible] = useState(isOpen);
useEffect(() => { useEffect(() => {
setIsVisible(isOpen); if (!isOpen) return;
if (isOpen) { const timer = setTimeout(() => { onClose(); }, 2500);
const timer = setTimeout(() => { return () => clearTimeout(timer);
setIsVisible(false);
onClose();
}, 2500);
return () => clearTimeout(timer);
}
}, [isOpen, onClose]); }, [isOpen, onClose]);
if (!isVisible) return null; if (!isOpen) return null;
return ( return (
<div className="modal2-success-overlay"> <div className="modal2-success-overlay">
@@ -104,7 +104,7 @@ function OrderDetails() {
if (commandId) { if (commandId) {
fetchOrderDetails(commandId); fetchOrderDetails(commandId);
} }
}, [orderId]); }, [orderId]); // eslint-disable-line react-hooks/exhaustive-deps
// ============================================ // ============================================
// FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD) // FONCTIONS POUR PHOTOS ET VIDÉOS (COMME PRODUCTCARD)
@@ -299,7 +299,7 @@ function OrderDetails() {
// ✅ Produits depuis items (command_items) // ✅ Produits depuis items (command_items)
products: products:
apiData.items?.map((item: any) => ({ apiData.items?.map((item: Record<string, unknown>) => ({
id: item.id, id: item.id,
product_id: item.product_id, product_id: item.product_id,
name_product: item.produit, name_product: item.produit,
@@ -63,7 +63,7 @@ function ProductDetail() {
useEffect(() => { useEffect(() => {
if (id) loadProduct(Number(id)); if (id) loadProduct(Number(id));
}, [id]); }, [id]); // eslint-disable-line react-hooks/exhaustive-deps
const loadProduct = async (productId: number) => { const loadProduct = async (productId: number) => {
// ✅ Vérifier l'auth avant de charger le produit // ✅ Vérifier l'auth avant de charger le produit
@@ -110,8 +110,8 @@ function ProductDetail() {
} else { } else {
setError(response.message || "Produit non trouvé"); setError(response.message || "Produit non trouvé");
} }
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Erreur lors du chargement du produit"); setError(err instanceof Error ? err.message : "Erreur lors du chargement du produit");
} finally { } finally {
setLoading(false); setLoading(false);
} }
+4 -9
View File
@@ -23,9 +23,9 @@ export default function ProfilePage() {
const [loadingProfile, setLoadingProfile] = useState(true); const [loadingProfile, setLoadingProfile] = useState(true);
// Données locales (localStorage) // Données locales (localStorage)
const [defaultAddress, setDefaultAddress] = useState(''); const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? '');
const [defaultPhone, setDefaultPhone] = useState(''); const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? '');
const [signalPseudo, setSignalPseudo] = useState(''); const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? '');
// Feedback // Feedback
const [savingContact, setSavingContact] = useState(false); const [savingContact, setSavingContact] = useState(false);
@@ -47,11 +47,6 @@ export default function ProfilePage() {
navigate('/login/client', { replace: true }); navigate('/login/client', { replace: true });
return; return;
} }
// Charger depuis localStorage
setDefaultAddress(localStorage.getItem(STORAGE_ADDRESS) ?? '');
setDefaultPhone(localStorage.getItem(STORAGE_PHONE) ?? '');
setSignalPseudo(localStorage.getItem(STORAGE_SIGNAL) || username);
// Statut Telegram // Statut Telegram
getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); }); getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
@@ -68,7 +63,7 @@ export default function ProfilePage() {
} }
setLoadingProfile(false); setLoadingProfile(false);
}); });
}, [navigate]); }, [navigate]); // eslint-disable-line react-hooks/exhaustive-deps
const showSuccess = (msg: string) => { const showSuccess = (msg: string) => {
setSuccessMsg(msg); setSuccessMsg(msg);
+21 -18
View File
@@ -27,6 +27,7 @@ import Navbar from "../../components/Navbar";
import Toast from "../../components/Toast"; import Toast from "../../components/Toast";
import "./SuiviLivraison.css"; import "./SuiviLivraison.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { import {
faHourglassHalf, faHourglassHalf,
faTruck, faTruck,
@@ -109,7 +110,7 @@ const getDeliveryAddress = (order: OrderWithTracking): string => {
return order.delivery_address || order.adresse || "Non disponible"; return order.delivery_address || order.adresse || "Non disponible";
}; };
const formatOrderItem = (item: any) => { const formatOrderItem = (item: Record<string, unknown>) => {
return { return {
name: name:
item.produit || item.product_name || item.name_product || "Produit", item.produit || item.product_name || item.name_product || "Produit",
@@ -153,8 +154,8 @@ const getStatusLabel = (status: string): string => {
return statusMap[status?.toLowerCase()] || "Statut inconnu"; return statusMap[status?.toLowerCase()] || "Statut inconnu";
}; };
const getStatusIcon = (status: string): any => { const getStatusIcon = (status: string): IconDefinition => {
const iconMap: Record<string, any> = { const iconMap: Record<string, IconDefinition> = {
pending: faHourglassHalf, pending: faHourglassHalf,
assigned: faBiking, assigned: faBiking,
en_route: faTruck, en_route: faTruck,
@@ -178,7 +179,7 @@ const calculateOrderPoints = (
points: number; points: number;
category: string; category: string;
categoryDisplay: string; categoryDisplay: string;
categoryIcon: any; categoryIcon: IconDefinition;
categoryColor: string; categoryColor: string;
} => { } => {
// Totaux indexés par pool (+ index spécial pour "gros&semi" exclu des points) // Totaux indexés par pool (+ index spécial pour "gros&semi" exclu des points)
@@ -186,7 +187,7 @@ const calculateOrderPoints = (
let excludedTotal = 0; let excludedTotal = 0;
if (order.items && order.items.length > 0) { if (order.items && order.items.length > 0) {
order.items.forEach((item: any) => { order.items.forEach((item: Record<string, unknown>) => {
const cat = (item.category || "").toLowerCase(); const cat = (item.category || "").toLowerCase();
const itemPrice = item.prix || item.price || 0; const itemPrice = item.prix || item.price || 0;
@@ -332,7 +333,7 @@ function SuiviLivraison() {
loadOrders(); loadOrders();
const interval = setInterval(loadOrders, 10000); const interval = setInterval(loadOrders, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const loadOrders = async () => { const loadOrders = async () => {
// ✅ Vérifier l'auth avant de charger les commandes // ✅ Vérifier l'auth avant de charger les commandes
@@ -359,7 +360,7 @@ function SuiviLivraison() {
try { try {
tracking = await getOrderTracking(order.id); tracking = await getOrderTracking(order.id);
} catch (err) { } catch {
console.warn( console.warn(
`Tracking non disponible pour commande ${order.id}`, `Tracking non disponible pour commande ${order.id}`,
); );
@@ -368,7 +369,7 @@ function SuiviLivraison() {
try { try {
eta = await getOrderETA(order.id); eta = await getOrderETA(order.id);
} catch (err) { } catch {
console.warn( console.warn(
`ETA non disponible pour commande ${order.id}`, `ETA non disponible pour commande ${order.id}`,
); );
@@ -389,10 +390,11 @@ function SuiviLivraison() {
setError("Impossible de charger les commandes"); setError("Impossible de charger les commandes");
showToast("Impossible de charger les commandes", "error"); showToast("Impossible de charger les commandes", "error");
} }
} catch (err: any) { } catch (err: unknown) {
console.error("Erreur loadOrders:", err); console.error("Erreur loadOrders:", err);
setError(err.message || "Erreur lors du chargement"); const msg = err instanceof Error ? err.message : "Erreur lors du chargement";
showToast(err.message || "Erreur lors du chargement", "error"); setError(msg);
showToast(msg, "error");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -462,7 +464,7 @@ function SuiviLivraison() {
const pointsEarned = const pointsEarned =
response.points_earned || selectedOrderPoints; response.points_earned || selectedOrderPoints;
const apiCategory = const apiCategory =
response.category || (response as any).data?.category || ""; response.category || (response as Record<string, unknown> & { data?: { category?: string } }).data?.category || "";
const displayCategory = const displayCategory =
apiCategory && apiCategory !== "total" apiCategory && apiCategory !== "total"
@@ -483,9 +485,10 @@ function SuiviLivraison() {
); );
setConfirming(null); setConfirming(null);
} }
} catch (err: any) { } catch (err: unknown) {
setError(err.message || "Erreur serveur"); const msg = err instanceof Error ? err.message : "Erreur serveur";
showToast(err.message || "Erreur serveur", "error"); setError(msg);
showToast(msg, "error");
setConfirming(null); setConfirming(null);
} }
}; };
@@ -567,9 +570,9 @@ function SuiviLivraison() {
"error", "error",
); );
} }
} catch (error: any) { } catch (error: unknown) {
console.error("❌ [CANCEL] Erreur:", error); 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 { } finally {
setCancellingOrder(null); setCancellingOrder(null);
} }
@@ -895,7 +898,7 @@ function SuiviLivraison() {
<div className="items-list"> <div className="items-list">
{order.items.map( {order.items.map(
( (
item: any, item: Record<string, unknown>,
idx: number, idx: number,
) => { ) => {
const formatted = const formatted =