import React, { useState, useEffect, useCallback, useMemo } from "react"; import { View, Text, StyleSheet, FlatList, ScrollView, RefreshControl, TouchableOpacity, TextInput as RNTextInput, ActivityIndicator, useWindowDimensions, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; import { getAllClients, getAllUsers, updateClientByAdmin, updateUserByAdmin, deleteUserAdmin, deleteClientAdmin, createClientByAdmin, createUserByAdmin, setClientParrain, getClientParrain, creditClientReferral, resetClientReferral, getClientCancelledOrders, getSettings, applyClientPenalty, resetClientPenalties, resetClientPoints, addClientPoints, subtractClientPoints, } from "../../api/api_admin"; import type { CancelledOrder } from "../../api/api_admin"; import type { ClientResponse } from "../../api/types"; import LoadingSpinner from "../../components/ui/LoadingSpinner"; import Card from "../../components/ui/Card"; import Button from "../../components/ui/Button"; import Modal from "../../components/ui/Modal"; import TextInput from "../../components/ui/TextInput"; import AlertModal from "../../components/ui/AlertModal"; import { useAlert } from "../../hooks/useAlert"; // -------------------------------------------------- // Types // -------------------------------------------------- interface UserItem { id: number; username: string; role: string; } type RoleFilter = "all" | "client" | "livreur" | "cabine" | "admin"; // ================================================== // SCREEN // ================================================== export default function UsersScreen() { const { colors } = useTheme(); const { width: screenWidth } = useWindowDimensions(); const ROLE_TABS: { key: RoleFilter; label: string; icon: keyof typeof Ionicons.glyphMap; color: string; }[] = useMemo( () => [ { key: "all", label: "Tous", icon: "people-outline", color: colors.accent, }, { key: "client", label: "Clients", icon: "person-outline", color: colors.info, }, { key: "livreur", label: "Livreurs", icon: "bicycle-outline", color: colors.success, }, { key: "cabine", label: "Cabines", icon: "desktop-outline", color: colors.warning, }, { key: "admin", label: "Admins", icon: "shield-outline", color: colors.accent, }, ], [colors], ); const getRoleBadgeColor = useCallback( (role: string) => { switch (role) { case "client": return colors.info; case "livreur": return colors.success; case "cabine": return colors.warning; case "admin": return colors.accent; default: return colors.textMuted; } }, [colors], ); const getRoleLabel = useCallback((role: string) => { switch (role) { case "client": return "Client"; case "livreur": return "Livreur"; case "cabine": return "Cabine"; case "admin": return "Admin"; default: return role; } }, []); const [users, setUsers] = useState([]); const [clients, setClients] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [poolNames, setPoolNames] = useState([]); const [poolKeys, setPoolKeys] = useState([]); const [pointsEnabled, setPointsEnabled] = useState(true); const [amendeEnabled, setAmendeEnabled] = useState(true); const [referralEnabled, setReferralEnabled] = useState(true); const [filter, setFilter] = useState("all"); const [search, setSearch] = useState(""); const [parrainMap, setParrainMap] = useState>( {}, ); // Edit client modal const [editClientModal, setEditClientModal] = useState<{ visible: boolean; client: ClientResponse | null; }>({ visible: false, client: null }); const [editNom, setEditNom] = useState(""); const [editPrenom, setEditPrenom] = useState(""); const [editTel, setEditTel] = useState(""); const [editClientUsername, setEditClientUsername] = useState(""); const [editClientPassword, setEditClientPassword] = useState(""); // Edit user modal (livreur/cabine) const [editUserModal, setEditUserModal] = useState<{ visible: boolean; user: UserItem | null; }>({ visible: false, user: null }); const [editUsername, setEditUsername] = useState(""); const [editRole, setEditRole] = useState(""); const [editUserPassword, setEditUserPassword] = useState(""); // Create user modal const [createModal, setCreateModal] = useState(false); const [createType, setCreateType] = useState< "client" | "cabine" | "livreur" >("client"); const [createUsername, setCreateUsername] = useState(""); const [createPassword, setCreatePassword] = useState(""); const [createNom, setCreateNom] = useState(""); const [createPrenom, setCreatePrenom] = useState(""); const [createTel, setCreateTel] = useState(""); const [createParrain, setCreateParrain] = useState(""); const [creating, setCreating] = useState(false); // Referral credit modal const [referralModal, setReferralModal] = useState<{ visible: boolean; username: string; currentBalance: number; }>({ visible: false, username: "", currentBalance: 0 }); const [referralAmount, setReferralAmount] = useState(""); const [referralLoading, setReferralLoading] = useState(false); // Points & amende modal const [sanctionModal, setSanctionModal] = useState<{ visible: boolean; client: ClientResponse | null; }>({ visible: false, client: null }); const [sanctionTab, setSanctionTab] = useState<"amende" | "points">( "amende", ); const [amendeAmount, setAmendeAmount] = useState(""); const [amendeReason, setAmendeReason] = useState(""); const [amendeLoading, setAmendeLoading] = useState(false); const [pointsInputs, setPointsInputs] = useState>( {}, ); const [pointsLoading, setPointsLoading] = useState>( {}, ); const [subtractInputs, setSubtractInputs] = useState< Record >({}); const [subtractLoading, setSubtractLoading] = useState< Record >({}); // Cancelled orders modal const [cancelledModal, setCancelledModal] = useState<{ visible: boolean; username: string; orders: CancelledOrder[]; loading: boolean; }>({ visible: false, username: "", orders: [], loading: false }); const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert(); // -------------------------------------------------- // Data // -------------------------------------------------- const loadData = useCallback(async () => { try { const [usersData, clientsData, settingsRes] = await Promise.all([ getAllUsers(), getAllClients(), getSettings(), ]); setUsers(usersData); setClients(clientsData); if (settingsRes.success && settingsRes.settings) { const pools = settingsRes.settings.points_pools ?? []; setPoolNames(pools.map((p) => p.name)); setPoolKeys(pools.map((p) => p.key)); setPointsEnabled(settingsRes.settings.points_enabled ?? true); setAmendeEnabled( settingsRes.settings.show_amende_score ?? true, ); setReferralEnabled( settingsRes.settings.referral_enabled ?? true, ); } // Charger le parrain de chaque client en parallèle (best-effort, // une erreur sur un client n'empêche pas les autres de s'afficher). const clientUsernames = (clientsData ?? []).map((c) => c.username); if (clientUsernames.length > 0) { const results = await Promise.all( clientUsernames.map(async (username) => { const res = await getClientParrain(username); return [ username, res.success ? (res.parrain ?? null) : null, ] as const; }), ); const map: Record = {}; for (const [username, parrain] of results) { map[username] = parrain; } setParrainMap(map); } else { setParrainMap({}); } } catch { /* ignore */ } setLoading(false); }, []); useEffect(() => { loadData(); }, [loadData]); const onRefresh = async () => { setRefreshing(true); await loadData(); setRefreshing(false); }; // -------------------------------------------------- // Merged & filtered list // -------------------------------------------------- const getMergedUsers = () => { // Build a map of client details by username const clientMap = new Map(); clients.forEach((c) => clientMap.set(c.username, c)); // Merge: for each user from getAllUsers, attach client details if role=client let merged = users.map((u) => ({ ...u, clientData: u.role === "client" ? clientMap.get(u.username) || null : null, })); // Also add clients that might not be in users list clients.forEach((c) => { if (!merged.find((m) => m.username === c.username)) { merged.push({ id: c.id, username: c.username, role: "client", clientData: c, }); } }); if (filter !== "all") { merged = merged.filter((u) => u.role === filter); } if (search.trim()) { const q = search.trim().toLowerCase(); merged = merged.filter((u) => { if (u.username.toLowerCase().includes(q)) return true; if (u.clientData) { const c = u.clientData; return ( c.nom.toLowerCase().includes(q) || c.prenom.toLowerCase().includes(q) || c.telephone.toLowerCase().includes(q) ); } return false; }); } return merged; }; const mergedUsers = getMergedUsers(); // Stats const totalClients = users.filter((u) => u.role === "client").length || clients.length; const totalLivreurs = users.filter((u) => u.role === "livreur").length; const totalCabines = users.filter((u) => u.role === "cabine").length; const totalAdmins = users.filter((u) => u.role === "admin").length; const getCountForFilter = (f: RoleFilter) => { if (f === "all") return mergedUsers.length; if (f === "client") return totalClients; if (f === "livreur") return totalLivreurs; if (f === "cabine") return totalCabines; if (f === "admin") return totalAdmins; return 0; }; // -------------------------------------------------- // Edit client // -------------------------------------------------- const openEditClient = (client: ClientResponse) => { setEditNom(client.nom); setEditPrenom(client.prenom); setEditTel(client.telephone); setEditClientUsername(client.username); setEditClientPassword(""); setEditClientModal({ visible: true, client }); }; const handleSaveClient = async () => { if (!editClientModal.client) return; if (editClientPassword.trim() && editClientPassword.trim().length < 8) { showError( "Erreur", "Le mot de passe doit contenir au moins 8 caractères", ); return; } try { const updates: Record = { nom: editNom, prenom: editPrenom, telephone: editTel, username: editClientUsername, }; if (editClientPassword.trim()) { updates.password = editClientPassword.trim(); } await updateClientByAdmin(editClientModal.client.id, updates); setEditClientModal({ visible: false, client: null }); await loadData(); } catch (e: any) { showError("Erreur", e.response?.data?.error || e.message); } }; // -------------------------------------------------- // Edit user (livreur/cabine) // -------------------------------------------------- const openEditUser = (user: UserItem) => { setEditUsername(user.username); setEditRole(user.role); setEditUserPassword(""); setEditUserModal({ visible: true, user }); }; const handleSaveUser = async () => { if (!editUserModal.user) return; if (editUserPassword.trim() && editUserPassword.trim().length < 8) { showError( "Erreur", "Le mot de passe doit faire au moins 8 caractères", ); return; } try { const updates: Record = {}; if (editUsername.trim() !== editUserModal.user.username) { updates.username = editUsername.trim(); } if (editRole !== editUserModal.user.role) { updates.role = editRole; } if (editUserPassword.trim()) { updates.password = editUserPassword.trim(); } if (Object.keys(updates).length === 0) { setEditUserModal({ visible: false, user: null }); return; } await updateUserByAdmin(editUserModal.user.id, updates); setEditUserModal({ visible: false, user: null }); await loadData(); } catch (e: any) { showError("Erreur", e.response?.data?.error || e.message); } }; // -------------------------------------------------- // Delete user/client // -------------------------------------------------- const handleDelete = (item: ReturnType[0]) => { const label = item.role === "client" ? "client" : getRoleLabel(item.role).toLowerCase(); showConfirm( "Supprimer", `Supprimer le ${label} "${item.username}" ?`, async () => { try { if (item.role === "client" && item.clientData) { await deleteClientAdmin(item.clientData.id); } else { await deleteUserAdmin(item.id); } await loadData(); } catch (e: any) { showError("Erreur", e.message); } }, "Supprimer", ); }; // -------------------------------------------------- // Referral credit // -------------------------------------------------- const openReferralModal = (client: ClientResponse) => { setReferralAmount(""); setReferralModal({ visible: true, username: client.username, currentBalance: client.referral_balance ?? 0, }); }; const handleCreditReferral = async () => { const amount = parseFloat(referralAmount); if (isNaN(amount) || amount <= 0) { showError("Erreur", "Entrez un montant valide"); return; } setReferralLoading(true); try { const res = await creditClientReferral( referralModal.username, amount, ); if (res.success) { setReferralModal((prev) => ({ ...prev, visible: false })); await loadData(); } else { showError("Erreur", res.message || "Echec du crédit"); } } catch (e: any) { showError("Erreur", e.message); } finally { setReferralLoading(false); } }; const handleResetReferral = async (username: string) => { try { const res = await resetClientReferral(username); if (res.success) { showSuccess("Parrainage", "Solde remis à zéro"); await loadData(); } else { showError("Erreur", res.message || "Echec du reset"); } } catch (e: any) { showError("Erreur", e.message); } }; // -------------------------------------------------- // Points & amende // -------------------------------------------------- const openSanctionModal = (client: ClientResponse) => { setAmendeAmount(""); setAmendeReason(""); setPointsInputs({}); setPointsLoading({}); setSubtractInputs({}); setSubtractLoading({}); setSanctionTab("amende"); setSanctionModal({ visible: true, client }); }; const handleAddAmende = () => { const pts = parseInt(amendeAmount, 10); if (isNaN(pts) || pts <= 0) { showError("Erreur", "Entrez un montant valide"); return; } if (!amendeReason.trim()) { showError("Erreur", "La raison est obligatoire"); return; } if (!sanctionModal.client) return; showConfirm( "Confirmer l'amende", `Ajouter ${pts} € d'amende à "${sanctionModal.client.username}" ?\nRaison : ${amendeReason.trim()}`, async () => { setAmendeLoading(true); const res = await applyClientPenalty( sanctionModal.client!.username, pts, amendeReason.trim(), ); setAmendeLoading(false); if (!res.success) { showError("Erreur", res.error || "Echec"); return; } setAmendeAmount(""); setAmendeReason(""); if (res.compensated) { showSuccess( "Compensation", "Amende annulée — solde parrainage débité en compensation", ); } else { showSuccess( "Validé", `Amende de ${pts} € appliquée à "${sanctionModal.client!.username}"`, ); } await loadData(); }, ); }; const handleResetAmende = () => { if (!sanctionModal.client) return; showConfirm( "Remettre à zéro", `Remettre l'amende de "${sanctionModal.client.username}" à 0 ?`, async () => { const res = await resetClientPenalties( sanctionModal.client!.username, false, ); if (!res.success) { showError("Erreur", res.error || "Echec"); return; } showSuccess( "Validé", `Amende de "${sanctionModal.client!.username}" remise à zéro`, ); await loadData(); }, "Remettre à zéro", ); }; const handleAddPoints = (poolKey: string, poolName: string) => { const pts = parseInt(pointsInputs[poolKey] || "", 10); if (isNaN(pts) || pts <= 0) { showError("Erreur", "Entrez un nombre de points valide"); return; } if (!sanctionModal.client) return; showConfirm( "Confirmer l'ajout", `Ajouter ${pts} points "${poolName}" à "${sanctionModal.client.username}" ?`, async () => { setPointsLoading((prev) => ({ ...prev, [poolKey]: true })); const res = await addClientPoints( sanctionModal.client!.username, poolKey, pts, ); setPointsLoading((prev) => ({ ...prev, [poolKey]: false })); if (!res.success) { showError("Erreur", res.error || "Echec"); return; } setPointsInputs((prev) => ({ ...prev, [poolKey]: "" })); showSuccess( "Validé", `${pts} points "${poolName}" ajoutés à "${sanctionModal.client!.username}"`, ); await loadData(); }, ); }; const handleSubtractPoints = (poolKey: string, poolName: string) => { const pts = parseInt(subtractInputs[poolKey] || "", 10); if (isNaN(pts) || pts <= 0) { showError("Erreur", "Entrez un nombre de points valide"); return; } if (!sanctionModal.client) return; showConfirm( "Confirmer le retrait", `Retirer ${pts} points "${poolName}" à "${sanctionModal.client.username}" ?`, async () => { setSubtractLoading((prev) => ({ ...prev, [poolKey]: true })); const res = await subtractClientPoints( sanctionModal.client!.username, poolKey, pts, ); setSubtractLoading((prev) => ({ ...prev, [poolKey]: false })); if (!res.success) { showError("Erreur", res.error || "Echec"); return; } setSubtractInputs((prev) => ({ ...prev, [poolKey]: "" })); showSuccess( "Validé", `${pts} points "${poolName}" retirés à "${sanctionModal.client!.username}"`, ); await loadData(); }, ); }; const handleResetPoints = ( poolKey: string, poolName: string, poolIdx: number, ) => { if (!sanctionModal.client) return; showConfirm( "Remettre à zéro", `Remettre les points "${poolName}" de "${sanctionModal.client.username}" à 0 ?`, async () => { const res = await resetClientPoints( sanctionModal.client!.username, poolIdx, ); if (!res.success) { showError("Erreur", res.error || "Echec"); return; } showSuccess( "Validé", `Points "${poolName}" de "${sanctionModal.client!.username}" remis à zéro`, ); await loadData(); }, "Remettre à zéro", ); }; // -------------------------------------------------- // Cancelled orders // -------------------------------------------------- const openCancelledOrders = async (client: ClientResponse) => { setCancelledModal({ visible: true, username: client.username, orders: [], loading: true, }); const res = await getClientCancelledOrders(client.username); setCancelledModal((prev) => ({ ...prev, orders: res.orders, loading: false, })); }; // -------------------------------------------------- // Create user // -------------------------------------------------- const openCreateModal = () => { setCreateType("client"); setCreateUsername(""); setCreatePassword(""); setCreateNom(""); setCreatePrenom(""); setCreateTel(""); setCreateParrain(""); setCreateModal(true); }; const handleCreate = async () => { if (!createUsername.trim() || !createPassword.trim()) { showError("Erreur", "Nom d'utilisateur et mot de passe requis"); return; } if (createPassword.trim().length < 8) { showError( "Erreur", "Le mot de passe doit contenir au moins 8 caractères", ); return; } if (createType === "client") { if ( !createNom.trim() || !createPrenom.trim() || !createTel.trim() ) { showError( "Erreur", "Nom, prénom et téléphone requis pour un client", ); return; } } setCreating(true); const usernameToCreate = createUsername.trim(); try { if (createType === "client") { await createClientByAdmin({ username: usernameToCreate, password: createPassword.trim(), nom: createNom.trim(), prenom: createPrenom.trim(), telephone: createTel.trim(), }); if (createParrain.trim()) { await setClientParrain( usernameToCreate, createParrain.trim(), ); } } else { await createUserByAdmin({ username: usernameToCreate, password: createPassword.trim(), role: createType, }); } setCreateModal(false); await loadData(); } catch (e: any) { // Erreur réseau : pas de réponse HTTP reçue — le backend a peut-être // quand même créé l'utilisateur. On recharge et on vérifie. if (!e.response) { try { if (createType === "client") { const freshClients = await getAllClients(); setClients(freshClients); if ( freshClients.find( (c) => c.username === usernameToCreate, ) ) { setCreateModal(false); return; } } else { const freshUsers = await getAllUsers(); setUsers(freshUsers); if ( freshUsers.find( (u: any) => u.username === usernameToCreate, ) ) { setCreateModal(false); return; } } } catch { /* ignore, on affiche l'erreur originale */ } } showError("Erreur", e.response?.data?.error || e.message); } finally { setCreating(false); } }; // -------------------------------------------------- // Styles // -------------------------------------------------- const styles = useMemo( () => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bgPrimary }, // Filter grid filterGrid: { flexDirection: "row", paddingHorizontal: screenWidth < 380 ? spacing.s : spacing.m, paddingVertical: spacing.s, gap: screenWidth < 380 ? spacing.xs : spacing.s, }, filterCard: { flex: 1, alignItems: "center", paddingVertical: spacing.s, paddingHorizontal: 2, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.bgSecondary, }, filterIconCircle: { width: screenWidth < 380 ? 28 : 36, height: screenWidth < 380 ? 28 : 36, borderRadius: screenWidth < 380 ? 14 : 18, justifyContent: "center", alignItems: "center", marginBottom: 2, }, filterCount: { color: colors.textWhite, fontSize: screenWidth < 380 ? fontSize.sm : fontSize.md, fontWeight: "700", }, filterLabel: { color: colors.textMuted, fontSize: screenWidth < 380 ? 10 : fontSize.xs, fontWeight: "600", textAlign: "center", }, // Card cardHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", }, userInfo: { flexDirection: "row", alignItems: "center", flex: 1, gap: spacing.m, }, roleIcon: { width: 40, height: 40, borderRadius: 20, justifyContent: "center", alignItems: "center", }, username: { fontSize: fontSize.md, fontWeight: "700", color: colors.textWhite, }, roleBadge: { alignSelf: "flex-start", paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: borderRadius.sm, marginTop: 4, }, roleBadgeText: { fontSize: fontSize.xs, fontWeight: "600" }, editIconBtn: { padding: spacing.s }, // Client details clientDetails: { marginTop: spacing.s, marginLeft: 52 }, clientName: { color: colors.textSecondary, fontSize: fontSize.sm, }, clientTel: { color: colors.textMuted, fontSize: fontSize.sm, marginTop: 2, }, parrainRow: { flexDirection: "row", alignItems: "center", gap: 4, marginTop: 4, }, parrainText: { color: colors.textMuted, fontSize: fontSize.xs, }, parrainName: { color: colors.success, fontWeight: "600", }, statsRow: { flexDirection: "row", flexWrap: "wrap", marginTop: spacing.s, gap: spacing.s, marginLeft: 52, }, stat: { alignItems: "center", minWidth: 52, paddingVertical: spacing.xs, }, statValue: { fontSize: fontSize.sm, fontWeight: "bold", color: colors.textWhite, textAlign: "center", }, statLabel: { fontSize: fontSize.xs, color: colors.textMuted, textAlign: "center", }, // Non-client nonClientInfo: { color: colors.textMuted, fontSize: fontSize.sm, marginTop: spacing.xs, marginLeft: 52, }, empty: { color: colors.textMuted, textAlign: "center", marginTop: spacing.xxl, }, // Search bar searchContainer: { flexDirection: "row", alignItems: "center", marginHorizontal: spacing.m, marginBottom: spacing.s, paddingHorizontal: spacing.m, borderRadius: borderRadius.md, backgroundColor: colors.bgSecondary, borderWidth: 1, borderColor: colors.border, height: 44, gap: spacing.s, }, searchInput: { flex: 1, color: colors.textWhite, fontSize: fontSize.sm, }, // Add button addButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.s, marginHorizontal: spacing.m, marginBottom: spacing.s, paddingVertical: spacing.m, borderRadius: borderRadius.md, backgroundColor: colors.accent, }, addButtonText: { color: colors.textWhite, fontSize: fontSize.md, fontWeight: "700", }, // Edit user modal roleEditLabel: { color: colors.textSecondary, fontSize: fontSize.sm, fontWeight: "600", marginBottom: spacing.s, marginTop: spacing.s, }, roleEditRow: { flexDirection: "row", gap: spacing.s, marginBottom: spacing.l, }, roleEditBtn: { flex: 1, alignItems: "center", paddingVertical: spacing.s, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.bgInput, }, roleEditBtnText: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: "600", }, }), [colors, screenWidth], ); // -------------------------------------------------- // Render // -------------------------------------------------- const renderUser = ({ item, }: { item: ReturnType[0]; }) => { const isClient = item.role === "client" && item.clientData; const roleColor = getRoleBadgeColor(item.role); return ( {item.username} {getRoleLabel(item.role)} {isClient && ( <> {referralEnabled && ( openReferralModal(item.clientData!) } > )} {referralEnabled && (item.clientData!.referral_balance ?? 0) > 0 && ( handleResetReferral( item.clientData!.username, ) } > )} openCancelledOrders(item.clientData!) } > openSanctionModal(item.clientData!) } > openEditClient(item.clientData!) } > )} {(item.role === "livreur" || item.role === "cabine") && ( openEditUser(item)} > )} {item.role !== "admin" && ( handleDelete(item)} > )} {/* Client details */} {isClient && item.clientData && ( <> {item.clientData.prenom} {item.clientData.nom} {item.clientData.telephone} {parrainMap[item.clientData.username] ? ( Parrainé par{" "} { parrainMap[ item.clientData.username ] } ) : null} {item.clientData.command} Cmd {pointsEnabled && poolNames.map((name, i) => { const key = poolKeys[i] ?? ""; const value = key ? (item.clientData!.points_extra?.[ key ] ?? 0) : 0; const color = i === 0 ? colors.success : i === 1 ? colors.info : colors.warning; return ( {value} {name} ); })} {amendeEnabled && ( {item.clientData.amende}€ Amende )} {item.clientData.cancellations_count} Annul. {referralEnabled && (item.clientData.referral_balance ?? 0) > 0 && ( {( item.clientData .referral_balance ?? 0 ).toFixed(0)} € Parrain )} )} {/* Non-client: just show role + ID */} {!isClient && ( ID: {item.id} )} ); }; if (loading) return ; return ( {/* Role filter grid */} {ROLE_TABS.map((tab) => { const active = filter === tab.key; const count = getCountForFilter(tab.key); return ( setFilter(tab.key)} activeOpacity={0.7} > {count} {tab.label} ); })} {/* Search bar */} {search.length > 0 && ( setSearch("")}> )} {/* Add user button */} Créer un utilisateur {/* List */} `${item.role}-${item.id}`} renderItem={renderUser} refreshControl={ } contentContainerStyle={{ padding: spacing.l, paddingBottom: 100, }} ListEmptyComponent={ Aucun utilisateur } /> {/* Edit client modal */} setEditClientModal({ visible: false, client: null }) } title="Modifier client" icon="person-outline" >