chore: build
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s

This commit is contained in:
Xor290
2026-08-06 12:06:05 +02:00
parent c034088bee
commit 22a8d5026c
174 changed files with 30315 additions and 16120 deletions
@@ -13,7 +13,12 @@ import {
getMyPointsRewards,
claimMyReward,
} from "../../api/api";
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api";
import type {
PublicSettings,
PointsPoolInfo,
PointsRewardConfig,
RewardItemConfig,
} from "../../api/api";
import type {
CompletedOrder,
ClientStats,
@@ -38,6 +43,8 @@ import {
faChevronRight,
faCheckCircle,
faHistory,
faTimes,
faCheck,
} from "@fortawesome/free-solid-svg-icons";
function ConsultationHistorique() {
@@ -60,6 +67,13 @@ function ConsultationHistorique() {
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<number>(0);
const [pointsRewards, setPointsRewards] = useState<{
@@ -68,10 +82,21 @@ function ConsultationHistorique() {
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 [claimFeedback, setClaimFeedback] = useState<{
pool: string;
type: "success" | "error";
text: string;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string>("");
// ── Modal de sélection du produit récompense ──
const [rewardModalPool, setRewardModalPool] =
useState<PointsPoolInfo | null>(null);
const [selectedProductId, setSelectedProductId] = useState<number | null>(
null,
);
useEffect(() => {
if (!isUserAuthenticated())
navigate("/login/client", { replace: true });
@@ -152,37 +177,51 @@ function ConsultationHistorique() {
});
};
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>
</>
);
}
// 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 handleClaim = async (poolKey: string) => {
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);
const res = await claimMyReward(poolKey, productId);
setClaimingPool(null);
if (res.success) {
const text = res.product_added && res.product_name
? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.`
: res.description || "Récompense réclamée !";
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); });
getMyPointsRewards().then((r) => {
if (r.success) setPointsRewards(r);
});
} else {
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
setClaimFeedback({
pool: poolKey,
type: "error",
text: res.error || "Erreur",
});
}
};
@@ -202,6 +241,24 @@ function ConsultationHistorique() {
"#7c3aed",
];
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>
</>
);
}
return (
<>
<Navbar />
@@ -298,81 +355,178 @@ function ConsultationHistorique() {
</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>
{pointsRewards.reward.description && (
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
)}
{(pointsRewards.reward.reward_items ?? []).filter((it: RewardItemConfig) => it.product_name).length > 0 && (
<div className="reward-eligible-cats">
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => (
<span key={idx} className="reward-eligible-cat">
<FontAwesomeIcon icon={faGift} style={{ marginRight: 4 }} />
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? `${it.price}` : ""}
</span>
))}
</div>
)}
<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.flatMap((cfg) =>
cfg.all_products
? [<span key={cfg.category} className="reward-eligible-cat">
{cfg.category}
</span>]
: (cfg.product_names ?? []).map((name) => (
<span key={`${cfg.category}-${name}`} className="reward-eligible-cat">
{name}
</span>
))
{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>
{pointsRewards.reward.description && (
<p className="rewards-section-desc">
{pointsRewards.reward.description}
</p>
)}
{(pointsRewards.reward.reward_items ?? []).filter(
(it: RewardItemConfig) => it.product_name,
).length > 0 && (
<div className="reward-eligible-cats">
{(
pointsRewards.reward.reward_items ?? []
).map(
(it: RewardItemConfig, idx: number) => (
<span
key={idx}
className="reward-eligible-cat"
>
<FontAwesomeIcon
icon={faGift}
style={{ marginRight: 4 }}
/>
{it.product_name}
{it.quantity > 0 &&
it.quantity !== 1
? ` ×${it.quantity}`
: ""}
{it.price > 0
? `${it.price}`
: ""}
</span>
),
)}
</div>
)}
<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.flatMap(
(cfg) =>
cfg.all_products
? [
<span
key={
cfg.category
}
className="reward-eligible-cat"
>
{
cfg.category
}
</span>,
]
: (
cfg.product_names ??
[]
).map(
(
name,
) => (
<span
key={`${cfg.category}-${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>
)}
<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>
{feedback && (
<p
className={`reward-feedback reward-feedback-${feedback.type}`}
>
{feedback.text}
</p>
)}
{pool.rewards_available > 0 && (
<button
className="reward-claim-btn"
onClick={() =>
openRewardModal(pool)
}
disabled={isClaiming}
>
{isClaiming
? "..."
: "Réclamer ma récompense"}
</button>
)}
</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>
</div>
)}
)}
{/* Bouton parrainage */}
{appSettings.referral_enabled && (
@@ -494,10 +648,19 @@ function ConsultationHistorique() {
<div className="order-card-footer">
<span className="order-card-total">
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))}
{formatPrice(
(order.total_prix || 0) -
(order.referral_used || 0),
)}
{(order.referral_used || 0) > 0 && (
<span className="order-card-referral">
{" "} dont {formatPrice(order.referral_used ?? 0)} parrainage
{" "}
dont{" "}
{formatPrice(
order.referral_used ??
0,
)}{" "}
parrainage
</span>
)}
</span>
@@ -512,6 +675,111 @@ function ConsultationHistorique() {
</>
)}
</div>
{/* ============================================ */}
{/* MODAL — CHOIX DU PRODUIT RÉCOMPENSE */}
{/* ============================================ */}
{rewardModalPool && pointsRewards?.reward && (
<div
className="reward-modal-overlay"
onClick={closeRewardModal}
>
<div
className="reward-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="reward-modal-header">
<div className="reward-modal-header-title">
<FontAwesomeIcon
icon={faGift}
className="reward-modal-icon"
/>
<h2>Choisissez votre récompense</h2>
</div>
<button
className="reward-modal-close"
onClick={closeRewardModal}
aria-label="Fermer"
>
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
<p className="reward-modal-subtitle">
{rewardModalPool.name} {" "}
{rewardModalPool.rewards_available} récompense
{rewardModalPool.rewards_available > 1
? "s"
: ""}{" "}
disponible
{rewardModalPool.rewards_available > 1 ? "s" : ""}
</p>
<div className="reward-modal-products">
{(rewardModalPool.eligible_reward_items ?? []).map(
(item) => {
const isSelected =
selectedProductId === item.product_id;
return (
<button
key={item.product_id}
type="button"
className={`reward-product-card ${isSelected ? "selected" : ""}`}
onClick={() =>
setSelectedProductId(
item.product_id,
)
}
>
<div className="reward-product-radio">
{isSelected && (
<FontAwesomeIcon
icon={faCheck}
/>
)}
</div>
<div className="reward-product-info">
<span className="reward-product-name">
{item.product_name}
</span>
<span className="reward-product-meta">
{item.quantity > 0 &&
item.quantity !== 1
? `×${item.quantity}`
: ""}
{item.price > 0
? ` · valeur ${item.price.toFixed(2)}`
: ""}
</span>
</div>
<span className="reward-product-free">
Offert
</span>
</button>
);
},
)}
</div>
<div className="reward-modal-actions">
<button
className="reward-modal-btn reward-modal-btn-cancel"
onClick={closeRewardModal}
>
Annuler
</button>
<button
className="reward-modal-btn reward-modal-btn-confirm"
onClick={confirmRewardChoice}
disabled={selectedProductId === null}
>
<FontAwesomeIcon icon={faGift} />
Confirmer
</button>
</div>
</div>
</div>
)}
</>
);
}