From 7262d44f5a19c3ce244bdc5f45fb7d8ed1e7fcc9 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Tue, 9 Jun 2026 19:57:56 +0200 Subject: [PATCH] chore: build --- frontend-prep/package-lock.json | 15 -- frontend-prep/src/api/api.ts | 102 +++++++++--- frontend-prep/src/api/api_types.ts | 12 +- frontend-prep/src/pages/User/Checkout.css | 9 ++ frontend-prep/src/pages/User/Checkout.tsx | 17 +- .../src/pages/User/ConsultationHistorique.css | 148 ++++++++++++++++++ .../src/pages/User/ConsultationHistorique.tsx | 86 +++++++++- 7 files changed, 341 insertions(+), 48 deletions(-) diff --git a/frontend-prep/package-lock.json b/frontend-prep/package-lock.json index ad85e5b9..4d4dd89f 100644 --- a/frontend-prep/package-lock.json +++ b/frontend-prep/package-lock.json @@ -62,7 +62,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -874,7 +873,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1463,7 +1461,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1473,7 +1470,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1530,7 +1526,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -1769,7 +1764,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1869,7 +1863,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2108,7 +2101,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2677,7 +2669,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.2.tgz", "integrity": "sha512-CPjtWygL+f7naL+sGHoC2JQR0DG7u+9ik6WdkjjVmz2uy0kBC2l+aKfdi3ZzUR7VKSQJ6Mc/CeCN+6iVNah+ww==", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.0", "@mapbox/geojson-types": "^1.0.2", @@ -2887,7 +2878,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -2960,7 +2950,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2969,7 +2958,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3228,7 +3216,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3319,7 +3306,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3460,7 +3446,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index 5487d6fe..9cdc1a8c 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -1,7 +1,3 @@ -// ============================================ -// api/api.ts - VERSION CORRIGÉE -// ============================================ - const API_URL = "/api/v1"; const BACKEND_URL = ""; export function getMediaUrl(url: string): string { @@ -26,17 +22,11 @@ import type { CancelCommandResponse, PenaltiesResponse, } from "./api_types"; -// ============================================ -// 🔐 TYPES - AUTHRESPONSE COMPLETE -// ============================================ -/** - * ✅ TYPE CORRECT - Inclut access_token! - */ export interface AuthResponse { success: boolean; message?: string; - access_token?: string; // ✅ CRITICAL! + access_token?: string; token_type?: string; expires_in?: number; requires_2fa?: boolean; @@ -53,17 +43,8 @@ export interface AuthResponse { }; } -// ============================================ -// 🔐 GESTION CENTRALISÉE DU JWT -// ============================================ - -/** - * ✅ Extraire username du JWT - * Source de vérité UNIQUE pour le username - */ export const extractUsernameFromToken = (): string | null => { try { - // ✅ CRITICAL: sessionStorage (pas localStorage!) const token = sessionStorage.getItem("token"); if (!token) { @@ -2152,3 +2133,84 @@ export const unlinkTelegram = async (): Promise => { /* silencieux */ } }; + +// ============================================ +// 🏆 POINTS — RÉCOMPENSES +// ============================================ + +export type RewardCategoryConfig = { + category: string; + all_products: boolean; + product_ids: number[]; + amount: number; +}; + +export type PointsPoolInfo = { + key: string; + name: string; + points: number; + rewards_earned: number; + rewards_claimed: number; + rewards_available: number; + eligible_configs: RewardCategoryConfig[]; +}; + +export type PointsRewardConfig = { + threshold: number; + type: string; + description: string; +}; + +export const getMyPointsRewards = async (): Promise<{ + success: boolean; + enabled: boolean; + pools: PointsPoolInfo[]; + reward: PointsRewardConfig | null; +}> => { + const token = getAuthToken(); + if (!token) return { success: false, enabled: false, pools: [], reward: null }; + try { + const response = await fetch(`${API_URL}/points/rewards`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!response.ok) return { success: false, enabled: false, pools: [], reward: null }; + const data = await safeJson(response); + return { + success: true, + enabled: data.enabled ?? false, + pools: data.pools ?? [], + reward: data.reward ?? null, + }; + } catch { + return { success: false, enabled: false, pools: [], reward: null }; + } +}; + +export const claimMyReward = async (poolKey: string): Promise<{ + success: boolean; + description?: string; + remaining_rewards?: number; + error?: string; +}> => { + const token = getAuthToken(); + if (!token) return { success: false, error: "Non authentifié" }; + try { + const response = await fetch(`${API_URL}/points/claim`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ pool_key: poolKey }), + }); + const data = await safeJson(response); + if (!response.ok) return { success: false, error: data.error || "Erreur" }; + return { + success: true, + description: data.description, + remaining_rewards: data.remaining_rewards, + }; + } catch { + return { success: false, error: "Erreur de connexion" }; + } +}; diff --git a/frontend-prep/src/api/api_types.ts b/frontend-prep/src/api/api_types.ts index 1ed559af..ba383fed 100644 --- a/frontend-prep/src/api/api_types.ts +++ b/frontend-prep/src/api/api_types.ts @@ -4,13 +4,6 @@ // Interfaces complètes pour le frontend // À importer dans les composants -// ============================================ -// 🔐 AUTHENTIFICATION - TYPES -// ============================================ - -/** - * Réponse générique de l'API - */ export interface ApiResponse { success: boolean; message?: string; @@ -19,8 +12,7 @@ export interface ApiResponse { token_type?: string; expires_in?: number; user?: UserResponse; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; // Pour les champs additionnels + [key: string]: unknown; } /** @@ -779,6 +771,6 @@ export interface ConfirmReceptionResponse { category?: string; points_earned?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; + [key: string]: any; }; } diff --git a/frontend-prep/src/pages/User/Checkout.css b/frontend-prep/src/pages/User/Checkout.css index a8e2cd0e..45a2185e 100644 --- a/frontend-prep/src/pages/User/Checkout.css +++ b/frontend-prep/src/pages/User/Checkout.css @@ -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); diff --git a/frontend-prep/src/pages/User/Checkout.tsx b/frontend-prep/src/pages/User/Checkout.tsx index 5546653f..dada2372 100644 --- a/frontend-prep/src/pages/User/Checkout.tsx +++ b/frontend-prep/src/pages/User/Checkout.tsx @@ -417,8 +417,19 @@ function Checkout() {
Total: - {total.toFixed(2)} € + + {(useReferral && referralBalance > 0 + ? Math.max(0, total - referralBalance) + : total + ).toFixed(2)} € +
+ {useReferral && referralBalance > 0 && ( +
+ Dont crédit parrainage : + -{Math.min(referralBalance, total).toFixed(2)} € +
+ )} {/* Formulaire */} @@ -787,7 +798,9 @@ function Checkout() {
TOTAL
-
{confirmationData.total.toFixed(2)} €
+
+ {(confirmationData.total - (confirmationData.referral_used ?? 0)).toFixed(2)} € +
diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.css b/frontend-prep/src/pages/User/ConsultationHistorique.css index 2675b9b6..d0f95bda 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.css +++ b/frontend-prep/src/pages/User/ConsultationHistorique.css @@ -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 ============================================ */ diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.tsx b/frontend-prep/src/pages/User/ConsultationHistorique.tsx index 4f453899..3f797a94 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.tsx +++ b/frontend-prep/src/pages/User/ConsultationHistorique.tsx @@ -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(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(""); @@ -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() { )} + {/* Section récompenses par palier */} + {pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && ( +
+

+ + Récompenses +

+

{pointsRewards.reward.description}

+
+ {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.map((cfg) => ( + + {cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""} + + ))} +
+ )} +
+
+
+
+ {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.text}

+ )} + {pool.rewards_available > 0 && ( + + )} +
+ ); + })} +
+
+ )} + {/* Bouton parrainage */} {appSettings.referral_enabled && (