This commit is contained in:
2026-05-09 15:30:48 +02:00
parent 810d7bcabd
commit 97712f240e
2 changed files with 383 additions and 257 deletions
-4
View File
@@ -52,10 +52,6 @@ type GeoService struct {
httpClient *http.Client httpClient *http.Client
} }
// ============================================
// CONSTRUCTEUR
// ============================================
func NewGeoService(redisClient *redis.Client, ctx context.Context) *GeoService { func NewGeoService(redisClient *redis.Client, ctx context.Context) *GeoService {
return &GeoService{ return &GeoService{
redis: redisClient, redis: redisClient,
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import Navbar from '../../components/Navbar'; import Navbar from "../../components/Navbar";
import './ConsultationHistorique.css'; import "./ConsultationHistorique.css";
import { import {
getMyCompletedOrders, getMyCompletedOrders,
formatPrice, formatPrice,
@@ -10,11 +10,15 @@ import {
isUserAuthenticated, isUserAuthenticated,
getPublicSettings, getPublicSettings,
getReferralBalance, getReferralBalance,
} from '../../api/api'; } from "../../api/api";
import type { PublicSettings } from '../../api/api'; import type { PublicSettings } from "../../api/api";
import type { CompletedOrder, ClientStats, PenaltyInfo } from "../../api/api_types"; import type {
CompletedOrder,
ClientStats,
PenaltyInfo,
} from "../../api/api_types";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faCannabis, faCannabis,
faPills, faPills,
@@ -32,7 +36,7 @@ import {
faChevronRight, faChevronRight,
faCheckCircle, faCheckCircle,
faHistory, faHistory,
} from '@fortawesome/free-solid-svg-icons'; } from "@fortawesome/free-solid-svg-icons";
function ConsultationHistorique() { function ConsultationHistorique() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -40,18 +44,33 @@ function ConsultationHistorique() {
const [orders, setOrders] = useState<CompletedOrder[]>([]); const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [clientStats, setClientStats] = useState<ClientStats | null>(null); const [clientStats, setClientStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null); const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ 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 }); const [appSettings, setAppSettings] = useState<PublicSettings>({
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,
});
const [referralBalance, setReferralBalance] = useState<number>(0); const [referralBalance, setReferralBalance] = useState<number>(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>("");
useEffect(() => { useEffect(() => {
if (!isUserAuthenticated()) navigate('/login/client', { replace: true }); if (!isUserAuthenticated())
navigate("/login/client", { replace: true });
}, [navigate]); }, [navigate]);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
if (!isUserAuthenticated()) navigate('/login/client', { replace: true }); if (!isUserAuthenticated())
navigate("/login/client", { replace: true });
}, 5000); }, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [navigate]); }, [navigate]);
@@ -62,25 +81,30 @@ function ConsultationHistorique() {
getPublicSettings().then((s) => { getPublicSettings().then((s) => {
setAppSettings(s); setAppSettings(s);
if (s.referral_enabled) { if (s.referral_enabled) {
getReferralBalance().then((r) => { if (r.success) setReferralBalance(r.balance); }); getReferralBalance().then((r) => {
if (r.success) setReferralBalance(r.balance);
});
} }
}); });
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchHistory = async () => { const fetchHistory = async () => {
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; } if (!isUserAuthenticated()) {
navigate("/login/client", { replace: true });
return;
}
setIsLoading(true); setIsLoading(true);
setError(''); setError("");
try { try {
const result = await getMyCompletedOrders(); const result = await getMyCompletedOrders();
if (result.success) { if (result.success) {
setOrders(result.commands); setOrders(result.commands);
setClientStats(result.client_stats || null); setClientStats(result.client_stats || null);
} else { } else {
setError(result.message || 'Erreur lors du chargement'); setError(result.message || "Erreur lors du chargement");
} }
} catch { } catch {
setError('Erreur de connexion'); setError("Erreur de connexion");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -91,11 +115,16 @@ function ConsultationHistorique() {
try { try {
const result = await getMyPenalties(); const result = await getMyPenalties();
if (result.success && result.data) setPenalties(result.data); if (result.success && result.data) setPenalties(result.data);
} catch { /* ignore */ } } catch {
/* ignore */
}
}; };
const viewOrderDetails = (order: CompletedOrder) => { const viewOrderDetails = (order: CompletedOrder) => {
if (!isUserAuthenticated()) { navigate('/login/client', { replace: true }); return; } if (!isUserAuthenticated()) {
navigate("/login/client", { replace: true });
return;
}
navigate(`/user/commande/${order.client_order_number ?? order.id}`, { navigate(`/user/commande/${order.client_order_number ?? order.id}`, {
state: { commandId: order.id }, state: { commandId: order.id },
}); });
@@ -103,7 +132,11 @@ function ConsultationHistorique() {
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
const d = new Date(dateStr); const d = new Date(dateStr);
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }); return d.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
year: "numeric",
});
}; };
if (isLoading) { if (isLoading) {
@@ -112,58 +145,84 @@ function ConsultationHistorique() {
<Navbar /> <Navbar />
<div className="history-container"> <div className="history-container">
<div className="loading-container"> <div className="loading-container">
<div className="loading-spinner"><div className="spinner"></div></div> <div className="loading-spinner">
<p className="loading-text">Chargement de l'historique...</p> <div className="spinner"></div>
</div>
<p className="loading-text">
Chargement de l'historique...
</p>
</div> </div>
</div> </div>
</> </>
); );
} }
const poolNames = clientStats?.pool_names?.length ? clientStats.pool_names : appSettings.pool_names; const poolNames = clientStats?.pool_names?.length
? clientStats.pool_names
: appSettings.pool_names;
const poolPoints = clientStats?.pool_points ?? [clientStats?.points ?? 0]; const poolPoints = clientStats?.pool_points ?? [clientStats?.points ?? 0];
const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0); const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0);
const penaltyCount = penalties?.total_penalty || clientStats?.penalties || 0; const penaltyCount =
penalties?.total_penalty || clientStats?.penalties || 0;
const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar]; const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar];
const poolIconColors = ['#10b981', '#e879f9', '#fb923c', '#38bdf8', '#7c3aed']; const poolIconColors = [
"#10b981",
"#e879f9",
"#fb923c",
"#38bdf8",
"#7c3aed",
];
return ( return (
<> <>
<Navbar /> <Navbar />
<div className="history-container"> <div className="history-container">
<h1 className="history-title">Historique des commandes</h1> <h1 className="history-title">Historique des commandes</h1>
{/* Stats grid */} {/* Stats grid */}
<div className="stats-grid"> <div className="stats-grid">
{/* Total commandes */} {/* Total commandes */}
<div className="stat-card2"> <div className="stat-card2">
<div className="stat-icon icon-total-orders"> <div className="stat-icon icon-total-orders">
<FontAwesomeIcon icon={faReceipt} /> <FontAwesomeIcon icon={faReceipt} />
</div> </div>
<p className="stat-value">{clientStats?.total_commands ?? orders.length}</p> <p className="stat-value">
{clientStats?.total_commands ?? orders.length}
</p>
<p className="stat-label">Commandes</p> <p className="stat-label">Commandes</p>
</div> </div>
{/* Points */} {/* Points */}
{appSettings.points_enabled && ( {appSettings.points_enabled &&
poolNames.length <= 1 ? ( (poolNames.length <= 1 ? (
<div className="stat-card2"> <div className="stat-card2">
<div className="stat-icon icon-total"> <div className="stat-icon icon-total">
<FontAwesomeIcon icon={faTrophy} /> <FontAwesomeIcon icon={faTrophy} />
</div> </div>
<p className="stat-value">{poolPoints[0] || 0}</p> <p className="stat-value">
<p className="stat-label">Pts {poolNames[0] ?? 'Points'}</p> {poolPoints[0] || 0}
</p>
<p className="stat-label">
Pts {poolNames[0] ?? "Points"}
</p>
</div> </div>
) : ( ) : (
<> <>
{poolNames.map((name, i) => ( {poolNames.map((name, i) => (
<div key={i} className="stat-card2"> <div key={i} className="stat-card2">
<div className="stat-icon" style={{ background: `linear-gradient(135deg, ${poolIconColors[i] ?? '#7c3aed'}cc, ${poolIconColors[i] ?? '#7c3aed'})` }}> <div
<FontAwesomeIcon icon={poolIcons[i] ?? faStar} /> className="stat-icon"
style={{
background: `linear-gradient(135deg, ${poolIconColors[i] ?? "#7c3aed"}cc, ${poolIconColors[i] ?? "#7c3aed"})`,
}}
>
<FontAwesomeIcon
icon={poolIcons[i] ?? faStar}
/>
</div> </div>
<p className="stat-value">{poolPoints[i] || 0}</p> <p className="stat-value">
{poolPoints[i] || 0}
</p>
<p className="stat-label">Pts {name}</p> <p className="stat-label">Pts {name}</p>
</div> </div>
))} ))}
@@ -172,21 +231,38 @@ function ConsultationHistorique() {
<div className="stat-icon icon-total"> <div className="stat-icon icon-total">
<FontAwesomeIcon icon={faTrophy} /> <FontAwesomeIcon icon={faTrophy} />
</div> </div>
<p className="stat-value">{totalPoints}</p> <p className="stat-value">
<p className="stat-label">Total Points</p> {totalPoints}
</p>
<p className="stat-label">
Total Points
</p>
</div> </div>
)} )}
</> </>
) ))}
)}
{/* Score amendes */} {/* Score amendes */}
{appSettings.show_amende_score && ( {appSettings.show_amende_score && (
<div className={`stat-card2${penaltyCount >= 3 ? ' stat-card-danger' : penaltyCount > 0 ? ' stat-card-warning' : ''}`}> <div
<div className={`stat-icon ${penaltyCount >= 3 ? 'icon-penalty-critical' : penaltyCount > 0 ? 'icon-penalty-warning' : 'icon-penalty-ok'}`}> className={`stat-card2${penaltyCount >= 3 ? " stat-card-danger" : penaltyCount > 0 ? " stat-card-warning" : ""}`}
<FontAwesomeIcon icon={penaltyCount > 0 ? faExclamationTriangle : faShieldAlt} /> >
<div
className={`stat-icon ${penaltyCount >= 3 ? "icon-penalty-critical" : penaltyCount > 0 ? "icon-penalty-warning" : "icon-penalty-ok"}`}
>
<FontAwesomeIcon
icon={
penaltyCount > 0
? faExclamationTriangle
: faShieldAlt
}
/>
</div> </div>
<p className={`stat-value ${penaltyCount >= 3 ? 'value-danger' : penaltyCount > 0 ? 'value-warning' : ''}`}>{penaltyCount}</p> <p
className={`stat-value ${penaltyCount >= 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`}
>
{penaltyCount}
</p>
<p className="stat-label">Score amendes</p> <p className="stat-label">Score amendes</p>
</div> </div>
)} )}
@@ -194,34 +270,57 @@ function ConsultationHistorique() {
{/* Bouton parrainage */} {/* Bouton parrainage */}
{appSettings.referral_enabled && ( {appSettings.referral_enabled && (
<div className="referral-btn-row" onClick={() => navigate('/user/parrainage')}> <div
<FontAwesomeIcon icon={faGift} className="referral-btn-icon" /> className="referral-btn-row"
onClick={() => navigate("/user/parrainage")}
>
<FontAwesomeIcon
icon={faGift}
className="referral-btn-icon"
/>
<span className="referral-btn-text"> <span className="referral-btn-text">
Parrainage{referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ''} Parrainage
{referralBalance > 0
? `${referralBalance.toFixed(2)}`
: ""}
</span> </span>
<FontAwesomeIcon icon={faChevronRight} className="referral-btn-chevron" /> <FontAwesomeIcon
icon={faChevronRight}
className="referral-btn-chevron"
/>
</div> </div>
)} )}
{error && ( {error && (
<div className="error-banner"> <div className="error-banner">
<FontAwesomeIcon icon={faExclamationTriangle} size="2x" /> <FontAwesomeIcon
icon={faExclamationTriangle}
size="2x"
/>
<p>{error}</p> <p>{error}</p>
</div> </div>
)} )}
{orders.length === 0 ? ( {orders.length === 0 ? (
<div className="empty-history"> <div className="empty-history">
<FontAwesomeIcon icon={faHistory} className="empty-icon" /> <FontAwesomeIcon
icon={faHistory}
className="empty-icon"
/>
<h2>Aucun historique</h2> <h2>Aucun historique</h2>
<p>Vos commandes terminées apparaîtront ici</p> <p>Vos commandes terminées apparaîtront ici</p>
<button className="browse-button" onClick={() => navigate('/user/accueil')}> <button
className="browse-button"
onClick={() => navigate("/user/accueil")}
>
Découvrir nos produits Découvrir nos produits
</button> </button>
</div> </div>
) : ( ) : (
<> <>
<p className="section-title">Historique des commandes</p> <p className="section-title">
Historique des commandes
</p>
<div className="orders-list"> <div className="orders-list">
{orders.map((order) => ( {orders.map((order) => (
<div <div
@@ -231,45 +330,76 @@ function ConsultationHistorique() {
> >
<div className="order-card-header"> <div className="order-card-header">
<span className="order-card-number"> <span className="order-card-number">
Commande #{(order.client_order_number ?? 0).toString().padStart(4, '0')} Commande #
{(order.client_order_number ?? 0)
.toString()
.padStart(4, "0")}
</span> </span>
<span className="order-card-badge"> <span className="order-card-badge">
<FontAwesomeIcon icon={faCheckCircle} style={{ marginRight: '0.35rem' }} /> <FontAwesomeIcon
icon={faCheckCircle}
style={{
marginRight: "0.35rem",
}}
/>
Livrée Livrée
</span> </span>
</div> </div>
<div className="order-card-row"> <div className="order-card-row">
<FontAwesomeIcon icon={faMapMarkerAlt} className="order-card-row-icon" /> <FontAwesomeIcon
icon={faMapMarkerAlt}
className="order-card-row-icon"
/>
<span className="order-card-row-text"> <span className="order-card-row-text">
{order.adresse ? (order.adresse.length > 50 ? order.adresse.substring(0, 50) + '' : order.adresse) : 'N/A'} {order.adresse
? order.adresse.length > 50
? order.adresse.substring(
0,
50,
) + "…"
: order.adresse
: "N/A"}
</span> </span>
</div> </div>
<div className="order-card-row"> <div className="order-card-row">
<FontAwesomeIcon icon={faClock} className="order-card-row-icon" /> <FontAwesomeIcon
icon={faClock}
className="order-card-row-icon"
/>
<span className="order-card-row-text"> <span className="order-card-row-text">
{formatDate(order.created_at)} · {getOrderAge(order.created_at)} {formatDate(order.created_at)} ·{" "}
{getOrderAge(order.created_at)}
</span> </span>
</div> </div>
{order.livreur_assign && ( {order.livreur_assign && (
<div className="order-card-row"> <div className="order-card-row">
<FontAwesomeIcon icon={faBicycle} className="order-card-row-icon" /> <FontAwesomeIcon
<span className="order-card-row-text">{order.livreur_assign}</span> icon={faBicycle}
className="order-card-row-icon"
/>
<span className="order-card-row-text">
{order.livreur_assign}
</span>
</div> </div>
)} )}
<div className="order-card-footer"> <div className="order-card-footer">
<span className="order-card-total">{formatPrice(order.total_prix || 0)}</span> <span className="order-card-total">
<FontAwesomeIcon icon={faChevronRight} className="order-card-chevron" /> {formatPrice(order.total_prix || 0)}
</span>
<FontAwesomeIcon
icon={faChevronRight}
className="order-card-chevron"
/>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</> </>
)} )}
</div> </div>
</> </>
); );