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,420 @@
import React, { useState, useMemo } from "react";
import {
View,
Text,
ScrollView,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { Ionicons } from "@expo/vector-icons";
import { useCart } from "../../context/CartContext";
import { checkoutCart } from "../../api/api";
import type { ClientStackParamList } from "../../navigation/types";
import TextInput from "../../components/ui/TextInput";
import Button from "../../components/ui/Button";
import Modal from "../../components/ui/Modal";
import { useTheme } from "../../context/ThemeContext";
import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
type Nav = NativeStackNavigationProp<ClientStackParamList>;
export default function CheckoutScreen() {
const navigation = useNavigation<Nav>();
const { colors } = useTheme();
const { cartItems, cartTotal, clearCart, refreshCart } = useCart();
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [telephone, setTelephone] = useState("");
const [address, setAddress] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmationData, setConfirmationData] = useState<any>(null);
const handleCheckout = async () => {
if (!nom.trim()) {
setError("Veuillez saisir votre nom");
return;
}
if (!prenom.trim()) {
setError("Veuillez saisir votre prenom");
return;
}
if (!telephone.trim()) {
setError("Veuillez saisir votre numero de telephone");
return;
}
if (!address.trim()) {
setError("Veuillez saisir une adresse de livraison");
return;
}
if (cartItems.length === 0) {
setError("Votre panier est vide");
return;
}
setError(null);
setLoading(true);
try {
const res = await checkoutCart(
address.trim(),
nom.trim(),
prenom.trim(),
telephone.trim(),
);
if (res.success) {
setConfirmationData(res);
setShowConfirmation(true);
await refreshCart();
} else {
setError(res.message || "Erreur lors de la commande");
}
} catch {
setError("Erreur de connexion au serveur");
} finally {
setLoading(false);
}
};
const handleConfirmClose = () => {
setShowConfirmation(false);
navigation.navigate("ClientTabs");
};
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
content: { padding: spacing.xl, paddingBottom: spacing.xxxl },
section: { marginBottom: spacing.xl },
sectionTitle: {
color: colors.textSecondary,
fontSize: fontSize.sm,
fontWeight: fontWeight.medium,
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: spacing.m,
},
fieldGroup: { gap: spacing.m },
row: { flexDirection: "row", gap: spacing.m },
halfField: { flex: 1 },
summaryCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.l,
borderWidth: 1,
borderColor: colors.borderLight,
},
summaryItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: spacing.s,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
summaryItemLeft: { flex: 1, marginRight: spacing.m },
summaryItemName: {
color: colors.textPrimary,
fontSize: fontSize.md,
},
summaryItemQty: {
color: colors.textMuted,
fontSize: fontSize.sm,
},
summaryItemPrice: {
color: colors.textPrimary,
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
},
summaryTotal: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingTop: spacing.m,
marginTop: spacing.s,
},
summaryTotalLabel: {
color: colors.textSecondary,
fontSize: fontSize.lg,
fontWeight: fontWeight.semibold,
},
summaryTotalValue: {
color: colors.success,
fontSize: fontSize.xl,
fontWeight: fontWeight.bold,
},
errorText: {
color: colors.danger,
fontSize: fontSize.sm,
textAlign: "center",
},
confirmContent: { gap: spacing.m },
confirmIconContainer: { alignItems: "center" },
confirmIconCircle: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: "rgba(74,222,128,0.1)",
justifyContent: "center",
alignItems: "center",
},
confirmText: {
color: colors.textPrimary,
fontSize: fontSize.md,
textAlign: "center",
lineHeight: 22,
},
confirmInfo: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
backgroundColor: colors.bgInput,
padding: spacing.m,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.borderLight,
},
confirmInfoText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
flex: 1,
},
}),
[colors],
);
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Resume de la commande
</Text>
<View style={styles.summaryCard}>
{cartItems.map((item) => (
<View key={item.id} style={styles.summaryItem}>
<View style={styles.summaryItemLeft}>
<Text
style={styles.summaryItemName}
numberOfLines={1}
>
{item.name_product}
</Text>
<Text style={styles.summaryItemQty}>
{item.quantity}g
</Text>
</View>
<Text style={styles.summaryItemPrice}>
{item.price.toFixed(2)}
</Text>
</View>
))}
<View style={styles.summaryTotal}>
<Text style={styles.summaryTotalLabel}>Total</Text>
<Text style={styles.summaryTotalValue}>
{cartTotal.toFixed(2)}
</Text>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Informations personnelles
</Text>
<View style={styles.fieldGroup}>
<View style={styles.row}>
<View style={styles.halfField}>
<TextInput
placeholder="Nom"
value={nom}
onChangeText={(text) => {
setNom(text);
setError(null);
}}
icon={
<Ionicons
name="person-outline"
size={20}
color={colors.textMuted}
/>
}
error={
error && !nom.trim() ? error : undefined
}
/>
</View>
<View style={styles.halfField}>
<TextInput
placeholder="Prenom"
value={prenom}
onChangeText={(text) => {
setPrenom(text);
setError(null);
}}
icon={
<Ionicons
name="person-outline"
size={20}
color={colors.textMuted}
/>
}
error={
error && nom.trim() && !prenom.trim()
? error
: undefined
}
/>
</View>
</View>
<TextInput
placeholder="Numero de telephone"
value={telephone}
onChangeText={(text) => {
setTelephone(text);
setError(null);
}}
icon={
<Ionicons
name="call-outline"
size={20}
color={colors.textMuted}
/>
}
keyboardType="phone-pad"
error={
error &&
nom.trim() &&
prenom.trim() &&
!telephone.trim()
? error
: undefined
}
/>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Adresse de livraison
</Text>
<TextInput
placeholder="Entrez votre adresse complete"
value={address}
onChangeText={(text) => {
setAddress(text);
setError(null);
}}
icon={
<Ionicons
name="location-outline"
size={20}
color={colors.textMuted}
/>
}
error={
error &&
nom.trim() &&
prenom.trim() &&
telephone.trim() &&
!address.trim()
? error
: undefined
}
multiline
numberOfLines={3}
/>
</View>
{error &&
nom.trim() &&
prenom.trim() &&
telephone.trim() &&
address.trim() && (
<Text style={styles.errorText}>{error}</Text>
)}
<Button
title="Valider la commande"
onPress={handleCheckout}
loading={loading}
disabled={loading || cartItems.length === 0}
variant="success"
size="lg"
fullWidth
style={{ marginTop: spacing.l }}
/>
</ScrollView>
<Modal
visible={showConfirmation}
onClose={handleConfirmClose}
title="Commande confirmee !"
icon="checkmark-circle"
iconColor={colors.success}
>
<View style={styles.confirmContent}>
<View style={styles.confirmIconContainer}>
<View style={styles.confirmIconCircle}>
<Ionicons
name="checkmark-circle"
size={48}
color={colors.success}
/>
</View>
</View>
<Text style={styles.confirmText}>
Votre commande #{confirmationData?.command_id} a ete
creee avec succes.
</Text>
{confirmationData?.assigned_to && (
<View style={styles.confirmInfo}>
<Ionicons
name="bicycle-outline"
size={16}
color={colors.info}
/>
<Text style={styles.confirmInfoText}>
Livreur: {confirmationData.assigned_to.username}{" "}
(
{confirmationData.assigned_to.distance_km?.toFixed(
1,
)}{" "}
km)
</Text>
</View>
)}
{confirmationData?.queue_info && (
<View style={styles.confirmInfo}>
<Ionicons
name="time-outline"
size={16}
color={colors.warning}
/>
<Text style={styles.confirmInfoText}>
Position: {confirmationData.queue_info.position}{" "}
- {confirmationData.queue_info.estimated_wait}
</Text>
</View>
)}
<Button
title="Voir mes commandes"
onPress={handleConfirmClose}
variant="primary"
size="md"
fullWidth
style={{ marginTop: spacing.l }}
/>
</View>
</Modal>
</KeyboardAvoidingView>
);
}