chore: build
This commit is contained in:
@@ -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 {
|
||||
@@ -24,13 +23,12 @@ import {
|
||||
getCommandItems,
|
||||
updateCommandStatus,
|
||||
validateCommand,
|
||||
confirmReceptionAdmin,
|
||||
notifyClientToDescend,
|
||||
getDeliveryPersonDetails,
|
||||
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 +53,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();
|
||||
@@ -72,7 +71,7 @@ export default function OrderDetailScreen() {
|
||||
getCommandItems(orderId),
|
||||
]);
|
||||
setCommand(cmdRes.command);
|
||||
setItems(itemsRes.items);
|
||||
setItems(itemsRes.items ?? []);
|
||||
|
||||
// If livreur assigned, fetch their location and calc route
|
||||
const cmd = cmdRes.command;
|
||||
@@ -99,13 +98,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 +120,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 +147,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);
|
||||
@@ -205,7 +193,7 @@ export default function OrderDetailScreen() {
|
||||
try {
|
||||
await deleteCommandItem(orderId, itemId);
|
||||
const itemsRes = await getCommandItems(orderId);
|
||||
setItems(itemsRes.items);
|
||||
setItems(itemsRes.items ?? []);
|
||||
const cmdRes = await getCommandByID(orderId);
|
||||
setCommand(cmdRes.command);
|
||||
} catch (e: any) {
|
||||
@@ -217,20 +205,6 @@ export default function OrderDetailScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
@@ -316,39 +290,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,
|
||||
@@ -409,6 +350,81 @@ export default function OrderDetailScreen() {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
|
||||
// Grouped items
|
||||
subItemRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: spacing.xs,
|
||||
paddingLeft: spacing.m,
|
||||
borderLeftWidth: 2,
|
||||
borderLeftColor: colors.border,
|
||||
marginLeft: spacing.xs,
|
||||
marginBottom: 2,
|
||||
},
|
||||
subItemText: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fontSize.sm,
|
||||
flex: 1,
|
||||
},
|
||||
groupTotalRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: spacing.s,
|
||||
paddingTop: spacing.s,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
groupTotalQty: {
|
||||
color: colors.textWhite,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: "700",
|
||||
},
|
||||
groupTotalPrice: {
|
||||
color: colors.accent,
|
||||
fontSize: fontSize.md,
|
||||
fontWeight: "700",
|
||||
},
|
||||
categoryBadge: {
|
||||
fontSize: fontSize.xs,
|
||||
color: colors.accent,
|
||||
fontWeight: "600",
|
||||
marginRight: spacing.s,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
// Category summary
|
||||
categorySummaryCard: {
|
||||
marginTop: spacing.s,
|
||||
},
|
||||
categoryRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: spacing.s,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
categoryName: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: "600",
|
||||
flex: 1,
|
||||
},
|
||||
categoryQty: {
|
||||
color: colors.textWhite,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: "700",
|
||||
marginRight: spacing.l,
|
||||
},
|
||||
categoryTotal: {
|
||||
color: colors.accent,
|
||||
fontSize: fontSize.sm,
|
||||
fontWeight: "700",
|
||||
minWidth: 70,
|
||||
textAlign: "right",
|
||||
},
|
||||
|
||||
// Items modal
|
||||
itemsModalOverlay: {
|
||||
flex: 1,
|
||||
@@ -476,6 +492,30 @@ export default function OrderDetailScreen() {
|
||||
[colors, MAP_HEIGHT],
|
||||
);
|
||||
|
||||
// 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 groupedList = Object.values(productGroups);
|
||||
|
||||
// Récapitulatif par catégorie
|
||||
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;
|
||||
acc[cat].total += item.prix ?? 0;
|
||||
return acc;
|
||||
}, {});
|
||||
const categoryEntries = Object.entries(categoryTotals);
|
||||
|
||||
if (loading) return <LoadingSpinner message="Chargement..." />;
|
||||
if (!command)
|
||||
return (
|
||||
@@ -496,74 +536,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
|
||||
@@ -584,12 +571,30 @@ export default function OrderDetailScreen() {
|
||||
<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)} €
|
||||
Total brut: {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>
|
||||
<>
|
||||
<Text style={[styles.info, { color: colors.success }]}>
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
{command.livreur_assign && (
|
||||
<Text style={styles.info}>
|
||||
@@ -600,29 +605,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>
|
||||
@@ -635,69 +664,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>
|
||||
)}
|
||||
@@ -742,40 +723,101 @@ export default function OrderDetailScreen() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Items */}
|
||||
{/* Items groupés par produit */}
|
||||
<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>
|
||||
))}
|
||||
{groupedList.map((group, gi) => {
|
||||
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 isMultiple = group.length > 1;
|
||||
return (
|
||||
<Card
|
||||
key={`${rep.product_id}-${gi}`}
|
||||
style={{ marginBottom: spacing.s }}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<Text style={[styles.itemName, { flex: 1 }]}>
|
||||
{name}
|
||||
</Text>
|
||||
{rep.category ? (
|
||||
<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}
|
||||
/>
|
||||
</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}`}
|
||||
</Text>
|
||||
<Text style={styles.groupTotalPrice}>
|
||||
{totalPrice.toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Récapitulatif par catégorie */}
|
||||
{categoryEntries.length > 0 && (
|
||||
<>
|
||||
<Text style={styles.sectionTitle}>Par catégorie</Text>
|
||||
<Card style={styles.categorySummaryCard}>
|
||||
{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>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<Text style={styles.sectionTitle}>Actions</Text>
|
||||
@@ -823,15 +865,7 @@ export default function OrderDetailScreen() {
|
||||
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}
|
||||
@@ -865,48 +899,132 @@ export default function OrderDetailScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{items.map((item: any) => (
|
||||
{groupedList.map((group, gi) => {
|
||||
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 isMultiple = group.length > 1;
|
||||
return (
|
||||
<View
|
||||
key={`modal-${rep.product_id}-${gi}`}
|
||||
style={styles.itemsModalCard}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<Text
|
||||
style={[
|
||||
styles.itemName,
|
||||
{ flex: 1 },
|
||||
]}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
{rep.category ? (
|
||||
<Text
|
||||
style={styles.categoryBadge}
|
||||
>
|
||||
{rep.category}
|
||||
</Text>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
style={styles.itemDeleteBtn}
|
||||
onPress={() => {
|
||||
setShowItemsModal(false);
|
||||
handleDeleteItem(
|
||||
rep.id,
|
||||
name,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
<View style={styles.groupTotalRow}>
|
||||
<Text style={styles.groupTotalQty}>
|
||||
{isMultiple
|
||||
? `Total: ${totalQty}${unit}`
|
||||
: `${totalQty}${unit}`}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.groupTotalPrice}
|
||||
>
|
||||
{totalPrice.toFixed(2)} €
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{/* Récapitulatif catégories dans le modal */}
|
||||
{categoryEntries.length > 0 && (
|
||||
<View
|
||||
key={item.id}
|
||||
style={styles.itemsModalCard}
|
||||
style={{
|
||||
marginTop: spacing.m,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
paddingTop: spacing.m,
|
||||
}}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<Text
|
||||
style={[
|
||||
styles.itemName,
|
||||
{ flex: 1 },
|
||||
]}
|
||||
<Text
|
||||
style={[
|
||||
styles.itemsModalTitle,
|
||||
{
|
||||
fontSize: fontSize.md,
|
||||
marginBottom: spacing.s,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Par catégorie
|
||||
</Text>
|
||||
{categoryEntries.map(([cat, data]) => (
|
||||
<View
|
||||
key={cat}
|
||||
style={styles.categoryRow}
|
||||
>
|
||||
{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>
|
||||
<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>
|
||||
))}
|
||||
)}
|
||||
{items.length === 0 && (
|
||||
<Text style={styles.empty}>Aucun article</Text>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user