535 lines
22 KiB
TypeScript
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>
|
|
);
|
|
}
|