From f46a5d96528cbcef2332554c95c635ba24c9261e Mon Sep 17 00:00:00 2001 From: Xor290 Date: Wed, 21 Jan 2026 16:26:15 +0100 Subject: [PATCH] chore: add create user route --- backend/gestion/db/db_alert.go | 3 +- backend/gestion/docker-compose.yml | 51 ++ backend/gestion/handlers/alert.go | 12 +- backend/gestion/handlers/auth.go | 55 +- backend/gestion/routes/routes.go | 1 + frontend-prep/src/api/api.ts | 2 +- frontend-prep/src/api/api_admin.ts | 47 +- frontend-prep/src/api/api_cabine.ts | 2 +- frontend-prep/src/api/api_delivery.ts | 2 +- .../src/components/ProductDetailsModal.tsx | 612 ++++++++++-------- .../src/pages/AdminAlerts/AdminAlerts.tsx | 6 +- .../src/pages/AdminUsers/AdminUsers.css | 153 +++++ .../src/pages/AdminUsers/AdminUsers.tsx | 247 ++++++- .../src/pages/CabineAlert/CabineAlerts.css | 8 +- .../src/pages/CabineAlert/CabineAlerts.tsx | 6 +- .../src/pages/User/ProductDetail.tsx | 480 +++++++------- 16 files changed, 1160 insertions(+), 527 deletions(-) create mode 100644 backend/gestion/docker-compose.yml diff --git a/backend/gestion/db/db_alert.go b/backend/gestion/db/db_alert.go index 820d85da..282f33e9 100644 --- a/backend/gestion/db/db_alert.go +++ b/backend/gestion/db/db_alert.go @@ -68,8 +68,7 @@ func (d *Database) GetAllAlerts() ([]models.AlertPolicy, error) { func (d *Database) DeleteAlertPolicy(id int) error { query := ` - UPDATE alerte_policy - SET status = 'false', updated_at = CURRENT_TIMESTAMP + DELETE FROM alerte_policy WHERE id = $1 ` _, err := d.Exec(query, id) diff --git a/backend/gestion/docker-compose.yml b/backend/gestion/docker-compose.yml new file mode 100644 index 00000000..7820ec89 --- /dev/null +++ b/backend/gestion/docker-compose.yml @@ -0,0 +1,51 @@ +services: + postgres: + image: postgres:16 + container_name: gestion_postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + ports: + - "${DB_PORT}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - gestion-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7 + container_name: gestion_redis + restart: unless-stopped + command: + [ + "redis-server", + "--appendonly", + "yes", + "--requirepass", + "${REDIS_PASSWORD}", + ] + ports: + - "${REDIS_PORT}:6379" + volumes: + - redis_data:/data + networks: + - gestion-net + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +networks: + gestion-net: + +volumes: + postgres_data: + redis_data: diff --git a/backend/gestion/handlers/alert.go b/backend/gestion/handlers/alert.go index 4d70e01a..34a062f3 100644 --- a/backend/gestion/handlers/alert.go +++ b/backend/gestion/handlers/alert.go @@ -46,7 +46,11 @@ func DeleteAlert(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'alerte invalide"}) return } - + userRole := c.GetString("role") + if userRole != "livreur" && userRole != "admin" { + 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()}) @@ -68,7 +72,11 @@ func GetAlert(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'alerte invalide"}) return } - + userRole := c.GetString("role") + if userRole != "livreur" && userRole != "admin" && userRole != "cabine" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } alert, err := database.GetAlertPolicy(alertID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) diff --git a/backend/gestion/handlers/auth.go b/backend/gestion/handlers/auth.go index 743368a7..579e6277 100644 --- a/backend/gestion/handlers/auth.go +++ b/backend/gestion/handlers/auth.go @@ -607,8 +607,14 @@ func HealthCheck(c *gin.Context) { // GetAllUsers récupère tous les utilisateurs (Admin only) func GetAllUsers(c *gin.Context) { database := c.MustGet("database").(*db.Database) + userRole := c.GetString("role") + if userRole != "admin" && userRole != "cabine" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } users, err := database.GetAllUsers() + if err != nil { log.Printf("❌ [GET_ALL_USERS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération"}) @@ -634,7 +640,11 @@ func GetAllUsers(c *gin.Context) { func GetAllDeliveryMen(c *gin.Context) { database := c.MustGet("database").(*db.Database) - + userRole := c.GetString("role") + if userRole != "cabine" && userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } users, err := database.GetAllDeliveryMen() if err != nil { log.Printf("❌ [GET_ALL_USERS] Erreur: %v", err) @@ -662,7 +672,11 @@ func GetAllDeliveryMen(c *gin.Context) { // GET /api/v1/admin/clients func GetAllClients(c *gin.Context) { database := c.MustGet("database").(*db.Database) - + userRole := c.GetString("role") + if userRole != "cabine" && userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } clients, err := database.GetAllClients() if err != nil { log.Printf("❌ [GET_ALL_CLIENTS] Erreur: %v", err) @@ -705,7 +719,11 @@ func DeleteUser(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } - + userRole := c.GetString("role") + if userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } err = database.DeleteUser(id) if err != nil { log.Printf("❌ [DELETE_USER] Erreur: %v", err) @@ -728,7 +746,11 @@ func DeleteClient(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } - + userRole := c.GetString("role") + if userRole != "cabine" && userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } err = database.DeleteClient(id) if err != nil { log.Printf("❌ [DELETE_CLIENT] Erreur: %v", err) @@ -739,3 +761,28 @@ func DeleteClient(c *gin.Context) { log.Printf("✅ [DELETE_CLIENT] Client %d supprimé", id) c.JSON(http.StatusOK, gin.H{"message": "Client supprimé"}) } + +func CreateUser(c *gin.Context) { + database := c.MustGet("database").(*db.Database) + + var user models.User + if err := c.ShouldBindJSON(&user); err != nil { + log.Printf("❌ [CREATE_USER] Erreur de liaison JSON: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Erreur de liaison JSON"}) + return + } + userRole := c.GetString("role") + if userRole != "cabine" && userRole != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs et aux administrateurs"}) + return + } + err := database.CreateUser(&user) + if err != nil { + log.Printf("❌ [CREATE_USER] Erreur: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création"}) + return + } + + log.Printf("✅ [CREATE_USER] Utilisateur %d créé", user.ID) + c.JSON(http.StatusCreated, gin.H{"message": "Utilisateur créé"}) +} diff --git a/backend/gestion/routes/routes.go b/backend/gestion/routes/routes.go index ff171efe..a46a3cbf 100644 --- a/backend/gestion/routes/routes.go +++ b/backend/gestion/routes/routes.go @@ -127,6 +127,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services adminGroupV2.PUT("/users/:id", handlers.UpdateUserByAdmin) // ✅ Modifier un user adminGroupV2.DELETE("/clients/:id", handlers.DeleteClient) adminGroupV2.DELETE("/users/:id", handlers.DeleteUser) + adminGroupV2.POST("/users", handlers.CreateUser) // Get location livreur adminGroupV2.GET("/commands/:id/deliveryman/location", handlers.GetDeliverymanLocationForCommand) // ============================================ diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index 3681e2ca..964ef90f 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -5,7 +5,7 @@ // ✅ loginUser et registerUser retournent AuthResponse // ✅ sessionStorage (pas localStorage) -const API_URL = "/api/v1"; +const API_URL = "http://localhost:8080/api/v1"; import type { ConfirmReceptionResponse, CheckoutCartResponse, diff --git a/frontend-prep/src/api/api_admin.ts b/frontend-prep/src/api/api_admin.ts index 4229774e..c9058a56 100644 --- a/frontend-prep/src/api/api_admin.ts +++ b/frontend-prep/src/api/api_admin.ts @@ -15,7 +15,7 @@ import type { DeliveryPersonDetails, DeliveryPersonStats, } from "./api_admin_types"; -const API_URL = "/api/v2"; +const API_URL = "http://localhost:8080/api/v2"; // ============================================ // 🔐 TYPES - ADMIN @@ -2583,6 +2583,50 @@ export const deleteAlertAdmin = async ( } }; +export const CreateUser = async ( + username: string, + password: string, + role: string, +) => { + try { + const token = sessionStorage.getItem("admin_token"); + const response = await fetch(`${API_URL}/admin/protected/users`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ username, password, role }), + }); + + const data = await response.json(); + + if (!response.ok) { + console.error("❌ [CREATE_USER] Erreur API:", data); + return { + success: false, + error: data.error || "Erreur création utilisateur", + }; + } + + console.log("✅ [CREATE_USER] Utilisateur créé avec succès"); + + return { + success: true, + message: data.message || "Utilisateur créé avec succès", + }; + } catch (error) { + console.error("❌ [CREATE_USER] Erreur fetch:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Erreur réseau inconnue", + }; + } +}; + // ============================================ // 🔄 EXPORT PAR DÉFAUT // ============================================ @@ -2592,6 +2636,7 @@ export default { registerAdmin, loginAdmin, logoutAdmin, + CreateUser, // JWT Utils extractAdminUsernameFromToken, diff --git a/frontend-prep/src/api/api_cabine.ts b/frontend-prep/src/api/api_cabine.ts index 4b97222b..dec754b5 100644 --- a/frontend-prep/src/api/api_cabine.ts +++ b/frontend-prep/src/api/api_cabine.ts @@ -5,7 +5,7 @@ // ✅ Utilise /api/v1/cabine/* endpoints uniquement // ✅ sessionStorage pour la persistance -const API_URL = "/api/v1/cabine"; +const API_URL = "http://localhost:8080/api/v1/cabine"; import type { DeliveryPerson, DeliveryPersonsStats, diff --git a/frontend-prep/src/api/api_delivery.ts b/frontend-prep/src/api/api_delivery.ts index 2f233d2f..c1eba814 100644 --- a/frontend-prep/src/api/api_delivery.ts +++ b/frontend-prep/src/api/api_delivery.ts @@ -4,7 +4,7 @@ // ✅ Fonctions helper pour le dashboard livreur // ✅ Utilise /api/v1/livreur/* endpoints -const API_URL = "/api/v1/livreur"; +const API_URL = "http://localhost:8080/api/v1/livreur"; // ============================================ // 🔐 TYPES - LIVREUR diff --git a/frontend-prep/src/components/ProductDetailsModal.tsx b/frontend-prep/src/components/ProductDetailsModal.tsx index be32d478..99b8ac3e 100644 --- a/frontend-prep/src/components/ProductDetailsModal.tsx +++ b/frontend-prep/src/components/ProductDetailsModal.tsx @@ -5,296 +5,358 @@ // ✅ Galerie de médias (images/vidéos) // ✅ Informations complètes -import React, { useState } from 'react'; -import { - X, - Package, - DollarSign, - Box, - Calendar, - ChevronLeft, - ChevronRight, - Image as ImageIcon, - Video as VideoIcon, - Play -} from 'lucide-react'; -import type { Product } from '../api/api_admin_types'; // ✅ CORRIGÉ -import './ProductModal.css'; +import React, { useState } from "react"; +import { + X, + Package, + DollarSign, + Box, + Calendar, + ChevronLeft, + ChevronRight, + Image as ImageIcon, + Video as VideoIcon, + Play, +} from "lucide-react"; +import type { Product } from "../api/api_admin_types"; // ✅ CORRIGÉ +import "./ProductModal.css"; interface ProductDetailsModalProps { - product: Product; - onClose: () => void; + product: Product; + onClose: () => void; } -const ProductDetailsModal: React.FC = ({ product, onClose }) => { - // ============================================ - // 📝 STATE - // ============================================ - const [currentMediaIndex, setCurrentMediaIndex] = useState(0); +const ProductDetailsModal: React.FC = ({ + product, + onClose, +}) => { + // ============================================ + // 📝 STATE + // ============================================ + const [currentMediaIndex, setCurrentMediaIndex] = useState(0); - // ============================================ - // 🎨 HELPER FUNCTIONS - // ============================================ - const getCategoryLabel = (category: string): string => { - switch (category) { - case 'weed&hash': return 'Weed & Hash'; - case 'zipette&co': return 'Zipette & Co'; - case 'gros&semi': return 'Gros & Semi'; - default: return category; - } - }; + // ============================================ + // 🎨 HELPER FUNCTIONS + // ============================================ + const getCategoryLabel = (category: string): string => { + switch (category) { + case "weed&hash": + return "Weed & Hash"; + case "zipette&co": + return "Zipette & Co"; + case "gros&semi": + return "Gros & Semi"; + default: + return category; + } + }; - const formatDate = (dateString?: string): string => { - if (!dateString) return 'N/A'; - const date = new Date(dateString); - return date.toLocaleDateString('fr-FR', { - day: '2-digit', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; + const formatDate = (dateString?: string): string => { + if (!dateString) return "N/A"; + const date = new Date(dateString); + return date.toLocaleDateString("fr-FR", { + day: "2-digit", + month: "long", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; - const hasMedia = product.media && product.media.length > 0; - const currentMedia = hasMedia && product.media ? product.media[currentMediaIndex] : null; + const hasMedia = product.media && product.media.length > 0; + const currentMedia = + hasMedia && product.media ? product.media[currentMediaIndex] : null; - const nextMedia = () => { - if (hasMedia && product.media && currentMediaIndex < product.media.length - 1) { - setCurrentMediaIndex(currentMediaIndex + 1); - } - }; + const nextMedia = () => { + if ( + hasMedia && + product.media && + currentMediaIndex < product.media.length - 1 + ) { + setCurrentMediaIndex(currentMediaIndex + 1); + } + }; - const prevMedia = () => { - if (hasMedia && currentMediaIndex > 0) { - setCurrentMediaIndex(currentMediaIndex - 1); - } - }; + const prevMedia = () => { + if (hasMedia && currentMediaIndex > 0) { + setCurrentMediaIndex(currentMediaIndex - 1); + } + }; - // ============================================ - // 🎨 RENDER - // ============================================ - return ( - <> -
-
- {/* Header */} -
-
- -
-

{product.name}

-

{getCategoryLabel(product.category)}

-
-
- -
- - {/* Content */} -
- {/* Galerie de médias */} - {hasMedia && ( -
-
- {currentMedia?.type === 'image' ? ( - {product.name} - ) : currentMedia?.type === 'video' ? ( -
- -
- ) : ( -
- -

Aucun média disponible

-
- )} - - {/* Navigation */} - {product.media && product.media.length > 1 && ( - <> - - - - )} - - {/* Compteur */} -
- {currentMediaIndex + 1} / {product.media?.length || 0} -
-
- - {/* Miniatures */} - {product.media && product.media.length > 1 && ( -
- {product.media.map((media, index) => ( - - ))} -
- )} -
- )} - - {/* Pas de médias */} - {!hasMedia && ( -
- -

Aucun média disponible pour ce produit

-
- )} - - {/* Informations détaillées */} -
- {/* Description */} -
-

- - Description -

-

{product.description}

-
- - {/* Stock */} -
-

- - Stock Disponible -

-
0 ? 'in-stock' : 'out-of-stock'}`}> - {product.stock}g -
-
- - {/* Catégorie */} -
-

- - Catégorie -

-
- {getCategoryLabel(product.category)} -
-
- - {/* Prix */} -
-

- - Tarifs -

-
- {product.prices && product.prices.length > 0 ? ( - product.prices.map((price, index) => ( -
-
- {price.quantity}g -
-
-
- {price.price.toFixed(2)}€ -
- )) - ) : ( -

Aucun tarif défini

- )} -
-
- - {/* Statistiques médias */} - {hasMedia && product.media && ( -
-

- - Médias -

-
-
- - {product.media.filter(m => m.type === 'image').length} Image{product.media.filter(m => m.type === 'image').length > 1 ? 's' : ''} -
-
- - {product.media.filter(m => m.type === 'video').length} Vidéo{product.media.filter(m => m.type === 'video').length > 1 ? 's' : ''} -
+
-
- )} - {/* Dates */} - {product.created_at && ( -
-

- - Créé le -

-

{formatDate(product.created_at)}

-
- )} + {/* Content */} +
+ {/* Galerie de médias */} + {hasMedia && ( +
+
+ {currentMedia?.type === "image" ? ( + {product.name} + ) : currentMedia?.type === "video" ? ( +
+ +
+ ) : ( +
+ +

Aucun média disponible

+
+ )} - {product.updated_at && ( -
-

- - Modifié le -

-

{formatDate(product.updated_at)}

-
- )} -
-
+ {/* Navigation */} + {product.media && product.media.length > 1 && ( + <> + + + + )} - {/* Footer */} -
- -
-
- - ); + {/* Compteur */} +
+ {currentMediaIndex + 1} /{" "} + {product.media?.length || 0} +
+
+ + {/* Miniatures */} + {product.media && product.media.length > 1 && ( +
+ {product.media.map((media, index) => ( + + ))} +
+ )} +
+ )} + + {/* Pas de médias */} + {!hasMedia && ( +
+ +

Aucun média disponible pour ce produit

+
+ )} + + {/* Informations détaillées */} +
+ {/* Description */} +
+

+ + Description +

+

+ {product.description} +

+
+ + {/* Stock */} +
+

+ + Stock Disponible +

+
0 ? "in-stock" : "out-of-stock"}`} + > + {product.stock}g +
+
+ + {/* Catégorie */} +
+

+ + Catégorie +

+
+ {getCategoryLabel(product.category)} +
+
+ + {/* Prix */} +
+

+ + Tarifs +

+
+ {product.prices && product.prices.length > 0 ? ( + product.prices.map((price, index) => ( +
+
+ + {price.quantity}g + +
+
+
+ + {price.price.toFixed(2)}€ + +
+
+ )) + ) : ( +

+ Aucun tarif défini +

+ )} +
+
+ + {/* Statistiques médias */} + {hasMedia && product.media && ( +
+

+ + Médias +

+
+
+ + + { + product.media.filter( + (m) => m.type === "image", + ).length + }{" "} + Image + {product.media.filter( + (m) => m.type === "image", + ).length > 1 + ? "s" + : ""} + +
+
+ + + { + product.media.filter( + (m) => m.type === "video", + ).length + }{" "} + Vidéo + {product.media.filter( + (m) => m.type === "video", + ).length > 1 + ? "s" + : ""} + +
+
+
+ )} + + {/* Dates */} + {product.created_at && ( +
+

+ + Créé le +

+

+ {formatDate(product.created_at)} +

+
+ )} + + {product.updated_at && ( +
+

+ + Modifié le +

+

+ {formatDate(product.updated_at)} +

+
+ )} +
+
+ + {/* Footer */} +
+ +
+
+ + ); }; -export default ProductDetailsModal; \ No newline at end of file +export default ProductDetailsModal; diff --git a/frontend-prep/src/pages/AdminAlerts/AdminAlerts.tsx b/frontend-prep/src/pages/AdminAlerts/AdminAlerts.tsx index 4d4887a0..fbfdc8f6 100644 --- a/frontend-prep/src/pages/AdminAlerts/AdminAlerts.tsx +++ b/frontend-prep/src/pages/AdminAlerts/AdminAlerts.tsx @@ -377,21 +377,21 @@ function AdminAlerts() {
@@ -1420,6 +1547,118 @@ function AdminUsers() { )} + {showCreateModal && ( + <> +
+
+
+

Créer un nouvel utilisateur

+ +
+ +
+
+
+ + + setCreateUsername( + e.target.value, + ) + } + autoFocus + /> +
+ +
+ + + setCreatePassword( + e.target.value, + ) + } + /> +
+ +
+ + +
+
+
+ +
+ + +
+
+ + )}
{/* Modal d'édition */} diff --git a/frontend-prep/src/pages/CabineAlert/CabineAlerts.css b/frontend-prep/src/pages/CabineAlert/CabineAlerts.css index 4375f01d..49854d13 100644 --- a/frontend-prep/src/pages/CabineAlert/CabineAlerts.css +++ b/frontend-prep/src/pages/CabineAlert/CabineAlerts.css @@ -254,7 +254,7 @@ flex-wrap: wrap; } -.filter-btn { +.filter-btn-2 { padding: 0.75rem 1.5rem; background: linear-gradient( 135deg, @@ -274,7 +274,7 @@ white-space: nowrap; } -.filter-btn:hover { +.filter-btn-2:hover { background: linear-gradient( 135deg, rgba(255, 255, 255, 0.08) 0%, @@ -284,7 +284,7 @@ color: white; } -.filter-btn.active { +.filter-btn-2.active { background: linear-gradient(135deg, #ef4444, #dc2626); border-color: rgba(239, 68, 68, 0.5); color: white; @@ -862,7 +862,7 @@ padding: 0.75rem; } - .filter-btn { + .filter-btn-2 { padding: 0.65rem 1rem; font-size: 0.85rem; } diff --git a/frontend-prep/src/pages/CabineAlert/CabineAlerts.tsx b/frontend-prep/src/pages/CabineAlert/CabineAlerts.tsx index a7b74e0e..4e649c10 100644 --- a/frontend-prep/src/pages/CabineAlert/CabineAlerts.tsx +++ b/frontend-prep/src/pages/CabineAlert/CabineAlerts.tsx @@ -337,21 +337,21 @@ function CabineAlerts() {
+
+ + + ); + } + + const isOutOfStock = product.stock === 0; + const hasValidPrices = product.prices && product.prices.length > 0; - addToCart({ - product_id: product.id, - name_product: product.name, - category: product.category, - quantity: selectedGrams, - price: selectedPrice, - }); - }; - - // ✅ FIXED: Gestion correcte du type media (string[] | undefined) - const getProductImage = (product: Product): string => { - if (!product.media || product.media.length === 0) { - return '/default-product.jpg'; - } - - // ✅ product.media est de type string[] selon l'interface Product - const firstMedia = product.media[0]; - - // ✅ Vérifier si c'est une string directement ou un objet - if (typeof firstMedia === 'string') { - return firstMedia; - } - - // ✅ Si c'est un objet avec une propriété url, l'extraire - if (firstMedia && typeof firstMedia === 'object' && 'url' in firstMedia) { - const mediaUrl = (firstMedia as any).url; - return mediaUrl || '/default-product.jpg'; - } - - return '/default-product.jpg'; - }; - - if (loading) { return ( - <> - -
-
-

Chargement du produit...

-
-
- - ); - } - - if (error || !product) { - return ( - <> - -
-
-

{error || 'Produit non trouvé'}

- -
-
- - ); - } - - const isOutOfStock = product.stock === 0; - const hasValidPrices = product.prices && product.prices.length > 0; - - return ( - <> - + <> + -
- - -
- -
- {product.name} - {isOutOfStock &&
SOLD OUT
} -
- -
- -

{product.name}

- - {selectedPrice > 0 && ( -

- {selectedPrice.toFixed(2)} € {selectedGrams && `pour ${selectedGrams}g`} -

+ {/* ✅ TOAST NOTIFICATION */} + {toast.show && ( + setToast({ ...toast, show: false })} + /> )} -
-

Description

-

{product.description || 'Aucune description disponible.'}

-
- -
- {hasValidPrices && ( -
- +
+ - + {selectedPrice > 0 && ( +

+ {selectedPrice.toFixed(2)} €{" "} + {selectedGrams && `pour ${selectedGrams}g`} +

+ )} + +
+

Description

+

+ {product.description || + "Aucune description disponible."} +

+
+ +
+ {hasValidPrices && ( +
+ + + +
+ )} +
+ + +
- )}
- - - -
-
-
- - ); + + ); } -export default ProductDetail; \ No newline at end of file +export default ProductDetail;