chore: add create user route
This commit is contained in:
@@ -377,21 +377,21 @@ function AdminAlerts() {
|
||||
|
||||
<div className="alerts-filter-buttons">
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "all" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "all" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("all")}
|
||||
>
|
||||
<Filter size={16} />
|
||||
Toutes ({alerts.length})
|
||||
</button>
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "true" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "true" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("true")}
|
||||
>
|
||||
<Shield size={16} />
|
||||
Actives ({stats.active})
|
||||
</button>
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "false" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "false" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("false")}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
|
||||
@@ -1291,3 +1291,156 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
⭐ BOUTON CRÉER UTILISATEUR
|
||||
============================================ */
|
||||
.create-user-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.9rem 1.5rem;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.create-user-btn:hover {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
border-color: #5b21b6;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
.create-user-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
/* ============================================
|
||||
⭐ MODAL CRÉATION UTILISATEUR
|
||||
============================================ */
|
||||
.create-user-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 550px;
|
||||
max-height: 90vh;
|
||||
background: linear-gradient(145deg, #1f1f1f 0%, #0a0a0a 100%);
|
||||
border: 2px solid #5b21b6;
|
||||
border-radius: 24px;
|
||||
z-index: 10000;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.5),
|
||||
0 24px 80px rgba(16, 185, 129, 0.3),
|
||||
0 12px 40px rgba(0, 0, 0, 0.8),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.create-user-modal .modal-header {
|
||||
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||
border-bottom: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
.create-user-modal .modal-header h2 {
|
||||
color: white;
|
||||
}
|
||||
.create-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.form-group label {
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(20, 20, 20, 0.8),
|
||||
rgba(10, 10, 10, 0.9)
|
||||
);
|
||||
border: 2px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.2rem;
|
||||
color: #ffffff;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
}
|
||||
.form-group select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 20px;
|
||||
padding-right: 3rem;
|
||||
|
||||
/* ✅ Force le style natif du navigateur */
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ✅ STYLE DES OPTIONS - utiliser color-scheme */
|
||||
.form-group select option {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(25, 25, 25, 0.9),
|
||||
rgba(15, 15, 15, 0.95)
|
||||
);
|
||||
border-color: #5b21b6;
|
||||
box-shadow:
|
||||
inset 0 2px 6px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 4px rgba(16, 185, 129, 0.15),
|
||||
0 4px 16px rgba(16, 185, 129, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.form-group input::placeholder {
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
.form-group select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 20px;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.users-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
.create-user-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.create-user-modal {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CheckCircle,
|
||||
Leaf,
|
||||
Wind,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import AdminLayout from "../../components/AdminLayout";
|
||||
import "./AdminUsers.css";
|
||||
@@ -26,10 +27,12 @@ import {
|
||||
isAdminAuthenticated,
|
||||
deleteUserAdmin, // ⭐ AJOUTÉ
|
||||
deleteClientAdmin, // ⭐ AJOUTÉ
|
||||
CreateUser,
|
||||
} from "../../api/api_admin";
|
||||
import type { ClientResponse } from "../../api/api_admin";
|
||||
import type { AdminResponse } from "../../api/api_admin_types";
|
||||
import EditUserModal from "../../components/EditUserModal";
|
||||
import Toast from "../../components/Toast";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -72,7 +75,22 @@ function AdminUsers() {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [createUsername, setCreateUsername] = useState("");
|
||||
const [createPassword, setCreatePassword] = useState("");
|
||||
const [createRole, setCreateRole] = useState<
|
||||
"livreur" | "admin" | "cabine"
|
||||
>("admin");
|
||||
const [toast, setToast] = useState<{
|
||||
show: boolean;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}>({
|
||||
show: false,
|
||||
message: "",
|
||||
type: "success",
|
||||
});
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
// Stats
|
||||
const [stats, setStats] = useState({
|
||||
total: 0,
|
||||
@@ -181,7 +199,6 @@ function AdminUsers() {
|
||||
|
||||
// Si erreur 401, rediriger vers login
|
||||
if (error instanceof Error && error.message.includes("401")) {
|
||||
console.log("🔓 [AdminUsers] Token invalide - Redirection");
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
navigate("/login-admin/admin", { replace: true });
|
||||
@@ -193,7 +210,102 @@ function AdminUsers() {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleCreateUser = () => {
|
||||
setShowCreateModal(true);
|
||||
// Reset form
|
||||
setCreateUsername("");
|
||||
setCreatePassword("");
|
||||
setCreateRole("admin");
|
||||
};
|
||||
const confirmCreateUser = async () => {
|
||||
// Validation
|
||||
if (!createUsername.trim()) {
|
||||
setToast({
|
||||
show: true,
|
||||
message: "Le nom d'utilisateur est requis",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!createPassword.trim()) {
|
||||
setToast({
|
||||
show: true,
|
||||
message: "Le mot de passe est requis",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (createPassword.length < 6) {
|
||||
setToast({
|
||||
show: true,
|
||||
message: "Le mot de passe doit contenir au moins 6 caractères",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setCreateLoading(true);
|
||||
|
||||
try {
|
||||
console.log("➕ [CREATE] Création utilisateur:", {
|
||||
username: createUsername,
|
||||
role: createRole,
|
||||
});
|
||||
|
||||
const result = await CreateUser(
|
||||
createUsername.trim(),
|
||||
createPassword,
|
||||
createRole,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
// ✅ TOAST DE SUCCÈS
|
||||
setToast({
|
||||
show: true,
|
||||
message: `${createUsername} (${createRole}) créé avec succès !`,
|
||||
type: "success",
|
||||
});
|
||||
|
||||
// Fermer la modal
|
||||
setShowCreateModal(false);
|
||||
|
||||
// Reset form
|
||||
setCreateUsername("");
|
||||
setCreatePassword("");
|
||||
setCreateRole("admin");
|
||||
|
||||
// Recharger la liste
|
||||
if (filterRole !== "all") {
|
||||
await fetchUsersByRole(filterRole);
|
||||
} else {
|
||||
await fetchUsers();
|
||||
}
|
||||
} else {
|
||||
// ✅ TOAST D'ERREUR
|
||||
setToast({
|
||||
show: true,
|
||||
message: `Erreur: ${result.error || "Impossible de créer l'utilisateur"}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// ✅ TOAST D'ERREUR
|
||||
setToast({
|
||||
show: true,
|
||||
message: `Erreur réseau: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelCreate = () => {
|
||||
setShowCreateModal(false);
|
||||
setCreateUsername("");
|
||||
setCreatePassword("");
|
||||
setCreateRole("admin");
|
||||
};
|
||||
// ✅ NOUVELLE FONCTION: Récupérer les utilisateurs par rôle
|
||||
const fetchUsersByRole = async (role: FilterRole) => {
|
||||
// ✅ Vérifier l'auth avant de charger les données
|
||||
@@ -431,10 +543,9 @@ function AdminUsers() {
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
const labels: { [key: string]: string } = {
|
||||
client: "Client",
|
||||
livreur: "Livreur",
|
||||
admin: "Administrateur",
|
||||
cabine: "Opérateur Cabine",
|
||||
cabine: "Cabine",
|
||||
};
|
||||
return labels[role] || role;
|
||||
};
|
||||
@@ -614,9 +725,25 @@ function AdminUsers() {
|
||||
<AdminLayout>
|
||||
<div className="admin-container">
|
||||
{/* Header */}
|
||||
{toast.show && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
duration={3000}
|
||||
onClose={() => setToast({ ...toast, show: false })}
|
||||
/>
|
||||
)}
|
||||
<div className="users-header">
|
||||
<div className="header-content">
|
||||
<h1>Gestion des Utilisateurs</h1>
|
||||
{/* ⭐ NOUVEAU BOUTON */}
|
||||
<button
|
||||
className="create-user-btn"
|
||||
onClick={handleCreateUser}
|
||||
>
|
||||
<UserPlus size={20} />
|
||||
<span>Créer un utilisateur</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1420,6 +1547,118 @@ function AdminUsers() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showCreateModal && (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={cancelCreate} />
|
||||
<div className="create-user-modal">
|
||||
<div className="modal-header">
|
||||
<h2>Créer un nouvel utilisateur</h2>
|
||||
<button
|
||||
className="close-modal"
|
||||
onClick={cancelCreate}
|
||||
>
|
||||
<XCircle size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-content">
|
||||
<div className="create-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="create-username">
|
||||
Nom d'utilisateur *
|
||||
</label>
|
||||
<input
|
||||
id="create-username"
|
||||
type="text"
|
||||
placeholder="Entrez le nom d'utilisateur"
|
||||
value={createUsername}
|
||||
onChange={(e) =>
|
||||
setCreateUsername(
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="create-password">
|
||||
Mot de passe *
|
||||
</label>
|
||||
<input
|
||||
id="create-password"
|
||||
type="password"
|
||||
placeholder="Minimum 8 caractères"
|
||||
value={createPassword}
|
||||
onChange={(e) =>
|
||||
setCreatePassword(
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="create-role">
|
||||
Rôle *
|
||||
</label>
|
||||
<select
|
||||
id="create-role"
|
||||
value={createRole}
|
||||
onChange={(e) =>
|
||||
setCreateRole(
|
||||
e.target
|
||||
.value as typeof createRole,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="livreur">
|
||||
Livreur
|
||||
</option>
|
||||
<option value="cabine">
|
||||
Cabine
|
||||
</option>
|
||||
<option value="admin">
|
||||
Administrateur
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="action-button secondary"
|
||||
onClick={cancelCreate}
|
||||
disabled={createLoading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
className="action-button primary"
|
||||
onClick={confirmCreateUser}
|
||||
disabled={
|
||||
createLoading ||
|
||||
!createUsername.trim() ||
|
||||
!createPassword.trim()
|
||||
}
|
||||
>
|
||||
{createLoading ? (
|
||||
<>
|
||||
<span className="spinner" />
|
||||
<span>Création...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus size={18} />
|
||||
<span>Créer l'utilisateur</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal d'édition */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -337,21 +337,21 @@ function CabineAlerts() {
|
||||
|
||||
<div className="alerts-filter-buttons">
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "all" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "all" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("all")}
|
||||
>
|
||||
<Filter size={16} />
|
||||
Toutes ({alerts.length})
|
||||
</button>
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "true" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "true" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("true")}
|
||||
>
|
||||
<Shield size={16} />
|
||||
Actives ({stats.active})
|
||||
</button>
|
||||
<button
|
||||
className={`filter-btn ${statusFilter === "false" ? "active" : ""}`}
|
||||
className={`filter-btn-2 ${statusFilter === "false" ? "active" : ""}`}
|
||||
onClick={() => setStatusFilter("false")}
|
||||
>
|
||||
<CheckCircle size={16} />
|
||||
|
||||
@@ -1,252 +1,280 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getProductById, isUserAuthenticated } from '../../api/api';
|
||||
import type { Product } from '../../api/api';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import './ProductDetail.css';
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { getProductById, isUserAuthenticated } from "../../api/api";
|
||||
import type { Product } from "../../api/api";
|
||||
import { useCart } from "../../context/CartContext";
|
||||
import Navbar from "../../components/Navbar";
|
||||
import Toast from "../../components/Toast";
|
||||
import "./ProductDetail.css";
|
||||
|
||||
function ProductDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
|
||||
// floats
|
||||
const [selectedGrams, setSelectedGrams] = useState<number | null>(null);
|
||||
const [selectedPrice, setSelectedPrice] = useState<number>(0.0);
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [ProductDetail] Utilisateur non authentifié, redirection vers /login/client');
|
||||
navigate('/login/client', { replace: true });
|
||||
}
|
||||
};
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
checkAuth();
|
||||
}, [navigate]);
|
||||
// floats
|
||||
const [selectedGrams, setSelectedGrams] = useState<number | null>(null);
|
||||
const [selectedPrice, setSelectedPrice] = useState<number>(0.0);
|
||||
|
||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
||||
useEffect(() => {
|
||||
const authInterval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [ProductDetail] Session expirée, redirection vers /login/client');
|
||||
navigate('/login/client', { replace: true });
|
||||
}
|
||||
}, 5000);
|
||||
// ✅ TOAST STATE
|
||||
const [toast, setToast] = useState<{
|
||||
show: boolean;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}>({
|
||||
show: false,
|
||||
message: "",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) loadProduct(Number(id));
|
||||
}, [id]);
|
||||
|
||||
const loadProduct = async (productId: number) => {
|
||||
// ✅ Vérifier l'auth avant de charger le produit
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [loadProduct] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await getProductById(productId);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const fixedProduct = {
|
||||
...response.data,
|
||||
prices: response.data.prices?.map((p: { quantity: number; price: number }) => ({
|
||||
quantity: parseFloat(String(p.quantity)),
|
||||
price: parseFloat(String(p.price)),
|
||||
})) || []
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log(
|
||||
"❌ [ProductDetail] Utilisateur non authentifié, redirection vers /login/client",
|
||||
);
|
||||
navigate("/login/client", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
setProduct(fixedProduct);
|
||||
checkAuth();
|
||||
}, [navigate]);
|
||||
|
||||
// initialise le prix par défaut (float)
|
||||
if (fixedProduct.prices.length > 0) {
|
||||
setSelectedGrams(fixedProduct.prices[0].quantity);
|
||||
setSelectedPrice(fixedProduct.prices[0].price);
|
||||
// ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes)
|
||||
useEffect(() => {
|
||||
const authInterval = setInterval(() => {
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log(
|
||||
"❌ [ProductDetail] Session expirée, redirection vers /login/client",
|
||||
);
|
||||
navigate("/login/client", { replace: true });
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) loadProduct(Number(id));
|
||||
}, [id]);
|
||||
|
||||
const loadProduct = async (productId: number) => {
|
||||
// ✅ Vérifier l'auth avant de charger le produit
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [loadProduct] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
setError(response.message || 'Produit non trouvé');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Erreur lors du chargement du produit');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGramsChange = (quantity: number) => {
|
||||
const floatQty = parseFloat(String(quantity));
|
||||
setSelectedGrams(floatQty);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const priceOption = product?.prices?.find(
|
||||
p => p.quantity === floatQty
|
||||
);
|
||||
try {
|
||||
const response = await getProductById(productId);
|
||||
|
||||
if (priceOption) {
|
||||
setSelectedPrice(parseFloat(String(priceOption.price)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
// ✅ Vérifier l'auth avant d'ajouter au panier
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log('❌ [handleAddToCart] Non authentifié');
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
if (response.success && response.data) {
|
||||
const fixedProduct = {
|
||||
...response.data,
|
||||
prices:
|
||||
response.data.prices?.map(
|
||||
(p: { quantity: number; price: number }) => ({
|
||||
quantity: parseFloat(String(p.quantity)),
|
||||
price: parseFloat(String(p.price)),
|
||||
}),
|
||||
) || [],
|
||||
};
|
||||
|
||||
setProduct(fixedProduct);
|
||||
|
||||
// initialise le prix par défaut (float)
|
||||
if (fixedProduct.prices.length > 0) {
|
||||
setSelectedGrams(fixedProduct.prices[0].quantity);
|
||||
setSelectedPrice(fixedProduct.prices[0].price);
|
||||
}
|
||||
} else {
|
||||
setError(response.message || "Produit non trouvé");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Erreur lors du chargement du produit");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGramsChange = (quantity: number) => {
|
||||
const floatQty = parseFloat(String(quantity));
|
||||
setSelectedGrams(floatQty);
|
||||
|
||||
const priceOption = product?.prices?.find(
|
||||
(p) => p.quantity === floatQty,
|
||||
);
|
||||
|
||||
if (priceOption) {
|
||||
setSelectedPrice(parseFloat(String(priceOption.price)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
// ✅ Vérifier l'auth avant d'ajouter au panier
|
||||
if (!isUserAuthenticated()) {
|
||||
console.log("❌ [handleAddToCart] Non authentifié");
|
||||
navigate("/login/client", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!product || isOutOfStock || selectedGrams === null) {
|
||||
// ✅ Toast d'erreur si conditions non remplies
|
||||
setToast({
|
||||
show: true,
|
||||
message: "Veuillez sélectionner une quantité",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
addToCart({
|
||||
product_id: product.id,
|
||||
name_product: product.name,
|
||||
category: product.category,
|
||||
quantity: selectedGrams,
|
||||
price: selectedPrice,
|
||||
});
|
||||
|
||||
// ✅ Toast de succès
|
||||
setToast({
|
||||
show: true,
|
||||
message: `${product.name} (${selectedGrams}g) ajouté au panier !`,
|
||||
type: "success",
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="product-detail-container">
|
||||
<div className="loading-container">
|
||||
<p>Chargement du produit...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!product || isOutOfStock || selectedGrams === null) return;
|
||||
if (error || !product) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="product-detail-container">
|
||||
<div className="error-message">
|
||||
<h2>{error || "Produit non trouvé"}</h2>
|
||||
<button
|
||||
onClick={() => navigate("/user/accueil")}
|
||||
className="back-button"
|
||||
>
|
||||
Retour aux produits
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="product-detail-container">
|
||||
<div className="loading-container">
|
||||
<p>Chargement du produit...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !product) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="product-detail-container">
|
||||
<div className="error-message">
|
||||
<h2>{error || 'Produit non trouvé'}</h2>
|
||||
<button onClick={() => navigate('/user/accueil')} className="back-button">
|
||||
Retour aux produits
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isOutOfStock = product.stock === 0;
|
||||
const hasValidPrices = product.prices && product.prices.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<div className="product-detail-container">
|
||||
<button onClick={() => navigate(-1)} className="back-button">
|
||||
← Retour
|
||||
</button>
|
||||
|
||||
<div className="product-detail-content">
|
||||
|
||||
<div className={`product-image-section ${isOutOfStock ? 'out-of-stock' : ''}`}>
|
||||
<img
|
||||
src={getProductImage(product)}
|
||||
alt={product.name}
|
||||
className="product-detail-image"
|
||||
/>
|
||||
{isOutOfStock && <div className="sold-out-badge">SOLD OUT</div>}
|
||||
</div>
|
||||
|
||||
<div className="product-info-section">
|
||||
|
||||
<h1 className="product-detail-name">{product.name}</h1>
|
||||
|
||||
{selectedPrice > 0 && (
|
||||
<p className="product-detail-price">
|
||||
{selectedPrice.toFixed(2)} € {selectedGrams && `pour ${selectedGrams}g`}
|
||||
</p>
|
||||
{/* ✅ TOAST NOTIFICATION */}
|
||||
{toast.show && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
duration={3000}
|
||||
onClose={() => setToast({ ...toast, show: false })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="product-description">
|
||||
<h3>Description</h3>
|
||||
<p>{product.description || 'Aucune description disponible.'}</p>
|
||||
</div>
|
||||
|
||||
<div className="product-stock-info">
|
||||
{hasValidPrices && (
|
||||
<div className="grams-selector">
|
||||
<label htmlFor="grams">Quantité:</label>
|
||||
<div className="product-detail-container">
|
||||
<button onClick={() => navigate(-1)} className="back-button">
|
||||
← Retour
|
||||
</button>
|
||||
|
||||
<select
|
||||
id="grams"
|
||||
value={selectedGrams ?? ''}
|
||||
onChange={(e) => handleGramsChange(parseFloat(e.target.value))}
|
||||
className="grams-dropdown"
|
||||
disabled={isOutOfStock}
|
||||
>
|
||||
<option value="">Choisir une quantité</option>
|
||||
<div className="product-detail-content">
|
||||
<div className="product-info-section">
|
||||
<h1 className="product-detail-name">{product.name}</h1>
|
||||
|
||||
{product.prices && product.prices.map((p) => (
|
||||
<option key={p.quantity} value={p.quantity}>
|
||||
{p.quantity}g - {p.price.toFixed(2)} €
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedPrice > 0 && (
|
||||
<p className="product-detail-price">
|
||||
{selectedPrice.toFixed(2)} €{" "}
|
||||
{selectedGrams && `pour ${selectedGrams}g`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="product-description">
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
{product.description ||
|
||||
"Aucune description disponible."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="product-stock-info">
|
||||
{hasValidPrices && (
|
||||
<div className="grams-selector">
|
||||
<label htmlFor="grams">Quantité:</label>
|
||||
|
||||
<select
|
||||
id="grams"
|
||||
value={selectedGrams ?? ""}
|
||||
onChange={(e) =>
|
||||
handleGramsChange(
|
||||
parseFloat(e.target.value),
|
||||
)
|
||||
}
|
||||
className="grams-dropdown"
|
||||
disabled={isOutOfStock}
|
||||
>
|
||||
<option value="">
|
||||
Choisir une quantité
|
||||
</option>
|
||||
|
||||
{product.prices &&
|
||||
product.prices.map((p) => (
|
||||
<option
|
||||
key={p.quantity}
|
||||
value={p.quantity}
|
||||
>
|
||||
{p.quantity}g -{" "}
|
||||
{p.price.toFixed(2)} €
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`add-to-cart-button ${isOutOfStock || selectedGrams === null ? "disabled" : ""}`}
|
||||
onClick={handleAddToCart}
|
||||
disabled={isOutOfStock || selectedGrams === null}
|
||||
>
|
||||
{isOutOfStock
|
||||
? "Rupture de stock"
|
||||
: "Ajouter au panier"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`add-to-cart-button ${(isOutOfStock || selectedGrams === null) ? 'disabled' : ''}`}
|
||||
onClick={handleAddToCart}
|
||||
disabled={isOutOfStock || selectedGrams === null}
|
||||
>
|
||||
{isOutOfStock ? 'Rupture de stock' : 'Ajouter au panier'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductDetail;
|
||||
export default ProductDetail;
|
||||
|
||||
Reference in New Issue
Block a user