chore: add corrige adresse
This commit is contained in:
@@ -407,6 +407,51 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DELETE COMMAND ITEM - ADMIN ONLY
|
||||
// ============================================
|
||||
|
||||
func (d *Database) DeleteCommandItem(commandID, itemID int) error {
|
||||
log.Printf("🗑️ [DeleteCommandItem] START - commandID=%d, itemID=%d", commandID, itemID)
|
||||
|
||||
if err := validateCommandID(commandID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateItemID(itemID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Récupérer le prix et la quantité avant suppression pour mettre à jour le total
|
||||
var prix, quantite float64
|
||||
checkQuery := `SELECT prix, quantite FROM command_items WHERE id = $1 AND command_id = $2`
|
||||
err := d.QueryRow(checkQuery, itemID, commandID).Scan(&prix, &quantite)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("item %d non trouvé dans la commande %d", itemID, commandID)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur vérification item: %w", err)
|
||||
}
|
||||
|
||||
// Supprimer l'item
|
||||
_, err = d.Exec(`DELETE FROM command_items WHERE id = $1`, itemID)
|
||||
if err != nil {
|
||||
log.Printf("❌ Erreur DELETE command_items: %v", err)
|
||||
return fmt.Errorf("erreur suppression item: %w", err)
|
||||
}
|
||||
|
||||
// Recalculer le total de la commande
|
||||
_, err = d.Exec(
|
||||
`UPDATE commandes SET total_prix = GREATEST(0, total_prix - $1) WHERE id = $2`,
|
||||
prix*quantite, commandID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [DeleteCommandItem] Erreur maj total commande: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ [DeleteCommandItem] Item %d supprimé de la commande %d", itemID, commandID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UPDATE COMMAND ITEM STATUS - VERSION SÉCURISÉE
|
||||
// ============================================
|
||||
|
||||
@@ -48,6 +48,7 @@ func validateCommandStatus(status string) error {
|
||||
"pending": true,
|
||||
"assigned": true,
|
||||
"en_route": true,
|
||||
"arrived": true,
|
||||
"livre": true,
|
||||
"approved": true,
|
||||
"cancelled": true,
|
||||
@@ -275,7 +276,9 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
return nil, fmt.Errorf("stock insuffisant pour produit %d", item.ProductID)
|
||||
// Le stock était déjà réservé lors de l'ajout au panier (DecrementProductStockByID).
|
||||
// On ne bloque pas la commande : tous les articles doivent être insérés.
|
||||
log.Printf("⚠️ [CHECKOUT] Stock déjà réservé pour produit %d (double réservation panier/checkout)", item.ProductID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +327,8 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
|
||||
}
|
||||
|
||||
query := `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
|
||||
FROM commandes c
|
||||
WHERE 1=1`
|
||||
|
||||
@@ -333,7 +337,7 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
|
||||
|
||||
// Filtrage du statut
|
||||
if status == "" {
|
||||
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'livre')`
|
||||
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')`
|
||||
} else {
|
||||
query += fmt.Sprintf(" AND c.status = $%d", argPosition)
|
||||
args = append(args, status)
|
||||
@@ -362,10 +366,12 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
|
||||
var id int
|
||||
var username, status, adresse string
|
||||
var livreurAssign sql.NullString
|
||||
var proposedAddress sql.NullString
|
||||
var addressProposalStatus string
|
||||
var totalPrix float64
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt)
|
||||
err := rows.Scan(&id, &username, &status, &adresse, &totalPrix, &livreurAssign, &createdAt, &updatedAt, &proposedAddress, &addressProposalStatus)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors du scan de la commande: %w", err)
|
||||
}
|
||||
@@ -374,13 +380,14 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
|
||||
adresse = sanitizeString(adresse)
|
||||
|
||||
command := map[string]interface{}{
|
||||
"id": id,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
"id": id,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
"address_proposal_status": addressProposalStatus,
|
||||
}
|
||||
|
||||
if livreurAssign.Valid {
|
||||
@@ -389,6 +396,12 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]interfa
|
||||
command["livreur_assign"] = nil
|
||||
}
|
||||
|
||||
if proposedAddress.Valid {
|
||||
command["proposed_address"] = proposedAddress.String
|
||||
} else {
|
||||
command["proposed_address"] = nil
|
||||
}
|
||||
|
||||
commands = append(commands, command)
|
||||
}
|
||||
|
||||
@@ -412,12 +425,15 @@ func (d *Database) GetCommandCount() (int, error) {
|
||||
|
||||
func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
|
||||
// ✅ Déjà sécurisé avec paramètre $1
|
||||
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at
|
||||
query := `SELECT id, username, status, adresse, total_prix, livreur_assign, created_at, updated_at,
|
||||
proposed_address, address_proposal_status
|
||||
FROM commandes WHERE id = $1`
|
||||
|
||||
var commandID int
|
||||
var username, status, adresse string
|
||||
var livreurAssign sql.NullString
|
||||
var proposedAddress sql.NullString
|
||||
var addressProposalStatus string
|
||||
var totalPrix float64
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
@@ -430,6 +446,8 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
|
||||
&livreurAssign,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&proposedAddress,
|
||||
&addressProposalStatus,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -440,13 +458,14 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
command := map[string]interface{}{
|
||||
"id": commandID,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
"id": commandID,
|
||||
"username": username,
|
||||
"status": status,
|
||||
"adresse": adresse,
|
||||
"total_prix": totalPrix,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
"address_proposal_status": addressProposalStatus,
|
||||
}
|
||||
|
||||
if livreurAssign.Valid {
|
||||
@@ -455,6 +474,12 @@ func (d *Database) GetCommandByID(id int) (map[string]interface{}, error) {
|
||||
command["livreur_assign"] = nil
|
||||
}
|
||||
|
||||
if proposedAddress.Valid {
|
||||
command["proposed_address"] = proposedAddress.String
|
||||
} else {
|
||||
command["proposed_address"] = nil
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
||||
@@ -503,10 +528,74 @@ func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProposeAddressChange propose une nouvelle adresse (admin/cabine) en attente de validation client
|
||||
func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposedBy string) error {
|
||||
if err := validateAddress(proposedAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := `UPDATE commandes
|
||||
SET proposed_address = $1, address_proposal_status = 'pending', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2`
|
||||
|
||||
result, err := d.Exec(query, proposedAddress, commandID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur proposition adresse: %w", err)
|
||||
}
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("commande non trouvée")
|
||||
}
|
||||
|
||||
d.AddCommandLog(commandID, "address_proposed",
|
||||
fmt.Sprintf("Nouvelle adresse proposée par %s: %s", proposedBy, proposedAddress),
|
||||
proposedBy)
|
||||
|
||||
log.Printf("✅ Adresse proposée pour commande %d par %s", commandID, proposedBy)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RespondToAddressProposal accepte ou refuse la proposition d'adresse
|
||||
func (d *Database) RespondToAddressProposal(commandID int, clientUsername string, accepted bool) error {
|
||||
var query string
|
||||
if accepted {
|
||||
// Remplace l'adresse par la proposition
|
||||
query = `UPDATE commandes
|
||||
SET adresse = proposed_address, proposed_address = NULL,
|
||||
address_proposal_status = 'accepted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND username = $2 AND address_proposal_status = 'pending'`
|
||||
} else {
|
||||
query = `UPDATE commandes
|
||||
SET proposed_address = NULL, address_proposal_status = 'rejected',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1 AND username = $2 AND address_proposal_status = 'pending'`
|
||||
}
|
||||
|
||||
result, err := d.Exec(query, commandID, clientUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur réponse proposition adresse: %w", err)
|
||||
}
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("aucune proposition en attente pour cette commande")
|
||||
}
|
||||
|
||||
action := "refusée"
|
||||
if accepted {
|
||||
action = "acceptée"
|
||||
}
|
||||
d.AddCommandLog(commandID, "address_proposal_"+action,
|
||||
fmt.Sprintf("Proposition d'adresse %s par le client %s", action, clientUsername),
|
||||
clientUsername)
|
||||
|
||||
log.Printf("✅ Proposition adresse %s pour commande %d", action, commandID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCommandStatus met à jour le statut d'une commande
|
||||
func (d *Database) UpdateCommandStatus(commandID int, status string) error {
|
||||
// ✅ SÉCURITÉ: Validation du statut
|
||||
validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled", "support"}
|
||||
validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled", "support"}
|
||||
isValid := false
|
||||
for _, vs := range validStatuses {
|
||||
if status == vs {
|
||||
@@ -617,7 +706,7 @@ func (d *Database) GetCommandsWithFilter(status, username string, excludeApprove
|
||||
|
||||
// ✅ Filtrer par status si fourni avec validation
|
||||
if status != "" {
|
||||
validStatuses := []string{"pending", "assigned", "en_route", "livre", "approved", "cancelled", "disabled", "support"}
|
||||
validStatuses := []string{"pending", "assigned", "en_route", "arrived", "livre", "approved", "cancelled", "disabled", "support"}
|
||||
isValid := false
|
||||
for _, vs := range validStatuses {
|
||||
if status == vs {
|
||||
|
||||
@@ -86,6 +86,14 @@ func InitDB() *Database {
|
||||
log.Fatalf("❌ Erreur migration push_token users: %v", err)
|
||||
}
|
||||
|
||||
// Migration: proposition de modification d'adresse par admin/cabine
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS proposed_address TEXT`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration proposed_address: %v", err)
|
||||
}
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS address_proposal_status VARCHAR(20) NOT NULL DEFAULT 'none'`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration address_proposal_status: %v", err)
|
||||
}
|
||||
|
||||
// Migration: ajouter colonne unit pour l'unité de mesure des produits (kg, g, bag, l, cl, pcs, u)
|
||||
if _, err = database.Exec(`ALTER TABLE products ADD COLUMN IF NOT EXISTS unit VARCHAR(10) NOT NULL DEFAULT 'u'`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration unit products: %v", err)
|
||||
|
||||
@@ -131,14 +131,16 @@ func GetMyCommandsWithTracking(c *gin.Context) {
|
||||
}
|
||||
|
||||
enrichedCommands[i] = gin.H{
|
||||
"id": cmd["id"],
|
||||
"status": cmd["status"],
|
||||
"status_message": getStatusMessage(cmd["status"].(string)),
|
||||
"adresse": cmd["adresse"],
|
||||
"total_prix": cmd["total_prix"],
|
||||
"created_at": cmd["created_at"],
|
||||
"livreur": livreurInfo,
|
||||
"eta": etaData,
|
||||
"id": cmd["id"],
|
||||
"status": cmd["status"],
|
||||
"status_message": getStatusMessage(cmd["status"].(string)),
|
||||
"adresse": cmd["adresse"],
|
||||
"total_prix": cmd["total_prix"],
|
||||
"created_at": cmd["created_at"],
|
||||
"livreur": livreurInfo,
|
||||
"eta": etaData,
|
||||
"proposed_address": cmd["proposed_address"],
|
||||
"address_proposal_status": cmd["address_proposal_status"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,120 @@ func UpdateCommandAddress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ProposeAddressChange propose une nouvelle adresse au client pour validation
|
||||
// POST /api/v2/admin/protected/orders/:id/propose-address
|
||||
// POST /api/v1/cabine/commands/:id/propose-address
|
||||
func ProposeAddressChange(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
userRole := c.GetString("role")
|
||||
if userRole != "admin" && userRole != "cabine" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
|
||||
staffUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || commandID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ProposedAddress string `json:"proposed_address" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateAddress(req.ProposedAddress); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer la commande pour vérifier statut et obtenir username client
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
return
|
||||
}
|
||||
|
||||
status, _ := command["status"].(string)
|
||||
if status == "livre" || status == "approved" || status == "cancelled" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de modifier l'adresse d'une commande terminée"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.ProposeAddressChange(commandID, req.ProposedAddress, staffUsername); err != nil {
|
||||
log.Printf("❌ [PROPOSE_ADDR] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Notifier le client
|
||||
clientUsername, _ := command["username"].(string)
|
||||
if clientUsername != "" {
|
||||
msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour la commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", commandID, req.ProposedAddress)
|
||||
database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
|
||||
}
|
||||
|
||||
log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposée par %s", commandID, staffUsername)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Nouvelle adresse proposée au client",
|
||||
"command_id": commandID,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondToAddressProposal permet au client d'accepter ou refuser une proposition d'adresse
|
||||
// POST /api/v1/commands/:id/address/respond
|
||||
func RespondToAddressProposal(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
clientUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || commandID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Accepted bool `json:"accepted"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.RespondToAddressProposal(commandID, clientUsername, req.Accepted); err != nil {
|
||||
log.Printf("❌ [RESPOND_ADDR] Erreur: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
action := "refusée"
|
||||
if req.Accepted {
|
||||
action = "acceptée"
|
||||
}
|
||||
|
||||
log.Printf("✅ [RESPOND_ADDR] Commande %d - proposition %s par %s", commandID, action, clientUsername)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("Proposition d'adresse %s", action),
|
||||
})
|
||||
}
|
||||
|
||||
func GetAllCommands(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -768,6 +882,54 @@ func GetClientCommandsHistory(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// NOTIFICATIONS CLIENT
|
||||
// ============================================
|
||||
|
||||
// NotifyClientToDescend envoie une notification push au client pour descendre récupérer sa commande
|
||||
// POST /api/v2/admin/protected/orders/:id/notify-client
|
||||
// POST /api/v1/cabine/commands/:id/notify-client
|
||||
func NotifyClientToDescend(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
role := c.GetString("role")
|
||||
if role != "admin" && role != "cabine" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || commandID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
cmd, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande introuvable"})
|
||||
return
|
||||
}
|
||||
|
||||
clientUsername, ok := cmd["username"].(string)
|
||||
if !ok || clientUsername == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Client introuvable"})
|
||||
return
|
||||
}
|
||||
|
||||
staffUsername, _ := c.Get("username")
|
||||
msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", commandID)
|
||||
database.NotifyClient(clientUsername, commandID, "ready_pickup", msg)
|
||||
database.AddCommandLog(commandID, "notification", fmt.Sprintf("Client notifié de descendre par %s", staffUsername), staffUsername.(string))
|
||||
|
||||
log.Printf("🔔 [NOTIFY] Client %s notifié pour commande %d par %s", clientUsername, commandID, staffUsername)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Client notifié",
|
||||
"client_username": clientUsername,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DÉTAILS COMMANDES & ITEMS
|
||||
// ============================================
|
||||
@@ -1145,6 +1307,104 @@ func GetCommandFullDetails(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCommandItem supprime un item d'une commande
|
||||
// DELETE /api/v2/admin/protected/orders/:id/items/:item_id
|
||||
func DeleteCommandItem(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
if c.GetString("role") != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || commandID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
itemID, err := strconv.Atoi(c.Param("item_id"))
|
||||
if err != nil || itemID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'item invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🗑️ [DEL_ITEM] Admin %s supprime item %d de cmd %d", adminUsername, itemID, commandID)
|
||||
|
||||
if err := database.DeleteCommandItem(commandID, itemID); err != nil {
|
||||
log.Printf("❌ [DEL_ITEM] Erreur: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
database.AddCommandLog(commandID, "item_deleted",
|
||||
fmt.Sprintf("Item %d supprimé par admin %s", itemID, adminUsername),
|
||||
adminUsername)
|
||||
|
||||
log.Printf("✅ [DEL_ITEM] Item %d supprimé", itemID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Item supprimé",
|
||||
"command_id": commandID,
|
||||
"item_id": itemID,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCommandStatusAdmin met à jour le statut d'une commande (Admin)
|
||||
// PUT /api/v2/admin/protected/orders/:id/status
|
||||
func UpdateCommandStatusAdmin(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
if c.GetString("role") != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil || commandID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Statut manquant"})
|
||||
return
|
||||
}
|
||||
|
||||
allowed := map[string]bool{
|
||||
"pending": true, "assigned": true, "en_route": true,
|
||||
"livre": true, "approved": true, "cancelled": true,
|
||||
}
|
||||
if !allowed[req.Status] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Statut invalide: " + req.Status})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
|
||||
log.Printf("❌ [STATUS_ADMIN] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [STATUS_ADMIN] Cmd %d → %s", commandID, req.Status)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Statut mis à jour",
|
||||
"command_id": commandID,
|
||||
"new_status": req.Status,
|
||||
})
|
||||
}
|
||||
|
||||
// GetCommandItemsStats récupère les stats d'une commande
|
||||
// GET /api/v1/commands/:id/stats
|
||||
func GetCommandItemsStats(c *gin.Context) {
|
||||
|
||||
@@ -373,7 +373,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID)
|
||||
}
|
||||
case "arrived":
|
||||
clientMsg = fmt.Sprintf("Votre livreur est arrivé pour la commande #%d", commandID)
|
||||
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
|
||||
case "livre":
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
||||
case "failed":
|
||||
|
||||
@@ -104,7 +104,7 @@ func main() {
|
||||
|
||||
// Configuration CORS
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"https://uber-stup.club", "https://mln-uber.club", "http://localhost:5173"},
|
||||
AllowOrigins: []string{"https://uber-stup.club", "https://mln-uber.club", "http://localhost:5173", "http://5.181.0.112"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
|
||||
@@ -79,6 +79,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
cartGroupV1.GET("/commands/:id/items", handlers.GetCommandItemsWithDetails)
|
||||
// Approbation livraison
|
||||
cartGroupV1.POST("/commands/:id/approve", handlers.ApproveDelivery)
|
||||
cartGroupV1.POST("/commands/:id/address/respond", handlers.RespondToAddressProposal)
|
||||
// ⭐ NOUVEAU - HISTORIQUE DES COMMANDES TERMINÉES
|
||||
cartGroupV1.GET("/my-commands/history/detailed", handlers.GetMyCompletedOrdersWithItems)
|
||||
cartGroupV1.GET("/commands/:id/history", handlers.GetOrderHistory)
|
||||
@@ -167,8 +168,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
adminGroupV2.GET("/orders", handlers.GetAllCommands)
|
||||
adminGroupV2.GET("/orders/:id", handlers.GetCommandByID)
|
||||
adminGroupV2.PUT("/orders/:id/address", handlers.UpdateCommandAddress)
|
||||
adminGroupV2.POST("/orders/:id/propose-address", handlers.ProposeAddressChange)
|
||||
adminGroupV2.PUT("/orders/:id/status", handlers.UpdateCommandStatusAdmin)
|
||||
adminGroupV2.POST("/orders/:id/force-validate", handlers.ValidateDelivery)
|
||||
adminGroupV2.POST("/orders/:id/confirm-reception", handlers.StaffApproveDelivery)
|
||||
adminGroupV2.POST("/orders/:id/notify-client", handlers.NotifyClientToDescend)
|
||||
adminGroupV2.GET("/orders/:id/items", handlers.ShowItems)
|
||||
adminGroupV2.DELETE("/orders/:id/items/:item_id", handlers.DeleteCommandItem)
|
||||
adminGroupV2.DELETE("/orders/:id", handlers.DeleteCommandByCabine)
|
||||
|
||||
// ============================================
|
||||
// ⭐ AUTO-ASSIGNATION GPS - ROUTES CRITIQUES
|
||||
@@ -237,10 +244,12 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
cabineGroupV1.GET("/commands/:id/items", handlers.ShowItems)
|
||||
cabineGroupV1.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery)
|
||||
cabineGroupV1.POST("/commands/:id/assign", handlers.AssignDeliveryPerson)
|
||||
cabineGroupV1.POST("/commands/:id/notify-client", handlers.NotifyClientToDescend)
|
||||
cabineGroupV1.PUT("/items/:item_id/status", handlers.UpdateItemStatus)
|
||||
cabineGroupV1.GET("/commands/:id/deliveryman/location", handlers.GetDeliverymanLocationForCommand)
|
||||
cabineGroupV1.GET("/all/deliveryman", handlers.GetAllDeliveryMen)
|
||||
cabineGroupV1.DELETE("/commands/:id", handlers.DeleteCommandByCabine)
|
||||
cabineGroupV1.POST("/commands/:id/propose-address", handlers.ProposeAddressChange)
|
||||
// ⭐ NOUVEAU - ANNULATION PAR CABINE
|
||||
cabineGroupV1.GET("/commands/cancelled", handlers.GetAllCancelledOrders)
|
||||
cabineGroupV1.GET("/client/:username/penalties", handlers.GetClientPenaltiesAdmin) // Voir pénalités client
|
||||
|
||||
@@ -173,6 +173,14 @@ export const validateCommand = async (commandId: number) => {
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const proposeAddressChangeAdmin = async (commandId: number, proposedAddress: string) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/orders/${commandId}/propose-address`,
|
||||
{ proposed_address: proposedAddress },
|
||||
);
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const notifyClientToDescend = async (commandId: number) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/orders/${commandId}/notify-client`,
|
||||
|
||||
@@ -117,6 +117,14 @@ export const deleteCommand = async (commandId: number) => {
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const proposeAddressChangeCabine = async (commandId: number, proposedAddress: string) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${API}/commands/${commandId}/propose-address`,
|
||||
{ proposed_address: proposedAddress },
|
||||
);
|
||||
return { success: true, message: data.message };
|
||||
};
|
||||
|
||||
export const notifyClientToDescendCabine = async (commandId: number) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${API}/commands/${commandId}/notify-client`,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
confirmReceptionAdmin,
|
||||
deleteCommand,
|
||||
deleteCommandItem,
|
||||
proposeAddressChangeAdmin,
|
||||
} from "../../api/api_admin";
|
||||
import type { CommandResponse } from "../../api/types";
|
||||
import type { AdminStackParamList } from "../../navigation/types";
|
||||
@@ -62,6 +64,12 @@ export default function OrdersScreen() {
|
||||
}>({ visible: false, commandId: null });
|
||||
const [livreurs, setLivreurs] = useState<any[]>([]);
|
||||
|
||||
const [addressModal, setAddressModal] = useState<{
|
||||
visible: boolean;
|
||||
commandId: number | null;
|
||||
input: string;
|
||||
}>({ visible: false, commandId: null, input: "" });
|
||||
|
||||
const [itemsModal, setItemsModal] = useState<{
|
||||
visible: boolean;
|
||||
commandId: number | null;
|
||||
@@ -180,6 +188,17 @@ export default function OrdersScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleProposeAddress = async () => {
|
||||
if (!addressModal.commandId || !addressModal.input.trim()) return;
|
||||
try {
|
||||
await proposeAddressChangeAdmin(addressModal.commandId, addressModal.input.trim());
|
||||
setAddressModal({ visible: false, commandId: null, input: "" });
|
||||
showSuccess("Proposition envoyée", "Le client a été notifié de la nouvelle adresse proposée");
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = (commandId: number, itemId: number, itemName: string) => {
|
||||
showConfirm(
|
||||
"Supprimer l'article",
|
||||
@@ -372,6 +391,28 @@ export default function OrdersScreen() {
|
||||
padding: spacing.xs,
|
||||
marginLeft: spacing.s,
|
||||
},
|
||||
addressInput: {
|
||||
backgroundColor: colors.bgPrimary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.sm,
|
||||
color: colors.textWhite,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.m,
|
||||
fontSize: fontSize.md,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
addressConfirmBtn: {
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingVertical: spacing.m,
|
||||
alignItems: "center",
|
||||
},
|
||||
addressConfirmText: {
|
||||
color: colors.textWhite,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: "700",
|
||||
},
|
||||
}),
|
||||
[colors],
|
||||
);
|
||||
@@ -394,6 +435,12 @@ export default function OrdersScreen() {
|
||||
icon: "receipt-outline" as keyof typeof Ionicons.glyphMap,
|
||||
onPress: () => { setOpenMenuId(null); openItems(item.id); },
|
||||
},
|
||||
{
|
||||
label: "Proposer adresse",
|
||||
icon: "location-outline" as keyof typeof Ionicons.glyphMap,
|
||||
onPress: () => { setOpenMenuId(null); setAddressModal({ visible: true, commandId: item.id, input: "" }); },
|
||||
condition: !isDone,
|
||||
},
|
||||
{
|
||||
label: "Le client est là",
|
||||
icon: "notifications-outline" as keyof typeof Ionicons.glyphMap,
|
||||
@@ -649,6 +696,29 @@ export default function OrdersScreen() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Modal proposition adresse */}
|
||||
<Modal
|
||||
visible={addressModal.visible}
|
||||
onClose={() => setAddressModal({ visible: false, commandId: null, input: "" })}
|
||||
title={`Proposer adresse — commande #${addressModal.commandId}`}
|
||||
icon="location-outline"
|
||||
>
|
||||
<TextInput
|
||||
style={styles.addressInput}
|
||||
placeholder="Nouvelle adresse..."
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={addressModal.input}
|
||||
onChangeText={(t) => setAddressModal((prev) => ({ ...prev, input: t }))}
|
||||
multiline
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.addressConfirmBtn}
|
||||
onPress={handleProposeAddress}
|
||||
>
|
||||
<Text style={styles.addressConfirmText}>Envoyer la proposition</Text>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
<AlertModal
|
||||
visible={alert.visible}
|
||||
type={alert.type}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
getCabineLivreursList,
|
||||
assignDeliveryPersonByCabine,
|
||||
resetClientPoints,
|
||||
proposeAddressChangeCabine,
|
||||
} from "../../api/api_cabine";
|
||||
import type { CommandResponse } from "../../api/types";
|
||||
import StatusBadge from "../../components/StatusBadge";
|
||||
@@ -71,6 +73,11 @@ export default function OrdersScreen() {
|
||||
visible: boolean;
|
||||
commandId: number | null;
|
||||
}>({ visible: false, commandId: null });
|
||||
const [addressModal, setAddressModal] = useState<{
|
||||
visible: boolean;
|
||||
commandId: number | null;
|
||||
input: string;
|
||||
}>({ visible: false, commandId: null, input: "" });
|
||||
const [livreurs, setLivreurs] = useState<{ id: number; username: string }[]>([]);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -148,6 +155,17 @@ export default function OrdersScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleProposeAddress = async () => {
|
||||
if (!addressModal.commandId || !addressModal.input.trim()) return;
|
||||
try {
|
||||
await proposeAddressChangeCabine(addressModal.commandId, addressModal.input.trim());
|
||||
setAddressModal({ visible: false, commandId: null, input: "" });
|
||||
showSuccess("Proposition envoyée", "Le client a été notifié de la nouvelle adresse proposée");
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPoints = (clientUsername: string) => {
|
||||
setOpenMenuId(null);
|
||||
showConfirm(
|
||||
@@ -283,6 +301,28 @@ export default function OrdersScreen() {
|
||||
color: colors.danger,
|
||||
fontSize: fontSize.sm,
|
||||
},
|
||||
addressInput: {
|
||||
backgroundColor: colors.bgPrimary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.sm,
|
||||
color: colors.textWhite,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.m,
|
||||
fontSize: fontSize.md,
|
||||
marginBottom: spacing.m,
|
||||
},
|
||||
addressConfirmBtn: {
|
||||
backgroundColor: colors.accent,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingVertical: spacing.m,
|
||||
alignItems: "center",
|
||||
},
|
||||
addressConfirmText: {
|
||||
color: colors.textWhite,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: "700",
|
||||
},
|
||||
// Modal summary
|
||||
modalSummary: {
|
||||
backgroundColor: colors.bgCard,
|
||||
@@ -396,6 +436,11 @@ export default function OrdersScreen() {
|
||||
icon: "receipt-outline" as keyof typeof Ionicons.glyphMap,
|
||||
onPress: () => openItems(item.id),
|
||||
},
|
||||
{
|
||||
label: "Proposer adresse",
|
||||
icon: "location-outline" as keyof typeof Ionicons.glyphMap,
|
||||
onPress: () => { setOpenMenuId(null); setAddressModal({ visible: true, commandId: item.id, input: "" }); },
|
||||
},
|
||||
{
|
||||
label: "Le client est là",
|
||||
icon: "notifications-outline" as keyof typeof Ionicons.glyphMap,
|
||||
@@ -622,6 +667,29 @@ export default function OrdersScreen() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Modal proposition adresse */}
|
||||
<Modal
|
||||
visible={addressModal.visible}
|
||||
onClose={() => setAddressModal({ visible: false, commandId: null, input: "" })}
|
||||
title={`Proposer adresse — commande #${addressModal.commandId}`}
|
||||
icon="location-outline"
|
||||
>
|
||||
<TextInput
|
||||
style={styles.addressInput}
|
||||
placeholder="Nouvelle adresse..."
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={addressModal.input}
|
||||
onChangeText={(t) => setAddressModal((prev) => ({ ...prev, input: t }))}
|
||||
multiline
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.addressConfirmBtn}
|
||||
onPress={handleProposeAddress}
|
||||
>
|
||||
<Text style={styles.addressConfirmText}>Envoyer la proposition</Text>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
|
||||
<AlertModal
|
||||
visible={alert.visible}
|
||||
type={alert.type}
|
||||
|
||||
Reference in New Issue
Block a user