Files
projet_gestion_commande/frontend-prep/src/pages/User/ConsultationHistorique.tsx
T
2026-06-12 09:43:29 +02:00

502 lines
22 KiB
TypeScript

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 } 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,
} from "@fortawesome/free-solid-svg-icons";
function ConsultationHistorique() {
const navigate = useNavigate();
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [clientStats, setClientStats] = useState<ClientStats | 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,
contact_telegram: "",
});
const [referralBalance, setReferralBalance] = useState<number>(0);
const [pointsRewards, setPointsRewards] = useState<{
enabled: boolean;
pools: PointsPoolInfo[];
reward: PointsRewardConfig | null;
} | null>(null);
const [claimingPool, setClaimingPool] = useState<string | null>(null);
const [claimFeedback, setClaimFeedback] = useState<{ pool: string; type: "success" | "error"; text: string } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string>("");
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",
});
};
if (isLoading) {
return (
<>
<Navbar />
<div className="history-container">
<div className="loading-container">
<div className="loading-spinner">
<div className="spinner"></div>
</div>
<p className="loading-text">
Chargement de l'historique...
</p>
</div>
</div>
</>
);
}
const handleClaim = async (poolKey: string) => {
setClaimingPool(poolKey);
setClaimFeedback(null);
const res = await claimMyReward(poolKey);
setClaimingPool(null);
if (res.success) {
setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" });
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",
];
return (
<>
<Navbar />
<div className="history-container">
<h1 className="history-title">Historique des commandes</h1>
{/* Stats grid */}
<div className="stats-grid">
{/* Total commandes */}
<div className="stat-card2">
<div className="stat-icon icon-total-orders">
<FontAwesomeIcon icon={faReceipt} />
</div>
<p className="stat-value">
{clientStats?.total_commands ?? orders.length}
</p>
<p className="stat-label">Commandes</p>
</div>
{/* Points */}
{appSettings.points_enabled &&
(poolNames.length <= 1 ? (
<div className="stat-card2">
<div className="stat-icon icon-total">
<FontAwesomeIcon icon={faTrophy} />
</div>
<p className="stat-value">
{poolPoints[0] || 0}
</p>
<p className="stat-label">
Pts {poolNames[0] ?? "Points"}
</p>
</div>
) : (
<>
{poolNames.map((name, i) => (
<div key={i} className="stat-card2">
<div
className="stat-icon"
style={{
background: `linear-gradient(135deg, ${poolIconColors[i] ?? "#7c3aed"}cc, ${poolIconColors[i] ?? "#7c3aed"})`,
}}
>
<FontAwesomeIcon
icon={poolIcons[i] ?? faStar}
/>
</div>
<p className="stat-value">
{poolPoints[i] || 0}
</p>
<p className="stat-label">Pts {name}</p>
</div>
))}
{totalPoints > 0 && (
<div className="stat-card2">
<div className="stat-icon icon-total">
<FontAwesomeIcon icon={faTrophy} />
</div>
<p className="stat-value">
{totalPoints}
</p>
<p className="stat-label">
Total Points
</p>
</div>
)}
</>
))}
{/* Score amendes */}
{appSettings.show_amende_score && (
<div
className={`stat-card2${penaltyCount >= 3 ? " stat-card-danger" : penaltyCount > 0 ? " stat-card-warning" : ""}`}
>
<div
className={`stat-icon ${penaltyCount >= 3 ? "icon-penalty-critical" : penaltyCount > 0 ? "icon-penalty-warning" : "icon-penalty-ok"}`}
>
<FontAwesomeIcon
icon={
penaltyCount > 0
? faExclamationTriangle
: faShieldAlt
}
/>
</div>
<p
className={`stat-value ${penaltyCount >= 3 ? "value-danger" : penaltyCount > 0 ? "value-warning" : ""}`}
>
{penaltyCount}
</p>
<p className="stat-label">Score amendes</p>
</div>
)}
</div>
{/* Section récompenses par palier */}
{pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && (
<div className="rewards-section">
<p className="rewards-section-title">
<FontAwesomeIcon icon={faTrophy} className="rewards-section-icon" />
Récompenses
</p>
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
<div className="rewards-pools">
{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 (
<div key={pool.key} className="reward-pool-card">
<div className="reward-pool-header">
<span className="reward-pool-name">{pool.name}</span>
<span className="reward-pool-pts">{pool.points} pts</span>
</div>
{pool.eligible_configs.length > 0 && (
<div className="reward-eligible-cats">
{pool.eligible_configs.map((cfg) =>
cfg.all_products ? (
<span key={cfg.category} className="reward-eligible-cat">
{cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""}
</span>
) : (
cfg.product_names.map((name) => (
<span key={name} className="reward-eligible-cat">
{name}
</span>
))
)
)}
</div>
)}
<div className="reward-progress-bar">
<div className="reward-progress-fill" style={{ width: `${progress}%` }} />
</div>
<div className="reward-pool-info">
{pool.rewards_available > 0 ? (
<span className="reward-available">{pool.rewards_available} récompense{pool.rewards_available > 1 ? "s" : ""} disponible{pool.rewards_available > 1 ? "s" : ""}</span>
) : (
<span className="reward-remaining">Encore {remaining} pts pour une récompense</span>
)}
</div>
{feedback && (
<p className={`reward-feedback reward-feedback-${feedback.type}`}>{feedback.text}</p>
)}
{pool.rewards_available > 0 && (
<button
className="reward-claim-btn"
onClick={() => handleClaim(pool.key)}
disabled={isClaiming}
>
{isClaiming ? "..." : "Réclamer ma récompense"}
</button>
)}
</div>
);
})}
</div>
</div>
)}
{/* Bouton parrainage */}
{appSettings.referral_enabled && (
<div
className="referral-btn-row"
onClick={() => navigate("/user/parrainage")}
>
<FontAwesomeIcon
icon={faGift}
className="referral-btn-icon"
/>
<span className="referral-btn-text">
Parrainage
{referralBalance > 0
? ` — ${referralBalance.toFixed(2)} €`
: ""}
</span>
<FontAwesomeIcon
icon={faChevronRight}
className="referral-btn-chevron"
/>
</div>
)}
{error && (
<div className="error-banner">
<FontAwesomeIcon
icon={faExclamationTriangle}
size="2x"
/>
<p>{error}</p>
</div>
)}
{orders.length === 0 ? (
<div className="empty-history">
<FontAwesomeIcon
icon={faHistory}
className="empty-icon"
/>
<h2>Aucun historique</h2>
<p>Vos commandes terminées apparaîtront ici</p>
<button
className="browse-button"
onClick={() => navigate("/user/accueil")}
>
Découvrir nos produits
</button>
</div>
) : (
<>
<p className="section-title">
Historique des commandes
</p>
<div className="orders-list">
{orders.map((order) => (
<div
key={order.id}
className="order-card"
onClick={() => viewOrderDetails(order)}
>
<div className="order-card-header">
<span className="order-card-number">
Commande #
{(order.client_order_number ?? 0)
.toString()
.padStart(4, "0")}
</span>
<span className="order-card-badge">
<FontAwesomeIcon
icon={faCheckCircle}
style={{
marginRight: "0.35rem",
}}
/>
Livrée
</span>
</div>
<div className="order-card-row">
<FontAwesomeIcon
icon={faMapMarkerAlt}
className="order-card-row-icon"
/>
<span className="order-card-row-text">
{order.adresse
? order.adresse.length > 50
? order.adresse.substring(
0,
50,
) + "…"
: order.adresse
: "N/A"}
</span>
</div>
<div className="order-card-row">
<FontAwesomeIcon
icon={faClock}
className="order-card-row-icon"
/>
<span className="order-card-row-text">
{formatDate(order.created_at)} ·{" "}
{getOrderAge(order.created_at)}
</span>
</div>
{order.livreur_assign && (
<div className="order-card-row">
<FontAwesomeIcon
icon={faBicycle}
className="order-card-row-icon"
/>
<span className="order-card-row-text">
{order.livreur_assign}
</span>
</div>
)}
<div className="order-card-footer">
<span className="order-card-total">
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))}
</span>
<FontAwesomeIcon
icon={faChevronRight}
className="order-card-chevron"
/>
</div>
</div>
))}
</div>
</>
)}
</div>
</>
);
}
export default ConsultationHistorique;