import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import Navbar from "../../components/Navbar"; import "./ConsultationHistorique.css"; import { getMyCompletedOrders, formatPrice, getOrderAge, getMyPenalties, isUserAuthenticated, getPublicSettings, getReferralBalance, getMyPointsRewards, claimMyReward, } from "../../api/api"; import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig, } from "../../api/api"; import type { CompletedOrder, ClientStats, PenaltyInfo, } from "../../api/api_types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCannabis, faPills, faFlask, faMortarPestle, faStar, faTrophy, faExclamationTriangle, faShieldAlt, faGift, faReceipt, faMapMarkerAlt, faClock, faBicycle, faChevronRight, faCheckCircle, faHistory, faTimes, faCheck, } from "@fortawesome/free-solid-svg-icons"; function ConsultationHistorique() { const navigate = useNavigate(); const [orders, setOrders] = useState([]); const [clientStats, setClientStats] = useState(null); const [penalties, setPenalties] = useState(null); const [appSettings, setAppSettings] = useState({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, referral_amount: 0, pool_names: ["Pool 1", "Pool 2"], crypto_payment_enabled: false, crypto_only: false, nowpayments_currencies: [], shop_name: "Milieu-Nantais", two_fa_enabled: false, contact_telegram: "", client_color_primary: "", client_color_secondary: "", client_color_success: "", client_color_danger: "", client_color_warning: "", client_title_gradient_from: "", client_title_gradient_to: "", }); const [referralBalance, setReferralBalance] = useState(0); const [pointsRewards, setPointsRewards] = useState<{ enabled: boolean; pools: PointsPoolInfo[]; reward: PointsRewardConfig | null; } | null>(null); const [claimingPool, setClaimingPool] = useState(null); const [claimFeedback, setClaimFeedback] = useState<{ pool: string; type: "success" | "error"; text: string; } | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); // ── Modal de sélection du produit récompense ── const [rewardModalPool, setRewardModalPool] = useState(null); const [selectedProductId, setSelectedProductId] = useState( null, ); useEffect(() => { if (!isUserAuthenticated()) navigate("/login/client", { replace: true }); }, [navigate]); useEffect(() => { const interval = setInterval(() => { if (!isUserAuthenticated()) navigate("/login/client", { replace: true }); }, 5000); return () => clearInterval(interval); }, [navigate]); useEffect(() => { fetchHistory(); fetchPenalties(); getPublicSettings().then((s) => { setAppSettings(s); if (s.referral_enabled) { getReferralBalance().then((r) => { if (r.success) setReferralBalance(r.balance); }); } }); getMyPointsRewards().then((r) => { if (r.success && r.enabled) setPointsRewards(r); }); }, []); // eslint-disable-line react-hooks/exhaustive-deps const fetchHistory = async () => { if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } setIsLoading(true); setError(""); try { const result = await getMyCompletedOrders(); if (result.success) { setOrders(result.commands); setClientStats(result.client_stats || null); } else { setError(result.message || "Erreur lors du chargement"); } } catch { setError("Erreur de connexion"); } finally { setIsLoading(false); } }; const fetchPenalties = async () => { if (!isUserAuthenticated()) return; try { const result = await getMyPenalties(); if (result.success && result.data) setPenalties(result.data); } catch { /* ignore */ } }; const viewOrderDetails = (order: CompletedOrder) => { if (!isUserAuthenticated()) { navigate("/login/client", { replace: true }); return; } navigate(`/user/commande/${order.client_order_number ?? order.id}`, { state: { commandId: order.id }, }); }; const formatDate = (dateStr: string) => { const d = new Date(dateStr); return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric", }); }; // Ouvre la modal de choix si plusieurs produits sont éligibles, // sinon réclame directement (1 seul produit configuré ou aucun item). const openRewardModal = (pool: PointsPoolInfo) => { const items = pool.eligible_reward_items ?? []; if (items.length > 1) { setSelectedProductId(items[0]?.product_id ?? null); setRewardModalPool(pool); } else { handleClaim(pool.key); } }; const closeRewardModal = () => { setRewardModalPool(null); setSelectedProductId(null); }; const confirmRewardChoice = async () => { if (!rewardModalPool) return; const poolKey = rewardModalPool.key; const productId = selectedProductId ?? undefined; closeRewardModal(); await handleClaim(poolKey, productId); }; const handleClaim = async (poolKey: string, productId?: number) => { setClaimingPool(poolKey); setClaimFeedback(null); const res = await claimMyReward(poolKey, productId); setClaimingPool(null); if (res.success) { const text = res.product_added && res.product_names?.length ? `${res.product_names.join(", ")} ajouté${res.product_names.length > 1 ? "s" : ""} à votre panier ! Commandez au moins un produit pour en profiter.` : res.description || "Récompense réclamée !"; setClaimFeedback({ pool: poolKey, type: "success", text }); getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); }); } else { setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur", }); } }; const poolNames = clientStats?.pool_names?.length ? clientStats.pool_names : appSettings.pool_names; const poolPoints = clientStats?.pool_points ?? [clientStats?.points ?? 0]; const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0); const penaltyCount = penalties?.total_penalty || clientStats?.penalties || 0; const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar]; const poolIconColors = [ "#10b981", "#e879f9", "#fb923c", "#38bdf8", "#7c3aed", ]; if (isLoading) { return ( <>

Chargement de l'historique...

); } return ( <>

Historique des commandes

{/* Stats grid */}
{/* Total commandes */}

{clientStats?.total_commands ?? orders.length}

Commandes

{/* Points */} {appSettings.points_enabled && (poolNames.length <= 1 ? (

{poolPoints[0] || 0}

Pts {poolNames[0] ?? "Points"}

) : ( <> {poolNames.map((name, i) => (

{poolPoints[i] || 0}

Pts {name}

))} {totalPoints > 0 && (

{totalPoints}

Total Points

)} ))} {/* Score amendes */} {appSettings.show_amende_score && (
= 3 ? " stat-card-danger" : penaltyCount > 0 ? " stat-card-warning" : ""}`} >
= 3 ? "icon-penalty-critical" : penaltyCount > 0 ? "icon-penalty-warning" : "icon-penalty-ok"}`} > 0 ? faExclamationTriangle : faShieldAlt } />

= 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`} > {penaltyCount} €

Score amendes

)}
{/* Section récompenses par palier */} {pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && (

Récompenses

{pointsRewards.reward.description && (

{pointsRewards.reward.description}

)} {(pointsRewards.reward.reward_items ?? []).filter( (it: RewardItemConfig) => it.product_name, ).length > 0 && (
{( pointsRewards.reward.reward_items ?? [] ).map( (it: RewardItemConfig, idx: number) => ( {it.product_name} {it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""} {it.price > 0 ? ` — ${it.price}€` : ""} ), )}
)}
{pointsRewards.pools.map((pool) => { const threshold = pointsRewards.reward!.threshold; const progress = Math.min( 100, Math.round( ((pool.points % threshold) / threshold) * 100, ), ); const remaining = threshold - (pool.points % threshold); const isClaiming = claimingPool === pool.key; const feedback = claimFeedback?.pool === pool.key ? claimFeedback : null; return (
{pool.name} {pool.points} pts
{pool.eligible_configs.length > 0 && (
{pool.eligible_configs.flatMap( (cfg) => cfg.all_products ? [ { cfg.category } , ] : ( cfg.products ?? [] ).map( ( p, ) => ( { p.product_name } ), ), )}
)}
{pool.rewards_available > 0 ? ( {pool.rewards_available}{" "} récompense {pool.rewards_available > 1 ? "s" : ""}{" "} disponible {pool.rewards_available > 1 ? "s" : ""} ) : ( Encore {remaining} pts pour une récompense )}
{feedback && (

{feedback.type === "success" && ( )} {feedback.text}

)} {pool.rewards_available > 0 && ( )}
); })}
)} {/* Bouton parrainage */} {appSettings.referral_enabled && (
navigate("/user/parrainage")} > Parrainage {referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ""}
)} {error && (

{error}

)} {orders.length === 0 ? (

Aucun historique

Vos commandes terminées apparaîtront ici

) : ( <>

Historique des commandes

{orders.map((order) => (
viewOrderDetails(order)} >
Commande # {(order.client_order_number ?? 0) .toString() .padStart(4, "0")} Livrée
{order.adresse ? order.adresse.length > 50 ? order.adresse.substring( 0, 50, ) + "…" : order.adresse : "N/A"}
{formatDate(order.created_at)} ·{" "} {getOrderAge(order.created_at)}
{order.livreur_assign && (
{order.livreur_assign}
)}
{formatPrice( (order.total_prix || 0) - (order.referral_used || 0), )} {(order.referral_used || 0) > 0 && ( {" "} — dont{" "} {formatPrice( order.referral_used ?? 0, )}{" "} parrainage )}
))}
)}
{/* ============================================ */} {/* MODAL — CHOIX DU PRODUIT RÉCOMPENSE */} {/* ============================================ */} {rewardModalPool && pointsRewards?.reward && (
e.stopPropagation()} >

Choisissez votre récompense

{rewardModalPool.name} —{" "} {rewardModalPool.rewards_available} récompense {rewardModalPool.rewards_available > 1 ? "s" : ""}{" "} disponible {rewardModalPool.rewards_available > 1 ? "s" : ""}

{(rewardModalPool.eligible_reward_items ?? []).map( (item) => { const isSelected = selectedProductId === item.product_id; return ( ); }, )}
)} ); } export default ConsultationHistorique;