>
);
diff --git a/frontend-prep/src/pages/User/ProductDetail.css b/frontend-prep/src/pages/User/ProductDetail.css
index 3a4c81ed..1a08706f 100644
--- a/frontend-prep/src/pages/User/ProductDetail.css
+++ b/frontend-prep/src/pages/User/ProductDetail.css
@@ -133,6 +133,40 @@
}
}
+/* Coming Soon Badge */
+.coming-soon-badge {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) rotate(-15deg);
+ background: rgba(0, 0, 0, 0.8);
+ color: rgba(34, 197, 94, 0.95);
+ border: 4px solid rgba(34, 197, 94, 0.95);
+ padding: clamp(1rem, 4vw, 1.5rem) clamp(2.5rem, 8vw, 4rem);
+ font-size: clamp(2rem, 8vw, 3.5rem);
+ font-weight: 900;
+ letter-spacing: 6px;
+ text-transform: uppercase;
+ white-space: nowrap;
+ box-shadow:
+ 0 0 40px rgba(34, 197, 94, 0.6),
+ 0 8px 32px rgba(0, 0, 0, 0.6);
+ text-shadow:
+ 2px 2px 12px rgba(0, 0, 0, 0.9),
+ 0 0 20px rgba(34, 197, 94, 0.3);
+ z-index: 10;
+ animation: pulseGreen 2s infinite;
+}
+
+@keyframes pulseGreen {
+ 0%, 100% {
+ transform: translate(-50%, -50%) rotate(-15deg) scale(1);
+ }
+ 50% {
+ transform: translate(-50%, -50%) rotate(-15deg) scale(1.05);
+ }
+}
+
/* Info Section */
.product-info-section {
display: flex;
diff --git a/frontend-prep/src/pages/User/ProductDetail.tsx b/frontend-prep/src/pages/User/ProductDetail.tsx
index 328faec7..00396572 100644
--- a/frontend-prep/src/pages/User/ProductDetail.tsx
+++ b/frontend-prep/src/pages/User/ProductDetail.tsx
@@ -1,6 +1,10 @@
import { useParams, useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
-import { getProductById, getCategories, isUserAuthenticated } from "../../api/api";
+import {
+ getProductById,
+ getCategories,
+ isUserAuthenticated,
+} from "../../api/api";
import type { Product } from "../../api/api";
import { useCart } from "../../context/useCart";
import Navbar from "../../components/Navbar";
@@ -86,12 +90,25 @@ function ProductDetail() {
const fixedProduct = {
...response.data,
prices:
- response.data.prices?.map(
- (p: { quantity: number; price: number }) => ({
- quantity: parseFloat(String(p.quantity)),
- price: parseFloat(String(p.price)),
- }),
- ) || [],
+ response.data.prices
+ ?.filter(
+ (p: {
+ quantity: number;
+ price: number;
+ active_price?: boolean;
+ }) => p.active_price !== false,
+ )
+ .map(
+ (p: {
+ quantity: number;
+ price: number;
+ active_price?: boolean;
+ }) => ({
+ quantity: parseFloat(String(p.quantity)),
+ price: parseFloat(String(p.price)),
+ active_price: p.active_price,
+ }),
+ ) || [],
};
setProduct(fixedProduct);
@@ -104,14 +121,20 @@ function ProductDetail() {
// Couleur de la catégorie depuis la DB
const matched = categories.find(
- (c) => c.name.toLowerCase() === (response.data.category || "").toLowerCase(),
+ (c) =>
+ c.name.toLowerCase() ===
+ (response.data.category || "").toLowerCase(),
);
if (matched?.color) setCatColor(matched.color);
} else {
setError(response.message || "Produit non trouvé");
}
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : "Erreur lors du chargement du produit");
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Erreur lors du chargement du produit",
+ );
} finally {
setLoading(false);
}
@@ -197,6 +220,7 @@ function ProductDetail() {
}
const isOutOfStock = product.stock === 0;
+ const isComingSoon = product.coming_soon === true;
const hasValidPrices = product.prices && product.prices.length > 0;
// Convertir la couleur hex en valeurs RGB pour les CSS rgba()
@@ -213,7 +237,8 @@ function ProductDetail() {
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
- const catTextColor = (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
+ const catTextColor =
+ (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
return (
<>
@@ -231,20 +256,38 @@ function ProductDetail() {
-
+

m.type === "image")?.url || ""}
+ src={
+ product.media?.find((m) => m.type === "image")
+ ?.url || ""
+ }
alt={product.name}
className="product-detail-image"
/>
- {isOutOfStock &&
SOLD OUT
}
+ {isOutOfStock && (
+
SOLD OUT
+ )}
+ {isComingSoon && (
+
+ COMMING SOON
+
+ )}
@@ -253,7 +296,8 @@ function ProductDetail() {
{selectedPrice > 0 && (
{selectedPrice.toFixed(2)} €{" "}
- {selectedGrams && `pour ${selectedGrams}${product.unit || "g"}`}
+ {selectedGrams &&
+ `pour ${selectedGrams}${product.unit || "g"}`}
)}
@@ -291,7 +335,8 @@ function ProductDetail() {
key={p.quantity}
value={p.quantity}
>
- {p.quantity}{product.unit || "g"} -{" "}
+ {p.quantity}
+ {product.unit || "g"} -{" "}
{p.price.toFixed(2)} €
))}
@@ -301,13 +346,19 @@ function ProductDetail() {
diff --git a/frontend-prep/src/pages/User/ProfilePage.css b/frontend-prep/src/pages/User/ProfilePage.css
index 83413ae4..fec5c0d8 100644
--- a/frontend-prep/src/pages/User/ProfilePage.css
+++ b/frontend-prep/src/pages/User/ProfilePage.css
@@ -179,6 +179,79 @@
font-weight: 500;
}
+/* Toggle 2FA */
+.profile-2fa-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 0.4rem;
+ gap: 1rem;
+}
+
+.profile-2fa-label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.profile-2fa-badge {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ font-size: 0.78rem;
+ font-weight: 500;
+ color: #10b981;
+}
+
+.profile-toggle {
+ position: relative;
+ width: 48px;
+ height: 26px;
+ border-radius: 13px;
+ border: none;
+ background: var(--border);
+ cursor: pointer;
+ padding: 0;
+ flex-shrink: 0;
+ transition: background 0.25s;
+}
+
+.profile-toggle--on {
+ background: #6366f155;
+ border: 1px solid #6366f1;
+}
+
+.profile-toggle-thumb {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--text-muted);
+ transition: transform 0.25s, background 0.25s;
+}
+
+.profile-toggle--on .profile-toggle-thumb {
+ transform: translateX(22px);
+ background: #6366f1;
+}
+
+.profile-2fa-spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid #6366f133;
+ border-top-color: #6366f1;
+ border-radius: 50%;
+ animation: spin 0.7s linear infinite;
+ flex-shrink: 0;
+}
+
+@keyframes spin { to { transform: rotate(360deg); } }
+
@media (max-width: 480px) {
.profile-row { grid-template-columns: 1fr; }
}
@@ -284,3 +357,27 @@
border: 1px solid rgba(37, 99, 235, 0.27);
color: #60a5fa;
}
+
+.profile-modal-btn--danger {
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.35);
+ color: #ef4444;
+}
+
+.profile-modal-icon--danger {
+ background: rgba(239, 68, 68, 0.1);
+ border-color: rgba(239, 68, 68, 0.3);
+ color: #ef4444;
+}
+
+.profile-modal-icon--success {
+ background: rgba(16, 185, 129, 0.1);
+ border-color: rgba(16, 185, 129, 0.3);
+ color: #10b981;
+}
+
+.profile-modal-btn--success {
+ background: transparent;
+ border: 1px solid rgba(16, 185, 129, 0.4);
+ color: #10b981;
+}
diff --git a/frontend-prep/src/pages/User/ProfilePage.tsx b/frontend-prep/src/pages/User/ProfilePage.tsx
index 6133d074..24442f30 100644
--- a/frontend-prep/src/pages/User/ProfilePage.tsx
+++ b/frontend-prep/src/pages/User/ProfilePage.tsx
@@ -1,433 +1,514 @@
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import Navbar from "../../components/Navbar";
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import Navbar from '../../components/Navbar';
+import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, get2FAStatus, toggle2FA, getPublicSettings } from '../../api/api';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
- isUserAuthenticated,
- extractUsernameFromToken,
- getMyProfile,
- updateMyProfile,
- getTelegramStatus,
- generateTelegramLinkToken,
- unlinkTelegram,
-} from "../../api/api";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- faUser,
- faMapMarkerAlt,
- faPhone,
- faCommentDots,
- faSave,
- faCheckCircle,
- faExclamationTriangle,
- faPaperPlane,
- faUnlink,
- faTimes,
- faLock,
-} from "@fortawesome/free-solid-svg-icons";
-import "./ProfilePage.css";
+ faUser, faMapMarkerAlt, faPhone, faCommentDots,
+ faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock, faShieldAlt,
+} from '@fortawesome/free-solid-svg-icons';
+import './ProfilePage.css';
-const STORAGE_ADDRESS = "profile_default_address";
-const STORAGE_PHONE = "profile_default_phone";
-const STORAGE_SIGNAL = "profile_signal_pseudo";
+const STORAGE_ADDRESS = 'profile_default_address';
+const STORAGE_PHONE = 'profile_default_phone';
+const STORAGE_SIGNAL = 'profile_signal_pseudo';
export default function ProfilePage() {
- const navigate = useNavigate();
+ const navigate = useNavigate();
- // Données compte (backend)
- const [nom, setNom] = useState("");
- const [prenom, setPrenom] = useState("");
- const [telephone, setTelephone] = useState("");
- const [loadingProfile, setLoadingProfile] = useState(true);
+ // Données compte (backend)
+ const [nom, setNom] = useState('');
+ const [prenom, setPrenom] = useState('');
+ const [telephone, setTelephone] = useState('');
+ const [loadingProfile, setLoadingProfile] = useState(true);
- // Données locales (localStorage)
- const [defaultAddress, setDefaultAddress] = useState(
- () => localStorage.getItem(STORAGE_ADDRESS) ?? "",
- );
- const [defaultPhone, setDefaultPhone] = useState(
- () => localStorage.getItem(STORAGE_PHONE) ?? "",
- );
- const [signalPseudo, setSignalPseudo] = useState(
- () => localStorage.getItem(STORAGE_SIGNAL) ?? "",
- );
+ // Données locales (localStorage)
+ const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? '');
+ const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? '');
+ const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? '');
- // Feedback
- const [savingContact, setSavingContact] = useState(false);
- const [successMsg, setSuccessMsg] = useState("");
- const [errorMsg, setErrorMsg] = useState("");
+ const [savingContact, setSavingContact] = useState(false);
- // Telegram
- const [tgLinked, setTgLinked] = useState(false);
- const [tgEnabled, setTgEnabled] = useState(false);
- const [tgLoading, setTgLoading] = useState(false);
+ // Modals succès / erreur (pattern identique à l'app mobile)
+ const [showSuccessModal, setShowSuccessModal] = useState(false);
+ const [successTitle, setSuccessTitle] = useState('');
+ const [successMsg, setSuccessMsg] = useState('');
+ const [showErrorModal, setShowErrorModal] = useState(false);
+ const [errorMsg, setErrorMsg] = useState('');
- // Modal confirmation infos par défaut
- const [showSaveModal, setShowSaveModal] = useState(false);
+ // Telegram
+ const [tgLinked, setTgLinked] = useState(false);
+ const [tgEnabled, setTgEnabled] = useState(false);
+ const [tgLoading, setTgLoading] = useState(false);
- const username = extractUsernameFromToken() ?? "";
+ // 2FA
+ const [twoFAEnabled, setTwoFAEnabled] = useState(false);
+ const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
+ const [twoFALoading, setTwoFALoading] = useState(false);
- useEffect(() => {
- if (!isUserAuthenticated()) {
- navigate("/login/client", { replace: true });
- return;
- }
- // Statut Telegram
- getTelegramStatus().then((s) => {
- setTgLinked(s.linked);
- setTgEnabled(s.enabled);
- });
+ // Modals confirmation
+ const [showSaveModal, setShowSaveModal] = useState(false);
+ const [showConfirmAddressModal, setShowConfirmAddressModal] = useState(false);
+ const [showConfirmContactModal, setShowConfirmContactModal] = useState(false);
+ const [showUnlinkModal, setShowUnlinkModal] = useState(false);
+ const [showUnlinkSuccessModal, setShowUnlinkSuccessModal] = useState(false);
- // Charger depuis backend
- getMyProfile().then((res) => {
- if (res.success && res.client) {
- setNom(res.client.nom ?? "");
- setPrenom(res.client.prenom ?? "");
- setTelephone(res.client.telephone ?? "");
- // Initialiser le téléphone par défaut si pas encore défini
- if (
- !localStorage.getItem(STORAGE_PHONE) &&
- res.client.telephone
- ) {
- setDefaultPhone(res.client.telephone);
- }
- }
- setLoadingProfile(false);
- });
- }, [navigate]);
+ const username = extractUsernameFromToken() ?? '';
- const showSuccess = (msg: string) => {
- setSuccessMsg(msg);
- setErrorMsg("");
- setTimeout(() => setSuccessMsg(""), 3000);
- };
- const showError = (msg: string) => {
- setErrorMsg(msg);
- setSuccessMsg("");
- setTimeout(() => setErrorMsg(""), 4000);
- };
-
- const saveLocal = () => {
- localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
- localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
- localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
- setShowSaveModal(false);
- showSuccess("Informations par défaut enregistrées");
- };
-
- const handleLinkTelegram = async () => {
- setTgLoading(true);
-
- // ✅ Ouvrir AVANT le await
- const newWindow = window.open("", "_blank");
-
- const res = await generateTelegramLinkToken();
- setTgLoading(false);
-
- if (res.error || !res.link_url) {
- newWindow?.close();
- showError(res.error || "Service Telegram non disponible");
- return;
- }
-
- const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
-
- if (isIOS) {
- const botUsername = res.link_url.split("t.me/")[1]?.split("?")[0];
- const token = new URL(res.link_url).searchParams.get("start");
- newWindow!.location.href = `tg://resolve?domain=${botUsername}&start=${token}`;
- setTimeout(() => {
- newWindow!.location.href = res.link_url!;
- }, 1500);
- } else {
- newWindow!.location.href = res.link_url;
- }
- };
-
- const handleUnlinkTelegram = async () => {
- if (
- !window.confirm(
- "Délier votre compte Telegram ? Vous ne recevrez plus de notifications.",
- )
- )
- return;
- await unlinkTelegram();
- setTgLinked(false);
- showSuccess("Compte Telegram délié");
- };
-
- const saveContact = async () => {
- setSavingContact(true);
- const res = await updateMyProfile({
- nom: nom.trim(),
- prenom: prenom.trim(),
- telephone: telephone.trim(),
- });
- setSavingContact(false);
- if (res.success) {
- showSuccess("Profil mis à jour");
- } else {
- showError(res.message ?? "Erreur lors de la mise à jour");
- }
- };
-
- if (loadingProfile) {
- return (
- <>
-
-
- >
- );
+ useEffect(() => {
+ if (!isUserAuthenticated()) {
+ navigate('/login/client', { replace: true });
+ return;
}
+ // Statut Telegram
+ getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
+ // Statut 2FA
+ Promise.all([get2FAStatus(), getPublicSettings()]).then(([status, pub]) => {
+ setTwoFAEnabled(status.two_fa_enabled);
+ setTwoFAAdminEnabled(pub.two_fa_enabled);
+ });
+
+ // Charger depuis backend
+ getMyProfile().then((res) => {
+ if (res.success && res.client) {
+ setNom(res.client.nom ?? '');
+ setPrenom(res.client.prenom ?? '');
+ setTelephone(res.client.telephone ?? '');
+ // Initialiser le téléphone par défaut si pas encore défini
+ if (!localStorage.getItem(STORAGE_PHONE) && res.client.telephone) {
+ setDefaultPhone(res.client.telephone);
+ }
+ }
+ setLoadingProfile(false);
+ });
+ }, [navigate]);
+
+ const showSuccess = (title: string, msg: string) => {
+ setSuccessTitle(title); setSuccessMsg(msg); setShowSuccessModal(true);
+ };
+ const showError = (msg: string) => {
+ setErrorMsg(msg); setShowErrorModal(true);
+ };
+
+ const saveAddress = () => {
+ localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
+ setShowConfirmAddressModal(false);
+ showSuccess('Adresse enregistrée', 'Votre adresse par défaut a été sauvegardée et sera pré-remplie à votre prochaine commande.');
+ };
+
+ const saveLocal = () => {
+ localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
+ localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
+ localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
+ setShowSaveModal(false);
+ showSuccess('Infos enregistrées', 'Adresse, téléphone et pseudo Signal sauvegardés. Ils seront pré-remplis à votre prochaine commande.');
+ };
+
+ const handleLinkTelegram = async () => {
+ setTgLoading(true);
+ const res = await generateTelegramLinkToken();
+ setTgLoading(false);
+ if (res.error || !res.link_url) {
+ showError(res.error || 'Service Telegram non disponible');
+ return;
+ }
+ window.open(res.link_url, '_blank');
+ };
+
+ const handleUnlinkTelegram = () => {
+ setShowUnlinkModal(true);
+ };
+
+ const confirmUnlinkTelegram = async () => {
+ setShowUnlinkModal(false);
+ await unlinkTelegram();
+ setTgLinked(false);
+ setTwoFAEnabled(false);
+ setShowUnlinkSuccessModal(true);
+ };
+
+ const handleToggle2FA = async () => {
+ const newVal = !twoFAEnabled;
+ setTwoFALoading(true);
+ const res = await toggle2FA(newVal);
+ setTwoFALoading(false);
+ if (res.success) {
+ setTwoFAEnabled(newVal);
+ showSuccess(newVal ? '2FA activée' : '2FA désactivée', newVal ? 'Un code vous sera envoyé sur Telegram à chaque connexion.' : 'La double authentification a été désactivée.');
+ } else {
+ showError(res.error || 'Erreur lors de la modification');
+ }
+ };
+
+ const saveContact = async () => {
+ setShowConfirmContactModal(false);
+ setSavingContact(true);
+ const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
+ setSavingContact(false);
+ if (res.success) {
+ showSuccess('Profil mis à jour', 'Vos informations de compte ont été enregistrées avec succès.');
+ } else {
+ showError(res.message ?? 'Erreur lors de la mise à jour du profil.');
+ }
+ };
+
+ if (loadingProfile) {
return (
- <>
-
-
-
-
-
-
-
-
Mon Profil
-
@{username}
-
-
-
- {successMsg && (
-
- {successMsg}
-
- )}
- {errorMsg && (
-
- {" "}
- {errorMsg}
-
- )}
-
- {/* Section compte */}
-
-
-
- Mon compte
-
-
-
-
-
- setTelephone(e.target.value)}
- placeholder="+33 6 12 34 56 78"
- />
-
-
-
-
-
-
- {/* Section adresse par défaut */}
-
-
-
- Adresse par défaut
-
-
- Sera pré-remplie dans le formulaire de commande. Vous
- pourrez la modifier si vous n'êtes pas à cette adresse.
-
-
-
- setDefaultAddress(e.target.value)}
- placeholder="Numéro, rue, ville, code postal"
- />
-
-
-
- {/* Section contact commande */}
-
-
-
- Contact livraison
-
-
- Numéro utilisé par le livreur lors de la livraison. Peut
- être différent du numéro de votre compte.
-
-
-
- setDefaultPhone(e.target.value)}
- placeholder="+33 6 12 34 56 78"
- />
-
-
-
- setSignalPseudo(e.target.value)}
- placeholder="@votre.pseudo.signal"
- />
-
-
-
- {/* Section Telegram */}
- {tgEnabled && (
-
-
-
- Notifications Telegram
-
-
- Recevez vos notifications sur Telegram, même quand
- le site est fermé.
-
- {tgLinked ? (
-
-
- {" "}
- Compte Telegram lié
-
-
-
- ) : (
-
- )}
-
- )}
-
-
- {showSaveModal && (
-
setShowSaveModal(false)}
- >
-
e.stopPropagation()}
- >
-
-
-
-
-
- Enregistrer les infos par défaut ?
-
-
- Adresse, téléphone de livraison et pseudo Signal
- seront sauvegardés localement et pré-remplis lors de
- vos prochaines commandes.
-
-
-
-
-
-
-
- )}
- >
+ <>
+
+
+ >
);
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
Mon Profil
+
@{username}
+
+
+
+ {/* Section compte */}
+
+
+
+ Mon compte
+
+
+
+
+
+ setTelephone(e.target.value)}
+ placeholder="+33 6 12 34 56 78"
+ />
+
+
+
+
+
+
+ {/* Section adresse par défaut */}
+
+
+
+ Adresse par défaut
+
+
+ Sera pré-remplie dans le formulaire de commande. Vous pourrez la modifier si vous n'êtes pas à cette adresse.
+
+
+
+ setDefaultAddress(e.target.value)}
+ placeholder="Numéro, rue, ville, code postal"
+ />
+
+
+
+
+ {/* Section contact commande */}
+
+
+
+ Contact livraison
+
+
+ Numéro utilisé par le livreur lors de la livraison. Peut être différent du numéro de votre compte.
+
+
+
+ setDefaultPhone(e.target.value)}
+ placeholder="+33 6 12 34 56 78"
+ />
+
+
+
+ setSignalPseudo(e.target.value)}
+ placeholder="@votre.pseudo.signal"
+ />
+
+
+
+ {/* Section Telegram */}
+ {tgEnabled && (
+
+
+
+ Notifications Telegram
+
+
+ Recevez vos notifications sur Telegram, même quand le site est fermé.
+
+ {tgLinked ? (
+
+
+ Compte Telegram lié
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Section 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
+ {twoFAAdminEnabled && tgLinked && (
+
+
+
+ Double authentification (2FA)
+
+
+ À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte.
+
+
+
+ {twoFAEnabled ? 'Activée' : 'Désactivée'}
+ {twoFAEnabled && (
+
+ Protection active
+
+ )}
+
+ {twoFALoading ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ {showSaveModal && (
+
setShowSaveModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Enregistrer les infos par défaut ?
+
+ Adresse, téléphone de livraison et pseudo Signal seront sauvegardés localement et pré-remplis lors de vos prochaines commandes.
+
+
+
+
+
+
+
+ )}
+
+ {showConfirmAddressModal && (
+
setShowConfirmAddressModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Enregistrer l'adresse ?
+
+ Cette adresse sera sauvegardée localement et pré-remplie lors de vos prochaines commandes.
+
+
+
+
+
+
+
+ )}
+
+ {showConfirmContactModal && (
+
setShowConfirmContactModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Enregistrer le compte ?
+
+ Vos informations (prénom, nom, téléphone) seront mises à jour sur votre compte.
+
+
+
+
+
+
+
+ )}
+
+ {showUnlinkModal && (
+
setShowUnlinkModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Délier Telegram ?
+
+ Vous ne recevrez plus de notifications Telegram. La double authentification sera également désactivée.
+
+
+
+
+
+
+
+ )}
+
+ {/* Modal succès */}
+ {showSuccessModal && (
+
setShowSuccessModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
{successTitle}
+
{successMsg}
+
+
+
+
+
+ )}
+
+ {/* Modal erreur */}
+ {showErrorModal && (
+
setShowErrorModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Une erreur est survenue
+
{errorMsg}
+
+
+
+
+
+ )}
+
+ {showUnlinkSuccessModal && (
+
setShowUnlinkSuccessModal(false)}>
+
e.stopPropagation()}>
+
+
+
+
+
Compte délié
+
+ Votre compte Telegram a été délié avec succès. Vous ne recevrez plus de notifications via Telegram.
+
+
+
+
+
+
+ )}
+ >
+ );
}
diff --git a/frontend-prep/src/pages/User/SuiviLivraison.tsx b/frontend-prep/src/pages/User/SuiviLivraison.tsx
index e57357c3..14d5f192 100644
--- a/frontend-prep/src/pages/User/SuiviLivraison.tsx
+++ b/frontend-prep/src/pages/User/SuiviLivraison.tsx
@@ -73,29 +73,18 @@ interface ToastMessage {
* Somme des prix individuels (pas de multiplication)
*/
const getTotalAmount = (order: OrderWithTracking): number => {
- // 1. Priorité: champ total stocké en DB
- if (typeof order.total === "number" && order.total > 0) {
- return order.total;
- }
-
- // 2. Fallback: total_prix
if (typeof order.total_prix === "number" && order.total_prix > 0) {
return order.total_prix;
}
-
- // 3. Calcul depuis items (comme dans Checkout: somme des prix)
+ if (typeof order.total === "number" && order.total > 0) {
+ return order.total;
+ }
if (order.items && order.items.length > 0) {
- const calculatedTotal = order.items.reduce((sum, item) => {
+ return order.items.reduce((sum, item) => {
const itemPrice = item.prix || item.price || 0;
- return sum + itemPrice; // ✅ Somme simple (pas de × quantity)
+ return sum + itemPrice;
}, 0);
-
- console.log(
- `💰 [getTotalAmount] Commande ${order.id}: ${calculatedTotal.toFixed(2)}€`,
- );
- return calculatedTotal;
}
-
return 0;
};
@@ -336,20 +325,17 @@ function SuiviLivraison() {
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const loadOrders = async () => {
- // ✅ Vérifier l'auth avant de charger les commandes
if (!isUserAuthenticated()) {
- console.log("❌ [loadOrders] Non authentifié");
navigate("/login/client", { replace: true });
return;
}
try {
- setLoading(true);
const response = await getMyOrders();
- if (response.success && response.commands) {
+ if (response.success) {
const ordersWithTracking = await Promise.all(
- response.commands.map(async (order: OrderDetail) => {
+ (response.commands || []).map(async (order: OrderDetail) => {
const normalizedOrder = {
...order,
total: getTotalAmount(order),
@@ -361,18 +347,12 @@ function SuiviLivraison() {
try {
tracking = await getOrderTracking(order.id);
} catch {
- console.warn(
- `Tracking non disponible pour commande ${order.id}`,
- );
tracking = undefined;
}
try {
eta = await getOrderETA(order.id);
} catch {
- console.warn(
- `ETA non disponible pour commande ${order.id}`,
- );
eta = undefined;
}
@@ -386,9 +366,6 @@ function SuiviLivraison() {
setOrders(ordersWithTracking);
setError("");
- } else {
- setError("Impossible de charger les commandes");
- showToast("Impossible de charger les commandes", "error");
}
} catch (err: unknown) {
console.error("Erreur loadOrders:", err);
@@ -945,14 +922,21 @@ function SuiviLivraison() {
/>{" "}
Montant total
+ {(order.referral_used ?? 0) > 0 && (
+
+ Brut : {(order.total_prix ?? 0).toFixed(2)} €
+
+ )}
- {getTotalAmount(
- order,
- ).toFixed(2)}{" "}
- €
+ {Math.max(0, getTotalAmount(order) - (order.referral_used ?? 0)).toFixed(2)} €
+ {(order.referral_used ?? 0) > 0 && (
+
+ — dont {(order.referral_used!).toFixed(2)} € parrainage déduit
+
+ )}