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,302 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
Dimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const { width } = Dimensions.get("window");
export default function AdminLoginScreen() {
const { colors } = useTheme();
const ACCENT = colors.accent;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: 60,
paddingBottom: spacing.xl,
},
heroBg: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: 88,
height: 88,
borderRadius: 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: fontSize.xxl,
fontWeight: "800",
color: colors.textWhite,
marginTop: spacing.l,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "admin");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
{/* Decorative top accent */}
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons
name="shield-checkmark"
size={48}
color={ACCENT}
/>
</View>
</View>
<Text style={styles.heroTitle}>Administration</Text>
<Text style={styles.heroSubtitle}>
Accès au panneau de contrôle
</Text>
</View>
<View style={styles.formSection}>
{/* Username */}
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
{/* Password */}
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
{/* Login button */}
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
{/* Back link */}
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,293 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
Dimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const { width } = Dimensions.get("window");
export default function CabineLoginScreen() {
const { colors } = useTheme();
const ACCENT = colors.info;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: 60,
paddingBottom: spacing.xl,
},
heroBg: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: 88,
height: 88,
borderRadius: 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: fontSize.xxl,
fontWeight: "800",
color: colors.textWhite,
marginTop: spacing.l,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "cabine");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons name="desktop" size={48} color={ACCENT} />
</View>
</View>
<Text style={styles.heroTitle}>Cabine</Text>
<Text style={styles.heroSubtitle}>
Suivi des commandes et livreurs
</Text>
</View>
<View style={styles.formSection}>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,293 @@
import React, { useState, useRef, useMemo } from "react";
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
TextInput as RNTextInput,
Animated,
Dimensions,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { useAuth } from "../../auth/AuthContext";
import { loginAdmin } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const { width } = Dimensions.get("window");
export default function DeliveryLoginScreen() {
const { colors } = useTheme();
const ACCENT = colors.success;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { loginAdmin: authLogin } = useAuth();
const navigation = useNavigation();
const buttonScale = useRef(new Animated.Value(1)).current;
const { alert, showError, hideAlert } = useAlert();
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
accentBar: { height: 4, width: "100%" },
heroSection: {
alignItems: "center",
paddingTop: 60,
paddingBottom: spacing.xl,
},
heroBg: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: "center",
alignItems: "center",
},
heroInner: {
width: 88,
height: 88,
borderRadius: 44,
justifyContent: "center",
alignItems: "center",
},
heroTitle: {
fontSize: fontSize.xxl,
fontWeight: "800",
color: colors.textWhite,
marginTop: spacing.l,
letterSpacing: 0.5,
},
heroSubtitle: {
fontSize: fontSize.md,
color: colors.textMuted,
marginTop: spacing.xs,
},
formSection: { paddingHorizontal: spacing.xl, flex: 1 },
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
overflow: "hidden",
},
inputIconBox: {
width: 52,
height: 52,
justifyContent: "center",
alignItems: "center",
},
input: {
flex: 1,
color: colors.textPrimary,
fontSize: fontSize.md,
paddingVertical: 15,
paddingHorizontal: spacing.m,
},
eyeBtn: { padding: spacing.m },
loginBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: 52,
borderRadius: borderRadius.md,
marginTop: spacing.m,
},
loginBtnText: {
color: colors.white,
fontSize: fontSize.lg,
fontWeight: "700",
},
backLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: spacing.xl,
paddingVertical: spacing.m,
},
backText: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginLeft: spacing.xs,
},
}),
[colors],
);
const handleLogin = async () => {
if (!username.trim() || !password.trim()) {
showError("Erreur", "Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const result = await loginAdmin(username.trim(), password);
if (result.success && result.access_token) {
await authLogin(result.access_token, "livreur");
} else {
showError("Erreur", result.message || "Connexion échouée");
}
} catch {
showError("Erreur", "Erreur de connexion");
} finally {
setLoading(false);
}
};
const onPressIn = () =>
Animated.spring(buttonScale, {
toValue: 0.96,
useNativeDriver: true,
}).start();
const onPressOut = () =>
Animated.spring(buttonScale, {
toValue: 1,
useNativeDriver: true,
}).start();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<View style={[styles.accentBar, { backgroundColor: ACCENT }]} />
<View style={styles.heroSection}>
<View
style={[styles.heroBg, { backgroundColor: ACCENT + "12" }]}
>
<View
style={[
styles.heroInner,
{ backgroundColor: ACCENT + "20" },
]}
>
<Ionicons name="bicycle" size={48} color={ACCENT} />
</View>
</View>
<Text style={styles.heroTitle}>Livreur</Text>
<Text style={styles.heroSubtitle}>
Gestion de vos livraisons
</Text>
</View>
<View style={styles.formSection}>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="person-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Nom d'utilisateur"
placeholderTextColor={colors.textMuted}
value={username}
onChangeText={setUsername}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.inputContainer}>
<View
style={[
styles.inputIconBox,
{ backgroundColor: ACCENT + "15" },
]}
>
<Ionicons
name="lock-closed-outline"
size={20}
color={ACCENT}
/>
</View>
<RNTextInput
style={styles.input}
placeholder="Mot de passe"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
/>
<TouchableOpacity
onPress={() => setShowPassword(!showPassword)}
style={styles.eyeBtn}
>
<Ionicons
name={
showPassword ? "eye-off-outline" : "eye-outline"
}
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
<Animated.View style={{ transform: [{ scale: buttonScale }] }}>
<TouchableOpacity
style={[styles.loginBtn, { backgroundColor: ACCENT }]}
onPress={handleLogin}
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={loading}
activeOpacity={0.9}
>
{loading ? (
<Text style={styles.loginBtnText}>
Connexion...
</Text>
) : (
<>
<Text style={styles.loginBtnText}>
Se connecter
</Text>
<Ionicons
name="arrow-forward"
size={20}
color={colors.white}
style={{ marginLeft: spacing.s }}
/>
</>
)}
</TouchableOpacity>
</Animated.View>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backLink}
>
<Ionicons
name="chevron-back"
size={18}
color={colors.textMuted}
/>
<Text style={styles.backText}>Choisir un autre rôle</Text>
</TouchableOpacity>
</View>
<AlertModal
visible={alert.visible}
type={alert.type}
title={alert.title}
message={alert.message}
onClose={hideAlert}
/>
</KeyboardAvoidingView>
);
}
@@ -0,0 +1,143 @@
import React, { useMemo } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { AuthStackParamList } from "../../navigation/types";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
type Nav = NativeStackNavigationProp<AuthStackParamList, "RoleSelect">;
export default function RoleSelectScreen() {
const { colors } = useTheme();
const navigation = useNavigation<Nav>();
const roles = [
{
key: "AdminLogin" as const,
label: "Admin",
desc: "Gestion complète du système",
icon: "shield-outline" as const,
color: colors.accent,
},
{
key: "CabineLogin" as const,
label: "Cabine",
desc: "Suivi des commandes et livreurs",
icon: "desktop-outline" as const,
color: colors.info,
},
{
key: "DeliveryLogin" as const,
label: "Livreur",
desc: "Gestion des livraisons",
icon: "bicycle-outline" as const,
color: colors.success,
},
];
const styles = useMemo(
() =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bgPrimary,
justifyContent: "center",
alignItems: "center",
padding: spacing.l,
},
card: {
width: "100%",
maxWidth: 400,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.lg,
padding: spacing.xl,
alignItems: "center",
},
title: {
fontSize: fontSize.xxl,
fontWeight: "bold",
color: colors.textWhite,
marginTop: spacing.m,
},
subtitle: {
color: colors.textSecondary,
fontSize: fontSize.md,
marginTop: spacing.xs,
marginBottom: spacing.xl,
},
roleBtn: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.l,
width: "100%",
borderWidth: 1,
borderColor: colors.border,
marginBottom: spacing.m,
},
iconCircle: {
width: 44,
height: 44,
borderRadius: 22,
justifyContent: "center",
alignItems: "center",
},
roleBtnText: { flex: 1, marginLeft: spacing.m },
roleTitle: {
color: colors.textWhite,
fontSize: fontSize.lg,
fontWeight: "600",
},
roleDesc: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginTop: 2,
},
}),
[colors],
);
return (
<View style={styles.container}>
<View style={styles.card}>
<Ionicons
name="people-outline"
size={48}
color={colors.accent}
/>
<Text style={styles.title}>Panel Administration</Text>
<Text style={styles.subtitle}>Sélectionnez votre rôle</Text>
{roles.map((r) => (
<TouchableOpacity
key={r.key}
style={styles.roleBtn}
onPress={() => navigation.navigate(r.key)}
activeOpacity={0.7}
>
<View
style={[
styles.iconCircle,
{ backgroundColor: r.color + "20" },
]}
>
<Ionicons name={r.icon} size={24} color={r.color} />
</View>
<View style={styles.roleBtnText}>
<Text style={styles.roleTitle}>{r.label}</Text>
<Text style={styles.roleDesc}>{r.desc}</Text>
</View>
<Ionicons
name="chevron-forward"
size={20}
color={colors.textMuted}
/>
</TouchableOpacity>
))}
</View>
</View>
);
}