import React, { useState, useEffect, useCallback, useMemo } from "react"; import { View, Text, StyleSheet, FlatList, RefreshControl, TouchableOpacity, TextInput as RNTextInput, } 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, creditClientReferral, getSettings, } 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 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 [filter, setFilter] = useState("all"); const [search, setSearch] = 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(""); // 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(""); // 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 [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); const { alert, showError, 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); } } 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); setEditClientModal({ visible: true, client }); }; const handleSaveClient = async () => { if (!editClientModal.client) return; try { await updateClientByAdmin(editClientModal.client.id, { nom: editNom, prenom: editPrenom, telephone: editTel, }); setEditClientModal({ visible: false, client: null }); await loadData(); } catch (e: any) { showError("Erreur", e.message); } }; // -------------------------------------------------- // Edit user (livreur/cabine) // -------------------------------------------------- const openEditUser = (user: UserItem) => { setEditUsername(user.username); setEditRole(user.role); setEditUserModal({ visible: true, user }); }; const handleSaveUser = async () => { if (!editUserModal.user) return; try { const updates: Record = {}; if (editUsername.trim() !== editUserModal.user.username) { updates.username = editUsername.trim(); } if (editRole !== editUserModal.user.role) { updates.role = editRole; } 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); } }; // -------------------------------------------------- // Create user // -------------------------------------------------- const openCreateModal = () => { setCreateType("client"); setCreateUsername(""); setCreatePassword(""); setCreateNom(""); setCreatePrenom(""); setCreateTel(""); 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(), }); } 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: spacing.m, paddingVertical: spacing.m, gap: spacing.s, }, filterCard: { flex: 1, alignItems: "center", paddingVertical: spacing.m, borderRadius: borderRadius.md, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.bgSecondary, }, filterIconCircle: { width: 36, height: 36, borderRadius: 18, justifyContent: "center", alignItems: "center", marginBottom: 4, }, filterCount: { color: colors.textWhite, fontSize: fontSize.lg, fontWeight: "700", }, filterLabel: { color: colors.textMuted, fontSize: fontSize.sm, fontWeight: "600", }, // 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, }, statsRow: { flexDirection: "row", marginTop: spacing.m, gap: spacing.l, marginLeft: 52, }, stat: { alignItems: "center" }, statValue: { fontSize: fontSize.md, fontWeight: "bold", color: colors.textWhite, }, statLabel: { fontSize: fontSize.xs, color: colors.textMuted }, // 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], ); // -------------------------------------------------- // 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 && ( <> openReferralModal(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} {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} Amendes )} {item.clientData.cancellations_count} Annul. {(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" >