chore: fix
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user