chore: build

This commit is contained in:
2026-06-09 19:57:56 +02:00
parent 72c8003acf
commit 7262d44f5a
7 changed files with 341 additions and 48 deletions
@@ -181,6 +181,15 @@
margin-top: clamp(1rem, 3vw, 1.5rem);
}
.summary-referral-deduction {
display: flex;
justify-content: space-between;
align-items: center;
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
color: #16a34a;
margin-top: 0.35rem;
}
.total-price {
color: #6d28d9;
font-size: clamp(1.2rem, 5vw, 1.5rem);
+15 -2
View File
@@ -417,8 +417,19 @@ function Checkout() {
</div>
<div className="summary-total">
<span>Total:</span>
<span className="total-price">{total.toFixed(2)} </span>
<span className="total-price">
{(useReferral && referralBalance > 0
? Math.max(0, total - referralBalance)
: total
).toFixed(2)}
</span>
</div>
{useReferral && referralBalance > 0 && (
<div className="summary-referral-deduction">
<span>Dont crédit parrainage :</span>
<span>-{Math.min(referralBalance, total).toFixed(2)} </span>
</div>
)}
</div>
{/* Formulaire */}
@@ -787,7 +798,9 @@ function Checkout() {
<div className="confirmation-total-label">
<i className="fas fa-euro-sign"></i> TOTAL
</div>
<div className="confirmation-total-amount">{confirmationData.total.toFixed(2)} </div>
<div className="confirmation-total-amount">
{(confirmationData.total - (confirmationData.referral_used ?? 0)).toFixed(2)}
</div>
</div>
</div>
@@ -151,6 +151,154 @@
font-size: 0.85rem;
}
/* ============================================
RÉCOMPENSES PAR PALIER
============================================ */
.rewards-section {
background: var(--surface);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1.25rem;
}
.rewards-section-title {
display: flex;
align-items: center;
gap: 0.5rem;
color: #f59e0b;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
margin: 0 0 0.4rem 0;
}
.rewards-section-icon {
font-size: 0.85rem;
}
.rewards-section-desc {
color: var(--text-muted);
font-size: 0.85rem;
font-style: italic;
margin: 0 0 0.875rem 0;
}
.rewards-pools {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.reward-pool-card {
background: rgba(245, 158, 11, 0.07);
border: 1px solid rgba(245, 158, 11, 0.2);
border-radius: 8px;
padding: 0.75rem;
}
.reward-pool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.reward-pool-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
}
.reward-pool-pts {
font-size: 0.85rem;
font-weight: 700;
color: #f59e0b;
}
.reward-eligible-cats {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.reward-eligible-cat {
font-size: 0.78rem;
font-weight: 600;
color: #10b981;
background: rgba(16, 185, 129, 0.12);
border-radius: 4px;
padding: 2px 7px;
}
.reward-progress-bar {
height: 6px;
background: rgba(245, 158, 11, 0.15);
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.4rem;
}
.reward-progress-fill {
height: 100%;
background: linear-gradient(90deg, #f59e0b, #fbbf24);
border-radius: 3px;
transition: width 0.4s ease;
}
.reward-pool-info {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.reward-available {
color: #10b981;
font-weight: 600;
}
.reward-remaining {
color: var(--text-muted);
}
.reward-feedback {
font-size: 0.82rem;
font-style: italic;
margin: 0.25rem 0 0.4rem;
}
.reward-feedback-success {
color: #10b981;
}
.reward-feedback-error {
color: #ef4444;
}
.reward-claim-btn {
width: 100%;
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.55rem 1rem;
font-size: 0.88rem;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s;
}
.reward-claim-btn:hover:not(:disabled) {
opacity: 0.88;
}
.reward-claim-btn:disabled {
opacity: 0.5;
cursor: default;
}
/* ============================================
SECTION TITLE
============================================ */
@@ -10,8 +10,10 @@ import {
isUserAuthenticated,
getPublicSettings,
getReferralBalance,
getMyPointsRewards,
claimMyReward,
} from "../../api/api";
import type { PublicSettings } from "../../api/api";
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig } from "../../api/api";
import type {
CompletedOrder,
ClientStats,
@@ -59,6 +61,13 @@ function ConsultationHistorique() {
two_fa_enabled: false,
});
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>("");
@@ -86,6 +95,9 @@ function ConsultationHistorique() {
});
}
});
getMyPointsRewards().then((r) => {
if (r.success && r.enabled) setPointsRewards(r);
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const fetchHistory = async () => {
@@ -157,6 +169,19 @@ function ConsultationHistorique() {
);
}
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;
@@ -268,6 +293,65 @@ 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>
<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) => (
<span key={cfg.category} className="reward-eligible-cat">
{cfg.category}{cfg.amount > 0 ? `${cfg.amount}` : ""}
</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