chore: build

This commit is contained in:
2026-06-13 21:55:21 +02:00
parent c0ff6dcd1d
commit 83075e30d6
@@ -6,7 +6,6 @@ import {
ScrollView,
TouchableOpacity,
Modal,
StatusBar,
Alert,
useWindowDimensions,
} from "react-native";
@@ -16,7 +15,7 @@ import {
type RouteProp,
} from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons";
import MapView, { Marker, Polyline, PROVIDER_DEFAULT } from "react-native-maps";
import TomTomMap, { type TomTomMapRef, type TomTomMarker } from "../../components/TomTomMap";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import {
@@ -30,7 +29,7 @@ import {
deleteCommandItem,
deleteCommand,
} from "../../api/api_admin";
import { geocodeAddress, calculateRoute } from "../../api/tomtom";
import { calculateRoute, geocodeAddress } from "../../api/tomtom";
import type { RouteInfo, LatLng } from "../../api/tomtom";
import type { AdminStackParamList } from "../../navigation/types";
import StatusBadge from "../../components/StatusBadge";
@@ -55,11 +54,12 @@ export default function OrderDetailScreen() {
const [showItemsModal, setShowItemsModal] = useState(false);
// Map / tracking
const mapRef = useRef<MapView | null>(null);
const fullscreenMapRef = useRef<MapView | null>(null);
const mapRef = useRef<TomTomMapRef | null>(null);
const fullscreenMapRef = useRef<TomTomMapRef | null>(null);
const [mapFullscreen, setMapFullscreen] = useState(false);
const [livreurCoords, setLivreurCoords] = useState<LatLng | null>(null);
const [destCoords, setDestCoords] = useState<LatLng | null>(null);
const [livreurMarkers, setLivreurMarkers] = useState<TomTomMarker[]>([]);
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
const [mapLoading, setMapLoading] = useState(false);
const { alert, showError, showSuccess, hideAlert } = useAlert();
@@ -99,13 +99,21 @@ export default function OrderDetailScreen() {
return () => clearInterval(interval);
}, [orderId]);
// Rejoue la route sur la carte fullscreen quand elle s'ouvre
useEffect(() => {
if (mapFullscreen && livreurCoords && destCoords) {
setTimeout(() => {
fullscreenMapRef.current?.calcRoute(livreurCoords, destCoords);
}, 600);
}
}, [mapFullscreen]);
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) {
@@ -113,24 +121,26 @@ export default function OrderDetailScreen() {
return;
}
const origin: LatLng = {
const origin: LatLng = { latitude: loc.latitude, longitude: loc.longitude };
setLivreurCoords(origin);
setLivreurMarkers([{
id: livreurUsername,
latitude: loc.latitude,
longitude: loc.longitude,
};
setLivreurCoords(origin);
color: "#22c55e",
label: livreurUsername,
description: "Livreur",
}]);
// Geocode destination
// Dessine la route sur la carte et récupère les infos (distance/durée)
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);
if (dest) {
setDestCoords(dest);
const result = await calculateRoute(origin, dest);
if (result) {
setRouteInfo(result.route);
mapRef.current?.calcRoute(origin, dest);
}
}
} catch {
/* silent */
@@ -138,27 +148,6 @@ export default function OrderDetailScreen() {
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);
@@ -316,39 +305,6 @@ export default function OrderDetailScreen() {
},
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,
@@ -552,16 +508,21 @@ export default function OrderDetailScreen() {
);
// Groupement des items par product_id
const productGroups = (items ?? []).reduce<Record<string, any[]>>((acc, item) => {
const key = String(item.product_id || item.produit);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
const productGroups = (items ?? []).reduce<Record<string, any[]>>(
(acc, item) => {
const key = String(item.product_id || item.produit);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
},
{},
);
const groupedList = Object.values(productGroups);
// Récapitulatif par catégorie
const categoryTotals = (items ?? []).reduce<Record<string, { qty: number; total: number }>>((acc, item) => {
const categoryTotals = (items ?? []).reduce<
Record<string, { qty: number; total: number }>
>((acc, item) => {
const cat = item.category || "Autre";
if (!acc[cat]) acc[cat] = { qty: 0, total: 0 };
acc[cat].qty += item.quantite ?? 0;
@@ -590,74 +551,21 @@ export default function OrderDetailScreen() {
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>
)}
<TomTomMap
ref={fullscreenMapRef}
style={styles.fullscreenMap}
markers={livreurMarkers}
initialCenter={livreurCoords ?? undefined}
initialZoom={14}
/>
<View style={styles.fullscreenTopBar}>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setMapFullscreen(false)}
>
<Ionicons
name="close"
size={24}
color={colors.white}
/>
<Ionicons name="close" size={24} color={colors.white} />
</TouchableOpacity>
<Text style={styles.fullscreenTitle}>
{routeInfo
@@ -683,10 +591,23 @@ export default function OrderDetailScreen() {
{command.referral_used > 0 && (
<>
<Text style={[styles.info, { color: colors.success }]}>
Parrainage utilisé: -{command.referral_used?.toFixed(2)}
Parrainage utilisé: -
{command.referral_used?.toFixed(2)}
</Text>
<Text style={[styles.info, { fontWeight: "700", color: colors.textPrimary }]}>
Net: {(command.total_prix - command.referral_used).toFixed(2)}
<Text
style={[
styles.info,
{
fontWeight: "700",
color: colors.textPrimary,
},
]}
>
Net:{" "}
{(
command.total_prix - command.referral_used
).toFixed(2)}{" "}
</Text>
</>
)}
@@ -699,29 +620,53 @@ export default function OrderDetailScreen() {
{new Date(command.created_at).toLocaleString("fr-FR")}
</Text>
{command.status === "cancelled" && command.cancel_reason ? (
<View style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.danger + "18",
borderRadius: borderRadius.sm,
borderLeftWidth: 3,
borderLeftColor: colors.danger,
}}>
<Text style={{ color: colors.danger, fontSize: fontSize.xs, fontWeight: "700", marginBottom: 4, textTransform: "uppercase", letterSpacing: 0.5 }}>
<View
style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.danger + "18",
borderRadius: borderRadius.sm,
borderLeftWidth: 3,
borderLeftColor: colors.danger,
}}
>
<Text
style={{
color: colors.danger,
fontSize: fontSize.xs,
fontWeight: "700",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: 0.5,
}}
>
Motif d'annulation
</Text>
<Text style={{ color: colors.textSecondary, fontSize: fontSize.sm }}>
<Text
style={{
color: colors.textSecondary,
fontSize: fontSize.sm,
}}
>
{command.cancel_reason}
</Text>
</View>
) : command.status === "cancelled" ? (
<View style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.sm,
}}>
<Text style={{ color: colors.textMuted, fontSize: fontSize.sm, fontStyle: "italic" }}>
<View
style={{
marginTop: spacing.m,
padding: spacing.m,
backgroundColor: colors.bgSecondary,
borderRadius: borderRadius.sm,
}}
>
<Text
style={{
color: colors.textMuted,
fontSize: fontSize.sm,
fontStyle: "italic",
}}
>
Aucun motif fourni
</Text>
</View>
@@ -734,69 +679,21 @@ export default function OrderDetailScreen() {
<Text style={styles.sectionTitle}>Suivi du livreur</Text>
{hasMap ? (
<View style={styles.mapContainer}>
<MapView
<TomTomMap
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>
markers={livreurMarkers}
initialCenter={livreurCoords ?? undefined}
initialZoom={14}
/>
{/* Route info overlay */}
{routeInfo && (
<View style={styles.routeOverlay}>
<Text style={styles.routeOverlayUser}>
{command.livreur_assign}
</Text>
<Text style={styles.routeOverlayInfo}>
{routeInfo.distance} ·{" "}
{routeInfo.duration}
{routeInfo.distance} · {routeInfo.duration}
</Text>
</View>
)}
@@ -847,40 +744,69 @@ export default function OrderDetailScreen() {
const rep = group[0];
const name = rep.produit ?? rep.product_name;
const unit = rep.unit || "";
const totalQty = group.reduce((s: number, it: any) => s + (it.quantite ?? 0), 0);
const totalPrice = group.reduce((s: number, it: any) => s + (it.prix ?? 0), 0);
const totalQty = group.reduce(
(s: number, it: any) => s + (it.quantite ?? 0),
0,
);
const totalPrice = group.reduce(
(s: number, it: any) => s + (it.prix ?? 0),
0,
);
const isMultiple = group.length > 1;
return (
<Card key={`${rep.product_id}-${gi}`} style={{ marginBottom: spacing.s }}>
<Card
key={`${rep.product_id}-${gi}`}
style={{ marginBottom: spacing.s }}
>
<View style={styles.row}>
<Text style={[styles.itemName, { flex: 1 }]}>{name}</Text>
<Text style={[styles.itemName, { flex: 1 }]}>
{name}
</Text>
{rep.category ? (
<Text style={styles.categoryBadge}>{rep.category}</Text>
<Text style={styles.categoryBadge}>
{rep.category}
</Text>
) : null}
<TouchableOpacity
style={styles.itemDeleteBtn}
onPress={() => handleDeleteItem(rep.id, name)}
>
<Ionicons name="trash-outline" size={18} color={colors.danger} />
<Ionicons
name="trash-outline"
size={18}
color={colors.danger}
/>
</TouchableOpacity>
</View>
{isMultiple && group.map((item: any, i: number) => (
<View key={item.id} style={styles.subItemRow}>
<Text style={styles.subItemText}>
{item.quantite}{unit} {item.prix?.toFixed(2)}
</Text>
<TouchableOpacity
onPress={() => handleDeleteItem(item.id, name)}
>
<Ionicons name="remove-circle-outline" size={16} color={colors.danger} />
</TouchableOpacity>
</View>
))}
{isMultiple &&
group.map((item: any, i: number) => (
<View key={item.id} style={styles.subItemRow}>
<Text style={styles.subItemText}>
{item.quantite}
{unit} {item.prix?.toFixed(2)}
</Text>
<TouchableOpacity
onPress={() =>
handleDeleteItem(item.id, name)
}
>
<Ionicons
name="remove-circle-outline"
size={16}
color={colors.danger}
/>
</TouchableOpacity>
</View>
))}
<View style={styles.groupTotalRow}>
<Text style={styles.groupTotalQty}>
{isMultiple ? `Total: ${totalQty}${unit}` : `${totalQty}${unit}`}
{isMultiple
? `Total: ${totalQty}${unit}`
: `${totalQty}${unit}`}
</Text>
<Text style={styles.groupTotalPrice}>
{totalPrice.toFixed(2)}
</Text>
<Text style={styles.groupTotalPrice}>{totalPrice.toFixed(2)} </Text>
</View>
</Card>
);
@@ -894,8 +820,14 @@ export default function OrderDetailScreen() {
{categoryEntries.map(([cat, data]) => (
<View key={cat} style={styles.categoryRow}>
<Text style={styles.categoryName}>{cat}</Text>
<Text style={styles.categoryQty}>{data.qty.toFixed(data.qty % 1 === 0 ? 0 : 2)}</Text>
<Text style={styles.categoryTotal}>{data.total.toFixed(2)} </Text>
<Text style={styles.categoryQty}>
{data.qty.toFixed(
data.qty % 1 === 0 ? 0 : 2,
)}
</Text>
<Text style={styles.categoryTotal}>
{data.total.toFixed(2)}
</Text>
</View>
))}
</Card>
@@ -994,53 +926,124 @@ export default function OrderDetailScreen() {
const rep = group[0];
const name = rep.produit ?? rep.product_name;
const unit = rep.unit || "";
const totalQty = group.reduce((s: number, it: any) => s + (it.quantite ?? 0), 0);
const totalPrice = group.reduce((s: number, it: any) => s + (it.prix ?? 0), 0);
const totalQty = group.reduce(
(s: number, it: any) =>
s + (it.quantite ?? 0),
0,
);
const totalPrice = group.reduce(
(s: number, it: any) => s + (it.prix ?? 0),
0,
);
const isMultiple = group.length > 1;
return (
<View key={`modal-${rep.product_id}-${gi}`} style={styles.itemsModalCard}>
<View
key={`modal-${rep.product_id}-${gi}`}
style={styles.itemsModalCard}
>
<View style={styles.row}>
<Text style={[styles.itemName, { flex: 1 }]}>{name}</Text>
<Text
style={[
styles.itemName,
{ flex: 1 },
]}
>
{name}
</Text>
{rep.category ? (
<Text style={styles.categoryBadge}>{rep.category}</Text>
<Text
style={styles.categoryBadge}
>
{rep.category}
</Text>
) : null}
<TouchableOpacity
style={styles.itemDeleteBtn}
onPress={() => {
setShowItemsModal(false);
handleDeleteItem(rep.id, name);
handleDeleteItem(
rep.id,
name,
);
}}
>
<Ionicons name="trash-outline" size={18} color={colors.danger} />
<Ionicons
name="trash-outline"
size={18}
color={colors.danger}
/>
</TouchableOpacity>
</View>
{isMultiple && group.map((item: any) => (
<View key={item.id} style={styles.subItemRow}>
<Text style={styles.subItemText}>
{item.quantite}{unit} {item.prix?.toFixed(2)}
</Text>
</View>
))}
{isMultiple &&
group.map((item: any) => (
<View
key={item.id}
style={styles.subItemRow}
>
<Text
style={
styles.subItemText
}
>
{item.quantite}
{unit} {" "}
{item.prix?.toFixed(2)}{" "}
</Text>
</View>
))}
<View style={styles.groupTotalRow}>
<Text style={styles.groupTotalQty}>
{isMultiple ? `Total: ${totalQty}${unit}` : `${totalQty}${unit}`}
{isMultiple
? `Total: ${totalQty}${unit}`
: `${totalQty}${unit}`}
</Text>
<Text
style={styles.groupTotalPrice}
>
{totalPrice.toFixed(2)}
</Text>
<Text style={styles.groupTotalPrice}>{totalPrice.toFixed(2)} </Text>
</View>
</View>
);
})}
{/* Récapitulatif catégories dans le modal */}
{categoryEntries.length > 0 && (
<View style={{ marginTop: spacing.m, borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.m }}>
<Text style={[styles.itemsModalTitle, { fontSize: fontSize.md, marginBottom: spacing.s }]}>
<View
style={{
marginTop: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
paddingTop: spacing.m,
}}
>
<Text
style={[
styles.itemsModalTitle,
{
fontSize: fontSize.md,
marginBottom: spacing.s,
},
]}
>
Par catégorie
</Text>
{categoryEntries.map(([cat, data]) => (
<View key={cat} style={styles.categoryRow}>
<Text style={styles.categoryName}>{cat}</Text>
<Text style={styles.categoryQty}>{data.qty.toFixed(data.qty % 1 === 0 ? 0 : 2)}</Text>
<Text style={styles.categoryTotal}>{data.total.toFixed(2)} </Text>
<View
key={cat}
style={styles.categoryRow}
>
<Text style={styles.categoryName}>
{cat}
</Text>
<Text style={styles.categoryQty}>
{data.qty.toFixed(
data.qty % 1 === 0 ? 0 : 2,
)}
</Text>
<Text style={styles.categoryTotal}>
{data.total.toFixed(2)}
</Text>
</View>
))}
</View>