chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
// ============================================
|
||||
// handlers/delivery_handlers.go
|
||||
// 🔧 VERSION MODIFIÉE avec ETA automatique
|
||||
// ============================================
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 🔧 GetMyDeliveries
|
||||
// ============================================
|
||||
func GetMyDeliveries(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
username, exists := c.Get("username")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
if c.GetString("role") != "livreur" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
|
||||
return
|
||||
}
|
||||
|
||||
usernameStr := username.(string)
|
||||
status := c.Query("status")
|
||||
|
||||
commands, err := database.GetDeliveryPersonCommands(usernameStr, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✨ FILTRAGE (SANS TÉLÉPHONE)
|
||||
filteredCommands := make([]gin.H, len(commands))
|
||||
for i, cmd := range commands {
|
||||
commandID, _ := cmd["id"].(int)
|
||||
items, _ := database.GetCommandItems(commandID)
|
||||
|
||||
// Client info SANS téléphone
|
||||
clientUsername, _ := cmd["username"].(string)
|
||||
client, _ := database.GetClientByUsername(clientUsername)
|
||||
|
||||
clientInfo := gin.H{"nom": "Client", "prenom": ""}
|
||||
if client != nil {
|
||||
clientInfo = gin.H{
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
}
|
||||
}
|
||||
|
||||
itemsSummary := make([]gin.H, len(items))
|
||||
for j, item := range items {
|
||||
itemsSummary[j] = gin.H{
|
||||
"produit": item["produit"],
|
||||
"quantite": item["quantite"],
|
||||
"prix": item["prix"],
|
||||
}
|
||||
}
|
||||
|
||||
etaData, _ := database.GetCommandETA(commandID)
|
||||
|
||||
filteredCommands[i] = gin.H{
|
||||
"id": cmd["id"],
|
||||
"status": cmd["status"],
|
||||
"adresse": cmd["adresse"],
|
||||
"total_prix": cmd["total_prix"],
|
||||
"created_at": cmd["created_at"],
|
||||
"client_info": clientInfo,
|
||||
"items": itemsSummary,
|
||||
"items_count": len(items),
|
||||
"eta": etaData,
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✅ [MY_DELIVERIES] %d livraisons (données filtrées)", len(filteredCommands))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"deliveries": filteredCommands,
|
||||
"count": len(filteredCommands),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GetDeliveryDetails
|
||||
// ============================================
|
||||
func GetDeliveryDetails(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
username := c.GetString("username")
|
||||
if c.GetString("role") != "livreur" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
|
||||
return
|
||||
}
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ VÉRIFIER PROPRIÉTÉ
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
if livreurAssign != username {
|
||||
log.Printf("❌ Accès refusé - cmd assignée à %s", livreurAssign)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Cette livraison ne vous est pas assignée",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
items, _ := database.GetCommandItems(commandID)
|
||||
|
||||
clientUsername, _ := command["username"].(string)
|
||||
client, _ := database.GetClientByUsername(clientUsername)
|
||||
|
||||
clientInfo := gin.H{"nom": "Client", "prenom": ""}
|
||||
if client != nil {
|
||||
clientInfo = gin.H{
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
}
|
||||
}
|
||||
|
||||
itemsSummary := make([]gin.H, len(items))
|
||||
for i, item := range items {
|
||||
itemsSummary[i] = gin.H{
|
||||
"produit": item["produit"],
|
||||
"quantite": item["quantite"],
|
||||
"prix": item["prix"],
|
||||
}
|
||||
}
|
||||
|
||||
etaData, _ := database.GetCommandETA(commandID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"delivery": gin.H{
|
||||
"id": command["id"],
|
||||
"status": command["status"],
|
||||
"adresse": command["adresse"],
|
||||
"total_prix": command["total_prix"],
|
||||
"created_at": command["created_at"],
|
||||
"client_info": clientInfo,
|
||||
"items": itemsSummary,
|
||||
"items_count": len(items),
|
||||
"eta": etaData,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE
|
||||
// ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route"
|
||||
// ============================================
|
||||
func UpdateDeliveryStatus(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
username, exists := c.Get("username")
|
||||
if !exists || c.GetString("role") != "livreur" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
|
||||
return
|
||||
}
|
||||
|
||||
usernameStr := username.(string)
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
Notes string `json:"notes"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📝 [UPD_STATUS] %s update cmd %d: %s", usernameStr, commandID, req.Status)
|
||||
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ VÉRIFIER PROPRIÉTÉ
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
if livreurAssign != usernameStr {
|
||||
log.Printf("❌ Accès refusé - assigné à %s", livreurAssign)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Cette commande ne vous est pas assignée",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
|
||||
validStatuses := []string{
|
||||
"support", // Prise en charge
|
||||
"assigned", // Assigné (si auto-assignation)
|
||||
"en_route", // En route vers le client
|
||||
"arrived", // Arrivé à destination
|
||||
"livre", // Livré (en attente confirmation client)
|
||||
"failed", // Échec de livraison
|
||||
"cancelled", // Annulée
|
||||
}
|
||||
|
||||
isValid := false
|
||||
for _, s := range validStatuses {
|
||||
if req.Status == s {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Statut invalide",
|
||||
"valid_statuses": validStatuses,
|
||||
"received": req.Status,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ VALIDATION GPS pour livraison finale (livre ou failed)
|
||||
if (req.Status == "livre" || req.Status == "failed") &&
|
||||
req.Latitude != 0 && req.Longitude != 0 {
|
||||
|
||||
destLat, _ := command["dest_latitude"].(float64)
|
||||
destLon, _ := command["dest_longitude"].(float64)
|
||||
|
||||
if destLat != 0 && destLon != 0 {
|
||||
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
|
||||
log.Printf("📍 [GPS] Distance: %.2f m", distance)
|
||||
|
||||
if distance > 100 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Vous êtes trop loin de la destination",
|
||||
"required_distance": 100,
|
||||
"current_distance": fmt.Sprintf("%.2f", distance),
|
||||
"unit": "meters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [GPS] Validation OK")
|
||||
} else {
|
||||
log.Printf("⚠️ [GPS] Coordonnées de destination non disponibles, validation ignorée")
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le statut
|
||||
if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ SI PASSAGE EN "EN_ROUTE" → CALCULER ET DÉFINIR L'ETA
|
||||
var etaMinutes int
|
||||
var etaMessage string
|
||||
|
||||
if req.Status == "en_route" {
|
||||
log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...")
|
||||
|
||||
// Récupérer les coordonnées destination
|
||||
var destLat, destLon float64
|
||||
|
||||
// 1. Essayer le cache Redis
|
||||
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
|
||||
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
|
||||
if err == nil && destData != "" {
|
||||
var coords struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(destData), &coords); err == nil && coords.Lat != 0 && coords.Lon != 0 {
|
||||
destLat = coords.Lat
|
||||
destLon = coords.Lon
|
||||
log.Printf("📍 [STATUS_LIVREUR] Coords depuis cache Redis: (%.6f, %.6f)", destLat, destLon)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: récupérer depuis la DB
|
||||
if destLat == 0 || destLon == 0 {
|
||||
if dLat, ok := command["dest_latitude"].(float64); ok && dLat != 0 {
|
||||
destLat = dLat
|
||||
}
|
||||
if dLon, ok := command["dest_longitude"].(float64); ok && dLon != 0 {
|
||||
destLon = dLon
|
||||
}
|
||||
if destLat != 0 && destLon != 0 {
|
||||
log.Printf("📍 [STATUS_LIVREUR] Coords depuis DB: (%.6f, %.6f)", destLat, destLon)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calculer l'ETA depuis la position du livreur
|
||||
if destLat != 0 && destLon != 0 {
|
||||
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
|
||||
|
||||
// Définir l'ETA dans Redis
|
||||
if err := database.SetCommandETA(commandID, etaMinutes); err != nil {
|
||||
log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
|
||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
|
||||
}
|
||||
} else {
|
||||
log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut")
|
||||
etaMinutes = 30 // Fallback
|
||||
database.SetCommandETA(commandID, etaMinutes)
|
||||
etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)"
|
||||
}
|
||||
|
||||
// Mettre à jour le statut du livreur en "delivering"
|
||||
database.SetDeliveryPersonStatus(usernameStr, "delivering", commandID)
|
||||
log.Printf("🚗 [STATUS_LIVREUR] Statut livreur mis à jour: delivering")
|
||||
}
|
||||
|
||||
// Log
|
||||
message := req.Notes
|
||||
if message == "" {
|
||||
message = getDeliveryStatusMessage(req.Status)
|
||||
}
|
||||
if etaMessage != "" {
|
||||
message += fmt.Sprintf(" - %s", etaMessage)
|
||||
}
|
||||
database.AddCommandLog(commandID, req.Status, message, usernameStr)
|
||||
|
||||
// ✅ GESTION SPÉCIALE SELON LE STATUT
|
||||
switch req.Status {
|
||||
case "livre":
|
||||
// Livraison terminée - Optimiser la queue
|
||||
log.Printf("📦 Livraison marquée 'livre' - Optimisation queue...")
|
||||
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
|
||||
|
||||
case "failed":
|
||||
// Échec de livraison - Optimiser la queue
|
||||
log.Printf("❌ Livraison échouée - Optimisation queue...")
|
||||
database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
|
||||
|
||||
// Créer un problème de livraison
|
||||
database.CreateDeliveryIssue(
|
||||
commandID,
|
||||
"delivery_failed",
|
||||
fmt.Sprintf("Échec de livraison: %s", req.Notes),
|
||||
usernameStr,
|
||||
)
|
||||
|
||||
case "arrived":
|
||||
log.Printf("📍 Livreur arrivé à destination - Commande %d", commandID)
|
||||
}
|
||||
|
||||
response := gin.H{
|
||||
"success": true,
|
||||
"message": "Statut mis à jour",
|
||||
"command_id": commandID,
|
||||
"status": req.Status,
|
||||
}
|
||||
|
||||
if req.Status == "en_route" && etaMinutes > 0 {
|
||||
response["eta_minutes"] = etaMinutes
|
||||
response["eta_message"] = etaMessage
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPERS
|
||||
// ============================================
|
||||
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
||||
const earthRadiusKm = 6371
|
||||
const metersPerKm = 1000
|
||||
|
||||
lat1Rad := degreesToRadians(lat1)
|
||||
lon1Rad := degreesToRadians(lon1)
|
||||
lat2Rad := degreesToRadians(lat2)
|
||||
lon2Rad := degreesToRadians(lon2)
|
||||
|
||||
dLat := lat2Rad - lat1Rad
|
||||
dLon := lon2Rad - lon1Rad
|
||||
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
return earthRadiusKm * c * metersPerKm
|
||||
}
|
||||
|
||||
func degreesToRadians(degrees float64) float64 {
|
||||
return degrees * math.Pi / 180
|
||||
}
|
||||
|
||||
func getDeliveryStatusMessage(status string) string {
|
||||
messages := map[string]string{
|
||||
"support": "Prise en charge de la livraison",
|
||||
"assigned": "Commande assignée",
|
||||
"en_route": "En route vers le client",
|
||||
"arrived": "Arrivé à destination",
|
||||
"livre": "Livraison effectuée",
|
||||
"failed": "Échec de livraison",
|
||||
"cancelled": "Livraison annulée",
|
||||
}
|
||||
if msg, ok := messages[status]; ok {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("Statut changé: %s", status)
|
||||
}
|
||||
Reference in New Issue
Block a user