diff --git a/frontend-admin/app.json b/frontend-admin/app.json index adf691b8..28f89dd6 100644 --- a/frontend-admin/app.json +++ b/frontend-admin/app.json @@ -18,6 +18,7 @@ }, "android": { "package": "com.uberstup.adminpanel", + "abiFilters": ["arm64-v8a"], "adaptiveIcon": { "foregroundImage": "./assets/icon.png", "backgroundColor": "#000000" diff --git a/frontend-admin/eas.json b/frontend-admin/eas.json index 1105f96e..3dfa22d4 100644 --- a/frontend-admin/eas.json +++ b/frontend-admin/eas.json @@ -22,7 +22,7 @@ "buildType": "apk" }, "env": { - "EXPO_PUBLIC_API_URL": "https://5.181.0.112.nip.io", + "EXPO_PUBLIC_API_URL": "https://uber-demo.club", "EXPO_PUBLIC_UPDATE_URL": "https://ota.uber-stup.club" }, "channel": "pre-prod-admin" diff --git a/frontend-admin/package-lock.json b/frontend-admin/package-lock.json index 1886645d..12738e27 100644 --- a/frontend-admin/package-lock.json +++ b/frontend-admin/package-lock.json @@ -80,7 +80,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1368,7 +1367,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -2561,7 +2559,6 @@ "version": "7.1.28", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz", "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==", - "peer": true, "dependencies": { "@react-navigation/core": "^7.14.0", "escape-string-regexp": "^4.0.0", @@ -2761,7 +2758,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3330,7 +3326,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3976,6 +3971,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -3985,6 +3981,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -4116,7 +4113,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz", "integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.24", @@ -4235,7 +4231,6 @@ "version": "14.0.11", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -7092,7 +7087,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7110,7 +7104,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7138,7 +7131,6 @@ "version": "0.81.5", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -7265,7 +7257,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -7275,7 +7266,6 @@ "version": "4.16.0", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -7290,7 +7280,6 @@ "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -7320,7 +7309,6 @@ "version": "13.15.0", "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", - "peer": true, "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" @@ -7334,7 +7322,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", - "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -7458,7 +7445,6 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7715,7 +7701,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/sax": { "version": "1.4.4", @@ -8321,7 +8308,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index d4615c70..cd72136c 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -151,9 +151,6 @@ export const updateUserByAdmin = async ( return { success: true, message: data.message, user: data.user }; }; -// ============================================ -// COMMANDES - export const getAllCommands = async (status?: string, username?: string) => { let url = `${V2}/admin/protected/orders`; const params: string[] = []; @@ -276,8 +273,6 @@ export const confirmReceptionAdmin = async (commandId: number) => { }; }; -// ============================================ -// LIVREURS // ============================================ export const getAvailableDeliveryPersons = async () => { @@ -930,6 +925,20 @@ export interface PointsTier { points: number; } +export interface RewardCategoryConfig { + category: string; + all_products: boolean; + product_ids: number[]; + amount: number; +} + +export interface PointsReward { + threshold: number; + type: "free_product" | "half_price_product" | "custom"; + description: string; + category_configs: RewardCategoryConfig[]; +} + export interface PointsPool { key: string; name: string; @@ -1028,6 +1037,7 @@ export interface AppSettings { penalty_tiers: PenaltyTier[]; points_enabled: boolean; points_pools: PointsPool[]; + points_reward?: PointsReward | null; referral_enabled: boolean; delivery_schedule: DeliverySchedule; postal_zones: PostalZone[]; @@ -1042,6 +1052,7 @@ export interface AppSettings { telegram_2fa_enabled: boolean; delivery_mode: DeliveryModeConfig; shop_name: string; + contact_telegram: string; } export const getSettings = async (): Promise<{ diff --git a/frontend-admin/src/api/api_cabine.ts b/frontend-admin/src/api/api_cabine.ts index 4321425c..d9b037c8 100644 --- a/frontend-admin/src/api/api_cabine.ts +++ b/frontend-admin/src/api/api_cabine.ts @@ -1,6 +1,7 @@ import apiClient from "./client"; import { API_BASE_URL } from "./client"; +//@ts import type { OrderItem, DeliveryPerson, @@ -41,9 +42,6 @@ export const confirmReceptionCabine = async (commandId: number) => { }; }; -// ============================================ -// PENALITES - export const applyClientPenalty = async ( clientUsername: string, reason: string, @@ -71,7 +69,6 @@ export const resetClientPenalties = async (clientUsername: string) => { return { success: true, message: data.message }; }; -// pool: index dans pool_names/pool_keys, -1 = reset tous les points export const resetClientPoints = async ( clientUsername: string, pool: number = -1, @@ -97,10 +94,6 @@ export const getPenaltiesStats = async () => { return { success: true, data: data.data }; }; -// ============================================ -// COMMANDES -// ============================================ - export const getCancelledOrders = async () => { const { data } = await apiClient.get(`${API}/commands/cancelled`); return { diff --git a/frontend-admin/src/api/api_delivery.ts b/frontend-admin/src/api/api_delivery.ts index 0daa6fff..047a7ff8 100644 --- a/frontend-admin/src/api/api_delivery.ts +++ b/frontend-admin/src/api/api_delivery.ts @@ -42,10 +42,6 @@ export const updateMyStatus = async ( } }; -// ============================================ -// QUEUE -// ============================================ - export const getMyQueue = async (): Promise<{ success: boolean; queue_info?: QueueInfo; @@ -62,10 +58,6 @@ export const getMyQueue = async (): Promise<{ } }; -// ============================================ -// LIVRAISONS -// ============================================ - export const getMyDeliveries = async (): Promise<{ success: boolean; deliveries?: DeliveryItem[]; diff --git a/frontend-admin/src/api/client.ts b/frontend-admin/src/api/client.ts index 1ff3f905..d52169b2 100644 --- a/frontend-admin/src/api/client.ts +++ b/frontend-admin/src/api/client.ts @@ -12,7 +12,6 @@ const apiClient = axios.create({ }, }); -// attach JWT token apiClient.interceptors.request.use(async (config) => { const isAdminRoute = config.url?.includes("/api/v2/") || @@ -27,7 +26,6 @@ apiClient.interceptors.request.use(async (config) => { return config; }); -// Response interceptor: handle common errors apiClient.interceptors.response.use( (response) => response, (error) => { diff --git a/frontend-admin/src/api/tomtom.ts b/frontend-admin/src/api/tomtom.ts index 5959c79e..2a3bb8fc 100644 --- a/frontend-admin/src/api/tomtom.ts +++ b/frontend-admin/src/api/tomtom.ts @@ -2,7 +2,6 @@ import axios from "axios"; const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB"; -// ---- Types ---- export interface LatLng { latitude: number; longitude: number; @@ -24,7 +23,6 @@ export interface NavigationInstruction { isActive: boolean; } -// ---- Maneuver translations (FR) ---- export const maneuverTranslations: Record = { TURN_LEFT: "Tournez à gauche", TURN_RIGHT: "Tournez à droite", @@ -48,7 +46,6 @@ export const maneuverTranslations: Record = { WAYPOINT_REACHED: "Point de passage atteint", }; -// Ionicons name per maneuver export const maneuverIcons: Record = { TURN_LEFT: "arrow-back", TURN_RIGHT: "arrow-forward", @@ -79,7 +76,6 @@ function formatDistance(meters: number): string { return `${(meters / 1000).toFixed(1)} km`; } -// ---- Geocode address → coords ---- export async function geocodeAddress(address: string): Promise { try { const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`; @@ -96,7 +92,6 @@ export async function geocodeAddress(address: string): Promise { return null; } -// ---- Calculate route ---- export async function calculateRoute( origin: LatLng, destination: LatLng, diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index 7d266512..926bdddf 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -15,8 +15,9 @@ import { useNavigation } from "@react-navigation/native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; -import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin"; -import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin"; +import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin"; +import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin"; +import type { Product } from "../../api/types"; import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; @@ -643,6 +644,358 @@ function TiersSection({ ); } +const REWARD_ACCENT = "#f59e0b"; + +const REWARD_TYPES: { value: PointsReward["type"]; label: string; icon: string }[] = [ + { value: "free_product", label: "Produit offert", icon: "gift-outline" }, + { value: "half_price_product", label: "Produit à -50%", icon: "pricetag-outline" }, + { value: "custom", label: "Personnalisé", icon: "star-outline" }, +]; + +const EMPTY_REWARD: PointsReward = { + threshold: 20, + type: "free_product", + description: "", + category_configs: [], +}; + +// ────────────────────────────────────────────────────────────── +// Sélecteur de produits pour une catégorie dans la récompense +// ────────────────────────────────────────────────────────────── +function CategoryProductPicker({ + catConfig, + products, + onChange, + colors, + s, +}: { + catConfig: RewardCategoryConfig; + products: Product[]; + onChange: (cfg: RewardCategoryConfig) => void; + colors: any; + s: any; +}) { + const catProducts = products.filter((p) => p.category === catConfig.category); + + const toggleProduct = (id: number) => { + const ids = catConfig.product_ids.includes(id) + ? catConfig.product_ids.filter((x) => x !== id) + : [...catConfig.product_ids, id]; + onChange({ ...catConfig, product_ids: ids, all_products: false }); + }; + + return ( + + {/* Montant pour cette catégorie */} + + + Valeur du produit offert + + + 0 ? String(catConfig.amount) : ""} + onChangeText={(v) => { + const n = parseFloat(v); + onChange({ ...catConfig, amount: isNaN(n) ? 0 : n }); + }} + placeholder="0" + placeholderTextColor={colors.textMuted} + /> + + + + + {/* Toggle tous / sélection */} + + onChange({ ...catConfig, all_products: true, product_ids: [] })} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + paddingHorizontal: spacing.m, paddingVertical: spacing.xs, + borderRadius: borderRadius.full, borderWidth: 1.5, + borderColor: catConfig.all_products ? REWARD_ACCENT : colors.border, + backgroundColor: catConfig.all_products ? REWARD_ACCENT + "22" : "transparent", + }} + > + + + Tous ({catProducts.length}) + + + onChange({ ...catConfig, all_products: false })} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + paddingHorizontal: spacing.m, paddingVertical: spacing.xs, + borderRadius: borderRadius.full, borderWidth: 1.5, + borderColor: !catConfig.all_products ? REWARD_ACCENT : colors.border, + backgroundColor: !catConfig.all_products ? REWARD_ACCENT + "22" : "transparent", + }} + > + + + Sélection + + + + + {/* Liste des produits si mode sélection */} + {!catConfig.all_products && ( + + {catProducts.length === 0 ? ( + + Aucun produit dans cette catégorie + + ) : catProducts.map((p) => { + const sel = catConfig.product_ids.includes(p.id); + return ( + toggleProduct(p.id)} + style={{ + paddingHorizontal: spacing.s, paddingVertical: 4, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: sel ? REWARD_ACCENT : colors.border, + backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent", + flexDirection: "row", alignItems: "center", gap: 4, + }} + > + {sel && } + + {p.name} + + + ); + })} + + )} + + ); +} + +// ────────────────────────────────────────────────────────────── +// Section centralisée récompenses par palier +// ────────────────────────────────────────────────────────────── +function CentralRewardSection({ + reward, + allCategories, + productsByCategory, + onChange, + colors, + s, +}: { + reward: PointsReward | null | undefined; + allCategories: Category[]; + productsByCategory: Record; + onChange: (reward: PointsReward | null) => void; + colors: any; + s: any; +}) { + const { width: screenWidth } = useWindowDimensions(); + const enabled = !!reward; + const r = reward ?? EMPTY_REWARD; + + const update = (patch: Partial) => + onChange({ ...r, ...patch }); + + const getCatConfig = (catName: string): RewardCategoryConfig => + r.category_configs.find((c) => c.category === catName) ?? + { category: catName, all_products: true, product_ids: [], amount: 0 }; + + const isCatSelected = (catName: string) => + r.category_configs.some((c) => c.category === catName); + + const toggleCategory = (catName: string) => { + if (isCatSelected(catName)) { + update({ category_configs: r.category_configs.filter((c) => c.category !== catName) }); + } else { + update({ category_configs: [...r.category_configs, { category: catName, all_products: true, product_ids: [], amount: 0 }] }); + } + }; + + const updateCatConfig = (cfg: RewardCategoryConfig) => { + update({ + category_configs: r.category_configs.map((c) => + c.category === cfg.category ? cfg : c + ), + }); + }; + + return ( + + + Récompenses par palier + + + {enabled ? "Activée" : "Désactivée"} + + + + + + + Récompense activée + + Le seuil s'applique indépendamment à chaque type de points. + + + onChange(v ? EMPTY_REWARD : null)} + trackColor={{ false: colors.border, true: REWARD_ACCENT }} + thumbColor="#fff" + /> + + + {enabled && ( + + + {/* Seuil */} + + + Seuil de points + + Dès X points cumulés dans un type, la récompense est débloquée pour ce type. + + + + { const n = parseInt(v, 10); if (!isNaN(n) && n > 0) update({ threshold: n }); }} + /> + pts + + + + {/* Type */} + + Type de récompense + + {REWARD_TYPES.map((rt) => { + const sel = r.type === rt.value; + return ( + update({ type: rt.value })} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + paddingHorizontal: spacing.m, paddingVertical: spacing.s, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: sel ? REWARD_ACCENT : colors.border, + backgroundColor: sel ? REWARD_ACCENT + "20" : "transparent", + }} + > + + + {rt.label} + + + ); + })} + + + + {/* Description */} + + Description affichée au client + update({ description: v })} + placeholder="Ex : Un produit 30€ de ton choix parmi les produits conditionnés en 30€" + placeholderTextColor={colors.textMuted} + multiline + /> + + + {/* Catégories éligibles */} + + Catégories éligibles + + Sélectionnez les catégories, puis pour chacune choisissez tous les produits ou une sélection. + + {allCategories.length === 0 ? ( + Aucune catégorie disponible + ) : ( + + {allCategories.map((cat) => { + const selected = isCatSelected(cat.name); + const catColor = cat.color || REWARD_ACCENT; + return ( + + {/* Chip catégorie */} + toggleCategory(cat.name)} + style={{ + flexDirection: "row", alignItems: "center", gap: spacing.xs, + alignSelf: "flex-start", + paddingHorizontal: spacing.m, paddingVertical: spacing.s, + borderRadius: borderRadius.full, borderWidth: 1.5, + borderColor: selected ? catColor : colors.border, + backgroundColor: selected ? catColor + "22" : "transparent", + }} + > + + + {cat.name} + + + + + {/* Sélecteur produits (visible si catégorie sélectionnée) */} + {selected && ( + + )} + + ); + })} + + )} + + + {/* Récapitulatif */} + {r.category_configs.length > 0 && ( + + Récapitulatif + + Dès {r.threshold} pts par type →{" "} + {REWARD_TYPES.find((x) => x.value === r.type)?.label} + + {r.description !== "" && ( + "{r.description}" + )} + {r.category_configs.map((cfg) => ( + + • {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} : {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s) sélectionné(s)`} + + ))} + + )} + + )} + + ); +} + export default function SettingsScreen() { const { colors } = useTheme(); const { alert, showError, showSuccess, hideAlert } = useAlert(); @@ -680,10 +1033,13 @@ export default function SettingsScreen() { telegram_2fa_enabled: false, delivery_mode: { mode: "single" as const, category_routes: [] }, shop_name: "Milieu-Nantais", + contact_telegram: "", + points_reward: null, }); const [showApiKey, setShowApiKey] = useState(false); const [showIpnSecret, setShowIpnSecret] = useState(false); const [categories, setCategories] = useState([]); + const [productsByCategory, setProductsByCategory] = useState>({}); const [livreurs, setLivreurs] = useState([]); // Refs pour l'auto-sauvegarde au départ de la page @@ -713,10 +1069,11 @@ export default function SettingsScreen() { const loadData = useCallback(async () => { setLoading(true); isLoaded.current = false; - const [settingsRes, categoriesRes, livreursRes] = await Promise.all([ + const [settingsRes, categoriesRes, livreursRes, productsRes] = await Promise.all([ getSettings(), getCategories(), getAvailableDeliveryPersons(), + getAllProductsAdmin(), ]); if (settingsRes.success && settingsRes.settings) { setSettings({ @@ -725,6 +1082,7 @@ export default function SettingsScreen() { delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE, postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES, delivery_mode: settingsRes.settings.delivery_mode ?? { mode: "single", category_routes: [] }, + points_reward: settingsRes.settings.points_reward ?? null, }); } if (categoriesRes) { @@ -733,6 +1091,15 @@ export default function SettingsScreen() { if (livreursRes.success) { setLivreurs(livreursRes.livreurs.map((l: any) => l.username)); } + if (productsRes.success) { + const byCategory: Record = {}; + for (const p of productsRes.data) { + const cat = p.category || ""; + if (!byCategory[cat]) byCategory[cat] = []; + byCategory[cat].push(p); + } + setProductsByCategory(byCategory); + } setLoading(false); // Marquer comme chargé après un tick pour que le useEffect de settings ne // considère pas le setSettings initial comme une modification utilisateur @@ -1256,6 +1623,16 @@ export default function SettingsScreen() { /> ))} + {/* Récompense centralisée par palier */} + setSettings((p) => ({ ...p, points_reward: reward }))} + colors={colors} + s={s} + /> + {/* Horaires de livraison */} Bot configuré — @{settings.telegram_bot_username} )} + + Contact SAV Telegram + + Username du compte Telegram SAV (sans le @). Utilisé pour les boutons de contact client. + + + @ + setSettings((p) => ({ ...p, contact_telegram: v }))} + placeholder="MonSAV" + placeholderTextColor={colors.textMuted} + autoCapitalize="none" + autoCorrect={false} + /> + + {settings.contact_telegram !== "" && ( + + + @{settings.contact_telegram} + + )} +