899 lines
35 KiB
TypeScript
899 lines
35 KiB
TypeScript
import React, { useState, useEffect, useRef, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Modal,
|
|
StatusBar,
|
|
Alert,
|
|
} from "react-native";
|
|
import {
|
|
useRoute,
|
|
useNavigation,
|
|
type RouteProp,
|
|
} from "@react-navigation/native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps";
|
|
import { spacing, fontSize, borderRadius } from "../../theme";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
import {
|
|
getCommandByID,
|
|
getCommandItems,
|
|
updateCommandStatus,
|
|
validateCommand,
|
|
confirmReceptionAdmin,
|
|
notifyClientToDescend,
|
|
getDeliveryPersonDetails,
|
|
deleteCommandItem,
|
|
deleteCommand,
|
|
} from "../../api/api_admin";
|
|
import { geocodeAddress, calculateRoute } from "../../api/tomtom";
|
|
import type { RouteInfo, LatLng } from "../../api/tomtom";
|
|
import type { AdminStackParamList } from "../../navigation/types";
|
|
import StatusBadge from "../../components/StatusBadge";
|
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
|
import Button from "../../components/ui/Button";
|
|
import Card from "../../components/ui/Card";
|
|
import AlertModal from "../../components/ui/AlertModal";
|
|
import { useAlert } from "../../hooks/useAlert";
|
|
|
|
type Route = RouteProp<AdminStackParamList, "OrderDetail">;
|
|
|
|
const MAP_HEIGHT = 240;
|
|
|
|
export default function OrderDetailScreen() {
|
|
const { colors } = useTheme();
|
|
const route = useRoute<Route>();
|
|
const navigation = useNavigation();
|
|
const { orderId } = route.params;
|
|
const [command, setCommand] = useState<any>(null);
|
|
const [items, setItems] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showItemsModal, setShowItemsModal] = useState(false);
|
|
|
|
// Map / tracking
|
|
const mapRef = useRef<MapView | null>(null);
|
|
const fullscreenMapRef = useRef<MapView | null>(null);
|
|
const [mapFullscreen, setMapFullscreen] = useState(false);
|
|
const [livreurCoords, setLivreurCoords] = useState<LatLng | null>(null);
|
|
const [destCoords, setDestCoords] = useState<LatLng | null>(null);
|
|
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
|
|
const [mapLoading, setMapLoading] = useState(false);
|
|
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
try {
|
|
const [cmdRes, itemsRes] = await Promise.all([
|
|
getCommandByID(orderId),
|
|
getCommandItems(orderId),
|
|
]);
|
|
setCommand(cmdRes.command);
|
|
setItems(itemsRes.items);
|
|
|
|
// If livreur assigned, fetch their location and calc route
|
|
const cmd = cmdRes.command;
|
|
if (cmd?.livreur_assign && cmd?.adresse) {
|
|
loadLivreurRoute(cmd.livreur_assign, cmd.adresse);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setLoading(false);
|
|
};
|
|
load();
|
|
}, [orderId]);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const res = await getCommandByID(orderId);
|
|
setCommand(res.command);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, 20000);
|
|
return () => clearInterval(interval);
|
|
}, [orderId]);
|
|
|
|
const loadLivreurRoute = async (
|
|
livreurUsername: string,
|
|
deliveryAddress: string,
|
|
) => {
|
|
setMapLoading(true);
|
|
try {
|
|
// Get livreur location
|
|
const details = await getDeliveryPersonDetails(livreurUsername);
|
|
const loc = details?.location;
|
|
if (!loc?.latitude || !loc?.longitude) {
|
|
setMapLoading(false);
|
|
return;
|
|
}
|
|
|
|
const origin: LatLng = {
|
|
latitude: loc.latitude,
|
|
longitude: loc.longitude,
|
|
};
|
|
setLivreurCoords(origin);
|
|
|
|
// Geocode destination
|
|
const dest = await geocodeAddress(deliveryAddress);
|
|
if (!dest) {
|
|
setMapLoading(false);
|
|
return;
|
|
}
|
|
setDestCoords(dest);
|
|
|
|
// Calculate route
|
|
const result = await calculateRoute(origin, dest);
|
|
if (result) {
|
|
setRouteInfo(result.route);
|
|
}
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
setMapLoading(false);
|
|
};
|
|
|
|
const fitMapToRoute = (ref: React.RefObject<MapView | null>) => {
|
|
if (ref.current && livreurCoords && destCoords) {
|
|
ref.current.fitToCoordinates(
|
|
[
|
|
{
|
|
latitude: livreurCoords.latitude,
|
|
longitude: livreurCoords.longitude,
|
|
},
|
|
{
|
|
latitude: destCoords.latitude,
|
|
longitude: destCoords.longitude,
|
|
},
|
|
],
|
|
{
|
|
edgePadding: { top: 80, right: 60, bottom: 80, left: 60 },
|
|
animated: true,
|
|
},
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleValidate = async () => {
|
|
try {
|
|
await validateCommand(orderId);
|
|
showSuccess("Succès", "Commande validée");
|
|
const res = await getCommandByID(orderId);
|
|
setCommand(res.command);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleStatusChange = async (status: string) => {
|
|
try {
|
|
await updateCommandStatus(orderId, status);
|
|
const res = await getCommandByID(orderId);
|
|
setCommand(res.command);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleNotifyClient = async () => {
|
|
try {
|
|
await notifyClientToDescend(orderId);
|
|
showSuccess(
|
|
"Notification envoyée",
|
|
"Le client a été prévenu de descendre",
|
|
);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleDeleteItem = (itemId: number, itemName: string) => {
|
|
Alert.alert(
|
|
"Supprimer l'article",
|
|
`Supprimer "${itemName}" de la commande ?`,
|
|
[
|
|
{ text: "Annuler", style: "cancel" },
|
|
{
|
|
text: "Supprimer",
|
|
style: "destructive",
|
|
onPress: async () => {
|
|
try {
|
|
await deleteCommandItem(orderId, itemId);
|
|
const itemsRes = await getCommandItems(orderId);
|
|
setItems(itemsRes.items);
|
|
const cmdRes = await getCommandByID(orderId);
|
|
setCommand(cmdRes.command);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
const handleConfirmReception = async () => {
|
|
try {
|
|
const res = await confirmReceptionAdmin(orderId);
|
|
showSuccess(
|
|
"Réception confirmée",
|
|
`${res.points_earned} point(s) attribués au client ${res.client_username}`,
|
|
);
|
|
const updated = await getCommandByID(orderId);
|
|
setCommand(updated.command);
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
};
|
|
|
|
const handleDeleteCommand = () => {
|
|
Alert.alert(
|
|
"Supprimer la commande",
|
|
`Supprimer définitivement la commande #${orderId} ?`,
|
|
[
|
|
{ text: "Annuler", style: "cancel" },
|
|
{
|
|
text: "Supprimer",
|
|
style: "destructive",
|
|
onPress: async () => {
|
|
try {
|
|
await deleteCommand(orderId);
|
|
navigation.goBack();
|
|
} catch (e: any) {
|
|
showError("Erreur", e.message);
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
const styles = useMemo(
|
|
() =>
|
|
StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
|
row: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: spacing.s,
|
|
},
|
|
title: {
|
|
fontSize: fontSize.xl,
|
|
fontWeight: "bold",
|
|
color: colors.textWhite,
|
|
},
|
|
info: {
|
|
color: colors.textSecondary,
|
|
fontSize: fontSize.sm,
|
|
marginTop: 2,
|
|
},
|
|
date: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.xs,
|
|
marginTop: spacing.s,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "600",
|
|
color: colors.textWhite,
|
|
marginTop: spacing.xl,
|
|
marginBottom: spacing.m,
|
|
},
|
|
itemName: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.md,
|
|
fontWeight: "600",
|
|
marginBottom: spacing.xs,
|
|
},
|
|
itemDetails: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
},
|
|
itemDeleteBtn: {
|
|
padding: spacing.xs,
|
|
marginLeft: spacing.s,
|
|
},
|
|
actions: { marginTop: spacing.m, marginBottom: spacing.xxl },
|
|
empty: {
|
|
color: colors.textMuted,
|
|
textAlign: "center",
|
|
marginTop: spacing.xxl,
|
|
fontSize: fontSize.md,
|
|
},
|
|
|
|
// Map
|
|
mapContainer: {
|
|
borderRadius: borderRadius.md,
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
},
|
|
map: { width: "100%", height: MAP_HEIGHT },
|
|
|
|
driverMarkerOuter: {
|
|
width: 34,
|
|
height: 34,
|
|
borderRadius: 17,
|
|
backgroundColor: colors.success + "40",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
driverMarkerInner: {
|
|
width: 26,
|
|
height: 26,
|
|
borderRadius: 13,
|
|
backgroundColor: colors.success,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
destMarkerOuter: {
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 15,
|
|
backgroundColor: colors.danger + "40",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
destMarkerInner: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 11,
|
|
backgroundColor: colors.danger,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
|
|
routeOverlay: {
|
|
position: "absolute",
|
|
top: spacing.s,
|
|
left: spacing.s,
|
|
backgroundColor: "rgba(0,0,0,0.75)",
|
|
borderRadius: borderRadius.sm,
|
|
padding: spacing.s,
|
|
paddingHorizontal: spacing.m,
|
|
},
|
|
routeOverlayUser: {
|
|
color: colors.white,
|
|
fontSize: fontSize.sm,
|
|
fontWeight: "700",
|
|
},
|
|
routeOverlayInfo: {
|
|
color: colors.accent,
|
|
fontSize: fontSize.xs,
|
|
marginTop: 2,
|
|
},
|
|
|
|
mapLoadingOverlay: {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: "rgba(0,0,0,0.3)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
mapLoadingText: {
|
|
color: colors.white,
|
|
fontSize: fontSize.sm,
|
|
fontWeight: "600",
|
|
},
|
|
|
|
expandBtn: {
|
|
position: "absolute",
|
|
top: spacing.s,
|
|
right: spacing.s,
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
backgroundColor: "rgba(0,0,0,0.6)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
|
|
noMapBox: {
|
|
alignItems: "center",
|
|
paddingVertical: spacing.l,
|
|
backgroundColor: colors.bgSecondary,
|
|
borderRadius: borderRadius.md,
|
|
},
|
|
noMapText: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.sm,
|
|
marginTop: spacing.xs,
|
|
},
|
|
|
|
// Items modal
|
|
itemsModalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0,0,0,0.6)",
|
|
justifyContent: "flex-end",
|
|
},
|
|
itemsModalContainer: {
|
|
backgroundColor: colors.bgPrimary,
|
|
borderTopLeftRadius: borderRadius.lg,
|
|
borderTopRightRadius: borderRadius.lg,
|
|
maxHeight: "75%",
|
|
padding: spacing.l,
|
|
},
|
|
itemsModalHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: spacing.m,
|
|
},
|
|
itemsModalTitle: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "700",
|
|
color: colors.textWhite,
|
|
},
|
|
itemsModalCard: {
|
|
backgroundColor: colors.bgCard,
|
|
borderRadius: borderRadius.sm,
|
|
padding: spacing.m,
|
|
marginBottom: spacing.s,
|
|
},
|
|
|
|
// Fullscreen
|
|
fullscreenContainer: {
|
|
flex: 1,
|
|
backgroundColor: colors.bgPrimary,
|
|
},
|
|
fullscreenMap: { ...StyleSheet.absoluteFillObject },
|
|
fullscreenTopBar: {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingTop: 50,
|
|
paddingHorizontal: spacing.l,
|
|
paddingBottom: spacing.m,
|
|
backgroundColor: "rgba(0,0,0,0.5)",
|
|
},
|
|
closeBtn: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
backgroundColor: "rgba(255,255,255,0.15)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
fullscreenTitle: {
|
|
color: colors.white,
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "700",
|
|
},
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
if (loading) return <LoadingSpinner message="Chargement..." />;
|
|
if (!command)
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.empty}>Commande introuvable</Text>
|
|
</View>
|
|
);
|
|
|
|
const hasMap = livreurCoords !== null;
|
|
|
|
return (
|
|
<ScrollView
|
|
style={styles.container}
|
|
contentContainerStyle={{ padding: spacing.l }}
|
|
>
|
|
{/* Fullscreen map modal */}
|
|
<Modal
|
|
visible={mapFullscreen}
|
|
animationType="fade"
|
|
onRequestClose={() => setMapFullscreen(false)}
|
|
statusBarTranslucent
|
|
>
|
|
<StatusBar hidden={mapFullscreen} />
|
|
<View style={styles.fullscreenContainer}>
|
|
{livreurCoords && (
|
|
<MapView
|
|
ref={fullscreenMapRef}
|
|
provider={PROVIDER_DEFAULT}
|
|
style={styles.fullscreenMap}
|
|
initialRegion={{
|
|
latitude: livreurCoords.latitude,
|
|
longitude: livreurCoords.longitude,
|
|
latitudeDelta: 0.02,
|
|
longitudeDelta: 0.02,
|
|
}}
|
|
onMapReady={() => fitMapToRoute(fullscreenMapRef)}
|
|
showsCompass
|
|
showsScale
|
|
>
|
|
<Marker
|
|
coordinate={livreurCoords}
|
|
title={command.livreur_assign}
|
|
>
|
|
<View style={styles.driverMarkerOuter}>
|
|
<View style={styles.driverMarkerInner}>
|
|
<Ionicons
|
|
name="bicycle"
|
|
size={14}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
{destCoords && (
|
|
<Marker
|
|
coordinate={destCoords}
|
|
title="Destination"
|
|
>
|
|
<View style={styles.destMarkerOuter}>
|
|
<View style={styles.destMarkerInner}>
|
|
<Ionicons
|
|
name="flag"
|
|
size={12}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
)}
|
|
{routeInfo && routeInfo.coordinates.length > 0 && (
|
|
<Polyline
|
|
coordinates={routeInfo.coordinates}
|
|
strokeColor="#4285F4"
|
|
strokeWidth={5}
|
|
/>
|
|
)}
|
|
</MapView>
|
|
)}
|
|
<View style={styles.fullscreenTopBar}>
|
|
<TouchableOpacity
|
|
style={styles.closeBtn}
|
|
onPress={() => setMapFullscreen(false)}
|
|
>
|
|
<Ionicons
|
|
name="close"
|
|
size={24}
|
|
color={colors.white}
|
|
/>
|
|
</TouchableOpacity>
|
|
<Text style={styles.fullscreenTitle}>
|
|
{routeInfo
|
|
? `${routeInfo.distance} · ${routeInfo.duration}`
|
|
: `Livreur: ${command.livreur_assign}`}
|
|
</Text>
|
|
<View style={{ width: 40 }} />
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
{/* Order info */}
|
|
<Card>
|
|
<View style={styles.row}>
|
|
<Text style={styles.title}>Commande #{command.id}</Text>
|
|
<StatusBadge status={command.status} />
|
|
</View>
|
|
<Text style={styles.info}>Client: {command.username}</Text>
|
|
<Text style={styles.info}>Adresse: {command.adresse}</Text>
|
|
<Text style={styles.info}>
|
|
Total: {command.total_prix?.toFixed(2)} €
|
|
</Text>
|
|
{command.referral_used > 0 && (
|
|
<Text style={[styles.info, { color: colors.success }]}>
|
|
Parrainage utilisé: -{command.referral_used?.toFixed(2)} €
|
|
</Text>
|
|
)}
|
|
{command.livreur_assign && (
|
|
<Text style={styles.info}>
|
|
Livreur: {command.livreur_assign}
|
|
</Text>
|
|
)}
|
|
<Text style={styles.date}>
|
|
{new Date(command.created_at).toLocaleString("fr-FR")}
|
|
</Text>
|
|
</Card>
|
|
|
|
{/* Livreur tracking map */}
|
|
{command.livreur_assign && (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Suivi du livreur</Text>
|
|
{hasMap ? (
|
|
<View style={styles.mapContainer}>
|
|
<MapView
|
|
ref={mapRef}
|
|
provider={PROVIDER_DEFAULT}
|
|
style={styles.map}
|
|
initialRegion={{
|
|
latitude: livreurCoords!.latitude,
|
|
longitude: livreurCoords!.longitude,
|
|
latitudeDelta: 0.02,
|
|
longitudeDelta: 0.02,
|
|
}}
|
|
onMapReady={() => fitMapToRoute(mapRef)}
|
|
>
|
|
<Marker
|
|
coordinate={livreurCoords!}
|
|
title={command.livreur_assign}
|
|
>
|
|
<View style={styles.driverMarkerOuter}>
|
|
<View style={styles.driverMarkerInner}>
|
|
<Ionicons
|
|
name="bicycle"
|
|
size={14}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
{destCoords && (
|
|
<Marker
|
|
coordinate={destCoords}
|
|
title="Destination"
|
|
>
|
|
<View style={styles.destMarkerOuter}>
|
|
<View
|
|
style={styles.destMarkerInner}
|
|
>
|
|
<Ionicons
|
|
name="flag"
|
|
size={12}
|
|
color={colors.white}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Marker>
|
|
)}
|
|
{routeInfo &&
|
|
routeInfo.coordinates.length > 0 && (
|
|
<Polyline
|
|
coordinates={routeInfo.coordinates}
|
|
strokeColor="#4285F4"
|
|
strokeWidth={4}
|
|
/>
|
|
)}
|
|
</MapView>
|
|
|
|
{/* Route info overlay */}
|
|
{routeInfo && (
|
|
<View style={styles.routeOverlay}>
|
|
<Text style={styles.routeOverlayUser}>
|
|
{command.livreur_assign}
|
|
</Text>
|
|
<Text style={styles.routeOverlayInfo}>
|
|
{routeInfo.distance} ·{" "}
|
|
{routeInfo.duration}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{mapLoading && (
|
|
<View style={styles.mapLoadingOverlay}>
|
|
<Text style={styles.mapLoadingText}>
|
|
Chargement...
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
style={styles.expandBtn}
|
|
onPress={() => setMapFullscreen(true)}
|
|
>
|
|
<Ionicons
|
|
name="expand-outline"
|
|
size={18}
|
|
color={colors.white}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : mapLoading ? (
|
|
<View style={styles.noMapBox}>
|
|
<Text style={styles.noMapText}>
|
|
Chargement position du livreur...
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<View style={styles.noMapBox}>
|
|
<Ionicons
|
|
name="location-outline"
|
|
size={28}
|
|
color={colors.textMuted}
|
|
/>
|
|
<Text style={styles.noMapText}>
|
|
Position GPS du livreur non disponible
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Items */}
|
|
<Text style={styles.sectionTitle}>Articles ({items.length})</Text>
|
|
{items.map((item: any) => (
|
|
<Card key={item.id} style={{ marginBottom: spacing.s }}>
|
|
<View style={styles.row}>
|
|
<Text style={[styles.itemName, { flex: 1 }]}>
|
|
{item.produit ?? item.product_name}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.itemDeleteBtn}
|
|
onPress={() =>
|
|
handleDeleteItem(
|
|
item.id,
|
|
item.produit ?? item.product_name,
|
|
)
|
|
}
|
|
>
|
|
<Ionicons
|
|
name="trash-outline"
|
|
size={18}
|
|
color={colors.danger}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View style={styles.itemDetails}>
|
|
<Text style={styles.info}>
|
|
Quantité: {item.quantite}
|
|
</Text>
|
|
<Text style={styles.info}>
|
|
Prix: {item.prix?.toFixed(2)} €
|
|
</Text>
|
|
</View>
|
|
</Card>
|
|
))}
|
|
|
|
{/* Actions */}
|
|
<Text style={styles.sectionTitle}>Actions</Text>
|
|
<View style={styles.actions}>
|
|
<Button
|
|
title="Voir les items"
|
|
onPress={() => setShowItemsModal(true)}
|
|
variant="secondary"
|
|
fullWidth
|
|
/>
|
|
{command.status !== "approved" &&
|
|
command.status !== "cancelled" && (
|
|
<Button
|
|
title="Valider commande"
|
|
onPress={handleValidate}
|
|
variant="success"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
)}
|
|
{command.status === "pending" && (
|
|
<Button
|
|
title="Passer en assignée"
|
|
onPress={() => handleStatusChange("assigned")}
|
|
variant="secondary"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
)}
|
|
{command.status === "assigned" && (
|
|
<Button
|
|
title="Passer en route"
|
|
onPress={() => handleStatusChange("en_route")}
|
|
variant="secondary"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
)}
|
|
{!["approved", "cancelled"].includes(command.status) && (
|
|
<Button
|
|
title="Le livreur est là"
|
|
onPress={handleNotifyClient}
|
|
variant="warning"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
)}
|
|
{!["approved", "cancelled"].includes(command.status) && (
|
|
<Button
|
|
title="Confirmer la commande"
|
|
onPress={handleConfirmReception}
|
|
variant="primary"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
)}
|
|
<Button
|
|
title="Supprimer la commande"
|
|
onPress={handleDeleteCommand}
|
|
variant="danger"
|
|
fullWidth
|
|
style={{ marginTop: spacing.s }}
|
|
/>
|
|
</View>
|
|
{/* Items modal */}
|
|
<Modal
|
|
visible={showItemsModal}
|
|
animationType="slide"
|
|
onRequestClose={() => setShowItemsModal(false)}
|
|
transparent
|
|
>
|
|
<View style={styles.itemsModalOverlay}>
|
|
<View style={styles.itemsModalContainer}>
|
|
<View style={styles.itemsModalHeader}>
|
|
<Text style={styles.itemsModalTitle}>
|
|
Articles ({items.length})
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.closeBtn}
|
|
onPress={() => setShowItemsModal(false)}
|
|
>
|
|
<Ionicons
|
|
name="close"
|
|
size={22}
|
|
color={colors.textWhite}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
{items.map((item: any) => (
|
|
<View
|
|
key={item.id}
|
|
style={styles.itemsModalCard}
|
|
>
|
|
<View style={styles.row}>
|
|
<Text
|
|
style={[
|
|
styles.itemName,
|
|
{ flex: 1 },
|
|
]}
|
|
>
|
|
{item.produit ?? item.product_name}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.itemDeleteBtn}
|
|
onPress={() => {
|
|
setShowItemsModal(false);
|
|
handleDeleteItem(
|
|
item.id,
|
|
item.produit ??
|
|
item.product_name,
|
|
);
|
|
}}
|
|
>
|
|
<Ionicons
|
|
name="trash-outline"
|
|
size={18}
|
|
color={colors.danger}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<View style={styles.itemDetails}>
|
|
<Text style={styles.info}>
|
|
Quantité: {item.quantite}
|
|
</Text>
|
|
<Text style={styles.info}>
|
|
Prix: {item.prix?.toFixed(2)} €
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{items.length === 0 && (
|
|
<Text style={styles.empty}>Aucun article</Text>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<AlertModal
|
|
visible={alert.visible}
|
|
type={alert.type}
|
|
title={alert.title}
|
|
message={alert.message}
|
|
onClose={hideAlert}
|
|
/>
|
|
</ScrollView>
|
|
);
|
|
}
|