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