Files
projet_gestion_commande/mobile/src/screens/client/OrderDetailsScreen.tsx
T
2026-03-28 17:39:45 +01:00

535 lines
22 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
import { View, Text, ScrollView, Image, StyleSheet } from "react-native";
import { useRoute } from "@react-navigation/native";
import type { RouteProp } from "@react-navigation/native";
import { Ionicons } from "@expo/vector-icons";
import {
getCommandItemsWithDetails,
getProductById,
formatOrderDate,
formatPrice,
} from "../../api/api";
import type { ClientStackParamList } from "../../navigation/types";
import StatusBadge from "../../components/StatusBadge";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import Card from "../../components/ui/Card";
import { useTheme } from "../../context/ThemeContext";
import { spacing, borderRadius, fontSize, fontWeight } from "../../theme";
import { API_BASE_URL } from "../../api/client";
type Route = RouteProp<ClientStackParamList, "OrderDetails">;
const TIMELINE_STEPS = [
{
key: "pending",
label: "Confirmee",
icon: "checkmark-circle-outline" as const,
},
{ key: "assigned", label: "Assignee", icon: "person-outline" as const },
{ key: "en_route", label: "En route", icon: "bicycle-outline" as const },
{ key: "arrived", label: "Arrivee", icon: "flag-outline" as const },
{ key: "livre", label: "Livree", icon: "cube-outline" as const },
{
key: "approved",
label: "Terminee",
icon: "shield-checkmark-outline" as const,
},
];
const STATUS_INDEX: Record<string, number> = {
pending: 0,
assigned: 1,
en_route: 2,
arrived: 3,
livre: 4,
delivered: 4,
approved: 5,
cancelled: -1,
};
export default function OrderDetailsScreen() {
const { params } = useRoute<Route>();
const { colors } = useTheme();
const [order, setOrder] = useState<any>(null);
const [products, setProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
try {
const res = await getCommandItemsWithDetails(params.orderId);
if (res.success && res.data) {
const data = res.data;
const cmd = data.command || data.command_info || data;
if (cmd.command_status && !cmd.status)
cmd.status = cmd.command_status;
if (cmd.command_address && !cmd.delivery_address)
cmd.delivery_address = cmd.command_address;
if (data.client_info) {
cmd.client_nom = cmd.client_nom || data.client_info.nom;
cmd.client_prenom =
cmd.client_prenom || data.client_info.prenom;
cmd.client_telephone =
cmd.client_telephone || data.client_info.telephone;
}
setOrder(cmd);
const items = data.items || data.products || [];
const enriched = await Promise.all(
items.map(async (item: any) => {
try {
const pRes = await getProductById(
item.product_id || item.id,
);
const p = pRes?.data || pRes?.product || pRes;
const img = p?.media?.find(
(m: any) => m.type === "image",
);
return {
...item,
imageUri: img
? `${API_BASE_URL}${img.url}`
: undefined,
};
} catch {
return item;
}
}),
);
setProducts(enriched);
} else {
setError("Commande introuvable");
}
} catch {
setError("Erreur de chargement");
} finally {
setLoading(false);
}
})();
}, [params.orderId]);
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
content: { padding: spacing.l, paddingBottom: spacing.xxxl },
center: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: colors.bgPrimary,
},
errorText: { color: colors.danger, fontSize: fontSize.md },
headerCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.xl,
marginBottom: spacing.l,
borderWidth: 1,
borderColor: colors.borderLight,
},
headerRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.s,
},
title: {
color: colors.textWhite,
fontSize: fontSize.xl,
fontWeight: fontWeight.bold,
},
date: { color: colors.textMuted, fontSize: fontSize.sm },
sectionTitle: {
color: colors.textSecondary,
fontSize: fontSize.sm,
fontWeight: fontWeight.medium,
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: spacing.l,
},
timeline: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
timelineStep: {
alignItems: "center",
flex: 1,
position: "relative",
},
timelineDot: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.bgInput,
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: colors.border,
zIndex: 1,
},
timelineDotCompleted: {
backgroundColor: colors.success,
borderColor: colors.successDark,
},
timelineDotCurrent: {
backgroundColor: colors.accent,
borderColor: colors.accentLight,
shadowColor: colors.accent,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.5,
shadowRadius: 8,
elevation: 6,
},
timelineLabel: {
color: colors.textMuted,
fontSize: 10,
marginTop: spacing.xs,
textAlign: "center",
},
timelineLabelCompleted: {
color: colors.success,
fontWeight: fontWeight.medium,
},
timelineLabelCurrent: {
color: colors.accent,
fontWeight: fontWeight.bold,
},
timelineLine: {
position: "absolute",
top: 15,
left: "58%",
right: "-42%",
height: 2,
backgroundColor: colors.border,
zIndex: 0,
},
timelineLineCompleted: { backgroundColor: colors.success },
timelineLineCurrent: { backgroundColor: colors.accent },
currentBadge: { marginTop: 4, alignItems: "center" },
currentDot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: colors.accent,
},
cancelledBanner: {
alignItems: "center",
gap: spacing.m,
paddingVertical: spacing.l,
},
cancelledIconCircle: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: "rgba(239,68,68,0.12)",
justifyContent: "center",
alignItems: "center",
},
cancelledText: {
color: colors.danger,
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
},
infoRow: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
marginBottom: spacing.m,
},
infoText: {
color: colors.textPrimary,
fontSize: fontSize.md,
flex: 1,
},
productRow: {
flexDirection: "row",
alignItems: "center",
paddingVertical: spacing.m,
},
productRowBorder: {
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
productImage: {
width: 50,
height: 50,
borderRadius: borderRadius.sm,
},
productImagePlaceholder: {
width: 50,
height: 50,
borderRadius: borderRadius.sm,
backgroundColor: colors.bgInput,
justifyContent: "center",
alignItems: "center",
},
productInfo: { flex: 1, marginLeft: spacing.m },
productName: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: fontWeight.medium,
},
productQty: {
color: colors.textMuted,
fontSize: fontSize.sm,
marginTop: 2,
},
productPrice: {
color: colors.success,
fontSize: fontSize.md,
fontWeight: fontWeight.bold,
},
totalRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
totalLabel: {
color: colors.textSecondary,
fontSize: fontSize.lg,
fontWeight: fontWeight.semibold,
},
totalValue: {
color: colors.success,
fontSize: fontSize.xxl,
fontWeight: fontWeight.bold,
},
}),
[colors],
);
if (loading) return <LoadingSpinner message="Chargement..." />;
if (error || !order) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>
{error || "Commande introuvable"}
</Text>
</View>
);
}
const status = order.status || "pending";
const currentIdx = STATUS_INDEX[status] ?? -1;
const isCancelled = status === "cancelled";
const address = order.delivery_address || order.adresse || "N/A";
const total =
order.total ||
order.total_prix ||
products.reduce((s: number, p: any) => s + (p.prix || p.price || 0), 0);
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
>
<View style={styles.headerCard}>
<View style={styles.headerRow}>
<Text style={styles.title}>Commande #{order.client_order_number}</Text>
<StatusBadge status={status} />
</View>
<Text style={styles.date}>
{formatOrderDate(order.created_at)}
</Text>
</View>
<Card style={{ marginBottom: spacing.l }}>
<Text style={styles.sectionTitle}>Suivi</Text>
{isCancelled ? (
<View style={styles.cancelledBanner}>
<View style={styles.cancelledIconCircle}>
<Ionicons
name="close-circle"
size={28}
color={colors.danger}
/>
</View>
<Text style={styles.cancelledText}>
Commande annulee
</Text>
</View>
) : (
<View style={styles.timeline}>
{TIMELINE_STEPS.map((step, idx) => {
const isFinished =
currentIdx >= TIMELINE_STEPS.length - 1;
const completed = isFinished
? true
: idx < currentIdx;
const current = isFinished
? false
: idx === currentIdx;
const lineCompleted = isFinished
? true
: idx < currentIdx;
return (
<View
key={step.key}
style={styles.timelineStep}
>
{idx < TIMELINE_STEPS.length - 1 && (
<View
style={[
styles.timelineLine,
lineCompleted &&
styles.timelineLineCompleted,
current &&
styles.timelineLineCurrent,
]}
/>
)}
<View
style={[
styles.timelineDot,
completed &&
styles.timelineDotCompleted,
current &&
styles.timelineDotCurrent,
]}
>
{completed ? (
<Ionicons
name="checkmark"
size={16}
color={colors.white}
/>
) : (
<Ionicons
name={step.icon}
size={16}
color={
current
? colors.white
: colors.textMuted
}
/>
)}
</View>
<Text
style={[
styles.timelineLabel,
completed &&
styles.timelineLabelCompleted,
current &&
styles.timelineLabelCurrent,
]}
>
{step.label}
</Text>
{current && (
<View style={styles.currentBadge}>
<View style={styles.currentDot} />
</View>
)}
</View>
);
})}
</View>
)}
</Card>
<Card style={{ marginBottom: spacing.l }}>
<Text style={styles.sectionTitle}>Livraison</Text>
<View style={styles.infoRow}>
<Ionicons
name="location-outline"
size={16}
color={colors.textMuted}
/>
<Text style={styles.infoText}>{address}</Text>
</View>
{order.livreur_assign && (
<View style={styles.infoRow}>
<Ionicons
name="bicycle-outline"
size={16}
color={colors.textMuted}
/>
<Text style={styles.infoText}>
{order.livreur_assign}
</Text>
</View>
)}
{(order.client_prenom || order.first_name) && (
<View style={styles.infoRow}>
<Ionicons
name="person-outline"
size={16}
color={colors.textMuted}
/>
<Text style={styles.infoText}>
{order.first_name || order.client_prenom}{" "}
{order.last_name || order.client_nom}
</Text>
</View>
)}
{(order.phone || order.client_telephone) && (
<View style={styles.infoRow}>
<Ionicons
name="call-outline"
size={16}
color={colors.textMuted}
/>
<Text style={styles.infoText}>
{order.phone || order.client_telephone}
</Text>
</View>
)}
</Card>
<Card style={{ marginBottom: spacing.l }}>
<Text style={styles.sectionTitle}>
Produits ({products.length})
</Text>
{products.map((product, idx) => (
<View
key={idx}
style={[
styles.productRow,
idx > 0 && styles.productRowBorder,
]}
>
{product.imageUri ? (
<Image
source={{ uri: product.imageUri }}
style={styles.productImage}
/>
) : (
<View style={styles.productImagePlaceholder}>
<Ionicons
name="leaf-outline"
size={20}
color={colors.textMuted}
/>
</View>
)}
<View style={styles.productInfo}>
<Text style={styles.productName} numberOfLines={1}>
{product.produit ||
product.product_name ||
product.name_product ||
"Produit"}
</Text>
<Text style={styles.productQty}>
{product.quantite || product.quantity || 0}g
</Text>
</View>
<Text style={styles.productPrice}>
{formatPrice(product.prix || product.price || 0)}
</Text>
</View>
))}
</Card>
<Card>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>{formatPrice(total)}</Text>
</View>
</Card>
</ScrollView>
);
}