chore: update

This commit is contained in:
2026-03-03 23:42:23 +01:00
parent 52b059fafa
commit 0073f80a48
95 changed files with 2720 additions and 40619 deletions
+2 -2
View File
@@ -432,8 +432,8 @@ func (d *Database) GetBasketItems(username string) ([]map[string]interface{}, er
var items []map[string]interface{}
for rows.Next() {
var productID, quantity int
var price float64
var productID int
var quantity, price float64
if err := rows.Scan(&productID, &quantity, &price); err != nil {
return nil, err
}
+30 -6
View File
@@ -876,7 +876,7 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
type ItemPoints struct {
Category string
Quantite int
Quantite float64
Prix float64
}
@@ -894,8 +894,11 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
items = append(items, item)
// Cumuler par catégorie
if item.Category == "zipette" {
categoryLower := strings.ToLower(item.Category)
if categoryLower == "zipette&co" || categoryLower == "zipette_co" {
totalPrixZipette += item.Prix
} else if categoryLower == "gros&semi" || categoryLower == "gros_semi" {
// gros&semi → 0 points, on ne cumule pas
} else {
// weed_hash ou autres catégories
totalPrixWeedHash += item.Prix
@@ -915,10 +918,31 @@ func (d *Database) CalculateAndAddPointsForCommandTx(tx *sql.Tx, commandID int,
log.Printf("📊 [CalcPointsTx] %d items - weed_hash: %.2f€, zipette: %.2f€",
len(items), totalPrixWeedHash, totalPrixZipette)
// ✅ ÉTAPE 2: Calculer les points par catégorie
// Règle: 1 point par tranche de 10€
pointsWeedHash := int(totalPrixWeedHash / 10.0)
pointsZipette := int(totalPrixZipette / 10.0)
// ✅ ÉTAPE 2: Calculer les points par catégorie avec le bon barème
pointsWeedHash := 0
switch {
case totalPrixWeedHash >= 30 && totalPrixWeedHash <= 50:
pointsWeedHash = 1
case totalPrixWeedHash >= 60 && totalPrixWeedHash <= 150:
pointsWeedHash = 2
case totalPrixWeedHash >= 160 && totalPrixWeedHash <= 300:
pointsWeedHash = 3
case totalPrixWeedHash >= 310 && totalPrixWeedHash <= 400:
pointsWeedHash = 5
case totalPrixWeedHash >= 400:
pointsWeedHash = 10
}
pointsZipette := 0
switch {
case totalPrixZipette >= 30 && totalPrixZipette <= 100:
pointsZipette = 1
case totalPrixZipette >= 110 && totalPrixZipette <= 200:
pointsZipette = 2
case totalPrixZipette >= 210:
pointsZipette = 3
}
totalPoints := pointsWeedHash + pointsZipette
log.Printf("💰 [CalcPointsTx] Points calculés - weed_hash: %d, zipette: %d, total: %d",
+7 -4
View File
@@ -31,7 +31,7 @@ func validateItemID(itemID int) error {
}
return nil
}
func validateQuantite(quantite int) error {
func validateQuantite(quantite float64) error {
if quantite <= 0 {
return fmt.Errorf("quantité doit être > 0")
}
@@ -96,7 +96,8 @@ func validateItemStatus(status string) error {
func (d *Database) InsertCommandItemWithClientInfo(
commandID int,
produit string,
productID, quantite int,
productID int,
quantite float64,
prix float64,
clientUsername, clientNom, clientPrenom, clientTelephone, deliveryAddress string,
) error {
@@ -231,7 +232,8 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
var items []map[string]interface{}
for rows.Next() {
var id, commandID, quantite int
var id, commandID int
var quantite float64
var productID sql.NullInt64 // ✅ FIX: Utiliser NullInt64 pour gérer NULL
var produit, clientUsername, clientNom, clientPrenom, clientTelephone string
var deliveryAddress, status sql.NullString
@@ -345,7 +347,8 @@ func (d *Database) GetCommandItemsByUsername(username string) ([]map[string]inte
var items []map[string]interface{}
for rows.Next() {
var id, commandID, quantite int
var id, commandID int
var quantite float64
var productID sql.NullInt64 // ✅ FIX: NullInt64
var produit, clientUsername, clientNom, clientPrenom, clientTelephone string
var deliveryAddress, status sql.NullString
+2 -2
View File
@@ -82,7 +82,7 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
type BasketItem struct {
ProductID int
Quantity int
Quantity float64
Price float64
}
@@ -190,7 +190,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
type BasketItem struct {
ProductID int
Quantity int
Quantity float64
Price float64
}
+46 -1
View File
@@ -76,11 +76,55 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur migration must_change_password: %v", err)
}
// Migration: ajouter colonne push_token pour les notifications push
// Migration: ajouter colonne push_token pour les notifications push (clients)
if _, err = database.Exec(`ALTER TABLE clients ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil {
log.Fatalf("❌ Erreur migration push_token: %v", err)
}
// Migration: ajouter colonne push_token pour les notifications push (livreurs/users)
if _, err = database.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS push_token TEXT`); err != nil {
log.Fatalf("❌ Erreur migration push_token users: %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)
}
// Migration: baskets.quantity INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires (ex: 0.5g)
if _, err = database.Exec(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'baskets' AND column_name = 'quantity'
AND data_type = 'integer'
) THEN
ALTER TABLE baskets ALTER COLUMN quantity TYPE NUMERIC(10,3) USING quantity::NUMERIC(10,3);
END IF;
END
$$;
`); err != nil {
log.Fatalf("❌ Erreur migration baskets.quantity: %v", err)
}
// Migration: command_items.quantite INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires
if _, err = database.Exec(`
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'command_items' AND column_name = 'quantite'
AND data_type = 'integer'
) THEN
ALTER TABLE command_items ALTER COLUMN quantite TYPE NUMERIC(10,3) USING quantite::NUMERIC(10,3);
END IF;
END
$$;
`); err != nil {
log.Fatalf("❌ Erreur migration command_items.quantite: %v", err)
}
// Lancer le nettoyage périodique des tokens expirés
go database.cleanExpiredTokensPeriodically()
@@ -151,6 +195,7 @@ func (db *Database) createTables() error {
name VARCHAR(255) NOT NULL,
category VARCHAR(100) NOT NULL,
stock DECIMAL(10,2) NOT NULL DEFAULT 0,
unit VARCHAR(10) NOT NULL DEFAULT 'u',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+59 -2
View File
@@ -2,6 +2,7 @@ package db
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"log"
@@ -36,11 +37,15 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
}
func sendExpoPush(token, title, body string, commandID int, notifType string) {
sendExpoPushWithChannel(token, title, body, commandID, notifType, "orders")
}
func sendExpoPushWithChannel(token, title, body string, commandID int, notifType, channelID string) {
payload := map[string]interface{}{
"to": token,
"title": title,
"body": body,
"channelId": "orders",
"channelId": channelID,
"data": map[string]interface{}{
"command_id": commandID,
"type": notifType,
@@ -71,7 +76,59 @@ func sendExpoPush(token, title, body string, commandID int, notifType string) {
}
defer resp.Body.Close()
log.Printf("✅ [EXPO_PUSH] Push envoyé à %s (status: %d)", token, resp.StatusCode)
log.Printf("✅ [EXPO_PUSH] Push envoyé à %s (channel: %s, status: %d)", token, channelID, resp.StatusCode)
}
// ============================================
// PUSH TOKEN - LIVREURS (table users)
// ============================================
// SaveUserPushToken sauvegarde le token push d'un livreur dans la table users
func (d *Database) SaveUserPushToken(username, token string) error {
_, err := d.Exec(`UPDATE users SET push_token = $1 WHERE username = $2`, token, username)
return err
}
// GetUserPushToken récupère le token push d'un livreur depuis la table users
func (d *Database) GetUserPushToken(username string) (string, error) {
var token sql.NullString
err := d.QueryRow(`SELECT push_token FROM users WHERE username = $1`, username).Scan(&token)
if err != nil || !token.Valid {
return "", err
}
return token.String, nil
}
// DeleteUserPushToken supprime le token push d'un livreur
func (d *Database) DeleteUserPushToken(username string) error {
_, err := d.Exec(`UPDATE users SET push_token = NULL WHERE username = $1`, username)
return err
}
// NotifyLivreur envoie une notification in-app (Redis) + push Expo à un livreur
func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error {
notifKey := fmt.Sprintf("notifications:%s", username)
notification := map[string]interface{}{
"command_id": commandID,
"type": notifType,
"message": message,
"created_at": time.Now().Format(time.RFC3339),
"read": false,
}
notifJSON, _ := json.Marshal(notification)
Redis.LPush(RedisCtx, notifKey, notifJSON)
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
// Push notification si le livreur a un token enregistré
pushToken, err := d.GetUserPushToken(username)
if err == nil && pushToken != "" {
go sendExpoPushWithChannel(pushToken, "Nouvelle commande assignée", message, commandID, notifType, "deliveries")
}
log.Printf("📬 [LIVREUR_NOTIF] Notification envoyée à %s: %s", username, message)
return nil
}
// AddDeliveryRating ajoute une note pour un livreur
+12 -13
View File
@@ -17,7 +17,8 @@ func (db *Database) CreateProduct(product interface{}) error {
GetName() string
GetCategory() string
GetDescription() string
GetStock() float64 // ← ajouter méthode pour le stock
GetStock() float64
GetUnit() string
GetPrices() []models.ProductPrice
SetID(int)
SetCreatedAt(time.Time)
@@ -45,15 +46,15 @@ func (db *Database) CreateProduct(product interface{}) error {
log.Printf("📦 [DB CreateProduct] Nombre de prix: %d", len(p.GetPrices()))
}
// Insérer le produit avec le stock
query := `INSERT INTO products (name, category, description, stock, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at, updated_at`
// Insérer le produit avec le stock et l'unité
query := `INSERT INTO products (name, category, description, stock, unit, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at`
now := time.Now()
var productID int
var createdAt, updatedAt time.Time
err := db.QueryRow(query, p.GetName(), p.GetCategory(), p.GetDescription(), p.GetStock(), now, now).
err := db.QueryRow(query, p.GetName(), p.GetCategory(), p.GetDescription(), p.GetStock(), p.GetUnit(), now, now).
Scan(&productID, &createdAt, &updatedAt)
if err != nil {
log.Printf("❌ [DB CreateProduct] Erreur INSERT: %v", err)
@@ -93,10 +94,10 @@ func (d *Database) GetProductByID(id int) (models.Product, error) {
// ✅ AJOUTER stock dans le SELECT
err := d.QueryRow(`
SELECT id, name, category, description, stock, created_at, updated_at
SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products
WHERE id=$1
`, id).Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.CreatedAt, &p.UpdatedAt)
`, id).Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt)
if err != nil {
log.Printf("❌ [GetProductByID] Erreur query: %v", err)
@@ -122,7 +123,7 @@ func (d *Database) GetAllProducts() ([]models.Product, error) {
log.Println("📦 [GetAllProducts] START")
rows, err := d.Query(`
SELECT id, name, category, description, stock, created_at, updated_at
SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products
ORDER BY id ASC
`)
@@ -135,7 +136,7 @@ func (d *Database) GetAllProducts() ([]models.Product, error) {
var products []models.Product
for rows.Next() {
var p models.Product
if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.CreatedAt, &p.UpdatedAt); err != nil {
if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt); err != nil {
log.Printf("❌ [GetAllProducts] Erreur scan: %v", err)
return nil, err
}
@@ -173,9 +174,8 @@ func (d *Database) GetAllProducts() ([]models.Product, error) {
func (db *Database) GetProductsByCategory(category string) ([]models.Product, error) {
log.Printf("📦 [GetProductsByCategory] START - Category=%s", category)
// ✅ AJOUTER stock dans le SELECT
rows, err := db.Query(`
SELECT id, name, category, description, stock, created_at, updated_at
SELECT id, name, category, description, stock, unit, created_at, updated_at
FROM products
WHERE category = $1
ORDER BY created_at DESC
@@ -190,8 +190,7 @@ func (db *Database) GetProductsByCategory(category string) ([]models.Product, er
var products []models.Product
for rows.Next() {
var p models.Product
// ✅ AJOUTER &p.Stock dans le Scan
if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.CreatedAt, &p.UpdatedAt); err != nil {
if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Description, &p.Stock, &p.Unit, &p.CreatedAt, &p.UpdatedAt); err != nil {
log.Printf("❌ [GetProductsByCategory] Erreur scan: %v", err)
return nil, fmt.Errorf("erreur lors du scan d'un produit: %w", err)
}
+6
View File
@@ -668,6 +668,12 @@ func ForceValidateDelivery(c *gin.Context) {
clientUsername, _ := command["username"].(string)
livreurAssign, _ := command["livreur_assign"].(string)
// Notifier le client
if clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
}
if err := database.IncrementClientCommandCount(clientUsername); err != nil {
log.Printf("⚠️ Erreur compteur commandes: %v", err)
}
+25
View File
@@ -359,6 +359,31 @@ func UpdateDeliveryStatus(c *gin.Context) {
}
database.AddCommandLog(commandID, req.Status, message, usernameStr)
// ✅ NOTIFICATION CLIENT
clientUsername, _ := command["username"].(string)
if clientUsername != "" {
var clientMsg string
switch req.Status {
case "support":
clientMsg = fmt.Sprintf("Votre commande #%d est prise en charge", commandID)
case "en_route":
if etaMinutes > 0 {
clientMsg = fmt.Sprintf("Votre commande #%d est en route ! Arrivée dans ~%d min", commandID, etaMinutes)
} else {
clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID)
}
case "arrived":
clientMsg = fmt.Sprintf("Votre livreur est arrivé pour la commande #%d", commandID)
case "livre":
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
case "failed":
clientMsg = fmt.Sprintf("Échec de livraison pour la commande #%d", commandID)
}
if clientMsg != "" {
database.NotifyClient(clientUsername, commandID, req.Status, clientMsg)
}
}
// ✅ GESTION SPÉCIALE SELON LE STATUT
switch req.Status {
case "livre":
+138
View File
@@ -107,6 +107,144 @@ func GetClientNotifications(c *gin.Context) {
})
}
// RegisterLivreurPushToken enregistre le push token Expo d'un livreur
// POST /api/v1/livreur/push-token
func RegisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
var req struct {
PushToken string `json:"push_token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur sauvegarde token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token enregistré pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// UnregisterLivreurPushToken supprime le push token d'un livreur (au logout)
// DELETE /api/v1/livreur/push-token
func UnregisterLivreurPushToken(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
database := c.MustGet("database").(*db.Database)
if err := database.DeleteUserPushToken(username); err != nil {
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur suppression token pour %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
return
}
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token supprimé pour livreur %s", username)
c.JSON(http.StatusOK, gin.H{"success": true})
}
// GetLivreurNotifications retourne les notifications du livreur connecté
// GET /api/v1/livreur/notifications
func GetLivreurNotifications(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
notifKey := "notifications:" + username
results, err := db.Redis.LRange(db.RedisCtx, notifKey, 0, 49).Result()
if err != nil {
log.Printf("❌ [LIVREUR_NOTIFICATIONS] Erreur Redis: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération notifications"})
return
}
type Notification struct {
CommandID int `json:"command_id"`
Type string `json:"type"`
Message string `json:"message"`
CreatedAt string `json:"created_at"`
Read bool `json:"read"`
}
notifications := make([]Notification, 0, len(results))
unreadCount := 0
for _, raw := range results {
var n Notification
if err := json.Unmarshal([]byte(raw), &n); err != nil {
continue
}
notifications = append(notifications, n)
if !n.Read {
unreadCount++
}
}
log.Printf("✅ [LIVREUR_NOTIFICATIONS] %d notifications pour %s (%d non lues)", len(notifications), username, unreadCount)
c.JSON(http.StatusOK, gin.H{
"notifications": notifications,
"unread_count": unreadCount,
"total": len(notifications),
})
}
// MarkLivreurNotificationsRead marque toutes les notifications du livreur comme lues
// POST /api/v1/livreur/notifications/read
func MarkLivreurNotificationsRead(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
return
}
notifKey := "notifications:" + username
results, err := db.Redis.LRange(db.RedisCtx, notifKey, 0, -1).Result()
if err != nil {
log.Printf("❌ [LIVREUR_MARK_READ] Erreur Redis LRange: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lecture notifications"})
return
}
markedCount := 0
for i, raw := range results {
var n map[string]interface{}
if err := json.Unmarshal([]byte(raw), &n); err != nil {
continue
}
if read, ok := n["read"].(bool); ok && read {
continue
}
n["read"] = true
updated, _ := json.Marshal(n)
db.Redis.LSet(db.RedisCtx, notifKey, int64(i), string(updated))
markedCount++
}
log.Printf("✅ [LIVREUR_MARK_READ] %d notifications marquées lues pour %s", markedCount, username)
c.JSON(http.StatusOK, gin.H{
"success": true,
"marked_count": markedCount,
})
}
// MarkNotificationsRead marque toutes les notifications comme lues
// POST /api/v1/notifications/read
func MarkNotificationsRead(c *gin.Context) {
+50
View File
@@ -5,6 +5,7 @@
package handlers
import (
"fmt"
"gestion/db"
"gestion/models"
"gestion/services"
@@ -352,6 +353,45 @@ func ValidateBasket(c *gin.Context) {
log.Printf("🛒 [CHECKOUT] Panier: %d articles", len(items))
// ============================================
// 1️⃣b Vérifier le minimum de commande selon la zone
// ============================================
var cartTotal float64
for _, item := range items {
if price, ok := item["price"].(float64); ok {
cartTotal += price
}
}
zoneResult := checkDeliveryZone(req.DeliveryAddress, cartTotal)
if !zoneResult.OK {
if zoneResult.ZoneName == "inconnue" {
log.Printf("❌ [CHECKOUT] Aucun code postal trouvé dans l'adresse: %s", req.DeliveryAddress)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Adresse invalide : aucun code postal détecté",
})
} else if zoneResult.ZoneName == "hors zone" {
log.Printf("❌ [CHECKOUT] Code postal %s hors zone de livraison", zoneResult.PostalCode)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Livraison non disponible pour ce code postal",
"postal_code": zoneResult.PostalCode,
})
} else {
log.Printf("❌ [CHECKOUT] Total %.2f€ insuffisant pour %s (minimum %.2f€)", cartTotal, zoneResult.ZoneName, zoneResult.MinAmount)
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Montant minimum de commande non atteint pour votre zone (%.0f€ minimum)", zoneResult.MinAmount),
"zone": zoneResult.ZoneName,
"minimum": zoneResult.MinAmount,
"cart_total": cartTotal,
"missing": zoneResult.MinAmount - cartTotal,
"postal_code": zoneResult.PostalCode,
})
}
return
}
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount)
// ============================================
// 2️⃣ Créer la commande (qui décrémente automatiquement le stock)
// ============================================
@@ -439,6 +479,16 @@ func ValidateBasket(c *gin.Context) {
log.Printf("⚠️ [CHECKOUT] Erreur mise à jour statut livreur: %v", err)
}
// Notifier le livreur de la nouvelle commande
notifMsg := fmt.Sprintf("Nouvelle commande #%d assignée - Livraison dans ~%d min (%.2f km)", commandID, travelTime, distance)
if notifErr := database.NotifyLivreur(nearest.Username, commandID, "new_assignment", notifMsg); notifErr != nil {
log.Printf("⚠️ [CHECKOUT] Erreur notification livreur: %v", notifErr)
}
// Notifier le client
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Livraison prévue dans ~%d min", commandID, travelTime)
database.NotifyClient(usernameStr, commandID, "assigned", clientMsg)
assigned = true
assignInfo = gin.H{
"username": nearest.Username,
+38 -2
View File
@@ -114,6 +114,22 @@ func validatePrice(quantity float64, price float64) error {
return nil
}
func validateUnit(unit string) error {
validUnits := map[string]bool{
"u": true, // unité
"kg": true, // kilogramme
"g": true, // gramme
"bag": true, // sac
"l": true, // litre
"cl": true, // centilitre
"pcs": true, // pièces
}
if !validUnits[unit] {
return fmt.Errorf("unité invalide : valeurs acceptées : u, kg, g, bag, l, cl, pcs")
}
return nil
}
func validateCategory(category string) error {
// Nettoyage
category = strings.ToLower(strings.TrimSpace(category))
@@ -205,6 +221,10 @@ func CreateProduct(c *gin.Context) {
category := strings.TrimSpace(c.PostForm("category"))
description := strings.TrimSpace(c.PostForm("description"))
stockStr := c.PostForm("stock")
unit := strings.ToLower(strings.TrimSpace(c.PostForm("unit")))
if unit == "" {
unit = "u"
}
// ✅ VALIDATION STRICTE
if err := validateProductName(name); err != nil {
@@ -231,6 +251,11 @@ func CreateProduct(c *gin.Context) {
return
}
if err := validateUnit(unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// ✅ VALIDER LE STOCK
stock, err := strconv.ParseFloat(stockStr, 64)
if err != nil {
@@ -296,6 +321,7 @@ func CreateProduct(c *gin.Context) {
Category: category,
Description: description,
Stock: stock,
Unit: unit,
Prices: prices,
}
@@ -581,6 +607,7 @@ func UpdateProduct(c *gin.Context) {
Category string `json:"category"`
Description string `json:"description"`
Stock float64 `json:"stock"`
Unit string `json:"unit"`
Prices []models.ProductPrice `json:"prices"`
}
@@ -605,6 +632,14 @@ func UpdateProduct(c *gin.Context) {
return
}
if updateData.Unit == "" {
updateData.Unit = "u"
}
if err := validateUnit(updateData.Unit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateStock(updateData.Stock); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -627,8 +662,8 @@ func UpdateProduct(c *gin.Context) {
// ✅ UPDATE PRODUIT
updateQuery := `
UPDATE products
SET name = $1, category = $2, description = $3, stock = $4, updated_at = $5
WHERE id = $6
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6
WHERE id = $7
`
_, err = database.Exec(updateQuery,
@@ -636,6 +671,7 @@ func UpdateProduct(c *gin.Context) {
updateData.Category,
updateData.Description,
updateData.Stock,
updateData.Unit,
time.Now(),
id,
)
+124
View File
@@ -0,0 +1,124 @@
package handlers
import "regexp"
// ============================================================
// Zones de livraison — minimum de commande par code postal
// ============================================================
// Remplis les listes de codes postaux quand tu les as.
// Un code postal absent de toutes les zones → commande refusée.
// ============================================================
type deliveryZone struct {
Name string
MinAmount float64
codes map[string]struct{}
}
var deliveryZones = []deliveryZone{
{
Name: "Zone 30€",
MinAmount: 30.0,
codes: postalSet([]string{
"44000",
"44100",
"44200",
"44300",
}),
},
{
Name: "Zone 50€",
MinAmount: 50.0,
codes: postalSet([]string{
"44400", // Rezé
"44880", // Les Sorinières / Sautron
"44120", // Vertou
"44230", // Saint-Sébastien-sur-Loire
"44115", // Basse-Goulaine / Haute-Goulaine
"44980", // Sainte-Luce-sur-Loire
"44470", // Carquefou
"44240", // La Chapelle-sur-Erdre
"44700", // Orvault
"44800", // Saint-Herblain
"44340", // Bouguenais
"44620", // La Montagne
"44830", // Bouaye
}),
},
{
Name: "Zone 100€",
MinAmount: 100.0,
codes: postalSet([]string{
"44860", // Pont-Saint-Martin / Saint-Aignan-Grandlieu
"44220", // Couëron
"44118", // La Chevrolière
"44830", // Brains
"44710", // Saint-Léger-les-Vignes
"44690", // La Haie-Fouassière
"44470", // Mauves-sur-Loire
"44240", // Sucé-sur-Erdre
"44119", // Grandchamp-des-Fontaines
}),
},
}
var postalCodeRe = regexp.MustCompile(`\b(\d{5})\b`)
// postalSet convertit une slice de codes en set pour lookup O(1).
func postalSet(codes []string) map[string]struct{} {
m := make(map[string]struct{}, len(codes))
for _, c := range codes {
m[c] = struct{}{}
}
return m
}
// extractPostalCode extrait le premier code postal à 5 chiffres d'une adresse.
func extractPostalCode(address string) string {
m := postalCodeRe.FindStringSubmatch(address)
if len(m) < 2 {
return ""
}
return m[1]
}
// zoneCheckResult est le résultat de la vérification de zone.
type zoneCheckResult struct {
PostalCode string
ZoneName string
MinAmount float64
OK bool
}
// checkDeliveryZone vérifie si le total respecte le minimum de la zone de l'adresse.
// Code postal introuvable → OK = false (refus).
// Code postal hors de toutes les zones → OK = false (refus).
func checkDeliveryZone(deliveryAddress string, total float64) zoneCheckResult {
code := extractPostalCode(deliveryAddress)
if code == "" {
return zoneCheckResult{
PostalCode: "",
ZoneName: "inconnue",
MinAmount: 0,
OK: false,
}
}
for _, zone := range deliveryZones {
if _, found := zone.codes[code]; found {
return zoneCheckResult{
PostalCode: code,
ZoneName: zone.Name,
MinAmount: zone.MinAmount,
OK: total >= zone.MinAmount,
}
}
}
return zoneCheckResult{
PostalCode: code,
ZoneName: "hors zone",
MinAmount: 0,
OK: false,
}
}
+3 -3
View File
@@ -96,9 +96,9 @@ func main() {
// Configuration CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://uber-stup.club", "http://localhost:5173"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
AllowOrigins: []string{"https://uber-stup.club", "https://mln-uber.club", "http://localhost:5173"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
@@ -9,7 +9,11 @@ import (
)
func OrderHoursMiddleware(c *gin.Context) {
now := time.Now()
loc, err := time.LoadLocation("Europe/Paris")
if err != nil {
loc = time.UTC
}
now := time.Now().In(loc)
hour := now.Hour()
min := now.Minute()
+3 -1
View File
@@ -8,6 +8,7 @@ type Product struct {
Category string `json:"category" binding:"required"`
Description string `json:"description"`
Stock float64 `json:"stock"` // ← ajouter le stock ici
Unit string `json:"unit"` // kg | g | bag | l | cl | pcs | u
Prices []ProductPrice `json:"prices"`
Media []Media `json:"media,omitempty"`
CreatedAt time.Time `json:"created_at"`
@@ -38,7 +39,8 @@ type StockInfo struct {
func (p *Product) GetName() string { return p.Name }
func (p *Product) GetCategory() string { return p.Category }
func (p *Product) GetDescription() string { return p.Description }
func (p *Product) GetStock() float64 { return p.Stock } // ← méthode pour le stock
func (p *Product) GetStock() float64 { return p.Stock }
func (p *Product) GetUnit() string { return p.Unit }
func (p *Product) GetPrices() []ProductPrice { return p.Prices }
func (p *Product) SetID(id int) { p.ID = id }
func (p *Product) SetCreatedAt(t time.Time) { p.CreatedAt = t }
+9 -1
View File
@@ -157,7 +157,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
adminGroupV2.POST("/products", handlers.CreateProduct)
adminGroupV2.GET("/products", handlers.GetAllProducts)
adminGroupV2.GET("/products/:id", handlers.GetProductByID)
adminGroupV2.PUT("/products/update/:id", handlers.UpdateProduct)
adminGroupV2.PUT("/products/:id", handlers.UpdateProduct)
adminGroupV2.DELETE("/products/:id", handlers.DeleteProduct)
adminGroupV2.POST("/products/:id/media", handlers.UploadMedia)
adminGroupV2.DELETE("/products/:id/media/:media_id", handlers.DeleteMedia)
@@ -292,6 +292,14 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
livreurGroupV1.DELETE("/alert/:id", handlers.EndAlert) // Mettre fin à une alerte
livreurGroupV1.GET("/alerts", handlers.GetMyAlerts) // Voir mes alertes
livreurGroupV1.GET("/alert/:id", handlers.GetAlert) // Détail d'une alerte
// ============================================
// NOTIFICATIONS LIVREUR
// ============================================
livreurGroupV1.GET("/notifications", handlers.GetLivreurNotifications)
livreurGroupV1.POST("/notifications/read", handlers.MarkLivreurNotificationsRead)
livreurGroupV1.POST("/push-token", handlers.RegisterLivreurPushToken)
livreurGroupV1.DELETE("/push-token", handlers.UnregisterLivreurPushToken)
}
}
@@ -182,6 +182,20 @@ func tryAssignCommandWithPriority(
database.AddCommandLog(commandID, "assigned", logMessage, "system-cron")
// 8. Notifier le livreur de la nouvelle commande
notifMsg := fmt.Sprintf("Nouvelle commande #%d assignée - Livraison dans ~%d min (%.2f km)", commandID, travelTime, distance)
if notifErr := database.NotifyLivreur(nearest.Username, commandID, "new_assignment", notifMsg); notifErr != nil {
log.Printf("⚠️ [CRON] Erreur notification livreur %s: %v", nearest.Username, notifErr)
}
// 9. Notifier le client
if cmd, err := database.GetCommandByID(commandID); err == nil {
if clientUsername, ok := cmd["username"].(string); ok && clientUsername != "" {
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Livraison prévue dans ~%d min", commandID, travelTime)
database.NotifyClient(clientUsername, commandID, "assigned", clientMsg)
}
}
log.Printf("✅ [CRON] Cmd %d → %s (%.2f km) | Priorité #%d | Attente: %d min",
commandID, nearest.Username, distance, priority, waitingMinutes)