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,251 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
RefreshControl,
} from "react-native";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getAllCommands } from "../../api/api_admin";
import {
getCommandItems,
updateItemStatus,
deleteCommand,
} from "../../api/api_cabine";
import type { CommandResponse } from "../../api/types";
import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Modal from "../../components/ui/Modal";
import Button from "../../components/ui/Button";
import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const ITEM_STATUSES = ["pending", "preparing", "ready"];
export default function OrdersScreen() {
const { colors } = useTheme();
const [commands, setCommands] = useState<CommandResponse[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const { alert, showError, showConfirm, hideAlert } = useAlert();
const [itemsModal, setItemsModal] = useState<{
visible: boolean;
commandId: number | null;
items: any[];
}>({ visible: false, commandId: null, items: [] });
const loadData = useCallback(async () => {
try {
const result = await getAllCommands();
setCommands(
result.commands.filter(
(c: CommandResponse) =>
!["approved", "cancelled"].includes(c.status),
),
);
} catch {
/* ignore */
}
setLoading(false);
}, []);
useEffect(() => {
loadData();
}, [loadData]);
const onRefresh = async () => {
setRefreshing(true);
await loadData();
setRefreshing(false);
};
const openItems = async (commandId: number) => {
try {
const result = await getCommandItems(commandId);
setItemsModal({ visible: true, commandId, items: result.items });
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleItemStatus = async (itemId: number, status: string) => {
try {
await updateItemStatus(itemId, status);
if (itemsModal.commandId) await openItems(itemsModal.commandId);
} catch (e: any) {
showError("Erreur", e.message);
}
};
const handleDelete = (commandId: number) => {
showConfirm(
"Supprimer",
`Supprimer la commande #${commandId} ?`,
async () => {
try {
await deleteCommand(commandId);
await loadData();
} catch (e: any) {
showError("Erreur", e.message);
}
},
"Supprimer",
);
};
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.s,
},
orderId: {
fontSize: fontSize.lg,
fontWeight: "bold",
color: colors.textWhite,
},
info: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginTop: 2,
},
actions: {
flexDirection: "row",
gap: spacing.s,
marginTop: spacing.m,
},
itemRow: {
flexDirection: "row",
alignItems: "center",
padding: spacing.m,
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
},
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
},
empty: {
color: colors.textMuted,
textAlign: "center",
marginTop: spacing.xl,
},
}),
[colors],
);
const renderOrder = ({ item }: { item: CommandResponse }) => (
<Card style={{ marginBottom: spacing.m }}>
<View style={styles.row}>
<Text style={styles.orderId}>#{item.id}</Text>
<StatusBadge status={item.status} />
</View>
<Text style={styles.info}>Client: {item.username}</Text>
<Text style={styles.info}>Adresse: {item.adresse}</Text>
<Text style={styles.info}>
Total: {item.total_prix.toFixed(2)}
</Text>
<View style={styles.actions}>
<Button
title="Voir items"
onPress={() => openItems(item.id)}
size="sm"
variant="primary"
/>
<Button
title="Supprimer"
onPress={() => handleDelete(item.id)}
size="sm"
variant="danger"
/>
</View>
</Card>
);
if (loading) return <LoadingSpinner message="Chargement..." />;
return (
<View style={styles.container}>
<FlatList
data={commands}
keyExtractor={(item) => item.id.toString()}
renderItem={renderOrder}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.info}
/>
}
contentContainerStyle={{ padding: spacing.l }}
ListEmptyComponent={
<Text style={styles.empty}>Aucune commande active</Text>
}
/>
<Modal
visible={itemsModal.visible}
onClose={() =>
setItemsModal({
visible: false,
commandId: null,
items: [],
})
}
title={`Items #${itemsModal.commandId}`}
icon="list-outline"
>
{itemsModal.items.map((item: any) => (
<View key={item.id} style={styles.itemRow}>
<View style={{ flex: 1 }}>
<Text style={styles.itemName}>
{item.product_name}
</Text>
<Text style={styles.info}>
Qté: {item.quantity} | {item.price?.toFixed(2)}{" "}
</Text>
<StatusBadge status={item.status} />
</View>
<View style={{ gap: spacing.xs }}>
{ITEM_STATUSES.filter((s) => s !== item.status).map(
(s) => (
<Button
key={s}
title={s}
onPress={() =>
handleItemStatus(item.id, s)
}
size="sm"
variant="outline"
/>
),
)}
</View>
</View>
))}
{itemsModal.items.length === 0 && (
<Text style={styles.empty}>Aucun item</Text>
)}
</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>
);
}