chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,239 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { View, Text, StyleSheet, FlatList, RefreshControl } from "react-native";
import { spacing, fontSize } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getAllClients } from "../../api/api_admin";
import { applyClientPenalty, resetClientPenalties } from "../../api/api_cabine";
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";
export default function UsersScreen() {
const { colors } = useTheme();
const [clients, setClients] = useState<ClientResponse[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [penaltyModal, setPenaltyModal] = useState<{
visible: boolean;
username: string;
}>({ visible: false, username: "" });
const [reason, setReason] = useState("");
const [penaltyAmount, setPenaltyAmount] = useState("");
const { alert, showError, showSuccess, showConfirm, hideAlert } =
useAlert();
const loadData = useCallback(async () => {
try {
setClients(await getAllClients());
} catch {
/* ignore */
}
setLoading(false);
}, []);
useEffect(() => {
loadData();
}, [loadData]);
const onRefresh = async () => {
setRefreshing(true);
await loadData();
setRefreshing(false);
};
const handleApplyPenalty = async () => {
if (!reason.trim()) {
showError("Erreur", "Raison requise");
return;
}
const amount = parseInt(penaltyAmount, 10);
if (!penaltyAmount.trim() || isNaN(amount) || amount <= 0) {
showError("Erreur", "Nombre de points invalide");
return;
}
try {
await applyClientPenalty(penaltyModal.username, reason, amount);
setPenaltyModal({ visible: false, username: "" });
setReason("");
setPenaltyAmount("");
await loadData();
showSuccess("Succès", "Pénalité appliquée");
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleReset = (username: string) => {
showConfirm(
"Reset",
`Réinitialiser les pénalités de ${username} ?`,
async () => {
try {
await resetClientPenalties(username);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Reset",
);
};
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
username: {
fontSize: fontSize.lg,
fontWeight: "bold",
color: colors.textWhite,
},
info: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginTop: 2,
},
statsRow: {
flexDirection: "row",
marginTop: spacing.m,
gap: spacing.l,
},
stat: { alignItems: "center" },
statValue: { fontSize: fontSize.lg, fontWeight: "bold" },
statLabel: { fontSize: fontSize.xs, color: colors.textMuted },
actions: {
flexDirection: "row",
gap: spacing.s,
marginTop: spacing.m,
},
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xxl,
},
}),
[colors],
);
const renderClient = ({ item }: { item: ClientResponse }) => (
<Card style={{ marginBottom: spacing.m }}>
<Text style={styles.username}>{item.username}</Text>
<Text style={styles.info}>
{item.prenom} {item.nom} - {item.telephone}
</Text>
<View style={styles.statsRow}>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.success }]}>
{item.point}
</Text>
<Text style={styles.statLabel}>Points</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.info }]}>
{item.points_zipette}
</Text>
<Text style={styles.statLabel}>Zipette</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.warning }]}>
{item.amende}
</Text>
<Text style={styles.statLabel}>Amendes</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statValue, { color: colors.danger }]}>
{item.cancellations_count}
</Text>
<Text style={styles.statLabel}>Annul.</Text>
</View>
</View>
<View style={styles.actions}>
<Button
title="Pénalité"
onPress={() => {
setPenaltyModal({
visible: true,
username: item.username,
});
setReason("");
setPenaltyAmount("");
}}
size="sm"
variant="danger"
/>
<Button
title="Reset"
onPress={() => handleReset(item.username)}
size="sm"
variant="outline"
/>
</View>
</Card>
);
if (loading) return <LoadingSpinner message="Chargement..." />;
return (
<View style={styles.container}>
<FlatList
data={clients}
keyExtractor={(item) => item.id.toString()}
renderItem={renderClient}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.info}
/>
}
contentContainerStyle={{ padding: spacing.l }}
ListEmptyComponent={
<Text style={styles.empty}>Aucun client</Text>
}
/>
<Modal
visible={penaltyModal.visible}
onClose={() =>
setPenaltyModal({ visible: false, username: "" })
}
title={`Pénalité - ${penaltyModal.username}`}
icon="warning-outline"
iconColor={colors.danger}
>
<TextInput
label="Nombre de points"
value={penaltyAmount}
onChangeText={setPenaltyAmount}
placeholder="Ex: 5"
keyboardType="numeric"
/>
<TextInput
label="Raison"
value={reason}
onChangeText={setReason}
placeholder="Raison de la pénalité"
/>
<Button
title="Appliquer"
onPress={handleApplyPenalty}
variant="danger"
fullWidth
/>
</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}
/>
</View>
);
}