chore: add new features for cabine

This commit is contained in:
2026-02-26 19:57:01 +01:00
parent 0fb4bd8a89
commit f91d4276cc
8 changed files with 467 additions and 323 deletions
@@ -4,15 +4,15 @@ import {
Text,
StyleSheet,
FlatList,
TouchableOpacity,
RefreshControl,
ScrollView,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
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";
@@ -24,7 +24,23 @@ import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
const ITEM_STATUSES = ["pending", "preparing", "ready"];
const STATUS_COLORS: Record<string, string> = {
pending: "#F59E0B",
preparing: "#3B82F6",
ready: "#10B981",
};
const STATUS_ICONS: Record<string, keyof typeof Ionicons.glyphMap> = {
pending: "time-outline",
preparing: "construct-outline",
ready: "checkmark-circle-outline",
};
const STATUS_LABELS: Record<string, string> = {
pending: "En attente",
preparing: "En préparation",
ready: "Prêt",
};
export default function OrdersScreen() {
const { colors } = useTheme();
@@ -36,7 +52,15 @@ export default function OrdersScreen() {
visible: boolean;
commandId: number | null;
items: any[];
}>({ visible: false, commandId: null, items: [] });
commandInfo: any;
clientInfo: any;
}>({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const loadData = useCallback(async () => {
try {
@@ -56,6 +80,7 @@ export default function OrdersScreen() {
useEffect(() => {
loadData();
}, [loadData]);
const onRefresh = async () => {
setRefreshing(true);
await loadData();
@@ -65,20 +90,18 @@ export default function OrdersScreen() {
const openItems = async (commandId: number) => {
try {
const result = await getCommandItems(commandId);
setItemsModal({ visible: true, commandId, items: result.items });
setItemsModal({
visible: true,
commandId,
items: result.items,
commandInfo: result.command_info,
clientInfo: result.client_info,
});
} 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(
@@ -96,6 +119,15 @@ export default function OrdersScreen() {
);
};
const closeModal = () =>
setItemsModal({
visible: false,
commandId: null,
items: [],
commandInfo: null,
clientInfo: null,
});
const styles = useMemo(
() =>
StyleSheet.create({
@@ -121,24 +153,124 @@ export default function OrdersScreen() {
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,
},
// Modal summary
modalSummary: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
padding: spacing.m,
marginBottom: spacing.m,
gap: spacing.xs,
},
modalSummaryRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
},
modalSummaryText: {
color: colors.textSecondary,
fontSize: fontSize.sm,
flex: 1,
},
modalTotal: {
color: colors.accent,
fontSize: fontSize.md,
fontWeight: "700",
marginTop: spacing.xs,
},
// Modal progress bar
progressContainer: {
marginBottom: spacing.m,
},
progressLabel: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginBottom: spacing.xs,
},
progressBar: {
height: 4,
backgroundColor: colors.bgCard,
borderRadius: 2,
overflow: "hidden",
},
progressFill: {
height: "100%",
borderRadius: 2,
},
// Item card
itemCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
marginBottom: spacing.s,
overflow: "hidden",
},
itemCardAccent: {
height: 3,
},
itemCardBody: {
padding: spacing.m,
flexDirection: "row",
alignItems: "flex-start",
gap: spacing.m,
},
itemIndex: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: colors.bgPrimary,
justifyContent: "center",
alignItems: "center",
marginTop: 2,
},
itemIndexText: {
color: colors.textMuted,
fontSize: fontSize.xs,
fontWeight: "700",
},
itemInfo: { flex: 1 },
itemName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: "600",
marginBottom: 2,
},
itemMeta: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginBottom: spacing.s,
},
itemStatusRow: {
flexDirection: "row",
alignItems: "center",
gap: 5,
},
itemStatusText: {
fontSize: fontSize.xs,
fontWeight: "600",
},
itemActions: {
gap: spacing.xs,
},
statusBtn: {
flexDirection: "row",
alignItems: "center",
gap: 4,
paddingHorizontal: spacing.s,
paddingVertical: 5,
borderRadius: borderRadius.sm,
borderWidth: 1,
},
statusBtnText: {
fontSize: fontSize.xs,
fontWeight: "600",
},
}),
[colors],
);
@@ -171,6 +303,12 @@ export default function OrdersScreen() {
</Card>
);
const readyCount = itemsModal.items.filter(
(i) => i.status === "ready",
).length;
const totalCount = itemsModal.items.length;
const progress = totalCount > 0 ? readyCount / totalCount : 0;
if (loading) return <LoadingSpinner message="Chargement..." />;
return (
@@ -191,51 +329,138 @@ export default function OrdersScreen() {
<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"
onClose={closeModal}
title={`Commande #${itemsModal.commandId}`}
icon="receipt-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"
<ScrollView
showsVerticalScrollIndicator={false}
bounces={false}
>
{/* Résumé commande / client */}
{(itemsModal.commandInfo || itemsModal.clientInfo) && (
<View style={styles.modalSummary}>
{itemsModal.clientInfo?.username && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="person-outline"
size={14}
color={colors.textMuted}
/>
),
<Text style={styles.modalSummaryText}>
{itemsModal.clientInfo.prenom}{" "}
{itemsModal.clientInfo.nom} ·{" "}
{itemsModal.clientInfo.username}
</Text>
</View>
)}
{(itemsModal.commandInfo?.address || itemsModal.commandInfo?.adresse) && (
<View style={styles.modalSummaryRow}>
<Ionicons
name="location-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.modalSummaryText}>
{itemsModal.commandInfo.address || itemsModal.commandInfo.adresse}
</Text>
</View>
)}
{itemsModal.commandInfo?.total_prix != null && (
<Text style={styles.modalTotal}>
{Number(
itemsModal.commandInfo.total_prix,
).toFixed(2)}{" "}
</Text>
)}
</View>
</View>
))}
{itemsModal.items.length === 0 && (
<Text style={styles.empty}>Aucun item</Text>
)}
)}
{/* Barre de progression */}
{totalCount > 0 && (
<View style={styles.progressContainer}>
<Text style={styles.progressLabel}>
{readyCount}/{totalCount} prêts
</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{
width: `${progress * 100}%`,
backgroundColor:
progress === 1
? STATUS_COLORS.ready
: colors.info,
},
]}
/>
</View>
</View>
)}
{/* Liste des items */}
{itemsModal.items.map((item: any, index: number) => {
const statusColor =
STATUS_COLORS[item.status] || colors.textMuted;
return (
<View key={item.id} style={styles.itemCard}>
<View
style={[
styles.itemCardAccent,
{ backgroundColor: statusColor },
]}
/>
<View style={styles.itemCardBody}>
<View style={styles.itemIndex}>
<Text style={styles.itemIndexText}>
{index + 1}
</Text>
</View>
<View style={styles.itemInfo}>
<Text style={styles.itemName}>
{item.produit ?? item.product_name}
</Text>
<Text style={styles.itemMeta}>
Qté: {item.quantite ?? item.quantity} ·{" "}
{(item.prix ?? item.price)?.toFixed(2)}
</Text>
<View style={styles.itemStatusRow}>
<Ionicons
name={
STATUS_ICONS[
item.status
] || "ellipse-outline"
}
size={13}
color={statusColor}
/>
<Text
style={[
styles.itemStatusText,
{ color: statusColor },
]}
>
{STATUS_LABELS[item.status] ||
item.status}
</Text>
</View>
</View>
</View>
</View>
);
})}
{itemsModal.items.length === 0 && (
<Text style={styles.empty}>Aucun item</Text>
)}
</ScrollView>
</Modal>
<AlertModal
visible={alert.visible}
type={alert.type}