Files
projet_gestion_commande/frontend-admin/src/screens/admin/UsersScreen.tsx
T
2026-03-17 09:18:23 +01:00

1167 lines
44 KiB
TypeScript

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<UserItem[]>([]);
const [clients, setClients] = useState<ClientResponse[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [poolNames, setPoolNames] = useState<string[]>([]);
const [poolKeys, setPoolKeys] = useState<string[]>([]);
const [pointsEnabled, setPointsEnabled] = useState(true);
const [amendeEnabled, setAmendeEnabled] = useState(true);
const [filter, setFilter] = useState<RoleFilter>("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<string, ClientResponse>();
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<string, any> = {};
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<typeof getMergedUsers>[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<typeof getMergedUsers>[0];
}) => {
const isClient = item.role === "client" && item.clientData;
const roleColor = getRoleBadgeColor(item.role);
return (
<Card style={{ marginBottom: spacing.m }}>
<View style={styles.cardHeader}>
<View style={styles.userInfo}>
<View
style={[
styles.roleIcon,
{ backgroundColor: roleColor + "20" },
]}
>
<Ionicons
name={
item.role === "client"
? "person"
: item.role === "livreur"
? "bicycle"
: item.role === "cabine"
? "desktop"
: "shield"
}
size={18}
color={roleColor}
/>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.username}>{item.username}</Text>
<View
style={[
styles.roleBadge,
{ backgroundColor: roleColor + "20" },
]}
>
<Text
style={[
styles.roleBadgeText,
{ color: roleColor },
]}
>
{getRoleLabel(item.role)}
</Text>
</View>
</View>
</View>
<View style={{ flexDirection: "row", gap: spacing.xs }}>
{isClient && (
<>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openReferralModal(item.clientData!)}
>
<Ionicons
name="gift-outline"
size={20}
color={colors.success}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openEditClient(item.clientData!)}
>
<Ionicons
name="create-outline"
size={20}
color={colors.info}
/>
</TouchableOpacity>
</>
)}
{(item.role === "livreur" ||
item.role === "cabine") && (
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => openEditUser(item)}
>
<Ionicons
name="create-outline"
size={20}
color={colors.info}
/>
</TouchableOpacity>
)}
{item.role !== "admin" && (
<TouchableOpacity
style={styles.editIconBtn}
onPress={() => handleDelete(item)}
>
<Ionicons
name="trash-outline"
size={20}
color={colors.danger}
/>
</TouchableOpacity>
)}
</View>
</View>
{/* Client details */}
{isClient && item.clientData && (
<>
<View style={styles.clientDetails}>
<Text style={styles.clientName}>
{item.clientData.prenom} {item.clientData.nom}
</Text>
<Text style={styles.clientTel}>
{item.clientData.telephone}
</Text>
</View>
<View style={styles.statsRow}>
<View style={styles.stat}>
<Text style={styles.statValue}>
{item.clientData.command}
</Text>
<Text style={styles.statLabel}>Cmd</Text>
</View>
{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 (
<View key={i} style={styles.stat}>
<Text style={[styles.statValue, { color }]}>{value}</Text>
<Text style={styles.statLabel}>{name}</Text>
</View>
);
})}
{amendeEnabled && (
<View style={styles.stat}>
<Text
style={[
styles.statValue,
{ color: colors.warning },
]}
>
{item.clientData.amende}
</Text>
<Text style={styles.statLabel}>Amendes</Text>
</View>
)}
<View style={styles.stat}>
<Text
style={[
styles.statValue,
{ color: colors.danger },
]}
>
{item.clientData.cancellations_count}
</Text>
<Text style={styles.statLabel}>Annul.</Text>
</View>
{(item.clientData.referral_balance ?? 0) > 0 && (
<View style={styles.stat}>
<Text
style={[
styles.statValue,
{ color: colors.success },
]}
>
{(item.clientData.referral_balance ?? 0).toFixed(0)}
</Text>
<Text style={styles.statLabel}>Parrain</Text>
</View>
)}
</View>
</>
)}
{/* Non-client: just show role + ID */}
{!isClient && (
<Text style={styles.nonClientInfo}>ID: {item.id}</Text>
)}
</Card>
);
};
if (loading) return <LoadingSpinner message="Chargement utilisateurs..." />;
return (
<View style={styles.container}>
{/* Role filter grid */}
<View style={styles.filterGrid}>
{ROLE_TABS.map((tab) => {
const active = filter === tab.key;
const count = getCountForFilter(tab.key);
return (
<TouchableOpacity
key={tab.key}
style={[
styles.filterCard,
active && {
backgroundColor: tab.color + "15",
borderColor: tab.color,
},
]}
onPress={() => setFilter(tab.key)}
activeOpacity={0.7}
>
<View
style={[
styles.filterIconCircle,
{
backgroundColor:
(active
? tab.color
: colors.textMuted) + "20",
},
]}
>
<Ionicons
name={tab.icon as any}
size={20}
color={
active ? tab.color : colors.textMuted
}
/>
</View>
<Text
style={[
styles.filterCount,
active && { color: tab.color },
]}
>
{count}
</Text>
<Text
style={[
styles.filterLabel,
active && { color: tab.color },
]}
>
{tab.label}
</Text>
</TouchableOpacity>
);
})}
</View>
{/* Search bar */}
<View style={styles.searchContainer}>
<Ionicons name="search-outline" size={18} color={colors.textMuted} />
<RNTextInput
style={styles.searchInput}
placeholder="Rechercher par nom, username, téléphone..."
placeholderTextColor={colors.textMuted}
value={search}
onChangeText={setSearch}
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="while-editing"
/>
{search.length > 0 && (
<TouchableOpacity onPress={() => setSearch("")}>
<Ionicons name="close-circle" size={18} color={colors.textMuted} />
</TouchableOpacity>
)}
</View>
{/* Add user button */}
<TouchableOpacity
style={styles.addButton}
onPress={openCreateModal}
activeOpacity={0.7}
>
<Ionicons
name="add-circle"
size={22}
color={colors.textWhite}
/>
<Text style={styles.addButtonText}>Créer un utilisateur</Text>
</TouchableOpacity>
{/* List */}
<FlatList
data={mergedUsers}
keyExtractor={(item) => `${item.role}-${item.id}`}
renderItem={renderUser}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.accent}
/>
}
contentContainerStyle={{
padding: spacing.l,
paddingBottom: 100,
}}
ListEmptyComponent={
<Text style={styles.empty}>Aucun utilisateur</Text>
}
/>
{/* Edit client modal */}
<Modal
visible={editClientModal.visible}
onClose={() =>
setEditClientModal({ visible: false, client: null })
}
title="Modifier client"
icon="person-outline"
>
<TextInput
label="Nom"
value={editNom}
onChangeText={setEditNom}
/>
<TextInput
label="Prénom"
value={editPrenom}
onChangeText={setEditPrenom}
/>
<TextInput
label="Téléphone"
value={editTel}
onChangeText={setEditTel}
keyboardType="phone-pad"
/>
<Button
title="Enregistrer"
onPress={handleSaveClient}
fullWidth
/>
</Modal>
{/* Edit user modal (livreur/cabine) */}
<Modal
visible={editUserModal.visible}
onClose={() => setEditUserModal({ visible: false, user: null })}
title={`Modifier ${editUserModal.user ? getRoleLabel(editUserModal.user.role) : ""}`}
icon="create-outline"
>
<TextInput
label="Nom d'utilisateur"
value={editUsername}
onChangeText={setEditUsername}
autoCapitalize="none"
/>
<Text style={styles.roleEditLabel}>Rôle</Text>
<View style={styles.roleEditRow}>
{["livreur", "cabine", "admin"].map((r) => (
<TouchableOpacity
key={r}
style={[
styles.roleEditBtn,
editRole === r && {
backgroundColor:
getRoleBadgeColor(r) + "20",
borderColor: getRoleBadgeColor(r),
},
]}
onPress={() => setEditRole(r)}
>
<Text
style={[
styles.roleEditBtnText,
editRole === r && {
color: getRoleBadgeColor(r),
},
]}
>
{getRoleLabel(r)}
</Text>
</TouchableOpacity>
))}
</View>
<Button
title="Enregistrer"
onPress={handleSaveUser}
fullWidth
/>
</Modal>
{/* Create user modal */}
<Modal
visible={createModal}
onClose={() => setCreateModal(false)}
title="Créer un utilisateur"
icon="person-add-outline"
>
<Text style={styles.roleEditLabel}>Type d'utilisateur</Text>
<View style={styles.roleEditRow}>
{(["client", "cabine", "livreur"] as const).map((r) => (
<TouchableOpacity
key={r}
style={[
styles.roleEditBtn,
createType === r && {
backgroundColor:
getRoleBadgeColor(r) + "20",
borderColor: getRoleBadgeColor(r),
},
]}
onPress={() => setCreateType(r)}
>
<Text
style={[
styles.roleEditBtnText,
createType === r && {
color: getRoleBadgeColor(r),
},
]}
>
{getRoleLabel(r)}
</Text>
</TouchableOpacity>
))}
</View>
<TextInput
label="Nom d'utilisateur"
value={createUsername}
onChangeText={setCreateUsername}
autoCapitalize="none"
/>
<TextInput
label="Mot de passe"
value={createPassword}
onChangeText={setCreatePassword}
secureTextEntry
/>
{createType === "client" && (
<>
<TextInput
label="Nom"
value={createNom}
onChangeText={setCreateNom}
/>
<TextInput
label="Prénom"
value={createPrenom}
onChangeText={setCreatePrenom}
/>
<TextInput
label="Téléphone"
value={createTel}
onChangeText={setCreateTel}
keyboardType="phone-pad"
/>
</>
)}
<Button
title={creating ? "Création..." : "Créer"}
onPress={handleCreate}
fullWidth
disabled={creating}
/>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
onConfirm={alert.onConfirm}
confirmText={alert.confirmText}
cancelText={alert.cancelText}
/>
{/* Modal crédit parrainage */}
<Modal
visible={referralModal.visible}
onClose={() => setReferralModal((prev) => ({ ...prev, visible: false }))}
title={`Crédit parrainage — ${referralModal.username}`}
icon="gift-outline"
iconColor={colors.success}
>
<Text style={{ color: colors.textSecondary, fontSize: fontSize.sm, marginBottom: spacing.s }}>
Solde actuel : {referralModal.currentBalance.toFixed(2)} €
</Text>
<TextInput
placeholder="Montant à créditer (ex: 10)"
value={referralAmount}
onChangeText={setReferralAmount}
keyboardType="decimal-pad"
icon={<Ionicons name="cash-outline" size={18} color={colors.textMuted} />}
/>
<Button
title={referralLoading ? "Chargement..." : "Créditer"}
onPress={handleCreditReferral}
disabled={referralLoading}
variant="success"
size="md"
fullWidth
style={{ marginTop: spacing.m }}
/>
</Modal>
</View>
);
}