diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index aae2d563..322c8a0f 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -562,7 +562,6 @@ export const deleteProductMediaAdmin = async ( return { success: true, message: data.message }; }; -// ============================================ // ============================================ // ADDRESSES // ============================================ @@ -711,31 +710,48 @@ export interface AppSettings { points_enabled: boolean; points_categories_weed: string[]; points_categories_zipette: string[]; + points_categories_total: string[]; points_separated: boolean; points_weed_tiers: PointsTier[]; points_zipette_tiers: PointsTier[]; + points_total_tiers: PointsTier[]; referral_enabled: boolean; } -export const getSettings = async (): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => { +export const getSettings = async (): Promise<{ + success: boolean; + settings?: AppSettings; + error?: string; +}> => { try { const { data } = await apiClient.get(`${V2}/admin/protected/settings`); return { success: true, settings: data.settings }; } catch (error: any) { - return { success: false, error: error.response?.data?.error || "Erreur" }; + return { + success: false, + error: error.response?.data?.error || "Erreur", + }; } }; -export const registerAdminPushToken = async (pushToken: string): Promise => { +export const registerAdminPushToken = async ( + pushToken: string, +): Promise => { try { - await apiClient.post(`${V2}/admin/protected/push-token`, { push_token: pushToken }); - } catch { /* ignore */ } + await apiClient.post(`${V2}/admin/protected/push-token`, { + push_token: pushToken, + }); + } catch { + /* ignore */ + } }; export const unregisterAdminPushToken = async (): Promise => { try { await apiClient.delete(`${V2}/admin/protected/push-token`); - } catch { /* ignore */ } + } catch { + /* ignore */ + } }; export interface AppNotification { @@ -746,7 +762,10 @@ export interface AppNotification { read: boolean; } -export const getAdminNotifications = async (): Promise<{ notifications: AppNotification[]; unread_count: number }> => { +export const getAdminNotifications = async (): Promise<{ + notifications: AppNotification[]; + unread_count: number; +}> => { const { data } = await apiClient.get(`${V2}/admin/protected/notifications`); return data; }; @@ -759,9 +778,15 @@ export const updateSettings = async ( settings: AppSettings, ): Promise<{ success: boolean; settings?: AppSettings; error?: string }> => { try { - const { data } = await apiClient.put(`${V2}/admin/protected/settings`, settings); + const { data } = await apiClient.put( + `${V2}/admin/protected/settings`, + settings, + ); return { success: true, settings: data.settings }; } catch (error: any) { - return { success: false, error: error.response?.data?.error || "Erreur" }; + return { + success: false, + error: error.response?.data?.error || "Erreur", + }; } }; diff --git a/frontend-admin/src/api/api_cabine.ts b/frontend-admin/src/api/api_cabine.ts index fefb9f8c..a715b0f0 100644 --- a/frontend-admin/src/api/api_cabine.ts +++ b/frontend-admin/src/api/api_cabine.ts @@ -351,16 +351,22 @@ export interface PublicSettings { points_separated: boolean; } -export const registerCabinePushToken = async (pushToken: string): Promise => { +export const registerCabinePushToken = async ( + pushToken: string, +): Promise => { try { await apiClient.post(`${API}/push-token`, { push_token: pushToken }); - } catch { /* ignore */ } + } catch { + /* ignore */ + } }; export const unregisterCabinePushToken = async (): Promise => { try { await apiClient.delete(`${API}/push-token`); - } catch { /* ignore */ } + } catch { + /* ignore */ + } }; export interface AppNotification { @@ -371,7 +377,10 @@ export interface AppNotification { read: boolean; } -export const getCabineNotifications = async (): Promise<{ notifications: AppNotification[]; unread_count: number }> => { +export const getCabineNotifications = async (): Promise<{ + notifications: AppNotification[]; + unread_count: number; +}> => { const { data } = await apiClient.get(`${API}/notifications`); return data; }; @@ -382,7 +391,9 @@ export const markCabineNotificationsRead = async (): Promise => { export const getPublicSettings = async (): Promise => { try { - const { data } = await apiClient.get(`http://5.181.0.112/api/v1/app-settings`); + const { data } = await apiClient.get( + `http://5.181.0.112/api/v1/app-settings`, + ); return { penalties_enabled: data.penalties_enabled ?? true, show_amende_score: data.show_amende_score ?? true, @@ -390,6 +401,49 @@ export const getPublicSettings = async (): Promise => { points_separated: data.points_separated ?? true, }; } catch { - return { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true }; + return { + penalties_enabled: true, + show_amende_score: true, + points_enabled: true, + points_separated: true, + }; } }; + +// ============================================ +// ADDRESSES +// ============================================ + +export const addAddress = async ( + invalidAddress: string, + correctAddress: string, +): Promise<{ success: boolean; message: string }> => { + const { data } = await apiClient.post(`${API}/add/address`, { + invalid_address: invalidAddress, + correct_address: correctAddress, + }); + return { success: true, message: data.message }; +}; + +export const deleteAddress = async ( + invalidAddress: string, + correctAddress: string, +): Promise<{ success: boolean; message: string }> => { + const { data } = await apiClient.delete(`${API}/delete/address`, { + data: { + invalid_address: invalidAddress, + correct_address: correctAddress, + }, + }); + return { success: true, message: data.message }; +}; + +export const getAllAddresses = async (): Promise< + { invalid_address: string; correct_address: string }[] +> => { + const { data } = await apiClient.get(`${API}/addresses`); + return (data.addresses ?? []).map((a: any) => ({ + invalid_address: a.invalid_address ?? a.InvalidAddress ?? "", + correct_address: a.correct_address ?? a.CorrectAddress ?? "", + })); +}; diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index f4dc669e..1d9c416d 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -17,7 +17,7 @@ import type { AppSettings, Category, PointsTier } from "../../api/api_admin"; import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; -type PoolAssignment = "weed" | "zipette" | "none"; +type PoolAssignment = "weed" | "zipette" | "total" | "none"; // ────────────────────────────────────────────────────────────── // Composant éditeur de paliers @@ -146,9 +146,11 @@ export default function SettingsScreen() { points_enabled: true, points_categories_weed: [], points_categories_zipette: [], + points_categories_total: [], points_separated: true, points_weed_tiers: [], points_zipette_tiers: [], + points_total_tiers: [], referral_enabled: true, }); const [categories, setCategories] = useState([]); @@ -164,6 +166,8 @@ export default function SettingsScreen() { ...settingsRes.settings, points_categories_weed: settingsRes.settings.points_categories_weed ?? [], points_categories_zipette: settingsRes.settings.points_categories_zipette ?? [], + points_categories_total: settingsRes.settings.points_categories_total ?? [], + points_total_tiers: settingsRes.settings.points_total_tiers ?? [], }); } if (categoriesRes) { @@ -179,6 +183,7 @@ export default function SettingsScreen() { const getPoolFor = (name: string): PoolAssignment => { if ((settings.points_categories_weed ?? []).includes(name)) return "weed"; if ((settings.points_categories_zipette ?? []).includes(name)) return "zipette"; + if ((settings.points_categories_total ?? []).includes(name)) return "total"; return "none"; }; @@ -186,9 +191,11 @@ export default function SettingsScreen() { setSettings((prev) => { const weed = (prev.points_categories_weed ?? []).filter((c) => c !== name); const zipette = (prev.points_categories_zipette ?? []).filter((c) => c !== name); + const total = (prev.points_categories_total ?? []).filter((c) => c !== name); if (pool === "weed") weed.push(name); else if (pool === "zipette") zipette.push(name); - return { ...prev, points_categories_weed: weed, points_categories_zipette: zipette }; + else if (pool === "total") total.push(name); + return { ...prev, points_categories_weed: weed, points_categories_zipette: zipette, points_categories_total: total }; }); }; @@ -310,6 +317,17 @@ export default function SettingsScreen() { const WEED_COLOR = "#10b981"; const ZIP_COLOR = "#9333ea"; + const TOTAL_COLOR = "#f97316"; + + // Le mode séparé nécessite que W ET Z aient au moins une catégorie + const hasBothPools = + (settings.points_categories_weed ?? []).length > 0 && + (settings.points_categories_zipette ?? []).length > 0; + + // Chips disponibles selon le mode + const availableChips: PoolAssignment[] = settings.points_separated + ? ["weed", "zipette", "none"] // mode séparé : T désactivé + : ["total", "none"]; // mode non-séparé : W/Z désactivés return ( @@ -393,16 +411,20 @@ export default function SettingsScreen() { thumbColor="#fff" /> - + Points séparés par pool Activé : Weed → point / Zipette → point_zipette{"\n"} - Désactivé : tout dans un seul compteur (point) + Désactivé : Barème Total sur la somme W+Z{"\n"} + {!hasBothPools && ( + "⚠️ Nécessite des catégories W ET Z configurées" + )} setSettings((prev) => ({ ...prev, points_separated: v })) } @@ -416,19 +438,30 @@ export default function SettingsScreen() { Attribution des catégories aux points - Pour chaque catégorie, choisis si elle génère des points Weed, Zipette, ou aucun. + {settings.points_separated + ? "Mode séparé : W = pool Weed, Z = pool Zipette." + : "Mode non-séparé : T = pool Total (barème sur W+Z). W et Z désactivés."} {/* Légende */} - - - - Weed - - - - Zipette - + + {settings.points_separated ? ( + <> + + + Weed + + + + Zipette + + + ) : ( + + + Total + + )} Aucun @@ -450,21 +483,24 @@ export default function SettingsScreen() { /> {cat.name} - {(["weed", "zipette", "none"] as PoolAssignment[]).map( - (p) => { + {availableChips.map((p) => { const active = pool === p; const chipColor = p === "weed" ? WEED_COLOR : p === "zipette" ? ZIP_COLOR - : colors.textMuted; + : p === "total" + ? TOTAL_COLOR + : colors.textMuted; const label = p === "weed" ? "W" : p === "zipette" ? "Z" - : "—"; + : p === "total" + ? "T" + : "—"; return ( ); - }, - )} + })} ); @@ -507,23 +542,36 @@ export default function SettingsScreen() { )} - {/* Barème de points */} - setSettings((p) => ({ ...p, points_weed_tiers: tiers }))} - colors={colors} - s={s} - accentColor={WEED_COLOR} - /> - setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))} - colors={colors} - s={s} - accentColor={ZIP_COLOR} - /> + {/* Barème de points — conditionnel selon le mode */} + {settings.points_separated ? ( + <> + setSettings((p) => ({ ...p, points_weed_tiers: tiers }))} + colors={colors} + s={s} + accentColor={WEED_COLOR} + /> + setSettings((p) => ({ ...p, points_zipette_tiers: tiers }))} + colors={colors} + s={s} + accentColor={ZIP_COLOR} + /> + + ) : ( + setSettings((p) => ({ ...p, points_total_tiers: tiers }))} + colors={colors} + s={s} + accentColor={TOTAL_COLOR} + /> + )} ([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [addModal, setAddModal] = useState(false); + const [invalidInput, setInvalidInput] = useState(""); + const [correctInput, setCorrectInput] = useState(""); + const [saving, setSaving] = useState(false); + + const loadAddresses = useCallback(async () => { + try { + const result = await getAllAddresses(); + setAddresses(result); + } catch (e: any) { + showError("Erreur", e.response?.data?.error ?? e.message); + } finally { + setLoading(false); + } + }, [showError]); + + useEffect(() => { + loadAddresses(); + }, [loadAddresses]); + + const onRefresh = async () => { + setRefreshing(true); + await loadAddresses(); + setRefreshing(false); + }; + + const openAddModal = () => { + setInvalidInput(""); + setCorrectInput(""); + setAddModal(true); + }; + + const handleAdd = useCallback(async () => { + const inv = invalidInput.trim(); + const cor = correctInput.trim(); + if (!inv || !cor) { + showError("Erreur", "Les deux champs sont obligatoires."); + return; + } + setSaving(true); + try { + await addAddress(inv, cor); + setAddresses((prev) => [ + ...prev, + { invalid_address: inv, correct_address: cor }, + ]); + setAddModal(false); + showSuccess("Succès", "Adresse ajoutée avec succès."); + } catch (e: any) { + showError("Erreur", e.response?.data?.error ?? e.message); + } finally { + setSaving(false); + } + }, [invalidInput, correctInput, showError, showSuccess]); + + const handleDelete = useCallback( + (item: AddressEntry) => { + showConfirm( + "Supprimer", + `Supprimer la correction :\n"${item.invalid_address}" → "${item.correct_address}" ?`, + async () => { + try { + await deleteAddress( + item.invalid_address, + item.correct_address, + ); + setAddresses((prev) => + prev.filter( + (a) => + a.invalid_address !== + item.invalid_address || + a.correct_address !== item.correct_address, + ), + ); + showSuccess("Supprimé", "Adresse supprimée."); + } catch (e: any) { + showError( + "Erreur", + e.response?.data?.error ?? e.message, + ); + } + }, + "Supprimer", + ); + }, + [showConfirm, showError, showSuccess], + ); + + const styles = useMemo( + () => + StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bgPrimary }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.l, + paddingBottom: spacing.s, + }, + title: { + color: colors.textWhite, + fontSize: fontSize.lg, + fontWeight: "700", + }, + addBtn: { + backgroundColor: colors.accent, + borderRadius: 20, + padding: spacing.s, + }, + invalid: { + color: colors.danger, + fontSize: fontSize.md, + fontWeight: "600", + }, + correct: { + color: colors.success, + fontSize: fontSize.md, + fontWeight: "600", + marginTop: 6, + }, + label: { + fontSize: fontSize.xs, + fontWeight: "700", + letterSpacing: 0.5, + marginBottom: 2, + }, + labelInvalid: { + color: colors.danger, + }, + labelCorrect: { + color: colors.success, + }, + arrow: { + color: colors.textMuted, + fontSize: fontSize.lg, + marginVertical: 4, + }, + row: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + empty: { + color: colors.textMuted, + textAlign: "center", + marginTop: spacing.xxl, + fontSize: fontSize.md, + }, + }), + [colors], + ); + + const renderItem = ({ item }: { item: AddressEntry }) => ( + + + + {item.invalid_address} + + {item.correct_address} + + handleDelete(item)}> + + + + + ); + + return ( + + + Corrections d'adresses + + + + + + `${item.invalid_address}-${i}`} + renderItem={renderItem} + contentContainerStyle={{ + padding: spacing.l, + paddingTop: spacing.s, + }} + refreshControl={ + + } + ListEmptyComponent={ + + {loading + ? "Chargement..." + : "Aucune correction.\nAppuyez sur + pour en ajouter une."} + + } + /> + + setAddModal(false)} + title="Ajouter une correction" + icon="map-outline" + > + + +