diff --git a/backend/gestion/handlers/stats.go b/backend/gestion/handlers/stats.go index 8d347db4..f866f272 100644 --- a/backend/gestion/handlers/stats.go +++ b/backend/gestion/handlers/stats.go @@ -296,6 +296,78 @@ func GetAdminStats(c *gin.Context) { } } + // ── Détail du jour (catégorie → produits) ──────────────────────────────── + var dailyRows []models.DailyProductRow + gdb.Raw(` + SELECT + ci.product_id, + ci.produit AS product_name, + COALESCE(p.category, 'Sans catégorie') AS category, + COALESCE(cat.color, '#7c3aed') AS category_color, + SUM(ci.quantite) AS total_quantity, + COUNT(DISTINCT ci.command_id) AS order_count, + SUM(ci.prix) AS revenue + FROM command_items ci + JOIN commandes c ON c.id = ci.command_id + LEFT JOIN products p ON p.id = ci.product_id + LEFT JOIN categories cat ON cat.name = p.category + WHERE DATE(c.created_at) = CURRENT_DATE + AND c.status != 'cancelled' + GROUP BY ci.product_id, ci.produit, p.category, cat.color + ORDER BY p.category, SUM(ci.quantite) DESC + `).Scan(&dailyRows) + + type dailyCatGroup struct { + Category string + CategoryColor string + TotalQuantity float64 + TotalRevenue float64 + Products []gin.H + } + var dailyCats []dailyCatGroup + dailyCatIdx := map[string]int{} + var dailyTotalOrders int64 + dailyTotalRevenue := 0.0 + dailyTotalQty := 0.0 + + gdb.Raw(` + SELECT COUNT(DISTINCT id) FROM commandes + WHERE DATE(created_at) = CURRENT_DATE AND status != 'cancelled' + `).Scan(&dailyTotalOrders) + + for _, r := range dailyRows { + dailyTotalRevenue += r.Revenue + dailyTotalQty += r.TotalQuantity + idx, ok := dailyCatIdx[r.Category] + if !ok { + idx = len(dailyCats) + dailyCats = append(dailyCats, dailyCatGroup{ + Category: r.Category, + CategoryColor: r.CategoryColor, + }) + dailyCatIdx[r.Category] = idx + } + dailyCats[idx].TotalQuantity += r.TotalQuantity + dailyCats[idx].TotalRevenue += r.Revenue + dailyCats[idx].Products = append(dailyCats[idx].Products, gin.H{ + "product_id": r.ProductID, + "name": r.ProductName, + "quantity": r.TotalQuantity, + "order_count": r.OrderCount, + "revenue": r.Revenue, + }) + } + dailyCatsJSON := make([]gin.H, len(dailyCats)) + for i, g := range dailyCats { + dailyCatsJSON[i] = gin.H{ + "category": g.Category, + "category_color": g.CategoryColor, + "total_quantity": g.TotalQuantity, + "total_revenue": g.TotalRevenue, + "products": g.Products, + } + } + // ── Résumé global ───────────────────────────────────────────────────────── var totalOrders int64 var totalRevenue float64 @@ -337,6 +409,13 @@ func GetAdminStats(c *gin.Context) { "by_day_revenue": byDayRevenue, "by_hour": byHour, "top_products": topProducts, - "by_quantity": byQuantity, + "by_quantity": byQuantity, + "daily_detail": gin.H{ + "date": time.Now().Format("02/01/2006"), + "total_orders": dailyTotalOrders, + "total_quantity": dailyTotalQty, + "total_revenue": dailyTotalRevenue, + "categories": dailyCatsJSON, + }, }) } diff --git a/backend/gestion/models/stats.go b/backend/gestion/models/stats.go index c8b3a2ee..11207521 100644 --- a/backend/gestion/models/stats.go +++ b/backend/gestion/models/stats.go @@ -42,3 +42,13 @@ type DayRevenueRow struct { Day time.Time `gorm:"column:day"` Revenue float64 `gorm:"column:revenue"` } + +type DailyProductRow struct { + ProductID int `gorm:"column:product_id"` + ProductName string `gorm:"column:product_name"` + Category string `gorm:"column:category"` + CategoryColor string `gorm:"column:category_color"` + TotalQuantity float64 `gorm:"column:total_quantity"` + OrderCount int `gorm:"column:order_count"` + Revenue float64 `gorm:"column:revenue"` +} diff --git a/frontend-prep/src/components/Navbar.css b/frontend-prep/src/components/Navbar.css index 7b960e47..1606fdde 100644 --- a/frontend-prep/src/components/Navbar.css +++ b/frontend-prep/src/components/Navbar.css @@ -48,16 +48,18 @@ border-bottom-color: var(--border); } -[data-theme="light"] .sidebar-footer { - background: #ffffff; - border-top-color: var(--border); -} /* ============================================================ Body offset ============================================================ */ +:root { + --bottomnav-h: 66px; + --bottomnav-offset: 15px; +} + body { padding-top: var(--topbar-h); + padding-bottom: calc(var(--bottomnav-h) + var(--bottomnav-offset) + 12px); } /* ============================================================ @@ -201,299 +203,134 @@ body { /* ============================================================ Overlay ============================================================ */ -.sidebar-overlay { - position: fixed; - inset: 0; - z-index: 950; - background: rgba(0, 0, 0, 0.45); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - animation: fadeIn 0.2s ease; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - /* ============================================================ - Sidebar — iOS frosted glass + Bottom nav — floating pill ============================================================ */ -.sidebar { +.bottom-nav { position: fixed; - top: 0; - left: 0; - width: var(--sidebar-w); - height: 100dvh; - z-index: 1000; + bottom: var(--bottomnav-offset); + left: 50%; + transform: translateX(-50%); + width: calc(100% - 30px); + max-width: 640px; + z-index: 900; display: flex; - flex-direction: column; - background: color-mix(in srgb, var(--primary) 28%, rgba(6, 6, 18, 0.88)); + align-items: stretch; + background: color-mix(in srgb, var(--primary) 30%, rgba(6, 6, 18, 0.88)); backdrop-filter: blur(50px) saturate(180%); -webkit-backdrop-filter: blur(50px) saturate(180%); - border-right: 1px solid rgba(255, 255, 255, 0.1); - transform: translateX(-100%); - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - overflow: hidden; + border-radius: 22px; + padding: 6px; + gap: 2px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.3); } -[data-theme="light"] .sidebar { - background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.92)); - border-right-color: rgba(0, 0, 0, 0.06); +[data-theme="light"] .bottom-nav { + background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.94)); + border-color: rgba(0, 0, 0, 0.06); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); } -.sidebar.open { - transform: translateX(0); - box-shadow: 8px 0 60px rgba(0, 0, 0, 0.55); -} - -/* ── Header ── */ -.sidebar-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1.2rem 1rem 1rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - flex-shrink: 0; -} - -[data-theme="light"] .sidebar-header { - border-bottom-color: rgba(0, 0, 0, 0.07); -} - -.sidebar-avatar { - width: 40px; - height: 40px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.18); +/* ── Tabs ── */ +.bottom-tab { + flex: 1; display: flex; + flex-direction: column; align-items: center; justify-content: center; - color: #fff; - font-size: 1rem; - flex-shrink: 0; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -[data-theme="light"] .sidebar-avatar { - background: rgba(255, 255, 255, 0.55); - color: var(--primary); - border-color: rgba(255, 255, 255, 0.7); -} - -.sidebar-header-info { - flex: 1; + gap: 3px; + padding: 8px 4px; + background: transparent; + border: none; + border-radius: 16px; + color: rgba(255, 255, 255, 0.45); + cursor: pointer; + transition: all 0.18s ease; + -webkit-tap-highlight-color: transparent; min-width: 0; } -.sidebar-brand-name { - margin: 0; - font-size: 0.92rem; +[data-theme="light"] .bottom-tab { + color: rgba(0, 0, 0, 0.35); +} + +.bottom-tab:hover { + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.75); +} + +[data-theme="light"] .bottom-tab:hover { + background: rgba(0, 0, 0, 0.04); + color: rgba(0, 0, 0, 0.6); +} + +.bottom-tab.active { + background: rgba(255, 255, 255, 0.14); + color: #fff; +} + +[data-theme="light"] .bottom-tab.active { + background: rgba(255, 255, 255, 0.65); + color: var(--primary); +} + +.bottom-tab:active { + transform: scale(0.93); +} + +/* ── Icon wrapper ── */ +.bottom-tab-icon-wrap { + position: relative; + font-size: 1.05rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; +} + +/* ── Cart badge ── */ +.bottom-tab-badge { + position: absolute; + top: -6px; + right: -8px; + min-width: 16px; + height: 16px; + border-radius: 999px; + background: var(--red); + color: #fff; + font-size: 0.58rem; font-weight: 700; - color: rgba(255, 255, 255, 0.95); + display: flex; + align-items: center; + justify-content: center; + padding: 0 3px; + line-height: 1; +} + +/* ── Labels ── */ +.bottom-tab-label { + font-size: 0.6rem; + font-weight: 600; letter-spacing: 0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + max-width: 100%; } -[data-theme="light"] .sidebar-brand-name { - color: rgba(0, 0, 0, 0.85); +/* Hide labels on very narrow screens */ +@media (max-width: 380px) { + .bottom-tab-label { display: none; } + .bottom-tab { padding: 10px 4px; } } -.sidebar-brand-sub { - margin: 0; - font-size: 0.7rem; - color: rgba(255, 255, 255, 0.5); - margin-top: 1px; +/* Logout button — couleur rouge subtile */ +.topbar-logout-btn { + color: rgba(239, 68, 68, 0.7); } - -[data-theme="light"] .sidebar-brand-sub { - color: rgba(0, 0, 0, 0.4); -} - -.sidebar-close { - width: 30px; - height: 30px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.12); - border: none; - border-radius: 50%; - color: rgba(255, 255, 255, 0.7); - font-size: 0.8rem; - cursor: pointer; - transition: background var(--transition); - -webkit-tap-highlight-color: transparent; - flex-shrink: 0; -} - -[data-theme="light"] .sidebar-close { - background: rgba(0, 0, 0, 0.08); - color: rgba(0, 0, 0, 0.5); -} - -.sidebar-close:hover { - background: rgba(255, 255, 255, 0.2); - color: #fff; -} - -[data-theme="light"] .sidebar-close:hover { - background: rgba(0, 0, 0, 0.12); - color: rgba(0, 0, 0, 0.8); -} - -/* ── Nav ── */ -.sidebar-nav { - flex: 1; - overflow-y: auto; - padding: 14px 12px 8px; - scrollbar-width: none; - display: flex; - flex-direction: column; - gap: 10px; -} - -.sidebar-nav::-webkit-scrollbar { display: none; } - -/* ── iOS card groups ── */ -.menu-group { - background: rgba(255, 255, 255, 0.09); - border-radius: 14px; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -[data-theme="light"] .menu-group { - background: rgba(255, 255, 255, 0.58); - border-color: rgba(255, 255, 255, 0.8); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); -} - -/* ── Menu items ── */ -.menu-item { - width: 100%; - display: flex; - align-items: center; - gap: 12px; - padding: 12px 14px; - background: transparent; - border: none; - border-top: 0.5px solid rgba(255, 255, 255, 0.07); - color: rgba(255, 255, 255, 0.88); - font-size: 0.92rem; - font-weight: 500; - cursor: pointer; - text-align: left; - transition: background 0.12s ease; - -webkit-tap-highlight-color: transparent; - position: relative; -} - -.menu-group .menu-item:first-child { - border-top: none; -} - -[data-theme="light"] .menu-item { - color: rgba(0, 0, 0, 0.82); - border-top-color: rgba(0, 0, 0, 0.06); -} - -.menu-item:hover:not(:disabled), -.menu-item:focus-visible { - background: rgba(255, 255, 255, 0.1); -} - -[data-theme="light"] .menu-item:hover:not(:disabled) { - background: rgba(0, 0, 0, 0.04); -} - -.menu-item.active { - background: rgba(255, 255, 255, 0.14); -} - -[data-theme="light"] .menu-item.active { - background: rgba(0, 0, 0, 0.06); -} - -.menu-item:active:not(:disabled) { - background: rgba(255, 255, 255, 0.06); - transform: scale(0.99); -} - -.menu-item:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -/* ── Icon ── */ -.menu-icon { - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: rgba(255, 255, 255, 0.15); - font-size: 0.82rem; - color: rgba(255, 255, 255, 0.92); - flex-shrink: 0; - transition: all var(--transition); -} - -[data-theme="light"] .menu-icon { - background: rgba(255, 255, 255, 0.7); - color: var(--primary); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.menu-item.active .menu-icon { - background: var(--primary); - color: #fff; - box-shadow: 0 2px 12px color-mix(in srgb, var(--primary) 55%, transparent); -} - -.menu-icon.icon-telegram { color: #22d3ee; } -.menu-icon.icon-danger { color: #f87171; } - -[data-theme="light"] .menu-icon.icon-telegram { color: #0891b2; } -[data-theme="light"] .menu-icon.icon-danger { color: var(--red); } - -/* ── Label ── */ -.menu-label { - flex: 1; - letter-spacing: 0.01em; -} - -.menu-item-danger .menu-label { - color: #f87171; -} - -[data-theme="light"] .menu-item-danger .menu-label { - color: var(--red); -} - -/* ── Chevron ── */ -.menu-chevron { - font-size: 0.65rem; - color: rgba(255, 255, 255, 0.28); - flex-shrink: 0; - transition: transform var(--transition); -} - -[data-theme="light"] .menu-chevron { - color: rgba(0, 0, 0, 0.2); -} - -.menu-item.active .menu-chevron { - color: rgba(255, 255, 255, 0.55); -} - -/* ── Footer ── */ -.sidebar-footer { - padding: 8px 12px 16px; - flex-shrink: 0; +.topbar-logout-btn:hover { + color: var(--red) !important; } /* ============================================================ @@ -623,11 +460,10 @@ body { } /* ============================================================ - Responsive — disable hover states on touch + Responsive — touch devices ============================================================ */ @media (hover: none) { - .menu-item:hover { background: transparent; } - .menu-item.active:hover { background: rgba(255, 255, 255, 0.14); } - .topbar-toggle:hover { background: transparent; border-color: transparent; } + .bottom-tab:hover { background: transparent; color: rgba(255, 255, 255, 0.45); } + .bottom-tab.active:hover { background: rgba(255, 255, 255, 0.14); color: #fff; } .topbar-icon-btn:hover { background: transparent; border-color: transparent; } } diff --git a/frontend-prep/src/components/Navbar.tsx b/frontend-prep/src/components/Navbar.tsx index ec21bb59..9780e04e 100644 --- a/frontend-prep/src/components/Navbar.tsx +++ b/frontend-prep/src/components/Navbar.tsx @@ -11,16 +11,13 @@ import { faTruck, faClockRotateLeft, faSignOutAlt, - faBars, faTimes, faBell, faGift, faUserCircle, faSun, faMoon, - faChevronRight, } from "@fortawesome/free-solid-svg-icons"; -import { faTelegram } from "@fortawesome/free-brands-svg-icons"; import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { getClientNotifications, markNotificationsRead, getPublicSettings } from "../api/api"; import type { ClientNotification } from "../api/api"; @@ -50,7 +47,6 @@ function formatNotifDate(dateStr: string): string { function Navbar() { const { theme, toggleTheme } = useTheme(); - const [isMenuOpen, setIsMenuOpen] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); @@ -72,9 +68,7 @@ function Navbar() { for (const n of res.notifications) { if (n.read) continue; const key = `${n.command_id}-${n.type}-${n.created_at}`; - if (!seenKeysRef.current.has(key)) { - seenKeysRef.current.add(key); - } + if (!seenKeysRef.current.has(key)) seenKeysRef.current.add(key); } } else { for (const n of res.notifications) { @@ -106,29 +100,6 @@ function Navbar() { } }; - const closeNotifPanel = () => setShowNotifPanel(false); - - const navItems: MenuItem[] = [ - { id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" }, - { id: "produits", label: "Nos Produits", icon: faBox, path: "/user/nos-produits" }, - { id: "panier", label: "Mon Panier", icon: faShoppingCart, path: "/user/panier" }, - { id: "suivi", label: "Suivi Livraison", icon: faTruck, path: "/user/suivi-livraison" }, - ]; - - const accountItems: MenuItem[] = [ - { id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" }, - ...(referralEnabled ? [{ id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" } as MenuItem] : []), - { id: "profil", label: "Mon Profil", icon: faUserCircle, path: "/user/profil" }, - ]; - - const toggleMenu = () => setIsMenuOpen((v) => !v); - const closeMenu = () => setIsMenuOpen(false); - - const handleNavigation = (path: string) => { - navigate(path); - closeMenu(); - }; - const handleLogout = async () => { if (isLoggingOut) return; setIsLoggingOut(true); @@ -138,10 +109,7 @@ function Navbar() { try { await fetch("/api/v2/admin/auth/logout", { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, }); } catch { /* noop */ } } @@ -157,157 +125,102 @@ function Navbar() { } }; - const handleTelegram = () => window.open("https://t.me/milieu_nantais", "_blank"); + const navItems: MenuItem[] = [ + { id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" }, + { id: "produits", label: "Produits", icon: faBox, path: "/user/nos-produits" }, + { id: "panier", label: "Panier", icon: faShoppingCart, path: "/user/panier" }, + { id: "suivi", label: "Suivi", icon: faTruck, path: "/user/suivi-livraison" }, + ]; - const renderItem = (item: MenuItem) => { + const accountItems: MenuItem[] = [ + { id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" }, + ...(referralEnabled ? [{ id: "parrainage", label: "Parrain.", icon: faGift, path: "/user/parrainage" } as MenuItem] : []), + { id: "profil", label: "Profil", icon: faUserCircle, path: "/user/profil" }, + ]; + + const allTabs = [...navItems, ...accountItems]; + + const renderTab = (item: MenuItem) => { const isActive = location.pathname === item.path; + const showBadge = item.id === "panier" && cartCount > 0; return ( ); }; return ( <> - {/* ── Top Bar ─────────────────────────────────── */} + {/* ── Top Bar ── */}
- - {shopName} -
- - - -
- {/* ── Notifications modal ─────────────────────── */} + {/* ── Notifications ── */} {showNotifPanel && ( -
+
setShowNotifPanel(false)}>
e.stopPropagation()}>
Notifications -
{notifications.length === 0 ? (
- +

Aucune notification

- ) : ( - notifications.map((n, i) => ( -
- {n.message} - {formatNotifDate(n.created_at)} -
- )) - )} + ) : notifications.map((n, i) => ( +
+ {n.message} + {formatNotifDate(n.created_at)} +
+ ))}
)} - {/* ── Overlay ─────────────────────────────────── */} - {isMenuOpen &&
} - - {/* ── Sidebar iOS-style ───────────────────────── */} - + {/* ── Bottom nav ── */} + ); } diff --git a/frontend-prep/src/pages/User/ProfilePage.tsx b/frontend-prep/src/pages/User/ProfilePage.tsx index 24442f30..18aa66d7 100644 --- a/frontend-prep/src/pages/User/ProfilePage.tsx +++ b/frontend-prep/src/pages/User/ProfilePage.tsx @@ -1,514 +1,757 @@ -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 { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Navbar from "../../components/Navbar"; import { - faUser, faMapMarkerAlt, faPhone, faCommentDots, - faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock, faShieldAlt, -} from '@fortawesome/free-solid-svg-icons'; -import './ProfilePage.css'; + isUserAuthenticated, + extractUsernameFromToken, + getMyProfile, + updateMyProfile, + getTelegramStatus, + generateTelegramLinkToken, + unlinkTelegram, + get2FAStatus, + toggle2FA, + getPublicSettings, +} from "../../api/api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + 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) ?? ''); - - const [savingContact, setSavingContact] = 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(''); - - // Telegram - const [tgLinked, setTgLinked] = useState(false); - const [tgEnabled, setTgEnabled] = useState(false); - const [tgLoading, setTgLoading] = useState(false); - - // 2FA - const [twoFAEnabled, setTwoFAEnabled] = useState(false); - const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false); - const [twoFALoading, setTwoFALoading] = useState(false); - - // 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); - - const username = extractUsernameFromToken() ?? ''; - - 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 ( - <> - -
-
-
- + // 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) ?? "", ); - } - return ( - <> - -
-
-
- -
-
-

Mon Profil

-

@{username}

-
-
+ const [savingContact, setSavingContact] = useState(false); - {/* Section compte */} -
-

- - Mon compte -

-
-
-
- - setPrenom(e.target.value)} - placeholder="Votre prénom" - /> -
-
- - setNom(e.target.value)} - placeholder="Votre nom" - /> -
-
-
- - setTelephone(e.target.value)} - placeholder="+33 6 12 34 56 78" - /> -
-
- - -
+ // 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(""); - {/* 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" - /> -
- -
+ // Telegram + const [tgLinked, setTgLinked] = useState(false); + const [tgEnabled, setTgEnabled] = useState(false); + const [tgLoading, setTgLoading] = useState(false); - {/* 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é - - -
- ) : ( - - )} -
- )} + // 2FA + const [twoFAEnabled, setTwoFAEnabled] = useState(false); + const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false); + const [twoFALoading, setTwoFALoading] = useState(false); - {/* 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 - + // 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); + + const username = extractUsernameFromToken() ?? ""; + + 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 ( + <> + +
+
+
+
+
+ + ); + } + + return ( + <> + +
+
+
+ +
+
+

Mon Profil

+

{username}

+
+
+ + {/* Section compte */} +
+

+ + Mon compte +

+
+
+
+ + setPrenom(e.target.value)} + placeholder="Votre prénom" + /> +
+
+ + setNom(e.target.value)} + placeholder="Votre nom" + /> +
+
+
+ + 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é + + +
+ ) : ( + + )} +
)} -
- {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. +

+
+ + +
+
+
+ )} - {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. +

+
+ + +
+
+
+ )} - {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. +

+
+ + +
+
+
+ )} - {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. +

+
+ + +
+
+
+ )} - {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 succès */} - {showSuccessModal && ( -
setShowSuccessModal(false)}> -
e.stopPropagation()}> - -
- -
-

{successTitle}

-

{successMsg}

-
- -
-
-
- )} + {/* Modal erreur */} + {showErrorModal && ( +
setShowErrorModal(false)} + > +
e.stopPropagation()} + > + +
+ +
+

+ Une erreur est survenue +

+

{errorMsg}

+
+ +
+
+
+ )} - {/* 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. -

-
- -
-
-
- )} - - ); + {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/mobile/src/screens/client/ProfileScreen.tsx b/mobile/src/screens/client/ProfileScreen.tsx index 1d86e42d..74eaf537 100644 --- a/mobile/src/screens/client/ProfileScreen.tsx +++ b/mobile/src/screens/client/ProfileScreen.tsx @@ -19,62 +19,76 @@ import { Feather } from "@expo/vector-icons"; import { useFocusEffect, useNavigation } from "@react-navigation/native"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { Ionicons } from "@expo/vector-icons"; -import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword, get2FAStatus, toggle2FA, getPublicSettings } from "../../api/api"; +import { + getMyProfile, + updateMyProfile, + getTelegramStatus, + generateTelegramLinkToken, + unlinkTelegram, + changePassword, + get2FAStatus, + toggle2FA, + getPublicSettings, +} from "../../api/api"; import TextInput from "../../components/ui/TextInput"; import Button from "../../components/ui/Button"; import { useTheme } from "../../context/ThemeContext"; import { spacing, borderRadius, fontSize, fontWeight } from "../../theme"; const STORAGE_ADDRESS = "profile_default_address"; -const STORAGE_PHONE = "profile_default_phone"; -const STORAGE_SIGNAL = "profile_signal_pseudo"; +const STORAGE_PHONE = "profile_default_phone"; +const STORAGE_SIGNAL = "profile_signal_pseudo"; export default function ProfileScreen() { const { colors } = useTheme(); const navigation = useNavigation(); // Données compte (backend) - const [nom, setNom] = useState(""); - const [prenom, setPrenom] = useState(""); + const [nom, setNom] = useState(""); + const [prenom, setPrenom] = useState(""); const [telephone, setTelephone] = useState(""); const [username, setUsername] = useState(""); // Données locales (AsyncStorage) const [defaultAddress, setDefaultAddress] = useState(""); - const [defaultPhone, setDefaultPhone] = useState(""); - const [signalPseudo, setSignalPseudo] = useState(""); + const [defaultPhone, setDefaultPhone] = useState(""); + const [signalPseudo, setSignalPseudo] = useState(""); const [loadingProfile, setLoadingProfile] = useState(true); - const [savingContact, setSavingContact] = useState(false); + const [savingContact, setSavingContact] = useState(false); // Telegram - const [telegramLinked, setTelegramLinked] = useState(false); + const [telegramLinked, setTelegramLinked] = useState(false); const [telegramEnabled, setTelegramEnabled] = useState(false); const [telegramLoading, setTelegramLoading] = useState(false); // 2FA - const [twoFAEnabled, setTwoFAEnabled] = useState(false); + const [twoFAEnabled, setTwoFAEnabled] = useState(false); const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false); - const [twoFALoading, setTwoFALoading] = useState(false); + const [twoFALoading, setTwoFALoading] = useState(false); // Modals confirmation sauvegarde - const [showSaveModal, setShowSaveModal] = useState(false); - const [showConfirmContactModal, setShowConfirmContactModal] = useState(false); - const [showConfirmAddressModal, setShowConfirmAddressModal] = useState(false); + const [showSaveModal, setShowSaveModal] = useState(false); + const [showConfirmContactModal, setShowConfirmContactModal] = + useState(false); + const [showConfirmAddressModal, setShowConfirmAddressModal] = + useState(false); // Modal succès / erreur générique const [showSuccessModal, setShowSuccessModal] = useState(false); - const [successTitle, setSuccessTitle] = useState(""); - const [successMsg, setSuccessMsg] = useState(""); - const [showErrorModal, setShowErrorModal] = useState(false); - const [errorMsg, setErrorMsg] = useState(""); + const [successTitle, setSuccessTitle] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + const [showErrorModal, setShowErrorModal] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); // Modal changement de mot de passe const [showPasswordModal, setShowPasswordModal] = useState(false); // Modals Telegram unlink - const [showTelegramUnlinkModal, setShowTelegramUnlinkModal] = useState(false); - const [showTelegramSuccessModal, setShowTelegramSuccessModal] = useState(false); + const [showTelegramUnlinkModal, setShowTelegramUnlinkModal] = + useState(false); + const [showTelegramSuccessModal, setShowTelegramSuccessModal] = + useState(false); const [currentPwd, setCurrentPwd] = useState(""); const [newPwd, setNewPwd] = useState(""); const [confirmPwd, setConfirmPwd] = useState(""); @@ -85,7 +99,15 @@ export default function ProfileScreen() { const loadData = useCallback(async () => { setLoadingProfile(true); - const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus, twoFAStatus, pubSettings] = await Promise.all([ + const [ + savedAddress, + savedPhone, + savedSignal, + profileRes, + tgStatus, + twoFAStatus, + pubSettings, + ] = await Promise.all([ AsyncStorage.getItem(STORAGE_ADDRESS), AsyncStorage.getItem(STORAGE_PHONE), AsyncStorage.getItem(STORAGE_SIGNAL), @@ -100,7 +122,7 @@ export default function ProfileScreen() { setTwoFAAdminEnabled(pubSettings.two_fa_enabled); if (savedAddress !== null) setDefaultAddress(savedAddress); - if (savedPhone !== null) setDefaultPhone(savedPhone); + if (savedPhone !== null) setDefaultPhone(savedPhone); if (profileRes.success && profileRes.client) { const c = profileRes.client; setNom(c.nom ?? ""); @@ -119,7 +141,11 @@ export default function ProfileScreen() { setLoadingProfile(false); }, []); - useFocusEffect(useCallback(() => { loadData(); }, [loadData])); + useFocusEffect( + useCallback(() => { + loadData(); + }, [loadData]), + ); const saveLocal = async () => { await Promise.all([ @@ -129,7 +155,9 @@ export default function ProfileScreen() { ]); setShowSaveModal(false); setSuccessTitle("Informations sauvegardées"); - setSuccessMsg("Téléphone et pseudo Signal seront pré-remplis lors de vos prochaines commandes."); + setSuccessMsg( + "Téléphone et pseudo Signal seront pré-remplis lors de vos prochaines commandes.", + ); setShowSuccessModal(true); }; @@ -137,7 +165,9 @@ export default function ProfileScreen() { setShowConfirmAddressModal(false); await AsyncStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim()); setSuccessTitle("Adresse sauvegardée"); - setSuccessMsg("Votre adresse par défaut sera pré-remplie à la prochaine commande."); + setSuccessMsg( + "Votre adresse par défaut sera pré-remplie à la prochaine commande.", + ); setShowSuccessModal(true); }; @@ -146,7 +176,10 @@ export default function ProfileScreen() { const res = await generateTelegramLinkToken(); setTelegramLoading(false); if (res.error || !res.link_url) { - Alert.alert("Erreur", res.error || "Service Telegram non disponible"); + Alert.alert( + "Erreur", + res.error || "Service Telegram non disponible", + ); return; } Alert.alert( @@ -154,7 +187,10 @@ export default function ProfileScreen() { "Appuyez sur OK pour ouvrir le bot Telegram et envoyer le message de liaison.", [ { text: "Annuler", style: "cancel" }, - { text: "Ouvrir Telegram", onPress: () => Linking.openURL(res.link_url!) }, + { + text: "Ouvrir Telegram", + onPress: () => Linking.openURL(res.link_url!), + }, ], ); }; @@ -178,7 +214,9 @@ export default function ProfileScreen() { return; } if (newPwd.length < 8) { - setErrorMsg("Le nouveau mot de passe doit contenir au moins 8 caractères."); + setErrorMsg( + "Le nouveau mot de passe doit contenir au moins 8 caractères.", + ); setShowErrorModal(true); return; } @@ -192,12 +230,17 @@ export default function ProfileScreen() { const result = await changePassword(currentPwd, newPwd); if (result.success) { setShowPasswordModal(false); - setCurrentPwd(""); setNewPwd(""); setConfirmPwd(""); + setCurrentPwd(""); + setNewPwd(""); + setConfirmPwd(""); setSuccessTitle("Mot de passe modifié"); setSuccessMsg("Votre mot de passe a été changé avec succès."); setShowSuccessModal(true); } else { - setErrorMsg(result.message || "Erreur inattendue lors du changement de mot de passe."); + setErrorMsg( + result.message || + "Erreur inattendue lors du changement de mot de passe.", + ); setShowErrorModal(true); } } finally { @@ -219,180 +262,217 @@ export default function ProfileScreen() { const saveContact = async () => { setShowConfirmContactModal(false); setSavingContact(true); - const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() }); + const res = await updateMyProfile({ + nom: nom.trim(), + prenom: prenom.trim(), + telephone: telephone.trim(), + }); setSavingContact(false); if (res.success) { setSuccessTitle("Compte mis à jour"); - setSuccessMsg(res.message ?? "Vos informations de compte ont été enregistrées avec succès."); + setSuccessMsg( + res.message ?? + "Vos informations de compte ont été enregistrées avec succès.", + ); setShowSuccessModal(true); } else { - setErrorMsg(res.message ?? "Erreur lors de la mise à jour du profil."); + setErrorMsg( + res.message ?? "Erreur lors de la mise à jour du profil.", + ); setShowErrorModal(true); } }; - const styles = useMemo(() => StyleSheet.create({ - container: { flex: 1, backgroundColor: colors.bgPrimary }, - content: { padding: spacing.l, paddingBottom: spacing.xxxl }, - header: { - flexDirection: "row", - alignItems: "center", - gap: spacing.m, - marginBottom: spacing.xl, - paddingTop: spacing.m, - }, - avatar: { - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: colors.accent, - justifyContent: "center", - alignItems: "center", - }, - username: { color: colors.textPrimary, fontSize: fontSize.lg, fontWeight: fontWeight.bold }, - usernameLabel: { color: colors.textMuted, fontSize: fontSize.sm }, - card: { - backgroundColor: colors.bgCard, - borderRadius: borderRadius.md, - padding: spacing.l, - marginBottom: spacing.m, - borderWidth: 1, - borderColor: colors.borderLight, - }, - cardTitle: { - flexDirection: "row", - alignItems: "center", - gap: spacing.s, - marginBottom: spacing.m, - }, - cardTitleText: { - color: colors.textPrimary, - fontSize: fontSize.md, - fontWeight: fontWeight.semibold, - }, - hint: { - color: colors.textMuted, - fontSize: fontSize.xs, - marginBottom: spacing.m, - lineHeight: 18, - }, - row: { flexDirection: "row", gap: spacing.m }, - half: { flex: 1 }, - fieldSpacing: { marginTop: spacing.m }, - saveBtn: { - marginTop: spacing.m, - backgroundColor: colors.accent, - borderRadius: borderRadius.sm, - paddingVertical: spacing.m, - paddingHorizontal: spacing.l, - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: spacing.s, - }, - saveBtnSecondary: { - backgroundColor: "transparent", - borderWidth: 1, - borderColor: colors.accent + "66", - }, - saveBtnText: { color: "#fff", fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, - saveBtnTextSecondary: { color: colors.accent }, - // Modal - modalOverlay: { - flex: 1, - backgroundColor: "rgba(0,0,0,0.6)", - justifyContent: "center", - alignItems: "center", - padding: spacing.l, - }, - modalBox: { - backgroundColor: colors.bgCard, - borderRadius: borderRadius.md, - borderWidth: 1, - borderColor: colors.borderLight, - padding: spacing.xl, - width: "100%", - maxWidth: 360, - alignItems: "center", - }, - modalIconWrap: { - width: 52, - height: 52, - borderRadius: 26, - backgroundColor: colors.accent + "22", - borderWidth: 1, - borderColor: colors.accent + "44", - justifyContent: "center", - alignItems: "center", - marginBottom: spacing.m, - }, - modalTitle: { - color: colors.textPrimary, - fontSize: fontSize.md, - fontWeight: fontWeight.bold, - textAlign: "center", - marginBottom: spacing.s, - }, - modalBody: { - color: colors.textMuted, - fontSize: fontSize.sm, - textAlign: "center", - lineHeight: 20, - marginBottom: spacing.l, - }, - modalActions: { - flexDirection: "row", - gap: spacing.m, - width: "100%", - }, - modalBtnCancel: { - flex: 1, - paddingVertical: spacing.m, - borderRadius: borderRadius.sm, - backgroundColor: "transparent", - borderWidth: 1, - borderColor: colors.borderLight, - alignItems: "center", - }, - modalBtnConfirm: { - flex: 1, - paddingVertical: spacing.m, - borderRadius: borderRadius.sm, - backgroundColor: "transparent", - borderWidth: 1, - borderColor: colors.accent + "66", - alignItems: "center", - }, - modalBtnCancelText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, - modalBtnConfirmText: { color: colors.accent, fontSize: fontSize.sm, fontWeight: fontWeight.semibold }, - pwdInputWrapper: { - flexDirection: "row" as const, - alignItems: "center" as const, - borderWidth: 1, - borderRadius: borderRadius.sm, - paddingHorizontal: spacing.m, - height: 46, - marginBottom: spacing.m, - width: "100%", - }, - pwdInputIcon: { marginRight: spacing.s }, - pwdInput: { flex: 1, fontSize: fontSize.sm }, - twoFARow: { - flexDirection: "row" as const, - alignItems: "center" as const, - justifyContent: "space-between" as const, - paddingTop: spacing.xs, - }, - twoFALabel: { - color: colors.textPrimary, - fontSize: fontSize.sm, - fontWeight: fontWeight.semibold, - }, - }), [colors]); + const styles = useMemo( + () => + StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bgPrimary }, + content: { padding: spacing.l, paddingBottom: spacing.xxxl }, + header: { + flexDirection: "row", + alignItems: "center", + gap: spacing.m, + marginBottom: spacing.xl, + paddingTop: spacing.m, + }, + avatar: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: colors.accent, + justifyContent: "center", + alignItems: "center", + }, + username: { + color: colors.textPrimary, + fontSize: fontSize.lg, + fontWeight: fontWeight.bold, + }, + usernameLabel: { + color: colors.textMuted, + fontSize: fontSize.sm, + }, + card: { + backgroundColor: colors.bgCard, + borderRadius: borderRadius.md, + padding: spacing.l, + marginBottom: spacing.m, + borderWidth: 1, + borderColor: colors.borderLight, + }, + cardTitle: { + flexDirection: "row", + alignItems: "center", + gap: spacing.s, + marginBottom: spacing.m, + }, + cardTitleText: { + color: colors.textPrimary, + fontSize: fontSize.md, + fontWeight: fontWeight.semibold, + }, + hint: { + color: colors.textMuted, + fontSize: fontSize.xs, + marginBottom: spacing.m, + lineHeight: 18, + }, + row: { flexDirection: "row", gap: spacing.m }, + half: { flex: 1 }, + fieldSpacing: { marginTop: spacing.m }, + saveBtn: { + marginTop: spacing.m, + backgroundColor: colors.accent, + borderRadius: borderRadius.sm, + paddingVertical: spacing.m, + paddingHorizontal: spacing.l, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: spacing.s, + }, + saveBtnSecondary: { + backgroundColor: "transparent", + borderWidth: 1, + borderColor: colors.accent + "66", + }, + saveBtnText: { + color: "#fff", + fontSize: fontSize.sm, + fontWeight: fontWeight.semibold, + }, + saveBtnTextSecondary: { color: colors.accent }, + // Modal + modalOverlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.6)", + justifyContent: "center", + alignItems: "center", + padding: spacing.l, + }, + modalBox: { + backgroundColor: colors.bgCard, + borderRadius: borderRadius.md, + borderWidth: 1, + borderColor: colors.borderLight, + padding: spacing.xl, + width: "100%", + maxWidth: 360, + alignItems: "center", + }, + modalIconWrap: { + width: 52, + height: 52, + borderRadius: 26, + backgroundColor: colors.accent + "22", + borderWidth: 1, + borderColor: colors.accent + "44", + justifyContent: "center", + alignItems: "center", + marginBottom: spacing.m, + }, + modalTitle: { + color: colors.textPrimary, + fontSize: fontSize.md, + fontWeight: fontWeight.bold, + textAlign: "center", + marginBottom: spacing.s, + }, + modalBody: { + color: colors.textMuted, + fontSize: fontSize.sm, + textAlign: "center", + lineHeight: 20, + marginBottom: spacing.l, + }, + modalActions: { + flexDirection: "row", + gap: spacing.m, + width: "100%", + }, + modalBtnCancel: { + flex: 1, + paddingVertical: spacing.m, + borderRadius: borderRadius.sm, + backgroundColor: "transparent", + borderWidth: 1, + borderColor: colors.borderLight, + alignItems: "center", + }, + modalBtnConfirm: { + flex: 1, + paddingVertical: spacing.m, + borderRadius: borderRadius.sm, + backgroundColor: "transparent", + borderWidth: 1, + borderColor: colors.accent + "66", + alignItems: "center", + }, + modalBtnCancelText: { + color: colors.textMuted, + fontSize: fontSize.sm, + fontWeight: fontWeight.semibold, + }, + modalBtnConfirmText: { + color: colors.accent, + fontSize: fontSize.sm, + fontWeight: fontWeight.semibold, + }, + pwdInputWrapper: { + flexDirection: "row" as const, + alignItems: "center" as const, + borderWidth: 1, + borderRadius: borderRadius.sm, + paddingHorizontal: spacing.m, + height: 46, + marginBottom: spacing.m, + width: "100%", + }, + pwdInputIcon: { marginRight: spacing.s }, + pwdInput: { flex: 1, fontSize: fontSize.sm }, + twoFARow: { + flexDirection: "row" as const, + alignItems: "center" as const, + justifyContent: "space-between" as const, + paddingTop: spacing.xs, + }, + twoFALabel: { + color: colors.textPrimary, + fontSize: fontSize.sm, + fontWeight: fontWeight.semibold, + }, + }), + [colors], + ); if (loadingProfile) { return ( - + Chargement... ); @@ -400,15 +480,17 @@ export default function ProfileScreen() { return ( - - + {/* Header */} - @{username || "..."} + {username || "..."} Mon compte @@ -416,7 +498,11 @@ export default function ProfileScreen() { {/* Carte compte */} - + Mon compte @@ -425,7 +511,13 @@ export default function ProfileScreen() { placeholder="Prénom" value={prenom} onChangeText={setPrenom} - icon={} + icon={ + + } /> @@ -433,7 +525,13 @@ export default function ProfileScreen() { placeholder="Nom" value={nom} onChangeText={setNom} - icon={} + icon={ + + } /> @@ -442,7 +540,13 @@ export default function ProfileScreen() { placeholder="Téléphone (compte)" value={telephone} onChangeText={setTelephone} - icon={} + icon={ + + } keyboardType="phone-pad" /> @@ -453,7 +557,9 @@ export default function ProfileScreen() { > - {savingContact ? "Enregistrement..." : "Enregistrer le compte"} + {savingContact + ? "Enregistrement..." + : "Enregistrer le compte"} @@ -461,26 +567,52 @@ export default function ProfileScreen() { {/* Carte adresse par défaut */} - - Adresse par défaut + + + Adresse par défaut + - Sera pré-remplie à la commande. Modifiable si vous n'êtes pas à cette adresse. + Sera pré-remplie à la commande. Modifiable si vous + n'êtes pas à cette adresse. } + icon={ + + } multiline numberOfLines={2} /> setShowConfirmAddressModal(true)} > - - + + Enregistrer l'adresse @@ -489,8 +621,14 @@ export default function ProfileScreen() { {/* Carte contact livraison */} - - Contact livraison + + + Contact livraison + Numéro et pseudo Signal utilisés lors de la livraison. @@ -499,7 +637,13 @@ export default function ProfileScreen() { placeholder="Téléphone par défaut" value={defaultPhone} onChangeText={setDefaultPhone} - icon={} + icon={ + + } keyboardType="phone-pad" /> @@ -507,15 +651,30 @@ export default function ProfileScreen() { placeholder="Pseudo Signal (optionnel)" value={signalPseudo} onChangeText={setSignalPseudo} - icon={} + icon={ + + } /> setShowSaveModal(true)} > - - + + Enregistrer les infos par défaut @@ -524,30 +683,68 @@ export default function ProfileScreen() { {/* Carte Changer le mot de passe */} - + Sécurité setShowPasswordModal(true)} > - - Changer le mot de passe + + + Changer le mot de passe + {/* Carte Parrainage */} - + Parrainage navigation.navigate("Parrainage")} > - - Voir mon parrainage + + + Voir mon parrainage + @@ -555,35 +752,87 @@ export default function ProfileScreen() { {telegramEnabled && ( - - Notifications Telegram + + + Notifications Telegram + - Recevez vos notifications sur Telegram même quand l'application est fermée. + Recevez vos notifications sur Telegram même quand + l'application est fermée. {telegramLinked ? ( - - - Compte Telegram lié + + + + Compte Telegram lié + - - Délier Telegram + + + Délier Telegram + ) : ( - + - {telegramLoading ? "Génération du lien..." : "Lier mon compte Telegram"} + {telegramLoading + ? "Génération du lien..." + : "Lier mon compte Telegram"} )} @@ -594,11 +843,18 @@ export default function ProfileScreen() { {twoFAAdminEnabled && telegramLinked && ( - - Double authentification (2FA) + + + Double authentification (2FA) + - À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte. + À chaque connexion, un code à 6 chiffres vous sera + envoyé sur Telegram avant d'accéder à votre compte. @@ -606,26 +862,53 @@ export default function ProfileScreen() { {twoFAEnabled ? "Activée" : "Désactivée"} {twoFAEnabled && ( - - - Protection activée + + + + Protection activée + )} {twoFALoading ? ( - + ) : ( )} )} - {}}> - + - Enregistrer les infos par défaut ? + + Enregistrer les infos par défaut ? + - Adresse, téléphone de livraison et pseudo Signal seront sauvegardés et pré-remplis lors de vos prochaines commandes. + Adresse, téléphone de livraison et pseudo Signal + seront sauvegardés et pré-remplis lors de vos + prochaines commandes. setShowSaveModal(false)} > - Annuler + + Annuler + - Confirmer + + Confirmer + @@ -680,17 +975,44 @@ export default function ProfileScreen() { onPress={() => setShowPasswordModal(false)} > {}}> - + - + - Changer le mot de passe + + Changer le mot de passe + {/* Champ mot de passe actuel */} - - + + - setShowCurrentPwd(!showCurrentPwd)}> - + + setShowCurrentPwd(!showCurrentPwd) + } + > + {/* Nouveau mot de passe */} - - + + - setShowNewPwd(!showNewPwd)}> - + setShowNewPwd(!showNewPwd)} + > + {/* Confirmer nouveau mot de passe */} - - + + - setShowConfirmPwd(!showConfirmPwd)}> - + + setShowConfirmPwd(!showConfirmPwd) + } + > + @@ -746,17 +1126,33 @@ export default function ProfileScreen() { onPress={() => setShowPasswordModal(false)} disabled={savingPassword} > - Annuler + + Annuler + - {savingPassword - ? - : Confirmer - } + {savingPassword ? ( + + ) : ( + + Confirmer + + )} @@ -778,25 +1174,58 @@ export default function ProfileScreen() { > {}}> - - + + - Délier Telegram ? + + Délier Telegram ? + - Vous ne recevrez plus de notifications Telegram. La double authentification sera également désactivée. + Vous ne recevrez plus de notifications Telegram. + La double authentification sera également + désactivée. setShowTelegramUnlinkModal(false)} + onPress={() => + setShowTelegramUnlinkModal(false) + } > - Annuler + + Annuler + - Délier + + Délier + @@ -818,19 +1247,45 @@ export default function ProfileScreen() { > {}}> - - + + Compte délié - Votre compte Telegram a été délié avec succès. Vous ne recevrez plus de notifications via Telegram. + Votre compte Telegram a été délié avec succès. + Vous ne recevrez plus de notifications via + Telegram. setShowTelegramSuccessModal(false)} + style={[ + styles.modalBtnConfirm, + { flex: 1, borderColor: "#10b98166" }, + ]} + onPress={() => + setShowTelegramSuccessModal(false) + } > - Fermer + + Fermer + @@ -853,24 +1308,37 @@ export default function ProfileScreen() { {}}> - + - Mettre à jour le compte ? + + Mettre à jour le compte ? + - Vos informations (prénom, nom, téléphone) seront enregistrées sur votre compte. + Vos informations (prénom, nom, téléphone) seront + enregistrées sur votre compte. setShowConfirmContactModal(false)} + onPress={() => + setShowConfirmContactModal(false) + } > - Annuler + + Annuler + - Confirmer + + Confirmer + @@ -892,25 +1360,54 @@ export default function ProfileScreen() { > {}}> - - + + - Enregistrer l'adresse ? + + Enregistrer l'adresse ? + - Cette adresse sera pré-remplie automatiquement lors de vos prochaines commandes. + Cette adresse sera pré-remplie automatiquement + lors de vos prochaines commandes. setShowConfirmAddressModal(false)} + onPress={() => + setShowConfirmAddressModal(false) + } > - Annuler + + Annuler + - Confirmer + + Confirmer + @@ -932,17 +1429,41 @@ export default function ProfileScreen() { > {}}> - - + + - {successTitle} + + {successTitle} + {successMsg} setShowSuccessModal(false)} > - Fermer + + Fermer + @@ -964,24 +1485,51 @@ export default function ProfileScreen() { > {}}> - - + + - Une erreur est survenue + + Une erreur est survenue + {errorMsg} setShowErrorModal(false)} > - Fermer + + Fermer + - ); }