844 lines
31 KiB
TypeScript
844 lines
31 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
RefreshControl,
|
|
ScrollView,
|
|
TextInput,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useNavigation } from "@react-navigation/native";
|
|
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
import {
|
|
getAllCommands,
|
|
getAvailableDeliveryPersons,
|
|
assignDeliveryPerson,
|
|
updateCommandStatus,
|
|
getCommandItems,
|
|
notifyClientToDescend,
|
|
confirmReceptionAdmin,
|
|
deleteCommand,
|
|
deleteCommandItem,
|
|
proposeAddressChangeAdmin,
|
|
} from "../../api/api_admin";
|
|
import type { CommandResponse } from "../../api/types";
|
|
import type { AdminStackParamList } from "../../navigation/types";
|
|
import StatusBadge from "../../components/StatusBadge";
|
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
|
import Modal from "../../components/ui/Modal";
|
|
import Card from "../../components/ui/Card";
|
|
import AlertModal from "../../components/ui/AlertModal";
|
|
import { useAlert } from "../../hooks/useAlert";
|
|
|
|
type Nav = NativeStackNavigationProp<AdminStackParamList>;
|
|
|
|
const STATUS_FILTERS = [
|
|
"all",
|
|
"pending",
|
|
"assigned",
|
|
"en_route",
|
|
"arrived",
|
|
"livre",
|
|
"approved",
|
|
"cancelled",
|
|
];
|
|
|
|
export default function OrdersScreen() {
|
|
const { colors } = useTheme();
|
|
const navigation = useNavigation<Nav>();
|
|
const [commands, setCommands] = useState<CommandResponse[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [filter, setFilter] = useState("all");
|
|
const { alert, showError, showSuccess, showConfirm, hideAlert } =
|
|
useAlert();
|
|
|
|
const [openMenuId, setOpenMenuId] = useState<number | null>(null);
|
|
const [assignModal, setAssignModal] = useState<{
|
|
visible: boolean;
|
|
commandId: number | null;
|
|
}>({ visible: false, commandId: null });
|
|
const [livreurs, setLivreurs] = useState<any[]>([]);
|
|
|
|
const [addressModal, setAddressModal] = useState<{
|
|
visible: boolean;
|
|
commandId: number | null;
|
|
input: string;
|
|
}>({ visible: false, commandId: null, input: "" });
|
|
|
|
const [itemsModal, setItemsModal] = useState<{
|
|
visible: boolean;
|
|
commandId: number | null;
|
|
items: any[];
|
|
commandInfo: any;
|
|
clientInfo: any;
|
|
}>({
|
|
visible: false,
|
|
commandId: null,
|
|
items: [],
|
|
commandInfo: null,
|
|
clientInfo: null,
|
|
});
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const status = filter === "all" ? undefined : filter;
|
|
const result = await getAllCommands(status);
|
|
setCommands(result.commands);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setLoading(false);
|
|
}, [filter]);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await loadData();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const openAssignModal = async (commandId: number) => {
|
|
try {
|
|
const result = await getAvailableDeliveryPersons();
|
|
setLivreurs(result.livreurs);
|
|
setAssignModal({ visible: true, commandId });
|
|
} catch {
|
|
showError("Erreur", "Impossible de charger les livreurs");
|
|
}
|
|
};
|
|
|
|
const handleAssign = async (livreurUsername: string) => {
|
|
if (!assignModal.commandId) return;
|
|
try {
|
|
await assignDeliveryPerson(assignModal.commandId, livreurUsername);
|
|
setAssignModal({ visible: false, commandId: null });
|
|
await loadData();
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleStatusUpdate = async (commandId: number, newStatus: string) => {
|
|
try {
|
|
await updateCommandStatus(commandId, newStatus);
|
|
await loadData();
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const openItems = async (commandId: number) => {
|
|
try {
|
|
const result = await getCommandItems(commandId);
|
|
setItemsModal({
|
|
visible: true,
|
|
commandId,
|
|
items: result.items,
|
|
commandInfo: result.command_info,
|
|
clientInfo: result.client_info,
|
|
});
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const closeItemsModal = () =>
|
|
setItemsModal({
|
|
visible: false,
|
|
commandId: null,
|
|
items: [],
|
|
commandInfo: null,
|
|
clientInfo: null,
|
|
});
|
|
|
|
const handleNotifyClient = async (commandId: number) => {
|
|
try {
|
|
await notifyClientToDescend(commandId);
|
|
showSuccess(
|
|
"Notification envoyée",
|
|
"Le client a été prévenu de descendre",
|
|
);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleConfirmReception = (commandId: number) => {
|
|
showConfirm(
|
|
"Confirmer la commande",
|
|
`Confirmer la réception de la commande #${commandId} ?`,
|
|
async () => {
|
|
try {
|
|
const res = await confirmReceptionAdmin(commandId);
|
|
showSuccess(
|
|
"Réception confirmée",
|
|
`${res.points_earned} point(s) attribués au client ${res.client_username}`,
|
|
);
|
|
await loadData();
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
},
|
|
"Confirmer",
|
|
);
|
|
};
|
|
|
|
const handleProposeAddress = async () => {
|
|
if (!addressModal.commandId || !addressModal.input.trim()) return;
|
|
try {
|
|
await proposeAddressChangeAdmin(
|
|
addressModal.commandId,
|
|
addressModal.input.trim(),
|
|
);
|
|
setAddressModal({ visible: false, commandId: null, input: "" });
|
|
showSuccess(
|
|
"Proposition envoyée",
|
|
"Le client a été notifié de la nouvelle adresse proposée",
|
|
);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleDeleteItem = (
|
|
commandId: number,
|
|
itemId: number,
|
|
itemName: string,
|
|
) => {
|
|
showConfirm(
|
|
"Supprimer l'article",
|
|
`Supprimer "${itemName}" de la commande #${commandId} ?`,
|
|
async () => {
|
|
try {
|
|
await deleteCommandItem(commandId, itemId);
|
|
const result = await getCommandItems(commandId);
|
|
setItemsModal((prev) => ({ ...prev, items: result.items }));
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
},
|
|
"Supprimer",
|
|
);
|
|
};
|
|
|
|
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 },
|
|
filterList: {
|
|
maxHeight: 50,
|
|
paddingHorizontal: spacing.l,
|
|
paddingVertical: spacing.s,
|
|
},
|
|
refreshRow: {
|
|
paddingHorizontal: spacing.l,
|
|
paddingBottom: spacing.s,
|
|
alignItems: "flex-start",
|
|
},
|
|
refreshBtn: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: spacing.xs,
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.s,
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.sm,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
refreshBtnText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
filterBtn: {
|
|
paddingHorizontal: spacing.l,
|
|
paddingVertical: spacing.s,
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.xl,
|
|
marginRight: spacing.s,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
filterActive: {
|
|
backgroundColor: colors.accent,
|
|
borderColor: colors.accent,
|
|
},
|
|
filterText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
filterTextActive: { color: colors.textWhite },
|
|
cardText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
marginTop: 2,
|
|
},
|
|
cardDate: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.xs,
|
|
marginTop: spacing.s,
|
|
},
|
|
orderId: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "bold",
|
|
color: colors.textWhite,
|
|
},
|
|
row: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: spacing.s,
|
|
},
|
|
selectBtn: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginTop: spacing.m,
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.s,
|
|
backgroundColor: colors.bgPrimary,
|
|
borderRadius: borderRadius.sm,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
selectBtnText: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
dropdown: {
|
|
marginTop: 2,
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.sm,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
overflow: "hidden",
|
|
},
|
|
dropdownItem: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: spacing.s,
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.m,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.border,
|
|
},
|
|
dropdownItemLast: { borderBottomWidth: 0 },
|
|
dropdownText: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
dropdownTextDanger: {
|
|
color: colors.danger,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
empty: {
|
|
color: colors.textMuted,
|
|
textAlign: "center",
|
|
marginTop: spacing.xxl,
|
|
fontSize: fontSize.md,
|
|
},
|
|
livreurItem: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
padding: spacing.m,
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.sm,
|
|
marginBottom: spacing.s,
|
|
gap: spacing.m,
|
|
},
|
|
livreurName: { color: colors.textWhite, fontSize: fontSize.md },
|
|
|
|
// Modal items
|
|
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,
|
|
},
|
|
itemCard: {
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.sm,
|
|
marginBottom: spacing.s,
|
|
overflow: "hidden",
|
|
},
|
|
itemCardBody: {
|
|
padding: spacing.m,
|
|
},
|
|
itemName: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "600",
|
|
marginBottom: 2,
|
|
},
|
|
itemMeta: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.xs,
|
|
},
|
|
itemRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
},
|
|
itemDeleteBtn: {
|
|
padding: spacing.xs,
|
|
marginLeft: spacing.s,
|
|
},
|
|
addressInput: {
|
|
backgroundColor: colors.bgPrimary,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
borderRadius: borderRadius.sm,
|
|
color: colors.textWhite,
|
|
paddingHorizontal: spacing.m,
|
|
paddingVertical: spacing.m,
|
|
fontSize: fontSize.md,
|
|
marginBottom: spacing.m,
|
|
},
|
|
addressConfirmBtn: {
|
|
backgroundColor: colors.accent,
|
|
borderRadius: borderRadius.sm,
|
|
paddingVertical: spacing.m,
|
|
alignItems: "center",
|
|
},
|
|
addressConfirmText: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "700",
|
|
},
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
const renderOrder = ({ item }: { item: CommandResponse }) => {
|
|
const isOpen = openMenuId === item.id;
|
|
const isDone = ["approved", "cancelled"].includes(item.status);
|
|
|
|
type Action = {
|
|
label: string;
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
onPress: () => void;
|
|
danger?: boolean;
|
|
condition?: boolean;
|
|
};
|
|
|
|
const actions: Action[] = (
|
|
[
|
|
{
|
|
label: "Voir items",
|
|
icon: "receipt-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
openItems(item.id);
|
|
},
|
|
},
|
|
{
|
|
label: "Proposer adresse",
|
|
icon: "location-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
setAddressModal({
|
|
visible: true,
|
|
commandId: item.id,
|
|
input: "",
|
|
});
|
|
},
|
|
condition: !isDone,
|
|
},
|
|
{
|
|
label: "Le livreur est là",
|
|
icon: "notifications-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
handleNotifyClient(item.id);
|
|
},
|
|
condition: !isDone,
|
|
},
|
|
{
|
|
label: "Assigner livreur",
|
|
icon: "bicycle-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
openAssignModal(item.id);
|
|
},
|
|
condition: !isDone,
|
|
},
|
|
{
|
|
label: "Passer en route",
|
|
icon: "car-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
handleStatusUpdate(item.id, "en_route");
|
|
},
|
|
condition: item.status === "assigned",
|
|
},
|
|
{
|
|
label: "Confirmer réception",
|
|
icon: "checkmark-circle-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
handleConfirmReception(item.id);
|
|
},
|
|
condition: item.status === "livre",
|
|
},
|
|
{
|
|
label: "Supprimer",
|
|
icon: "trash-outline" as keyof typeof Ionicons.glyphMap,
|
|
onPress: () => {
|
|
setOpenMenuId(null);
|
|
handleDelete(item.id);
|
|
},
|
|
danger: true,
|
|
},
|
|
] as Action[]
|
|
).filter((a) => a.condition !== false);
|
|
|
|
return (
|
|
<Card style={{ marginBottom: spacing.m }}>
|
|
<TouchableOpacity
|
|
onPress={() =>
|
|
navigation.navigate("OrderDetail", { orderId: item.id })
|
|
}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.row}>
|
|
<Text style={styles.orderId}>#{item.id}</Text>
|
|
<StatusBadge status={item.status} />
|
|
</View>
|
|
<Text style={styles.cardText}>Client: {item.username}</Text>
|
|
<Text style={styles.cardText}>Adresse: {item.adresse}</Text>
|
|
<Text style={styles.cardText}>
|
|
Total: {item.total_prix.toFixed(2)} €
|
|
</Text>
|
|
{item.livreur_assign && (
|
|
<Text style={styles.cardText}>
|
|
Livreur: {item.livreur_assign}
|
|
</Text>
|
|
)}
|
|
<Text style={styles.cardDate}>
|
|
{new Date(item.created_at).toLocaleString("fr-FR")}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.selectBtn}
|
|
onPress={() => setOpenMenuId(isOpen ? null : item.id)}
|
|
>
|
|
<Text style={styles.selectBtnText}>Actions</Text>
|
|
<Ionicons
|
|
name={isOpen ? "chevron-up" : "chevron-down"}
|
|
size={14}
|
|
color={colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
{isOpen && (
|
|
<View style={styles.dropdown}>
|
|
{actions.map((action, index) => (
|
|
<TouchableOpacity
|
|
key={action.label}
|
|
style={[
|
|
styles.dropdownItem,
|
|
index === actions.length - 1 &&
|
|
styles.dropdownItemLast,
|
|
]}
|
|
onPress={action.onPress}
|
|
>
|
|
<Ionicons
|
|
name={action.icon}
|
|
size={16}
|
|
color={
|
|
action.danger
|
|
? colors.danger
|
|
: colors.accent
|
|
}
|
|
/>
|
|
<Text
|
|
style={
|
|
action.danger
|
|
? styles.dropdownTextDanger
|
|
: styles.dropdownText
|
|
}
|
|
>
|
|
{action.label}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
if (loading) return <LoadingSpinner message="Chargement commandes..." />;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<FlatList
|
|
horizontal
|
|
data={STATUS_FILTERS}
|
|
keyExtractor={(i) => i}
|
|
renderItem={({ item }) => (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.filterBtn,
|
|
filter === item && styles.filterActive,
|
|
]}
|
|
onPress={() => setFilter(item)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.filterText,
|
|
filter === item && styles.filterTextActive,
|
|
]}
|
|
>
|
|
{item === "all" ? "Tous" : item}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
style={styles.filterList}
|
|
showsHorizontalScrollIndicator={false}
|
|
/>
|
|
<View style={styles.refreshRow}>
|
|
<TouchableOpacity
|
|
style={styles.refreshBtn}
|
|
onPress={onRefresh}
|
|
disabled={refreshing}
|
|
>
|
|
<Ionicons
|
|
name="refresh-outline"
|
|
size={16}
|
|
color={refreshing ? colors.textMuted : colors.accent}
|
|
/>
|
|
<Text style={styles.refreshBtnText}>Actualiser</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<FlatList
|
|
data={commands}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
renderItem={renderOrder}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
tintColor={colors.accent}
|
|
/>
|
|
}
|
|
contentContainerStyle={{ padding: spacing.l, paddingTop: 0 }}
|
|
ListEmptyComponent={
|
|
<Text style={styles.empty}>Aucune commande</Text>
|
|
}
|
|
/>
|
|
|
|
|
|
{/* Modal items */}
|
|
<Modal
|
|
visible={itemsModal.visible}
|
|
onClose={closeItemsModal}
|
|
title={`Commande #${itemsModal.commandId}`}
|
|
icon="receipt-outline"
|
|
>
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
bounces={false}
|
|
>
|
|
{(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>
|
|
)}
|
|
{itemsModal.items.map((item: any) => (
|
|
<View key={item.id} style={styles.itemCard}>
|
|
<View style={styles.itemCardBody}>
|
|
<View style={styles.itemRow}>
|
|
<View style={{ flex: 1 }}>
|
|
<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>
|
|
<TouchableOpacity
|
|
style={styles.itemDeleteBtn}
|
|
onPress={() =>
|
|
handleDeleteItem(
|
|
itemsModal.commandId!,
|
|
item.id,
|
|
item.produit ??
|
|
item.product_name,
|
|
)
|
|
}
|
|
>
|
|
<Ionicons
|
|
name="trash-outline"
|
|
size={18}
|
|
color={colors.danger}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{itemsModal.items.length === 0 && (
|
|
<Text style={styles.empty}>Aucun item</Text>
|
|
)}
|
|
</ScrollView>
|
|
</Modal>
|
|
|
|
{/* Modal assignation */}
|
|
<Modal
|
|
visible={assignModal.visible}
|
|
onClose={() =>
|
|
setAssignModal({ visible: false, commandId: null })
|
|
}
|
|
title={`Assigner commande #${assignModal.commandId}`}
|
|
icon="bicycle-outline"
|
|
>
|
|
{livreurs.map((l) => (
|
|
<TouchableOpacity
|
|
key={l.username}
|
|
style={styles.livreurItem}
|
|
onPress={() => handleAssign(l.username)}
|
|
>
|
|
<Ionicons
|
|
name="person-outline"
|
|
size={20}
|
|
color={colors.accent}
|
|
/>
|
|
<Text style={styles.livreurName}>{l.username}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
{livreurs.length === 0 && (
|
|
<Text style={styles.empty}>Aucun livreur disponible</Text>
|
|
)}
|
|
</Modal>
|
|
|
|
{/* Modal proposition adresse */}
|
|
<Modal
|
|
visible={addressModal.visible}
|
|
onClose={() =>
|
|
setAddressModal({
|
|
visible: false,
|
|
commandId: null,
|
|
input: "",
|
|
})
|
|
}
|
|
title={`Proposer adresse — commande #${addressModal.commandId}`}
|
|
icon="location-outline"
|
|
>
|
|
<TextInput
|
|
style={styles.addressInput}
|
|
placeholder="Nouvelle adresse..."
|
|
placeholderTextColor={colors.textMuted}
|
|
value={addressModal.input}
|
|
onChangeText={(t) =>
|
|
setAddressModal((prev) => ({ ...prev, input: t }))
|
|
}
|
|
multiline
|
|
/>
|
|
<TouchableOpacity
|
|
style={styles.addressConfirmBtn}
|
|
onPress={handleProposeAddress}
|
|
>
|
|
<Text style={styles.addressConfirmText}>
|
|
Envoyer la proposition
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</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>
|
|
);
|
|
}
|