chore: refacto
This commit is contained in:
@@ -65,10 +65,14 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
|
||||
return fmt.Errorf("confirmation requise")
|
||||
}
|
||||
|
||||
cancelMsg := reason
|
||||
if cancelMsg == "Annulation par le client" {
|
||||
cancelMsg = ""
|
||||
}
|
||||
result := tx.Exec(`
|
||||
UPDATE commandes
|
||||
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND status = ? AND username = ?`, commandID, cmdResult.Status, username)
|
||||
SET status = 'cancelled', cancel_reason = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND status = ? AND username = ?`, cancelMsg, commandID, cmdResult.Status, username)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
@@ -87,13 +91,20 @@ func (d *Database) CancelCommandAtomic(commandID int, username, reason string, f
|
||||
log.Printf("✅ [CancelAtomic] Stock remboursé")
|
||||
}
|
||||
|
||||
if err := tx.Exec(`
|
||||
UPDATE clients
|
||||
SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, username).Error; err != nil {
|
||||
log.Printf("⚠️ [CancelAtomic] Erreur incrémentation count: %v", err)
|
||||
}
|
||||
|
||||
if isLateCancel {
|
||||
log.Printf("⚠️ [CancelAtomic] Annulation tardive confirmée - Application pénalité")
|
||||
penalty, _ = d.CalculateCancellationPenalty(username)
|
||||
if err := tx.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = amende + ?,
|
||||
cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, penalty, username).Error; err != nil {
|
||||
log.Printf("❌ [CancelAtomic] Erreur pénalité: %v", err)
|
||||
@@ -250,7 +261,7 @@ func (d *Database) GetCommandPositionInQueue(livreurUsername string, commandID i
|
||||
|
||||
func (d *Database) GetCancelledCommands(username string, limit int) ([]map[string]any, error) {
|
||||
query := `
|
||||
SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as total_prix, created_at, updated_at
|
||||
SELECT id, client_order_id AS client_order_number, username, status, adresse, total_prix::float8 as total_prix, created_at, updated_at, COALESCE(cancel_reason, '') AS cancel_reason
|
||||
FROM commandes
|
||||
WHERE status = 'cancelled'`
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ func (d *Database) UpdateCategory(id int, name, color string, isComingSoon bool)
|
||||
if err := d.GDB.First(&c, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.GDB.Model(&c).Updates(Category{Name: name, Color: color, IsComingSoon: isComingSoon}).Error; err != nil {
|
||||
if err := d.GDB.Model(&c).Updates(map[string]interface{}{"name": name, "color": color, "is_coming_soon": isComingSoon}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
|
||||
@@ -79,20 +79,22 @@ func (d *Database) GetClientByID(id int) (*models.Client, error) {
|
||||
// GetAllClients récupère tous les clients
|
||||
func (d *Database) GetAllClients() ([]*models.Client, error) {
|
||||
var rows []struct {
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Password string `gorm:"column:password"`
|
||||
Nom string `gorm:"column:nom"`
|
||||
Prenom string `gorm:"column:prenom"`
|
||||
Telephone string `gorm:"column:telephone"`
|
||||
Command int `gorm:"column:command"`
|
||||
Amende float64 `gorm:"column:amende"`
|
||||
ReferralBalance float64 `gorm:"column:referral_balance"`
|
||||
PointsExtraJSON []byte `gorm:"column:points_extra"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Password string `gorm:"column:password"`
|
||||
Nom string `gorm:"column:nom"`
|
||||
Prenom string `gorm:"column:prenom"`
|
||||
Telephone string `gorm:"column:telephone"`
|
||||
Command int `gorm:"column:command"`
|
||||
Amende float64 `gorm:"column:amende"`
|
||||
ReferralBalance float64 `gorm:"column:referral_balance"`
|
||||
CancellationsCount int `gorm:"column:cancellations_count"`
|
||||
PointsExtraJSON []byte `gorm:"column:points_extra"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
err := d.GDB.Raw(`
|
||||
SELECT id, username, password, nom, prenom, telephone, command, amende, referral_balance,
|
||||
COALESCE(cancellations_count, 0) as cancellations_count,
|
||||
COALESCE(points_extra, '{}'::jsonb) as points_extra, created_at
|
||||
FROM clients ORDER BY created_at DESC`).Scan(&rows).Error
|
||||
if err != nil {
|
||||
@@ -102,16 +104,17 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
|
||||
clients := make([]*models.Client, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
client := &models.Client{
|
||||
ID: row.ID,
|
||||
Username: row.Username,
|
||||
Password: row.Password,
|
||||
Nom: row.Nom,
|
||||
Prenom: row.Prenom,
|
||||
Telephone: row.Telephone,
|
||||
Command: row.Command,
|
||||
Amende: row.Amende,
|
||||
ReferralBalance: row.ReferralBalance,
|
||||
CreatedAt: row.CreatedAt,
|
||||
ID: row.ID,
|
||||
Username: row.Username,
|
||||
Password: row.Password,
|
||||
Nom: row.Nom,
|
||||
Prenom: row.Prenom,
|
||||
Telephone: row.Telephone,
|
||||
Command: row.Command,
|
||||
Amende: row.Amende,
|
||||
ReferralBalance: row.ReferralBalance,
|
||||
CancellationsCount: row.CancellationsCount,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
client.PointsExtra = map[string]int{}
|
||||
if len(row.PointsExtraJSON) > 0 {
|
||||
@@ -125,14 +128,15 @@ func (d *Database) GetAllClients() ([]*models.Client, error) {
|
||||
|
||||
// UpdateClient met à jour un client existant
|
||||
func (d *Database) UpdateClient(client *models.Client) error {
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients
|
||||
SET username = ?, password = ?, nom = ?, prenom = ?, telephone = ?,
|
||||
command = ?, amende = ?
|
||||
WHERE id = ?`,
|
||||
client.Username, client.Password, client.Nom, client.Prenom, client.Telephone,
|
||||
client.Command, client.Amende, client.ID,
|
||||
)
|
||||
result := d.GDB.Model(&models.Client{}).Where("id = ?", client.ID).Updates(map[string]any{
|
||||
"username": client.Username,
|
||||
"password": client.Password,
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"command": client.Command,
|
||||
"amende": client.Amende,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la mise à jour du client: %w", result.Error)
|
||||
}
|
||||
@@ -147,7 +151,7 @@ func (d *Database) UpdateClient(client *models.Client) error {
|
||||
func (d *Database) DeleteClient(id int) error {
|
||||
_ = d.RevokeAllUserTokens(id, "client")
|
||||
|
||||
result := d.GDB.Exec(`DELETE FROM clients WHERE id = ?`, id)
|
||||
result := d.GDB.Delete(&models.Client{}, id)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la suppression du client: %w", result.Error)
|
||||
}
|
||||
@@ -160,7 +164,7 @@ func (d *Database) DeleteClient(id int) error {
|
||||
|
||||
// UpdateClientPassword met à jour le mot de passe d'un client
|
||||
func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) error {
|
||||
result := d.GDB.Exec(`UPDATE clients SET password = ? WHERE id = ?`, hashedPassword, clientID)
|
||||
result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Update("password", hashedPassword)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
|
||||
}
|
||||
@@ -173,9 +177,10 @@ func (d *Database) UpdateClientPassword(clientID int, hashedPassword string) err
|
||||
|
||||
// UpdateClientPasswordAndClearFlag met à jour le mot de passe et remet must_change_password à false
|
||||
func (d *Database) UpdateClientPasswordAndClearFlag(clientID int, hashedPassword string) error {
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients SET password = ?, must_change_password = FALSE, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`, hashedPassword, clientID)
|
||||
result := d.GDB.Model(&models.Client{}).Where("id = ?", clientID).Updates(map[string]any{
|
||||
"password": hashedPassword,
|
||||
"must_change_password": false,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la mise à jour du mot de passe: %w", result.Error)
|
||||
}
|
||||
@@ -254,9 +259,7 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
|
||||
return fmt.Errorf("montant insuffisant: %.2f payé, %.2f requis", amountPaid, currentAmount)
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients SET amende = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, username)
|
||||
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", 0.0)
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ [PayClientPenalties] Erreur UPDATE: %v", result.Error)
|
||||
return fmt.Errorf("erreur paiement pénalités: %w", result.Error)
|
||||
@@ -273,7 +276,7 @@ func (d *Database) PayClientPenalties(username string, amountPaid float64) error
|
||||
|
||||
// IncrementClientCommandCount incrémente le compteur de commandes du client
|
||||
func (d *Database) IncrementClientCommandCount(username string) error {
|
||||
result := d.GDB.Exec(`UPDATE clients SET command = command + 1 WHERE username = ?`, username)
|
||||
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).UpdateColumn("command", gorm.Expr("command + 1"))
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de l'incrémentation du compteur: %w", result.Error)
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
CommandCreatedAt *time.Time `gorm:"column:command_created_at"`
|
||||
Category string `gorm:"column:category"`
|
||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||
}
|
||||
|
||||
err := d.GDB.Raw(`
|
||||
@@ -236,7 +237,8 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
|
||||
c.referral_used,
|
||||
c.livreur_assign,
|
||||
c.created_at as command_created_at,
|
||||
p.category
|
||||
p.category,
|
||||
c.client_order_id as client_order_number
|
||||
FROM command_items ci
|
||||
LEFT JOIN commandes c ON ci.command_id = c.id
|
||||
LEFT JOIN products p ON ci.product_id = p.id
|
||||
@@ -280,8 +282,9 @@ func (d *Database) GetCommandItems(commandID int) ([]map[string]interface{}, err
|
||||
"total_prix": row.TotalPrix,
|
||||
"referral_used": row.ReferralUsed,
|
||||
"livreur_assign": ptrStr(row.LivreurAssign),
|
||||
"command_created_at": commandCreatedAt,
|
||||
"category": row.Category,
|
||||
"command_created_at": commandCreatedAt,
|
||||
"category": row.Category,
|
||||
"client_order_number": row.ClientOrderNumber,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
+110
-106
@@ -52,10 +52,9 @@ type basketItem struct {
|
||||
|
||||
func (d *Database) fetchBasketItems(username string) ([]basketItem, float64, error) {
|
||||
var items []basketItem
|
||||
if err := d.GDB.Raw(`SELECT product_id, quantity, price FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil {
|
||||
if err := d.GDB.Table("baskets").Select("product_id, quantity, price").Where("username = ?", username).Scan(&items).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("erreur récupération panier: %w", err)
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, item := range items {
|
||||
total += item.Price
|
||||
@@ -84,12 +83,10 @@ func validateCommandStatus(status string) error {
|
||||
}
|
||||
|
||||
func (d *Database) CreateCommand(username string) (*models.Command, error) {
|
||||
var addrResult struct {
|
||||
Username string `gorm:"column:username"`
|
||||
}
|
||||
adresse := "Adresse non spécifiée"
|
||||
if err := d.GDB.Raw(`SELECT username FROM clients WHERE username = ?`, username).Scan(&addrResult).Error; err == nil && addrResult.Username != "" {
|
||||
adresse = addrResult.Username
|
||||
var clientCheck models.Client
|
||||
if err := d.GDB.Select("username").Where("username = ?", username).First(&clientCheck).Error; err == nil && clientCheck.Username != "" {
|
||||
adresse = clientCheck.Username
|
||||
}
|
||||
|
||||
basketItems, totalPrix, err := d.fetchBasketItems(username)
|
||||
@@ -102,14 +99,15 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
|
||||
}
|
||||
|
||||
var cmdResult struct {
|
||||
ID int `gorm:"column:id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ID int `gorm:"column:id"`
|
||||
ClientOrderID int `gorm:"column:client_order_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
err = d.GDB.Raw(`
|
||||
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
RETURNING id, client_order_id, created_at, updated_at`,
|
||||
username, "pending", adresse, totalPrix, username).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la création de la commande: %w", err)
|
||||
@@ -123,12 +121,17 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
|
||||
productName = "Produit inconnu"
|
||||
}
|
||||
|
||||
if err := d.GDB.Exec(`
|
||||
INSERT INTO command_items (command_id, produit, product_id, quantite, prix)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
commandID, productName, item.ProductID, item.Quantity, item.Price).Error; err != nil {
|
||||
cmdItem := models.CommandItem{
|
||||
CommandID: commandID,
|
||||
Produit: productName,
|
||||
ProductID: item.ProductID,
|
||||
Quantity: item.Quantity,
|
||||
Price: item.Price,
|
||||
}
|
||||
if err := d.GDB.Create(&cmdItem).Error; err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de l'insertion des items: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
|
||||
@@ -136,9 +139,10 @@ func (d *Database) CreateCommand(username string) (*models.Command, error) {
|
||||
}
|
||||
|
||||
command := &models.Command{
|
||||
ID: commandID,
|
||||
Status: "pending",
|
||||
Total: totalPrix,
|
||||
ID: commandID,
|
||||
ClientOrderID: cmdResult.ClientOrderID,
|
||||
Status: "pending",
|
||||
Total: totalPrix,
|
||||
}
|
||||
|
||||
return command, nil
|
||||
@@ -188,14 +192,15 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
}
|
||||
|
||||
var cmdResult struct {
|
||||
ID int `gorm:"column:id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ID int `gorm:"column:id"`
|
||||
ClientOrderID int `gorm:"column:client_order_id"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
err = d.GDB.Raw(`
|
||||
INSERT INTO commandes (username, status, adresse, total_prix, client_order_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, (SELECT COALESCE(MAX(client_order_id), 0) + 1 FROM commandes WHERE username = ?), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
RETURNING id, client_order_id, created_at, updated_at`,
|
||||
username, "pending", deliveryAddress, totalPrix, username).Scan(&cmdResult).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur création commande: %w", err)
|
||||
@@ -226,7 +231,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
return nil, fmt.Errorf("erreur insertion items: %w", err)
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`, item.Quantity, item.ProductID, item.Quantity)
|
||||
result := d.GDB.Model(&models.Product{}).Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity))
|
||||
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)
|
||||
@@ -236,7 +241,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
|
||||
if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil {
|
||||
log.Printf("⚠️ Erreur vidage panier: %v", err)
|
||||
}
|
||||
|
||||
@@ -248,6 +253,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
||||
|
||||
command := &models.Command{
|
||||
ID: commandID,
|
||||
ClientOrderID: cmdResult.ClientOrderID,
|
||||
Username: username,
|
||||
Status: "pending",
|
||||
Total: totalPrix,
|
||||
@@ -272,44 +278,37 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er
|
||||
}
|
||||
}
|
||||
|
||||
query := `SELECT c.id, c.username, c.status, c.adresse, c.total_prix,
|
||||
c.livreur_assign, c.created_at, c.updated_at,
|
||||
c.proposed_address, c.address_proposal_status,
|
||||
c.client_order_id AS client_order_number
|
||||
FROM commandes c
|
||||
WHERE 1=1`
|
||||
var rows []struct {
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Status string `gorm:"column:status"`
|
||||
Adresse string `gorm:"column:adresse"`
|
||||
TotalPrix float64 `gorm:"column:total_prix"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ProposedAddress *string `gorm:"column:proposed_address"`
|
||||
AddressProposalStatus string `gorm:"column:address_proposal_status"`
|
||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||
}
|
||||
|
||||
args := []interface{}{}
|
||||
gdb := d.GDB.Table("commandes c").
|
||||
Select(`c.id, c.username, c.status, c.adresse, c.total_prix,
|
||||
c.livreur_assign, c.created_at, c.updated_at,
|
||||
c.proposed_address, c.address_proposal_status,
|
||||
c.client_order_id AS client_order_number`)
|
||||
|
||||
if status == "" {
|
||||
query += ` AND c.status IN ('pending', 'assigned', 'en_route', 'arrived', 'livre')`
|
||||
gdb = gdb.Where("c.status IN ?", []string{"pending", "assigned", "en_route", "arrived", "livre"})
|
||||
} else {
|
||||
query += ` AND c.status = ?`
|
||||
args = append(args, status)
|
||||
gdb = gdb.Where("c.status = ?", status)
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
query += ` AND c.username = ?`
|
||||
args = append(args, username)
|
||||
gdb = gdb.Where("c.username = ?", username)
|
||||
}
|
||||
|
||||
query += " ORDER BY c.created_at DESC LIMIT 1000"
|
||||
|
||||
var rows []struct {
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Status string `gorm:"column:status"`
|
||||
Adresse string `gorm:"column:adresse"`
|
||||
TotalPrix float64 `gorm:"column:total_prix"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ProposedAddress *string `gorm:"column:proposed_address"`
|
||||
AddressProposalStatus string `gorm:"column:address_proposal_status"`
|
||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||
}
|
||||
|
||||
if err := d.GDB.Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
if err := gdb.Order("c.created_at DESC").Limit(1000).Scan(&rows).Error; err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la récupération des commandes: %w", err)
|
||||
}
|
||||
|
||||
@@ -346,14 +345,11 @@ func (d *Database) GetAllCommands(status, username string) ([]map[string]any, er
|
||||
}
|
||||
|
||||
func (d *Database) GetCommandCount() (int, error) {
|
||||
var result struct {
|
||||
Count int `gorm:"column:count"`
|
||||
}
|
||||
err := d.GDB.Raw("SELECT COUNT(*) as count FROM commandes").Scan(&result).Error
|
||||
if err != nil {
|
||||
var count int64
|
||||
if err := d.GDB.Model(&models.Command{}).Count(&count).Error; err != nil {
|
||||
return 0, fmt.Errorf("erreur récupération count commandes: %w", err)
|
||||
}
|
||||
return result.Count, nil
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
|
||||
@@ -362,26 +358,28 @@ func (d *Database) SetCommandReferralUsed(commandID int, amount float64) error {
|
||||
|
||||
func (d *Database) GetCommandByID(id int) (map[string]any, error) {
|
||||
var row struct {
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Status string `gorm:"column:status"`
|
||||
Adresse string `gorm:"column:adresse"`
|
||||
TotalPrix float64 `gorm:"column:total_prix"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ProposedAddress *string `gorm:"column:proposed_address"`
|
||||
AddressProposalStatus string `gorm:"column:address_proposal_status"`
|
||||
ReferralUsed float64 `gorm:"column:referral_used"`
|
||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||
ID int `gorm:"column:id"`
|
||||
Username string `gorm:"column:username"`
|
||||
Status string `gorm:"column:status"`
|
||||
Adresse string `gorm:"column:adresse"`
|
||||
TotalPrix float64 `gorm:"column:total_prix"`
|
||||
LivreurAssign *string `gorm:"column:livreur_assign"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
ProposedAddress *string `gorm:"column:proposed_address"`
|
||||
AddressProposalStatus string `gorm:"column:address_proposal_status"`
|
||||
ReferralUsed float64 `gorm:"column:referral_used"`
|
||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||
CancelReason string `gorm:"column:cancel_reason"`
|
||||
}
|
||||
|
||||
err := d.GDB.Raw(`
|
||||
SELECT c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at,
|
||||
c.proposed_address, c.address_proposal_status, c.referral_used,
|
||||
c.client_order_id AS client_order_number
|
||||
FROM commandes c WHERE c.id = ?`, id).Scan(&row).Error
|
||||
if err != nil {
|
||||
if err := d.GDB.Table("commandes c").
|
||||
Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign,
|
||||
c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status,
|
||||
c.referral_used, c.client_order_id AS client_order_number,
|
||||
COALESCE(c.cancel_reason, '') AS cancel_reason`).
|
||||
Where("c.id = ?", id).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err)
|
||||
}
|
||||
if row.ID == 0 {
|
||||
@@ -399,6 +397,7 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
|
||||
"address_proposal_status": row.AddressProposalStatus,
|
||||
"referral_used": row.ReferralUsed,
|
||||
"client_order_number": row.ClientOrderNumber,
|
||||
"cancel_reason": row.CancelReason,
|
||||
}
|
||||
|
||||
if row.LivreurAssign != nil {
|
||||
@@ -416,12 +415,23 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
|
||||
return command, nil
|
||||
}
|
||||
|
||||
// GetClientOrderID retourne le client_order_id (numéro perso du client) pour un commandID global.
|
||||
// Retourne commandID en fallback si introuvable.
|
||||
func (d *Database) GetClientOrderID(commandID int) int {
|
||||
var result struct {
|
||||
ClientOrderID int `gorm:"column:client_order_id"`
|
||||
}
|
||||
if err := d.GDB.Model(&models.Command{}).Select("client_order_id").Where("id = ?", commandID).First(&result).Error; err != nil || result.ClientOrderID == 0 {
|
||||
return commandID
|
||||
}
|
||||
return result.ClientOrderID
|
||||
}
|
||||
|
||||
func (d *Database) GetCommandAddress(commandID int) (string, error) {
|
||||
var result struct {
|
||||
Adresse string `gorm:"column:adresse"`
|
||||
}
|
||||
err := d.GDB.Raw(`SELECT adresse FROM commandes WHERE id = ?`, commandID).Scan(&result).Error
|
||||
if err != nil {
|
||||
if err := d.GDB.Model(&models.Command{}).Select("adresse").Where("id = ?", commandID).First(&result).Error; err != nil {
|
||||
return "", fmt.Errorf("erreur lors de la récupération de l'adresse: %w", err)
|
||||
}
|
||||
if result.Adresse == "" {
|
||||
@@ -439,9 +449,10 @@ func (d *Database) UpdateCommandAddress(commandID int, deliveryAddress string) e
|
||||
return fmt.Errorf("adresse vide non autorisée")
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE commandes SET adresse = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`, deliveryAddress, commandID)
|
||||
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
|
||||
"adresse": deliveryAddress,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la mise à jour de l'adresse: %w", result.Error)
|
||||
}
|
||||
@@ -459,10 +470,11 @@ func (d *Database) ProposeAddressChange(commandID int, proposedAddress, proposed
|
||||
return err
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE commandes
|
||||
SET proposed_address = ?, address_proposal_status = 'pending', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`, proposedAddress, commandID)
|
||||
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
|
||||
"proposed_address": proposedAddress,
|
||||
"address_proposal_status": "pending",
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur proposition adresse: %w", result.Error)
|
||||
}
|
||||
@@ -519,7 +531,10 @@ func (d *Database) UpdateCommandStatus(commandID int, status string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`UPDATE commandes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, status, commandID)
|
||||
result := d.GDB.Model(&models.Command{}).Where("id = ?", commandID).Updates(map[string]any{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("erreur lors de la mise à jour du statut: %w", result.Error)
|
||||
}
|
||||
@@ -534,10 +549,12 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
|
||||
sanitizedMessage := sanitizeLogMessage(message)
|
||||
sanitizedAuthor := sanitizeLogMessage(author)
|
||||
|
||||
if err := d.GDB.Exec(`
|
||||
INSERT INTO command_logs (command_id, status, message, author, created_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
||||
commandID, status, sanitizedMessage, sanitizedAuthor).Error; err != nil {
|
||||
if err := d.GDB.Create(&models.CommandLog{
|
||||
CommandID: commandID,
|
||||
Status: status,
|
||||
Message: sanitizedMessage,
|
||||
Author: sanitizedAuthor,
|
||||
}).Error; err != nil {
|
||||
log.Printf("⚠️ Avertissement: impossible d'ajouter le log (table command_logs peut-être manquante): %v", err)
|
||||
return nil
|
||||
}
|
||||
@@ -547,22 +564,9 @@ func (d *Database) AddCommandLog(commandID int, status, message, author string)
|
||||
|
||||
// GetCommandLogs récupère tous les logs d'une commande
|
||||
func (d *Database) GetCommandLogs(commandID int) ([]map[string]any, error) {
|
||||
var rows []struct {
|
||||
ID int `gorm:"column:id"`
|
||||
CommandID int `gorm:"column:command_id"`
|
||||
Status string `gorm:"column:status"`
|
||||
Message string `gorm:"column:message"`
|
||||
Author string `gorm:"column:author"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
var rows []models.CommandLog
|
||||
|
||||
err := d.GDB.Raw(`
|
||||
SELECT id, command_id, status, message, author, created_at
|
||||
FROM command_logs
|
||||
WHERE command_id = ?
|
||||
ORDER BY created_at ASC`, commandID).Scan(&rows).Error
|
||||
if err != nil {
|
||||
// Si la table n'existe pas, retourner un tableau vide au lieu d'une erreur
|
||||
if err := d.GDB.Where("command_id = ?", commandID).Order("created_at ASC").Find(&rows).Error; err != nil {
|
||||
log.Printf("⚠️ Avertissement: impossible de récupérer les logs: %v", err)
|
||||
return []map[string]any{}, nil
|
||||
}
|
||||
|
||||
@@ -187,6 +187,25 @@ func InitDB() *Database {
|
||||
log.Fatalf("❌ Erreur backfill commandes.client_order_id: %v", err)
|
||||
}
|
||||
|
||||
// Migration: raison d'annulation par le client
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS cancel_reason TEXT`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration commandes.cancel_reason: %v", err)
|
||||
}
|
||||
|
||||
// Migration: coordonnées GPS de destination et du livreur
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS dest_latitude DOUBLE PRECISION`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration commandes.dest_latitude: %v", err)
|
||||
}
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS dest_longitude DOUBLE PRECISION`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration commandes.dest_longitude: %v", err)
|
||||
}
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS livreur_latitude DOUBLE PRECISION`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration commandes.livreur_latitude: %v", err)
|
||||
}
|
||||
if _, err = database.Exec(`ALTER TABLE commandes ADD COLUMN IF NOT EXISTS livreur_longitude DOUBLE PRECISION`); err != nil {
|
||||
log.Fatalf("❌ Erreur migration commandes.livreur_longitude: %v", err)
|
||||
}
|
||||
|
||||
// Migration: table de suivi des paiements crypto
|
||||
if _, err = database.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS crypto_payments (
|
||||
@@ -264,7 +283,6 @@ func (db *Database) createTables() error {
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);`,
|
||||
`ALTER TABLE clients ADD COLUMN IF NOT EXISTS referral_balance NUMERIC(10,2) DEFAULT 0.0;`,
|
||||
|
||||
// ============================
|
||||
// TABLE jwt_tokens
|
||||
@@ -423,7 +441,7 @@ func (db *Database) createTables() error {
|
||||
`CREATE INDEX IF NOT EXISTS idx_command_items_client_username ON command_items(client_username);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_command_items_status ON command_items(status);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_command_items_produit ON command_items(produit);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_jwt_user_id ON jwt_tokens(user_id);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_jwt_user_type ON jwt_tokens(user_type);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_jwt_token ON jwt_tokens(token);`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_jwt_date_fin ON jwt_tokens(date_fin);`,
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/models"
|
||||
"gestion/services"
|
||||
"log"
|
||||
"time"
|
||||
@@ -64,7 +65,7 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
|
||||
var users []struct {
|
||||
Username string `gorm:"column:username"`
|
||||
}
|
||||
if err := d.GDB.Raw(`SELECT username FROM users WHERE role IN ('admin','cabine')`).Scan(&users).Error; err != nil {
|
||||
if err := d.GDB.Model(&models.User{}).Select("username").Where("role IN ?", []string{"admin", "cabine"}).Scan(&users).Error; err != nil {
|
||||
log.Printf("❌ [ADMIN_NOTIF] Erreur lecture users admin/cabine: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
|
||||
var users []struct {
|
||||
Username string `gorm:"column:username"`
|
||||
}
|
||||
if err := d.GDB.Raw(`SELECT username FROM users WHERE role IN ('admin','cabine')`).Scan(&users).Error; err != nil {
|
||||
if err := d.GDB.Model(&models.User{}).Select("username").Where("role IN ?", []string{"admin", "cabine"}).Scan(&users).Error; err != nil {
|
||||
log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -136,18 +137,3 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
|
||||
}
|
||||
log.Printf("🚨 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
|
||||
}
|
||||
|
||||
// AddDeliveryRating ajoute une note pour un livreur
|
||||
func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error {
|
||||
err := d.GDB.Exec(`
|
||||
INSERT INTO delivery_ratings (livreur_username, command_id, rating, comment, created_at)
|
||||
VALUES (?, ?, ?, ?, NOW())`,
|
||||
livreurUsername, commandID, rating, comment).Error
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur sauvegarde note livreur: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("⭐ Note %d/5 ajoutée pour livreur %s (commande %d)", rating, livreurUsername, commandID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gestion/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -19,7 +20,9 @@ func (d *Database) CreditClientReferral(username string, amount float64) error {
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("le montant doit être positif")
|
||||
}
|
||||
result := d.GDB.Exec(`UPDATE clients SET referral_balance = referral_balance + ? WHERE username = ?`, amount, username)
|
||||
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Updates(map[string]any{
|
||||
"referral_balance": gorm.Expr("referral_balance + ?", amount),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"gestion/models"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetClientCancellationsCount récupère le nombre d'annulations tardives d'un client
|
||||
@@ -17,7 +19,7 @@ func (d *Database) GetClientCancellationsCount(username string) (int, error) {
|
||||
var result struct {
|
||||
Count int `gorm:"column:count"`
|
||||
}
|
||||
err := d.GDB.Raw(`SELECT COALESCE(cancellations_count, 0) as count FROM clients WHERE username = ?`, username).Scan(&result).Error
|
||||
err := d.GDB.Table("clients").Select("COALESCE(cancellations_count, 0) as count").Where("username = ?", username).Scan(&result).Error
|
||||
if err != nil {
|
||||
log.Printf("❌ [GetCancellationsCount] Erreur: %v", err)
|
||||
return 0, fmt.Errorf("erreur récupération compteur: %w", err)
|
||||
@@ -27,11 +29,9 @@ func (d *Database) GetClientCancellationsCount(username string) (int, error) {
|
||||
|
||||
// IncrementClientCancellationsCount incrémente le compteur d'annulations
|
||||
func (d *Database) IncrementClientCancellationsCount(username string) error {
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients
|
||||
SET cancellations_count = COALESCE(cancellations_count, 0) + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, username)
|
||||
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Updates(map[string]any{
|
||||
"cancellations_count": gorm.Expr("COALESCE(cancellations_count, 0) + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ [IncrementCancellations] Erreur: %v", result.Error)
|
||||
return fmt.Errorf("erreur incrémentation: %w", result.Error)
|
||||
@@ -98,11 +98,7 @@ func (d *Database) ApplyCancellationPenalty(username string) (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result := d.GDB.Exec(`
|
||||
UPDATE clients
|
||||
SET amende = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE username = ?`, float64(penalty), username)
|
||||
result := d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("amende", float64(penalty))
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ [ApplyCancellationPenalty] Erreur UPDATE: %v", result.Error)
|
||||
return 0, fmt.Errorf("erreur application pénalité: %w", result.Error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/models"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -40,6 +41,7 @@ func DefaultSettings() models.AppSettings {
|
||||
},
|
||||
PointsEnabled: true,
|
||||
ReferralEnabled: true,
|
||||
ReferralAmount: 0,
|
||||
PointsPools: []models.PointsPool{
|
||||
{
|
||||
Key: "pool_0",
|
||||
@@ -90,7 +92,7 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
|
||||
Key string `gorm:"column:key"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
if err := d.GDB.Raw(`SELECT key, value FROM app_settings`).Scan(&rows).Error; err != nil {
|
||||
if err := d.GDB.Table("app_settings").Select("key, value").Scan(&rows).Error; err != nil {
|
||||
return settings, fmt.Errorf("erreur lecture settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -109,6 +111,10 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
|
||||
}
|
||||
case "referral_enabled":
|
||||
settings.ReferralEnabled = row.Value == "true"
|
||||
case "referral_amount":
|
||||
if v, err := strconv.ParseFloat(row.Value, 64); err == nil {
|
||||
settings.ReferralAmount = v
|
||||
}
|
||||
case "crypto_payment_enabled":
|
||||
settings.CryptoPaymentEnabled = row.Value == "true"
|
||||
case "crypto_only":
|
||||
@@ -207,6 +213,7 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
||||
{"points_enabled", boolStr(s.PointsEnabled)},
|
||||
{"points_pools", string(poolsJSON)},
|
||||
{"referral_enabled", boolStr(s.ReferralEnabled)},
|
||||
{"referral_amount", strconv.FormatFloat(s.ReferralAmount, 'f', 2, 64)},
|
||||
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},
|
||||
{"crypto_only", boolStr(s.CryptoOnly)},
|
||||
{"nowpayments_api_key", s.NowPaymentsAPIKey},
|
||||
|
||||
@@ -67,15 +67,14 @@ func ValidateAndConsumeLinkToken(token string) (username, role string, err error
|
||||
}
|
||||
|
||||
func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error {
|
||||
return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
|
||||
return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error
|
||||
}
|
||||
|
||||
func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) {
|
||||
var result struct {
|
||||
ChatID *int64 `gorm:"column:telegram_chat_id"`
|
||||
}
|
||||
err := d.GDB.Raw(`SELECT telegram_chat_id FROM clients WHERE username = ?`, username).Scan(&result).Error
|
||||
if err != nil {
|
||||
if err := d.GDB.Table("clients").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if result.ChatID == nil {
|
||||
@@ -85,19 +84,18 @@ func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error)
|
||||
}
|
||||
|
||||
func (d *Database) DeleteClientTelegramChatID(username string) error {
|
||||
return d.GDB.Exec(`UPDATE clients SET telegram_chat_id = NULL WHERE username = ?`, username).Error
|
||||
return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error
|
||||
}
|
||||
|
||||
func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error {
|
||||
return d.GDB.Exec(`UPDATE users SET telegram_chat_id = ? WHERE username = ?`, chatID, username).Error
|
||||
return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error
|
||||
}
|
||||
|
||||
func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
|
||||
var result struct {
|
||||
ChatID *int64 `gorm:"column:telegram_chat_id"`
|
||||
}
|
||||
err := d.GDB.Raw(`SELECT telegram_chat_id FROM users WHERE username = ?`, username).Scan(&result).Error
|
||||
if err != nil {
|
||||
if err := d.GDB.Table("users").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if result.ChatID == nil {
|
||||
@@ -107,7 +105,7 @@ func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) {
|
||||
}
|
||||
|
||||
func (d *Database) DeleteUserTelegramChatID(username string) error {
|
||||
return d.GDB.Exec(`UPDATE users SET telegram_chat_id = NULL WHERE username = ?`, username).Error
|
||||
return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error
|
||||
}
|
||||
|
||||
// GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id
|
||||
@@ -115,7 +113,7 @@ func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string,
|
||||
var clientResult struct {
|
||||
Username string `gorm:"column:username"`
|
||||
}
|
||||
if err = d.GDB.Raw(`SELECT username FROM clients WHERE telegram_chat_id = ?`, chatID).Scan(&clientResult).Error; err == nil && clientResult.Username != "" {
|
||||
if err = d.GDB.Table("clients").Select("username").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&clientResult).Error; err == nil && clientResult.Username != "" {
|
||||
return clientResult.Username, "client", nil
|
||||
}
|
||||
|
||||
@@ -123,7 +121,7 @@ func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string,
|
||||
Username string `gorm:"column:username"`
|
||||
Role string `gorm:"column:role"`
|
||||
}
|
||||
if err = d.GDB.Raw(`SELECT username, role FROM users WHERE telegram_chat_id = ?`, chatID).Scan(&userResult).Error; err == nil && userResult.Username != "" {
|
||||
if err = d.GDB.Model(&models.User{}).Select("username, role").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&userResult).Error; err == nil && userResult.Username != "" {
|
||||
return userResult.Username, userResult.Role, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ func (d *Database) ProcessScheduledNotifications() error {
|
||||
|
||||
// SendETANotification envoie une notification ETA
|
||||
func (d *Database) SendETANotification(commandID int, notifType string) {
|
||||
message := fmt.Sprintf("Votre commande #%d arrive dans %s", commandID, notifType)
|
||||
message := fmt.Sprintf("Votre commande #%d arrive dans %s", d.GetClientOrderID(commandID), notifType)
|
||||
|
||||
channel := fmt.Sprintf("notifications:command:%d", commandID)
|
||||
Redis.Publish(RedisCtx, channel, message)
|
||||
|
||||
@@ -2,7 +2,7 @@ package db
|
||||
|
||||
import "fmt"
|
||||
|
||||
func extractCommandID(member interface{}) int {
|
||||
func extractCommandID(member any) int {
|
||||
switch v := member.(type) {
|
||||
case int:
|
||||
return v
|
||||
|
||||
@@ -70,7 +70,6 @@ func (d *Database) AssignCommandToDeliverymanQueue(commandID int, deliveryman st
|
||||
|
||||
d.UpdateCommandStatus(commandID, "assigned")
|
||||
d.AssignDeliveryPerson(commandID, deliveryman)
|
||||
// ✅ MODIFIÉ: Position dans la queue pour info seulement
|
||||
d.SetCommandETAWithDetails(commandID, totalETA, int(currentQueueSize)+1)
|
||||
|
||||
limitInfo := ""
|
||||
@@ -93,10 +92,8 @@ func (d *Database) AssignCommandToDeliverymanQueueUnlimited(deliveryman string,
|
||||
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
|
||||
currentQueueSize, _ := Redis.ZCard(RedisCtx, queueKey).Result()
|
||||
|
||||
// ✅ MODIFIÉ: Calculer le temps de trajet direct depuis la position du livreur
|
||||
travelTime := d.CalculateETAForDeliveryman(deliveryman, queueItem.Lat, queueItem.Lng)
|
||||
|
||||
// ✅ ETA = temps de trajet direct uniquement
|
||||
queueItem.EstimatedETA = travelTime
|
||||
|
||||
err := d.AddToDeliverymanQueue(deliveryman, queueItem)
|
||||
|
||||
@@ -216,17 +216,17 @@ func (d *Database) StartQueueCleanupScheduler() {
|
||||
// ============================================
|
||||
|
||||
// GetQueueValidationReport génère un rapport de validation sans supprimer
|
||||
func (d *Database) GetQueueValidationReport() (map[string]interface{}, error) {
|
||||
func (d *Database) GetQueueValidationReport() (map[string]any, error) {
|
||||
keys, err := Redis.Keys(RedisCtx, "queue:pending:*").Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report := map[string]interface{}{
|
||||
report := map[string]any{
|
||||
"total_commands": len(keys),
|
||||
"valid_commands": 0,
|
||||
"invalid_commands": 0,
|
||||
"invalid_details": []map[string]interface{}{},
|
||||
"invalid_details": []map[string]any{},
|
||||
"validation_results": []string{},
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ func (d *Database) GetQueueValidationReport() (map[string]interface{}, error) {
|
||||
|
||||
if len(issues) > 0 {
|
||||
report["invalid_commands"] = report["invalid_commands"].(int) + 1
|
||||
report["invalid_details"] = append(report["invalid_details"].([]map[string]interface{}), map[string]interface{}{
|
||||
report["invalid_details"] = append(report["invalid_details"].([]map[string]any), map[string]any{
|
||||
"command_id": queueItem.CommandID,
|
||||
"issues": issues,
|
||||
"data": queueItem,
|
||||
|
||||
@@ -11,10 +11,6 @@ import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 🆕 GESTION AUTOMATIQUE DU STATUT BUSY
|
||||
// ============================================
|
||||
|
||||
// UpdateDeliverymanStatusBasedOnQueue met à jour automatiquement le statut
|
||||
func (d *Database) UpdateDeliverymanStatusBasedOnQueue(deliveryman string) error {
|
||||
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
|
||||
|
||||
@@ -13,6 +13,8 @@ require (
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/redis/go-redis/v9 v9.17.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -56,6 +58,4 @@ require (
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gorm.io/driver/postgres v1.6.0 // indirect
|
||||
gorm.io/gorm v1.31.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -21,12 +22,12 @@ func AddAddress(c *gin.Context) {
|
||||
InvalidAddress string `json:"invalid_address" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.AddAddress(req.CorrectAddress, req.InvalidAddress); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
utils.ServerErr(c, "Impossible d'ajouter l'adresse", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,12 +47,12 @@ func DeleteAddress(c *gin.Context) {
|
||||
InvalidAddress string `json:"invalid_address" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DeleteAddress(req.InvalidAddress, req.CorrectAddress); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de supprimer l'adresse", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ func GetAllAddress(c *gin.Context) {
|
||||
|
||||
getAddress, err := database.AllAddress()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de récupérer les adresses", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -32,14 +33,13 @@ func AlertPolice(c *gin.Context) {
|
||||
usernameStr := username.(string)
|
||||
alert, err := database.CreateAlert(usernameStr, req.Message)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de créer l'alerte", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Notifier tous les admins/cabines en temps réel
|
||||
go database.NotifyAllAdminCabineAlert(alert.ID, usernameStr, req.Message)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"message": "Police alert created",
|
||||
"alert_id": alert.ID,
|
||||
@@ -61,13 +61,12 @@ func DeleteAlert(c *gin.Context) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
|
||||
return
|
||||
}
|
||||
err = database.DeleteAlertPolicy(alertID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
if err = database.DeleteAlertPolicy(alertID); err != nil {
|
||||
utils.ServerErr(c, "Impossible de supprimer l'alerte", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Alert deleted",
|
||||
})
|
||||
@@ -89,11 +88,11 @@ func GetAlert(c *gin.Context) {
|
||||
}
|
||||
alert, err := database.GetAlertPolicy(alertID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Alerte non trouvée"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"alert": alert,
|
||||
})
|
||||
@@ -124,7 +123,6 @@ func EndAlert(c *gin.Context) {
|
||||
|
||||
usernameStr := username.(string)
|
||||
|
||||
// Vérifier que l'alerte appartient bien à ce livreur
|
||||
alert, err := database.GetAlertPolicy(alertID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Alerte non trouvée"})
|
||||
@@ -136,9 +134,8 @@ func EndAlert(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = database.EndAlert(alertID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de mettre fin à l'alerte", "details": err.Error()})
|
||||
if err = database.EndAlert(alertID); err != nil {
|
||||
utils.ServerErr(c, "Impossible de mettre fin à l'alerte", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +166,7 @@ func GetMyAlerts(c *gin.Context) {
|
||||
|
||||
alerts, err := database.GetAlertsByUsername(usernameStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,7 +189,7 @@ func GetAllAlerts(c *gin.Context) {
|
||||
|
||||
alerts, err := database.GetAllAlerts()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -214,7 +211,7 @@ func GetActiveAlerts(c *gin.Context) {
|
||||
|
||||
alerts, err := database.GetActiveAlerts()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer les alertes", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de récupérer les alertes", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ func RegisterClient(c *gin.Context) {
|
||||
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -301,7 +300,7 @@ func ChangePassword(c *gin.Context) {
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides", "details": err.Error()})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur stockage Redis",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -217,7 +216,6 @@ func UpdateCommandAddressCabine(c *gin.Context) {
|
||||
if err := database.UpdateCommandAddress(commandID, req.DeliveryAddress); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la mise à jour de l'adresse",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -264,7 +262,6 @@ func GetLivreurPosition(c *gin.Context) {
|
||||
"success": false,
|
||||
"livreur": livreurUsername,
|
||||
"message": "Position non disponible (GPS désactivé ou livraison terminée)",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -394,7 +391,6 @@ func GetDeliveryIssues(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération problèmes",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -431,7 +427,6 @@ func CreateDeliveryIssue(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur création problème",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -468,7 +463,6 @@ func UpdateDeliveryIssue(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -516,7 +510,6 @@ func AddDeliverySupport(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur ajout support",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -540,7 +533,6 @@ func GetCommandLogs(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération logs",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -580,7 +572,6 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Raison requise pour validation forcée",
|
||||
"details": err.Error(),
|
||||
"example": gin.H{
|
||||
"reason": "Client confirmé par téléphone",
|
||||
},
|
||||
@@ -632,7 +623,6 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la validation forcée",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -641,7 +631,7 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
|
||||
if clientUsername != "" {
|
||||
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
||||
clientMsg := fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID))
|
||||
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -339,19 +339,22 @@ func GetAllCancelledOrders(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
cancelReason, _ := order["cancel_reason"].(string)
|
||||
enrichedOrder := map[string]any{
|
||||
"id": order["id"],
|
||||
"username": order["username"],
|
||||
"total_prix": order["total_prix"],
|
||||
"created_at": order["created_at"],
|
||||
"updated_at": order["updated_at"],
|
||||
"items_count": len(items),
|
||||
"id": order["id"],
|
||||
"username": order["username"],
|
||||
"total_prix": order["total_prix"],
|
||||
"created_at": order["created_at"],
|
||||
"updated_at": order["updated_at"],
|
||||
"items_count": len(items),
|
||||
"cancel_reason": cancelReason,
|
||||
}
|
||||
|
||||
if cancellationLog != nil {
|
||||
enrichedOrder["cancellation"] = gin.H{
|
||||
"cancelled_at": cancellationLog["created_at"],
|
||||
"cancelled_by": cancellationLog["author"],
|
||||
"reason": cancelReason,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func CreateCategory(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.ValidateCategoryColor(req.Color); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Couleur invalide (format hex requis, ex: #ff0000)"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func UpdateCategory(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.ValidateCategoryColor(req.Color); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Couleur invalide (format hex requis, ex: #ff0000)"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func DeleteCategory(c *gin.Context) {
|
||||
if err := database.DeleteCategory(id); err != nil {
|
||||
log.Printf("❌ [CATEGORIES] Suppression erreur: %v", err)
|
||||
if strings.Contains(err.Error(), "utilisée par") {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Catégorie utilisée par des produits existants"})
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Catégorie non trouvée"})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -90,7 +91,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -118,6 +118,7 @@ func GetMyCommandsWithTracking(c *gin.Context) {
|
||||
|
||||
enrichedCommands[i] = gin.H{
|
||||
"id": cmd["id"],
|
||||
"client_order_number": cmd["client_order_number"],
|
||||
"status": cmd["status"],
|
||||
"status_message": getStatusMessage(cmd["status"].(string)),
|
||||
"adresse": cmd["adresse"],
|
||||
|
||||
@@ -81,7 +81,7 @@ func UpdateCommandAddress(c *gin.Context) {
|
||||
// ✅ Récupération sécurisée du username
|
||||
adminUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func ProposeAddressChange(c *gin.Context) {
|
||||
|
||||
staffUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,15 +198,14 @@ func ProposeAddressChange(c *gin.Context) {
|
||||
}
|
||||
|
||||
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()})
|
||||
utils.ServerErr(c, "Impossible de proposer l'adresse", err)
|
||||
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)
|
||||
msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour votre commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", database.GetClientOrderID(commandID), req.ProposedAddress)
|
||||
database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
|
||||
}
|
||||
|
||||
@@ -248,8 +247,7 @@ func RespondToAddressProposal(c *gin.Context) {
|
||||
}
|
||||
|
||||
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()})
|
||||
utils.ServerErr(c, "Impossible de traiter la réponse", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +294,6 @@ func GetAllCommands(c *gin.Context) {
|
||||
log.Printf("❌ [GET_CMDS] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des commandes",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -444,7 +441,7 @@ func StaffApproveDelivery(c *gin.Context) {
|
||||
totalPoints, pointCategory, clientUsername, err := database.ApproveDeliveryAtomicByStaff(commandID, staffUsername)
|
||||
if err != nil {
|
||||
log.Printf("❌ [STAFF_APPROVE] Erreur: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de confirmer la réception: " + err.Error()})
|
||||
utils.ServerErr(c, "Impossible de confirmer la réception", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -474,7 +471,7 @@ func ValidateDelivery(c *gin.Context) {
|
||||
|
||||
adminUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -604,7 +601,6 @@ func GetAvailableDeliveryPersons(c *gin.Context) {
|
||||
log.Printf("❌ [GET_LIVREURS] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des livreurs",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -651,7 +647,6 @@ func AssignDeliveryPerson(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -665,7 +660,6 @@ func AssignDeliveryPerson(c *gin.Context) {
|
||||
log.Printf("❌ [ASSIGN] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur assignation livreur",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -704,7 +698,6 @@ func GetClientCommandsHistory(c *gin.Context) {
|
||||
log.Printf("❌ [HISTORY] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération historique",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -805,7 +798,7 @@ func NotifyClientToDescend(c *gin.Context) {
|
||||
}
|
||||
|
||||
staffUsername, _ := c.Get("username")
|
||||
msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", commandID)
|
||||
msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", database.GetClientOrderID(commandID))
|
||||
database.NotifyClient(clientUsername, commandID, "ready_pickup", msg)
|
||||
database.AddCommandLog(commandID, "notification", fmt.Sprintf("Client notifié de descendre par %s", staffUsername), staffUsername.(string))
|
||||
|
||||
@@ -859,7 +852,6 @@ func ShowItems(c *gin.Context) {
|
||||
log.Printf("❌ [ITEMS] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des items",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -951,7 +943,6 @@ func GetCommandItemsWithDetails(c *gin.Context) {
|
||||
log.Printf("❌ [ITEMS_DETAILED] Erreur DB: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération items",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -964,12 +955,13 @@ func GetCommandItemsWithDetails(c *gin.Context) {
|
||||
}
|
||||
|
||||
commandInfo := map[string]interface{}{
|
||||
"id": items[0]["command_id"],
|
||||
"command_status": items[0]["command_status"],
|
||||
"command_address": items[0]["command_address"],
|
||||
"total_prix": items[0]["total_prix"],
|
||||
"livreur_assign": items[0]["livreur_assign"],
|
||||
"command_created_at": items[0]["command_created_at"],
|
||||
"id": items[0]["command_id"],
|
||||
"command_status": items[0]["command_status"],
|
||||
"command_address": items[0]["command_address"],
|
||||
"total_prix": items[0]["total_prix"],
|
||||
"livreur_assign": items[0]["livreur_assign"],
|
||||
"command_created_at": items[0]["command_created_at"],
|
||||
"client_order_number": items[0]["client_order_number"],
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -1007,7 +999,6 @@ func UpdateItemStatus(c *gin.Context) {
|
||||
log.Printf("❌ [UPD_ITEM] Erreur JSON: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Status requis",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1038,7 +1029,6 @@ func UpdateItemStatus(c *gin.Context) {
|
||||
log.Printf("❌ [UPD_ITEM] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la mise à jour",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1065,7 +1055,7 @@ func DeleteCommandItem(c *gin.Context) {
|
||||
|
||||
adminUsername, err := safeGetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1084,8 +1074,7 @@ func DeleteCommandItem(c *gin.Context) {
|
||||
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()})
|
||||
utils.ServerErr(c, "Impossible de supprimer l'item", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1137,8 +1126,7 @@ func UpdateCommandStatusAdmin(c *gin.Context) {
|
||||
}
|
||||
|
||||
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()})
|
||||
utils.ServerErr(c, "Impossible de mettre à jour le statut", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ func GetMyDeliveries(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -190,7 +189,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -261,7 +259,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
if err := database.UpdateCommandStatus(commandID, req.Status); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -361,16 +358,16 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
} else {
|
||||
etaStr = fmt.Sprintf("%d min", etaMinutes)
|
||||
}
|
||||
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route ! Arrivée dans ~%s", commandID, etaStr)
|
||||
clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route (~%s)", database.GetClientOrderID(commandID), etaStr)
|
||||
} else {
|
||||
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
|
||||
clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(commandID))
|
||||
}
|
||||
case "arrived":
|
||||
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
|
||||
clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID))
|
||||
case "livre":
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
||||
clientMsg = fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊", database.GetClientOrderID(commandID))
|
||||
case "cancelled":
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", commandID)
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(commandID))
|
||||
}
|
||||
if clientMsg != "" {
|
||||
database.NotifyClient(clientUsername, commandID, req.Status, clientMsg)
|
||||
|
||||
@@ -40,7 +40,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Livreur non trouvé",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -118,7 +117,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Statut requis",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -163,7 +161,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour statut",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -325,7 +322,6 @@ func GetDeliveryPersonHistory(c *gin.Context) {
|
||||
log.Printf("❌ [GET_DELIVERY_HISTORY] Erreur récupération: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération historique",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -373,7 +369,6 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Coordonnées GPS requises",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -421,7 +416,6 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
|
||||
log.Printf("❌ [UPDATE_DELIVERY_LOCATION] Erreur mise à jour: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour position",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -515,7 +509,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
|
||||
log.Printf("❌ [REMOVE_FROM_QUEUE] Erreur suppression: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur suppression de la queue",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,34 +21,24 @@ import (
|
||||
// GÉOCODAGE D'ADRESSES
|
||||
// ============================================
|
||||
|
||||
// GeocodeAddress convertit une adresse en coordonnées GPS
|
||||
func GeocodeAddress(c *gin.Context) {
|
||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||
|
||||
var req struct {
|
||||
Address string `json:"address" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Adresse requise",
|
||||
"details": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"})
|
||||
return
|
||||
}
|
||||
|
||||
location, err := geoService.GeocodeAddress(req.Address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Impossible de géocoder cette adresse",
|
||||
"details": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Impossible de géocoder cette adresse"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)",
|
||||
req.Address, location.Latitude, location.Longitude)
|
||||
|
||||
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", req.Address, location.Latitude, location.Longitude)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"latitude": location.Latitude,
|
||||
@@ -76,8 +66,7 @@ func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
"error": "Données invalides",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -89,8 +78,7 @@ func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
location, err := geoService.GeocodeAddress(req.Address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Impossible de géocoder l'adresse",
|
||||
"details": err.Error(),
|
||||
"error": "Impossible de géocoder l'adresse",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -110,8 +98,7 @@ func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
// Valider les coordonnées
|
||||
if err := services.ValidateCoordinates(targetCoords); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Coordonnées invalides",
|
||||
"details": err.Error(),
|
||||
"error": "Coordonnées invalides",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -135,8 +122,7 @@ func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Aucun livreur avec position GPS valide",
|
||||
"details": err.Error(),
|
||||
"error": "Aucun livreur avec position GPS valide",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -193,8 +179,7 @@ func GetAllDeliveryDistances(c *gin.Context) {
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
"error": "Données invalides",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -205,8 +190,7 @@ func GetAllDeliveryDistances(c *gin.Context) {
|
||||
location, err := geoService.GeocodeAddress(req.Address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Impossible de géocoder l'adresse",
|
||||
"details": err.Error(),
|
||||
"error": "Impossible de géocoder l'adresse",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -224,8 +208,7 @@ func GetAllDeliveryDistances(c *gin.Context) {
|
||||
|
||||
if err := services.ValidateCoordinates(targetCoords); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Coordonnées invalides",
|
||||
"details": err.Error(),
|
||||
"error": "Coordonnées invalides",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -246,8 +229,7 @@ func GetAllDeliveryDistances(c *gin.Context) {
|
||||
distances, err := geoService.GetAllDeliveryDistances(targetCoords, usernames)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur calcul des distances",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur calcul des distances",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -317,7 +299,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Impossible de géocoder l'adresse de livraison",
|
||||
"address": address,
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -375,8 +356,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, singleDeliveryman, location.Latitude, location.Longitude)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur calcul ETA",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur calcul ETA",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -385,8 +365,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, singleDeliveryman, travelTime, location.Latitude, location.Longitude, address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de l'assignation",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur lors de l'assignation",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -440,8 +419,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, leastLoaded, location.Latitude, location.Longitude)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur calcul ETA",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur calcul ETA",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -450,8 +428,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
err = database.ForceAssignCommandToDeliverymanWithCoords(commandID, leastLoaded, travelTime, location.Latitude, location.Longitude, address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de l'assignation forcée",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur lors de l'assignation forcée",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -506,8 +483,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Aucun livreur avec position GPS valide",
|
||||
"details": err.Error(),
|
||||
"error": "Aucun livreur avec position GPS valide",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -527,8 +503,7 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
|
||||
err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, nearest.Username, travelTime, location.Latitude, location.Longitude, address)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de l'assignation",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur lors de l'assignation",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -582,8 +557,7 @@ func AutoAssignAllPendingCommands(c *gin.Context) {
|
||||
commands, err := database.GetAllCommands("pending", "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération des commandes",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur récupération des commandes",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -742,8 +716,7 @@ func GetAllDeliveryQueues(c *gin.Context) {
|
||||
overview, err := database.GetAllQueuesOverview()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération des queues",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur récupération des queues",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -802,8 +775,7 @@ func GetDeliverymanQueue(c *gin.Context) {
|
||||
queueInfo, err := database.GetDeliverymanQueueInfo(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération de la queue",
|
||||
"details": err.Error(),
|
||||
"error": "Erreur récupération de la queue",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -814,41 +786,23 @@ func GetDeliverymanQueue(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VALIDATION D'ADRESSE
|
||||
// ============================================
|
||||
|
||||
// ValidateAddress vérifie si une adresse peut être géocodée
|
||||
// POST /api/v1/validate-address
|
||||
// Body: {"address": "123 Main St, Paris"}
|
||||
func ValidateAddress(c *gin.Context) {
|
||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||
|
||||
var req struct {
|
||||
Address string `json:"address" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Adresse requise",
|
||||
"details": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"})
|
||||
return
|
||||
}
|
||||
|
||||
isValid := geoService.IsValidAddress(req.Address)
|
||||
|
||||
if !isValid {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": false,
|
||||
"message": "Adresse introuvable ou invalide",
|
||||
})
|
||||
if !geoService.IsValidAddress(req.Address) {
|
||||
c.JSON(http.StatusOK, gin.H{"valid": false, "message": "Adresse introuvable ou invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer les détails
|
||||
location, _ := geoService.GeocodeAddress(req.Address)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": true,
|
||||
"message": "Adresse valide",
|
||||
@@ -858,13 +812,7 @@ func ValidateAddress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTION - CALCUL ETA AVEC TOMTOM
|
||||
// ============================================
|
||||
|
||||
// calculateTravelTimeWithTomTom calcule l'ETA avec TomTom ou fallback local
|
||||
func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanUsername string, targetLat, targetLon float64) (int, float64, error) {
|
||||
// Récupérer position du livreur
|
||||
deliverymanLoc, err := geoService.GetDeliveryPersonLocation(deliverymanUsername)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("position du livreur introuvable: %w", err)
|
||||
@@ -875,10 +823,8 @@ func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanU
|
||||
Longitude: targetLon,
|
||||
}
|
||||
|
||||
// Calculer ETA avec TomTom (avec fallback automatique intégré)
|
||||
travelTime, distance, err := services.GetETAWithTraffic(*deliverymanLoc, targetCoords)
|
||||
if err != nil {
|
||||
// Fallback sur calcul local
|
||||
distance = services.CalculateDistance(*deliverymanLoc, targetCoords)
|
||||
travelTime = services.CalculateETA(distance)
|
||||
log.Printf("⚠️ TomTom indisponible pour %s, fallback: %.2f km -> %d min",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,6 @@ func GetDeliveryPersonMapLinks(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"success": false,
|
||||
"error": "Position GPS non disponible pour ce livreur",
|
||||
"details": err.Error(),
|
||||
"message": "Le livreur n'a pas encore partagé sa position ou est hors ligne",
|
||||
})
|
||||
return
|
||||
@@ -117,7 +117,6 @@ func GetCommandNavigationLinks(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur génération des liens",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -41,7 +42,6 @@ func GetMyCompletedOrders(c *gin.Context) {
|
||||
log.Printf("❌ [HISTORY] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération de l'historique",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -120,7 +120,6 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
|
||||
log.Printf("❌ [HISTORY_DETAILED] Erreur: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération de l'historique",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"gestion/services"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -32,7 +33,7 @@ func AddProductsBasket(c *gin.Context) {
|
||||
|
||||
var req BasketsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Requête invalide", "details": err.Error()})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,17 +63,17 @@ func AddProductsBasket(c *gin.Context) {
|
||||
}
|
||||
if err := database.DecrementProductStockByID(req.ProductID, req.Quantity); err != nil {
|
||||
log.Printf("❌ [ADD_PANIER] Erreur décrement stock product_id=%d: %v", req.ProductID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
|
||||
return
|
||||
}
|
||||
panier, err := database.AddProductInBasketByID(req.Username, req.ProductID, req.Quantity)
|
||||
if err != nil {
|
||||
log.Printf("❌ [ADD_PANIER] Erreur ajout product_id=%d qty=%.3f: %v", req.ProductID, req.Quantity, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible d'ajouter le produit au panier", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible d'ajouter le produit au panier", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Produit ajouté au panier avec succès", "panier": panier})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,11 +129,7 @@ func GetAllBaskets(c *gin.Context) {
|
||||
|
||||
baskets, err := database.GetAllProductsInBasket(username)
|
||||
if err != nil {
|
||||
log.Printf("❌ [GET_PANIER] Erreur récupération: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération du panier",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération du panier", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -165,11 +162,7 @@ func DeleteProductFromBasket(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("❌ [DEL_PANIER] Erreur JSON: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données requises manquantes",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,11 +204,7 @@ func DeleteProductFromBasket(c *gin.Context) {
|
||||
// Supprimer l'article
|
||||
err = database.DeleteProductFromBasket(req.ID)
|
||||
if err != nil {
|
||||
log.Printf("❌ [DEL_PANIER] Erreur suppression: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la suppression",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la suppression", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -259,11 +248,7 @@ func ClearBasket(c *gin.Context) {
|
||||
|
||||
err = database.ClearBasket(authUsernameStr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CLEAR_PANIER] Erreur vidage: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors du vidage du panier",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors du vidage du panier", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -305,7 +290,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
|
||||
cmd := &models.Command{DeliveryAddress: req.DeliveryAddress}
|
||||
if err := database.CheckAddress(cmd); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "corrected_address": cmd.DeliveryAddress})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse non reconnue", "corrected_address": cmd.DeliveryAddress})
|
||||
return
|
||||
}
|
||||
req.DeliveryAddress = cmd.DeliveryAddress
|
||||
@@ -323,8 +308,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
// ============================================
|
||||
items, err := database.GetBasketItems(usernameStr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CHECKOUT] Erreur récupération panier: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de récupérer le panier", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de récupérer le panier", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -487,7 +471,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
_, _ = database.CreateCryptoPayment(commandID, payResp.PaymentID.String(), payResp.Status, payResp.PriceCurrency, payResp.PayCurrency, payResp.PayAddress, priceAmt, payAmt)
|
||||
|
||||
log.Printf("✅ [CHECKOUT] Commande %d en attente paiement crypto (%s)", commandID, req.PayCurrency)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"command_id": commandID,
|
||||
"payment_method": "crypto",
|
||||
@@ -510,8 +494,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
// ============================================
|
||||
err = database.ClearBasket(usernameStr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CHECKOUT] Erreur vidage panier: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Impossible de vider le panier", "details": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de vider le panier", err)
|
||||
return
|
||||
}
|
||||
log.Printf("🧹 [CHECKOUT] Panier vidé")
|
||||
@@ -585,7 +568,8 @@ func ValidateBasket(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Notifier le client
|
||||
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Un livreur est en route.", commandID)
|
||||
clientOrderID := database.GetClientOrderID(commandID)
|
||||
clientMsg := fmt.Sprintf("Ta commande #%d est prise en compte ! Merci de rester branché et vigilant sur les notifs à venir.", clientOrderID)
|
||||
database.NotifyClient(usernameStr, commandID, "assigned", clientMsg)
|
||||
|
||||
assigned = true
|
||||
@@ -611,12 +595,13 @@ func ValidateBasket(c *gin.Context) {
|
||||
// ============================================
|
||||
newBalance, _ := database.GetClientReferralBalance(usernameStr)
|
||||
resp := gin.H{
|
||||
"success": true,
|
||||
"command_id": commandID,
|
||||
"delivery_address": req.DeliveryAddress,
|
||||
"status": "pending",
|
||||
"referral_used": referralUsed,
|
||||
"referral_balance": newBalance,
|
||||
"success": true,
|
||||
"command_id": commandID,
|
||||
"client_order_number": command.ClientOrderID,
|
||||
"delivery_address": req.DeliveryAddress,
|
||||
"status": "pending",
|
||||
"referral_used": referralUsed,
|
||||
"referral_balance": newBalance,
|
||||
}
|
||||
|
||||
if assigned {
|
||||
@@ -631,7 +616,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
log.Printf("✅ [CHECKOUT] Réponse 200 - Commande %d en attente", commandID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
c.JSON(http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
// getBaseURL construit l'URL de base depuis la requête en cours
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/services"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -94,10 +95,7 @@ func AutoAssignNextCommand(c *gin.Context) {
|
||||
|
||||
err = database.AutoAssignCommand(nextCommand.CommandID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de l'assignation automatique",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de l'assignation automatique", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,12 +135,8 @@ func UpdateLivreurLocation(c *gin.Context) {
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides - latitude et longitude requises",
|
||||
"details": err.Error(),
|
||||
"format": gin.H{
|
||||
"latitude": "number (required)",
|
||||
"longitude": "number (required)",
|
||||
},
|
||||
"error": "Données invalides - latitude et longitude requises",
|
||||
"format": gin.H{"latitude": "number (required)", "longitude": "number (required)"},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -169,10 +163,7 @@ func UpdateLivreurLocation(c *gin.Context) {
|
||||
// ✅ 1. Mettre à jour la position GPS dans Redis
|
||||
err := database.UpdateDeliveryPersonLocation(usernameStr, req.Latitude, req.Longitude)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la mise à jour de la position",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la mise à jour de la position", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -251,7 +242,6 @@ func GetMyLocation(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Position non disponible",
|
||||
"details": err.Error(),
|
||||
"message": "Veuillez d'abord mettre à jour votre position",
|
||||
})
|
||||
return
|
||||
@@ -286,10 +276,7 @@ func GetDeliveryPersonLocation(c *gin.Context) {
|
||||
|
||||
position, err := database.GetLivreurPosition(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Position non trouvée pour ce livreur",
|
||||
"details": err.Error(),
|
||||
})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Position non trouvée pour ce livreur"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -373,7 +360,6 @@ func GetDeliverymanLocationForCommand(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Position du livreur non disponible",
|
||||
"details": err.Error(),
|
||||
"message": "Le livreur n'a pas encore partagé sa position",
|
||||
"command_info": gin.H{
|
||||
"command_id": commandID,
|
||||
@@ -501,10 +487,7 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -531,10 +514,7 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
|
||||
|
||||
err := database.SetDeliveryPersonStatus(usernameStr, req.Status, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la mise à jour du statut",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la mise à jour du statut", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -613,10 +593,7 @@ func GetMyQueue(c *gin.Context) {
|
||||
|
||||
queueInfo, err := database.GetDeliverymanQueueInfo(usernameStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur récupération de la queue",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur récupération de la queue", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -640,10 +617,7 @@ func GetAvailableDeliveryPersonsRealtime(c *gin.Context) {
|
||||
|
||||
livreurs, err := database.GetAvailableDeliveryPersonsRedis()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des livreurs",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération des livreurs", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -687,10 +661,7 @@ func SetCommandETAHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.BindErr(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -724,10 +695,7 @@ func SetCommandETAHandler(c *gin.Context) {
|
||||
// Mettre à jour l'ETA dans Redis
|
||||
err = database.SetCommandETA(commandID, req.ETAMinutes)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la mise à jour de l'ETA",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la mise à jour de l'ETA", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -830,10 +798,7 @@ func GetMyPenalties(c *gin.Context) {
|
||||
// ✅ UTILISE LA MÉTHODE DÉDIÉE GetClientPenaltiesInfo
|
||||
penaltiesInfo, err := database.GetClientPenaltiesInfo(usernameStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des pénalités",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -869,10 +834,7 @@ func GetClientPenaltiesAdmin(c *gin.Context) {
|
||||
// ✅ UTILISE GetClientPenaltiesInfo
|
||||
penaltiesInfo, err := database.GetClientPenaltiesInfo(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des pénalités",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -896,10 +858,7 @@ func GetAllClientsWithPenalties(c *gin.Context) {
|
||||
// ✅ UTILISE GetAllClientsWithPenalties
|
||||
clients, err := database.GetAllClientsWithPenalties()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des clients",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération des clients", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -926,10 +885,7 @@ func GetPenaltiesStats(c *gin.Context) {
|
||||
// ✅ UTILISE GetClientPenaltiesStats
|
||||
stats, err := database.GetClientPenaltiesStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la récupération des statistiques",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la récupération des statistiques", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -970,10 +926,7 @@ func ResetClientPointAdmin(c *gin.Context) {
|
||||
|
||||
err := database.ResetClientPoint(username, req.Pool, extraPoolKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la réinitialisation",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1008,10 +961,7 @@ func ResetClientPenaltiesAdmin(c *gin.Context) {
|
||||
// ✅ UTILISE ResetClientPenalties
|
||||
err := database.ResetClientPenalties(username, req.ResetCancellationsCount)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur lors de la réinitialisation",
|
||||
"details": err.Error(),
|
||||
})
|
||||
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -26,7 +27,7 @@ func GetMyReferralBalance(c *gin.Context) {
|
||||
|
||||
balance, err := database.GetClientReferralBalance(username.(string))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Solde de parrainage introuvable"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,8 +48,7 @@ func CreditClientReferralAdmin(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := database.CreditClientReferral(targetUsername, req.Amount); err != nil {
|
||||
log.Printf("❌ [REFERRAL] Crédit échoué pour %s: %v", targetUsername, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.ServerErr(c, "Impossible de créditer le solde", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func GetClientReferralAdmin(c *gin.Context) {
|
||||
|
||||
balance, err := database.GetClientReferralBalance(targetUsername)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Client introuvable"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ func GetPublicSettings(c *gin.Context) {
|
||||
"pool_names": poolNames,
|
||||
"pool_keys": poolKeys,
|
||||
"referral_enabled": settings.ReferralEnabled,
|
||||
"referral_amount": settings.ReferralAmount,
|
||||
"delivery_schedule": settings.DeliverySchedule,
|
||||
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
|
||||
"crypto_only": settings.CryptoOnly,
|
||||
|
||||
@@ -34,7 +34,6 @@ func UpdateMyProfile(c *gin.Context) {
|
||||
log.Printf("❌ [UPDATE_MY_PROFILE] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -186,7 +185,6 @@ func UpdateClientByAdmin(c *gin.Context) {
|
||||
log.Printf("❌ [UPDATE_CLIENT_ADMIN] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Données invalides",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -56,7 +57,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
|
||||
log.Printf("❌ [VALIDATE_LIVREUR] Erreur JSON: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Coordonnées GPS requises",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -99,7 +99,6 @@ func ValidateDeliveryByLivreur(c *gin.Context) {
|
||||
log.Printf("❌ [VALIDATE_LIVREUR] Erreur update statut: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur validation",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -299,7 +298,6 @@ func StartDelivery(c *gin.Context) {
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Coordonnées GPS requises",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -336,7 +334,6 @@ func StartDelivery(c *gin.Context) {
|
||||
if err := database.UpdateCommandStatus(commandID, "en_route"); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Erreur mise à jour statut",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -354,7 +351,7 @@ func StartDelivery(c *gin.Context) {
|
||||
|
||||
// Notifier le client
|
||||
if clientUsername, _ := command["username"].(string); clientUsername != "" {
|
||||
msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
|
||||
msg := fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(commandID))
|
||||
database.NotifyClient(clientUsername, commandID, "en_route", msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +47,6 @@ type AdminClaims struct {
|
||||
var (
|
||||
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients
|
||||
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur
|
||||
roleHierarchy = map[string][]string{
|
||||
"admin": {"admin", "cabine", "livreur", "client"},
|
||||
"cabine": {"cabine", "livreur", "client"},
|
||||
"livreur": {"livreur", "client"},
|
||||
}
|
||||
)
|
||||
|
||||
// ============================================
|
||||
@@ -68,7 +63,7 @@ func validateClientToken(tokenString string, database *db.Database) (*ClientClai
|
||||
log.Printf("🔍 [VALIDATE-CLIENT] Validating client token...")
|
||||
|
||||
// Parser JWT EN PREMIER avec userJWTSecret (CLIENT)
|
||||
token, err := jwt.ParseWithClaims(tokenString, &ClientClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &ClientClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// Vérifier explicitement l'algorithme
|
||||
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
|
||||
@@ -116,8 +111,7 @@ func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims
|
||||
|
||||
log.Printf("🔍 [VALIDATE-ADMIN] Validating admin token...")
|
||||
|
||||
// Parser JWT EN PREMIER avec adminJWTSecret (ADMIN/CABINE/LIVREUR)
|
||||
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &AdminClaims{}, func(token *jwt.Token) (any, error) {
|
||||
if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Method.Alg())
|
||||
}
|
||||
@@ -155,11 +149,6 @@ func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MIDDLEWARE AUTHENTIFICATION CLIENT
|
||||
// ============================================
|
||||
|
||||
// ClientMiddleware valide le JWT d'un client
|
||||
func ClientMiddleware(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
@@ -180,7 +169,6 @@ func ClientMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ NOUVEAU : Vérifier que le token n'a pas été révoqué
|
||||
valid, err := database.IsTokenValid(tokenStr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [CLIENT-MWARE] Erreur vérification token DB: %v", err)
|
||||
@@ -195,7 +183,6 @@ func ClientMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Stocker les infos du client dans le contexte
|
||||
c.Set("client_id", claims.ClientID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
@@ -206,12 +193,6 @@ func ClientMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MIDDLEWARE AUTHENTIFICATION ADMIN
|
||||
// ============================================
|
||||
|
||||
// AdminMiddleware valide le JWT d'un admin (role == "admin" SEULEMENT)
|
||||
// ✅ Remplace le AdminMiddleware de handlers/auth.go
|
||||
func AdminMiddleware(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
@@ -232,7 +213,6 @@ func AdminMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Check révocation
|
||||
valid, err := database.IsTokenValid(tokenStr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [ADMIN-MWARE] Erreur vérification token DB: %v", err)
|
||||
@@ -247,23 +227,14 @@ func AdminMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier rôle
|
||||
validRoles := []string{"admin", "cabine", "livreur"}
|
||||
isValid := false
|
||||
for _, role := range validRoles {
|
||||
if claims.Role == role {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValid {
|
||||
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s", claims.Role)
|
||||
// Vérifier rôle — admin uniquement
|
||||
if claims.Role != "admin" {
|
||||
log.Printf("❌ [ADMIN-MWARE] Role invalide: %s (admin requis)", claims.Role)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès admin requis"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Stocker les infos de l'admin dans le contexte
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
@@ -275,12 +246,6 @@ func AdminMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MIDDLEWARE AUTHENTIFICATION CABINE
|
||||
// ============================================
|
||||
|
||||
// CabineMiddleware valide que l'utilisateur a accès à la cabine
|
||||
// ✅ Remplace le CabineMiddleware de handlers/auth.go
|
||||
func CabineMiddleware(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
@@ -316,22 +281,10 @@ func CabineMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier hiérarchie des rôles
|
||||
allowedRoles := roleHierarchy[claims.Role]
|
||||
authorized := false
|
||||
for _, r := range allowedRoles {
|
||||
if r == "cabine" {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
// Vérifier rôle — admin ou cabine uniquement
|
||||
if claims.Role != "admin" && claims.Role != "cabine" {
|
||||
log.Printf("❌ [CABINE-MWARE] Role non autorisé: %s", claims.Role)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Droits insuffisants - Accès cabine requis",
|
||||
"your_role": claims.Role,
|
||||
"allowed_roles": "admin, cabine",
|
||||
})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès cabine requis"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -347,12 +300,6 @@ func CabineMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MIDDLEWARE AUTHENTIFICATION LIVREUR
|
||||
// ============================================
|
||||
|
||||
// LivreurMiddleware valide que l'utilisateur est livreur
|
||||
// ✅ Remplace le LivreurMiddleware de handlers/auth.go
|
||||
func LivreurMiddleware(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
@@ -388,22 +335,10 @@ func LivreurMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier hiérarchie des rôles
|
||||
allowedRoles := roleHierarchy[claims.Role]
|
||||
authorized := false
|
||||
for _, r := range allowedRoles {
|
||||
if r == "livreur" {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
// Vérifier rôle — admin ou livreur uniquement
|
||||
if claims.Role != "admin" && claims.Role != "livreur" {
|
||||
log.Printf("❌ [LIVREUR-MWARE] Role non autorisé: %s", claims.Role)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Droits insuffisants - Accès livreur requis",
|
||||
"your_role": claims.Role,
|
||||
"allowed_roles": "admin, livreur",
|
||||
})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Droits insuffisants - Accès livreur requis"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -419,11 +354,6 @@ func LivreurMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SESSION MIDDLEWARE CLIENT (Existant)
|
||||
// ============================================
|
||||
|
||||
// ClientSessionMiddleware valide la session Redis du client
|
||||
func ClientSessionMiddleware(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -507,17 +437,10 @@ func ClientSessionMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RATE LIMITING MIDDLEWARE
|
||||
// ============================================
|
||||
|
||||
// RateLimitMiddleware limite le nombre de requêtes par client
|
||||
// Config: 100 requêtes par minute par client
|
||||
func RateLimitMiddleware(c *gin.Context) {
|
||||
// Récupérer le client_id
|
||||
clientID, ok := c.Get("client_id")
|
||||
if !ok {
|
||||
// Pas de client_id (requête publique), pas de rate limit
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -533,12 +456,10 @@ func RateLimitMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialiser le TTL à la première requête
|
||||
if count == 1 {
|
||||
db.Redis.Expire(db.RedisCtx, rateLimitKey, 60*time.Second) // 1 minute
|
||||
}
|
||||
|
||||
// Vérifier si dépassement (100 requêtes/min)
|
||||
if count > 100 {
|
||||
log.Printf("❌ [RATELIMIT] Client %d dépassé le limite: %d requêtes/min", clientIDInt, count)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
@@ -548,7 +469,6 @@ func RateLimitMiddleware(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ajouter le header du remaining
|
||||
c.Header("X-RateLimit-Remaining", strconv.FormatInt(100-count, 10))
|
||||
|
||||
log.Printf("📊 [RATELIMIT] Client %d: %d/%d requêtes", clientIDInt, count, 100)
|
||||
@@ -556,6 +476,40 @@ func RateLimitMiddleware(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// LoginRateLimitMiddleware limite les tentatives de connexion par IP.
|
||||
// Config: 10 tentatives par 15 minutes.
|
||||
func LoginRateLimitMiddleware(c *gin.Context) {
|
||||
ip := c.GetHeader("X-Real-IP")
|
||||
if ip == "" {
|
||||
ip = c.ClientIP()
|
||||
}
|
||||
|
||||
rateLimitKey := "ratelimit:login:" + ip
|
||||
|
||||
count, err := db.Redis.Incr(db.RedisCtx, rateLimitKey).Result()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [LOGIN-RATELIMIT] Erreur Redis: %v", err)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
db.Redis.Expire(db.RedisCtx, rateLimitKey, 15*time.Minute)
|
||||
}
|
||||
|
||||
if count > 10 {
|
||||
log.Printf("❌ [LOGIN-RATELIMIT] IP %s bloquée: %d tentatives/15min", ip, count)
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "Trop de tentatives de connexion - Réessayez dans 15 minutes",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("X-RateLimit-Remaining", strconv.FormatInt(10-count, 10))
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER MIDDLEWARE
|
||||
// ============================================
|
||||
@@ -640,11 +594,6 @@ func LoadClientContext(c *gin.Context, database *db.Database) (*db.SessionData,
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATABASE MIDDLEWARE
|
||||
// ============================================
|
||||
|
||||
// DatabaseMiddleware injecte la base de données dans le contexte
|
||||
func DatabaseMiddleware(db *db.Database) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("database", db)
|
||||
|
||||
@@ -4,20 +4,23 @@ package models
|
||||
import "time"
|
||||
|
||||
type Client struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username" json:"username"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
Nom string `gorm:"column:nom" json:"nom"`
|
||||
Prenom string `gorm:"column:prenom" json:"prenom"`
|
||||
Telephone string `gorm:"column:telephone" json:"telephone"`
|
||||
Command int `gorm:"column:command" json:"command"`
|
||||
PointsExtra map[string]int `gorm:"-" json:"points_extra"`
|
||||
Amende float64 `gorm:"column:amende" json:"amende"`
|
||||
CancellationsCount int `gorm:"column:cancellations_count" json:"cancellations_count"`
|
||||
LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"`
|
||||
MustChangePassword bool `gorm:"column:must_change_password" json:"must_change_password"`
|
||||
ReferralBalance float64 `gorm:"column:referral_balance" json:"referral_balance"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;not null" json:"username"`
|
||||
Password string `gorm:"column:password" json:"-"`
|
||||
Nom string `gorm:"column:nom" json:"nom"`
|
||||
Prenom string `gorm:"column:prenom" json:"prenom"`
|
||||
Telephone string `gorm:"column:telephone;not null" json:"telephone"`
|
||||
Command int `gorm:"column:command;default:0" json:"command"`
|
||||
CancelCommande int `gorm:"column:cancel_commande;default:0" json:"cancel_commande"`
|
||||
Amende float64 `gorm:"column:amende;default:0" json:"amende"`
|
||||
CancellationsCount int `gorm:"column:cancellations_count;not null;default:0" json:"cancellations_count"`
|
||||
LastPenaltyReason string `gorm:"column:last_penalty_reason" json:"last_penalty_reason"`
|
||||
MustChangePassword bool `gorm:"column:must_change_password;default:false" json:"must_change_password"`
|
||||
ReferralBalance float64 `gorm:"column:referral_balance;default:0" json:"referral_balance"`
|
||||
// points_extra est un JSONB géré manuellement (type non supporté nativement par GORM)
|
||||
PointsExtra map[string]int `gorm:"-" json:"points_extra"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Client) TableName() string { return "clients" }
|
||||
|
||||
@@ -3,28 +3,43 @@ package models
|
||||
import "time"
|
||||
|
||||
type Command struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Status string `json:"status"` // "pending", "assigned", "livre", "approved", "cancelled", "disabled"
|
||||
Total float64 `json:"total"`
|
||||
DeliveryAddress string `json:"delivery_address"` // Adresse de livraison pour cette commande
|
||||
LivreurAssign string `json:"livreur_assign,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
ClientOrderID int `gorm:"column:client_order_id" json:"client_order_id"`
|
||||
UserID int `gorm:"column:user_id" json:"user_id"`
|
||||
Username string `gorm:"column:username" json:"username"`
|
||||
Status string `gorm:"column:status" json:"status"`
|
||||
Total float64 `gorm:"column:total_prix" json:"total"`
|
||||
DeliveryAddress string `gorm:"column:adresse" json:"delivery_address"`
|
||||
LivreurAssign string `gorm:"column:livreur_assign" json:"livreur_assign,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Command) TableName() string { return "commandes" }
|
||||
|
||||
// CommandItem représente un produit dans une commande
|
||||
type CommandItem struct {
|
||||
ID int `json:"id"`
|
||||
CommandID int `json:"command_id"`
|
||||
ProductID int `json:"product_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
Price float64 `json:"price"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
CommandID int `gorm:"column:command_id" json:"command_id"`
|
||||
Produit string `gorm:"column:produit" json:"produit"`
|
||||
ProductID int `gorm:"column:product_id" json:"product_id"`
|
||||
Quantity float64 `gorm:"column:quantite" json:"quantity"`
|
||||
Price float64 `gorm:"column:prix" json:"price"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
type CommandLog struct {
|
||||
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
CommandID int `gorm:"column:command_id" json:"command_id"`
|
||||
Status string `gorm:"column:status" json:"status"`
|
||||
Message string `gorm:"column:message" json:"message"`
|
||||
Author string `gorm:"column:author" json:"author"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (CommandLog) TableName() string { return "command_logs" }
|
||||
|
||||
type CommandPriority struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
|
||||
@@ -6,7 +6,7 @@ import "time"
|
||||
type CryptoPayment struct {
|
||||
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
CommandID int `json:"command_id" gorm:"column:command_id;index"`
|
||||
NowPaymentID string `json:"nowpayment_id" gorm:"column:nowpayment_id;uniqueIndex"`
|
||||
NowPaymentID string `json:"nowpayment_id" gorm:"column:nowpayment_id;not null"`
|
||||
Status string `json:"status" gorm:"column:status"`
|
||||
PriceAmount float64 `json:"price_amount" gorm:"column:price_amount"`
|
||||
PriceCurrency string `json:"price_currency" gorm:"column:price_currency"`
|
||||
|
||||
@@ -67,6 +67,7 @@ type AppSettings struct {
|
||||
PointsEnabled bool `json:"points_enabled"` // afficher/activer le système de points
|
||||
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
|
||||
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
|
||||
ReferralAmount float64 `json:"referral_amount"` // montant crédité par parrainage
|
||||
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
|
||||
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
|
||||
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
|
||||
|
||||
@@ -3,12 +3,14 @@ package models
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username" gorm:"column:username;uniqueIndex"`
|
||||
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username" gorm:"column:username;not null"`
|
||||
Password string `json:"password,omitempty" gorm:"column:password"`
|
||||
Role string `json:"role" gorm:"column:role"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
Role string `json:"role" gorm:"column:role"`
|
||||
Total float64 `json:"total" gorm:"column:total;default:0"`
|
||||
Livraison float64 `json:"livraison" gorm:"column:livraison;default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "users" }
|
||||
|
||||
@@ -32,7 +32,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
// ============================================
|
||||
authGroupV1 := router.Group("/api/v1/auth")
|
||||
{
|
||||
authGroupV1.POST("/login", handlers.LoginClient)
|
||||
authGroupV1.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginClient)
|
||||
authGroupV1.POST("/logout", handlers.LogoutClient)
|
||||
}
|
||||
|
||||
@@ -61,21 +61,22 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
cartGroupV1 := router.Group("/api/v1")
|
||||
cartGroupV1.Use(middleware.ClientMiddleware)
|
||||
cartGroupV1.Use(middleware.ClientSessionMiddleware)
|
||||
cartGroupV1.Use(middleware.RateLimitMiddleware)
|
||||
{
|
||||
// Panier
|
||||
cartGroupV1.POST("/panier/add", handlers.AddProductsBasket)
|
||||
cartGroupV1.GET("/panier/:username", handlers.GetAllBaskets)
|
||||
cartGroupV1.DELETE("/panier/remove", handlers.DeleteProductFromBasket)
|
||||
cartGroupV1.DELETE("/panier/clear", handlers.ClearBasket) // ✅ CORRIGÉ - Sans :username
|
||||
cartGroupV1.DELETE("/panier/clear", handlers.ClearBasket)
|
||||
|
||||
// Commandes
|
||||
cartGroupV1.POST("/checkout", middleware.OrderHoursMiddleware, middleware.BlockClientIfPenalty, handlers.ValidateBasket) // ✅ Auto-assign GPS
|
||||
cartGroupV1.GET("/my-commands", handlers.GetMyCommandsWithTracking) // ✅ Avec suivi
|
||||
|
||||
// ⭐ NOUVEAUX - SUIVI CLIENT TEMPS RÉEL
|
||||
cartGroupV1.GET("/commands/:id/eta", handlers.GetOrderETA) // ✅ AJOUTÉ
|
||||
cartGroupV1.GET("/commands/:id/status", handlers.GetCommandStatus) // ✅ AJOUTÉ
|
||||
cartGroupV1.GET("/commands/:id/tracking", handlers.GetCommandTracking) // ✅ AJOUTÉ
|
||||
cartGroupV1.GET("/commands/:id/eta", handlers.GetOrderETA)
|
||||
cartGroupV1.GET("/commands/:id/status", handlers.GetCommandStatus)
|
||||
cartGroupV1.GET("/commands/:id/tracking", handlers.GetCommandTracking)
|
||||
cartGroupV1.GET("/commands/:id", handlers.GetCommandByID)
|
||||
cartGroupV1.GET("/commands/:id/items", handlers.GetCommandItemsWithDetails)
|
||||
// Approbation livraison
|
||||
@@ -103,8 +104,8 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
cartGroupV1.DELETE("/telegram/unlink", handlers.UnlinkClientTelegram)
|
||||
|
||||
// 👤 PROFIL CLIENT
|
||||
cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil
|
||||
cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil
|
||||
cartGroupV1.GET("/profile", handlers.GetMyProfile) // ✅ Récupérer mon profil
|
||||
cartGroupV1.PUT("/profile/update", handlers.UpdateMyProfile) // ✅ Modifier mon profil
|
||||
|
||||
// 🎁 PARRAINAGE CLIENT
|
||||
cartGroupV1.GET("/referral/balance", handlers.GetMyReferralBalance)
|
||||
@@ -123,15 +124,6 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
// ============================================
|
||||
router.POST("/webhook/telegram", handlers.TelegramWebhook)
|
||||
|
||||
// ============================================
|
||||
// 🌍 GÉOCODAGE PUBLIC (v1)
|
||||
// ============================================
|
||||
geoGroupV1 := router.Group("/api/v1")
|
||||
{
|
||||
geoGroupV1.POST("/geocode", handlers.GeocodeAddress)
|
||||
geoGroupV1.POST("/validate-address", handlers.ValidateAddress)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 📋 PATTERN v2: ADMIN API
|
||||
// ============================================
|
||||
@@ -141,8 +133,8 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
||||
// ============================================
|
||||
adminAuthGroupV2 := router.Group("/api/v2/admin/auth")
|
||||
{
|
||||
adminAuthGroupV2.POST("/register", handlers.RegisterAdmin)
|
||||
adminAuthGroupV2.POST("/login", handlers.LoginAdmin)
|
||||
//adminAuthGroupV2.POST("/register", handlers.RegisterAdmin)
|
||||
adminAuthGroupV2.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginAdmin)
|
||||
adminAuthGroupV2.POST("/logout", handlers.LogoutAdmin)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BindErr log l'erreur de binding et renvoie 400 sans détails internes.
|
||||
func BindErr(c *gin.Context, err error) {
|
||||
log.Printf("⚠️ [BIND] %s %s: %v", c.Request.Method, c.Request.URL.Path, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
}
|
||||
|
||||
// ServerErr log l'erreur interne et renvoie 500 avec un message générique.
|
||||
func ServerErr(c *gin.Context, msg string, err error) {
|
||||
log.Printf("❌ [SERVER] %s %s — %s: %v", c.Request.Method, c.Request.URL.Path, msg, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
|
||||
}
|
||||
@@ -189,7 +189,8 @@ func tryAssignCommandWithPriority(
|
||||
|
||||
// 9. Notifier le client
|
||||
if clientUsername != "" {
|
||||
clientMsg := fmt.Sprintf("Votre commande #%d est confirmée ! Un livreur est en route.", commandID)
|
||||
clientOrderID := database.GetClientOrderID(commandID)
|
||||
clientMsg := fmt.Sprintf("Ta commande #%d est prise en compte ! Merci de rester branché et vigilant sur les notifs à venir.", clientOrderID)
|
||||
database.NotifyClient(clientUsername, commandID, "assigned", clientMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ function ProductDetail() {
|
||||
<div className="product-detail-content">
|
||||
<div className={`product-image-section ${isOutOfStock ? "out-of-stock" : ""}`}>
|
||||
<img
|
||||
src={product.image || ""}
|
||||
src={product.media?.find(m => m.type === "image")?.url || ""}
|
||||
alt={product.name}
|
||||
className="product-detail-image"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user