chore: add new features for cabine
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user