Files
projet_gestion_commande/mobile/src/screens/client/OrderHistoryScreen.tsx
T
2026-05-09 15:17:19 +02:00

449 lines
20 KiB
TypeScript

import React, { useState, useCallback, useMemo } from "react";
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
RefreshControl,
} from "react-native";
import { useNavigation, useFocusEffect } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { Ionicons } from "@expo/vector-icons";
import {
getMyCompletedOrders,
getMyPenalties,
getPublicSettings,
getReferralBalance,
formatOrderDate,
formatPrice,
} from "../../api/api";
import type { PublicSettings } from "../../api/api";
import type {
CompletedOrder,
ClientStats,
PenaltyInfo,
} from "../../api/api_types";
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,
shadows,
} from "../../theme";
type Nav = NativeStackNavigationProp<ClientStackParamList>;
export default function OrderHistoryScreen() {
const navigation = useNavigation<Nav>();
const { colors } = useTheme();
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_enabled: false });
const [referralBalance, setReferralBalance] = useState(0);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchData = useCallback(async () => {
try {
const [histRes, penRes, settings, refRes] = await Promise.all([
getMyCompletedOrders(),
getMyPenalties(),
getPublicSettings(),
getReferralBalance(),
]);
if (histRes.success) {
setOrders(histRes.commands || []);
setStats(histRes.client_stats || null);
}
if (penRes.success) {
setPenalties(penRes.data || null);
}
if (refRes.success) {
setReferralBalance(refRes.balance);
}
setAppSettings(settings);
} catch {
/* ignore */
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useFocusEffect(
useCallback(() => {
setLoading(true);
fetchData();
}, [fetchData]),
);
const onRefresh = () => {
setRefreshing(true);
fetchData();
};
const styles = useMemo(
() =>
StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
listContent: {
padding: spacing.l,
paddingBottom: spacing.xxxl,
},
statsGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: spacing.m,
marginBottom: spacing.xl,
},
statCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.l,
alignItems: "center",
flex: 1,
minWidth: "45%",
borderWidth: 1,
borderColor: colors.borderLight,
},
statValue: {
color: colors.textWhite,
fontSize: fontSize.xxl,
fontWeight: fontWeight.bold,
marginTop: spacing.s,
},
statLabel: {
color: colors.textMuted,
fontSize: fontSize.xs,
marginTop: spacing.xs,
},
sectionTitle: {
color: colors.textSecondary,
fontSize: fontSize.sm,
fontWeight: fontWeight.medium,
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: spacing.m,
},
referralBtn: {
flexDirection: "row",
alignItems: "center",
gap: spacing.s,
backgroundColor: colors.bgCard,
borderRadius: borderRadius.sm,
borderWidth: 1,
borderColor: colors.accent + "44",
paddingVertical: spacing.m,
paddingHorizontal: spacing.l,
marginBottom: spacing.l,
},
referralBtnText: {
color: colors.accent,
fontSize: fontSize.sm,
fontWeight: fontWeight.semibold,
flex: 1,
},
emptyContainer: {
alignItems: "center",
paddingTop: spacing.xxxl,
},
emptyTitle: {
color: colors.textPrimary,
fontSize: fontSize.xl,
fontWeight: fontWeight.bold,
marginTop: spacing.l,
},
emptySubtitle: {
color: colors.textMuted,
fontSize: fontSize.md,
marginTop: spacing.s,
},
orderCard: {
backgroundColor: colors.bgCard,
borderRadius: borderRadius.md,
padding: spacing.l,
marginBottom: spacing.m,
borderWidth: 1,
borderColor: colors.borderLight,
},
orderHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.m,
},
orderIdText: {
color: colors.textWhite,
fontSize: fontSize.md,
fontWeight: fontWeight.semibold,
},
orderRow: {
flexDirection: "row",
alignItems: "center",
marginBottom: spacing.xs,
},
orderDetail: {
color: colors.textSecondary,
fontSize: fontSize.sm,
marginLeft: spacing.s,
flex: 1,
},
orderFooter: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: spacing.m,
paddingTop: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
orderTotal: {
color: colors.success,
fontSize: fontSize.lg,
fontWeight: fontWeight.bold,
},
deliveredBadge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: spacing.m,
paddingVertical: spacing.xs,
borderRadius: borderRadius.xl,
borderWidth: 1.5,
borderColor: "#7c3aed",
},
deliveredBadgeText: {
color: "#a78bfa",
fontSize: fontSize.xs,
fontWeight: fontWeight.bold,
textTransform: "uppercase",
letterSpacing: 0.5,
marginLeft: 4,
},
}),
[colors],
);
if (loading && !refreshing)
return <LoadingSpinner message="Chargement historique..." />;
const poolNames = stats?.pool_names?.length ? stats.pool_names : appSettings.pool_names;
const poolPoints = stats?.pool_points ?? [stats?.points ?? 0];
const totalPoints = poolPoints.reduce((s, v) => s + (v || 0), 0);
const penaltyCount = penalties?.total_penalty || stats?.penalties || 0;
return (
<View style={styles.container}>
<FlatList
data={orders}
keyExtractor={(item) => String(item.id)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.accent}
/>
}
contentContainerStyle={styles.listContent}
ListHeaderComponent={
<View>
<View style={styles.statsGrid}>
<View style={[styles.statCard, shadows.sm]}>
<Ionicons
name="receipt-outline"
size={24}
color={colors.accent}
/>
<Text style={styles.statValue}>
{stats?.total_commands || orders.length}
</Text>
<Text style={styles.statLabel}>Commandes</Text>
</View>
{appSettings.points_enabled && (
poolNames.length <= 1 ? (
<View style={[styles.statCard, shadows.sm]}>
<Ionicons name="trophy-outline" size={24} color={colors.warning} />
<Text style={styles.statValue}>{poolPoints[0] || 0}</Text>
<Text style={styles.statLabel}>Pts {poolNames[0] ?? 'Points'}</Text>
</View>
) : (
<>
{poolNames.map((name, i) => {
const poolIconNames = ["leaf-outline", "medical-outline", "flask-outline", "color-fill-outline", "star-outline"] as const;
const poolIconColors = [colors.categoryWeedHash, "#e879f9", "#fb923c", "#38bdf8", colors.accent];
const icon = poolIconNames[i] ?? "star-outline";
const color = poolIconColors[i] ?? colors.accent;
return (
<View key={i} style={[styles.statCard, shadows.sm]}>
<Ionicons name={icon} size={24} color={color} />
<Text style={styles.statValue}>{poolPoints[i] || 0}</Text>
<Text style={styles.statLabel}>Pts {name}</Text>
</View>
);
})}
{totalPoints > 0 && (
<View style={[styles.statCard, shadows.sm]}>
<Ionicons name="trophy-outline" size={24} color={colors.warning} />
<Text style={styles.statValue}>{totalPoints}</Text>
<Text style={styles.statLabel}>Total Points</Text>
</View>
)}
</>
)
)}
{appSettings.show_amende_score && (
<View
style={[
styles.statCard,
shadows.sm,
penaltyCount > 0 && {
borderWidth: 1,
borderColor:
penaltyCount >= 3
? colors.danger + "88"
: colors.warning + "88",
backgroundColor:
penaltyCount >= 3
? colors.danger + "18"
: colors.warning + "18",
},
]}
>
<Ionicons
name={penaltyCount > 0 ? "warning" : "shield-checkmark-outline"}
size={24}
color={
penaltyCount >= 3
? colors.danger
: penaltyCount > 0
? colors.warning
: colors.textMuted
}
/>
<Text
style={[
styles.statValue,
penaltyCount >= 3 && { color: colors.danger },
penaltyCount > 0 && penaltyCount < 3 && { color: colors.warning },
]}
>
{penaltyCount}
</Text>
<Text style={styles.statLabel}>
Amende
</Text>
</View>
)}
</View>
{/* Bouton parrainage — visible seulement si activé dans les settings */}
{appSettings.referral_enabled && <TouchableOpacity
style={styles.referralBtn}
onPress={() => navigation.navigate("Parrainage")}
>
<Ionicons name="gift-outline" size={18} color={colors.accent} />
<Text style={styles.referralBtnText}>
Parrainage
{referralBalance > 0 ? ` — ${referralBalance.toFixed(2)} €` : ""}
</Text>
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
</TouchableOpacity>}
{orders.length > 0 && (
<Text style={styles.sectionTitle}>
Historique des commandes
</Text>
)}
</View>
}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Ionicons
name="time-outline"
size={80}
color={colors.textMuted}
/>
<Text style={styles.emptyTitle}>Aucun historique</Text>
<Text style={styles.emptySubtitle}>
Vos commandes terminees apparaitront ici
</Text>
</View>
}
renderItem={({ item: order }) => (
<TouchableOpacity
activeOpacity={0.7}
onPress={() =>
navigation.navigate("OrderDetails", {
orderId: order.id,
})
}
style={[styles.orderCard, shadows.sm]}
>
<View style={styles.orderHeader}>
<Text style={styles.orderIdText}>
Commande #{order.client_order_number}
</Text>
{order.status === "cancelled" ? (
<StatusBadge status={order.status} />
) : (
<View style={styles.deliveredBadge}>
<Ionicons name="checkmark-circle" size={12} color="#a78bfa" />
<Text style={styles.deliveredBadgeText}>Livrée</Text>
</View>
)}
</View>
<View style={styles.orderRow}>
<Ionicons
name="location-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.orderDetail} numberOfLines={1}>
{order.adresse || "N/A"}
</Text>
</View>
<View style={styles.orderRow}>
<Ionicons
name="time-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.orderDetail}>
{formatOrderDate(order.created_at)}
</Text>
</View>
{order.livreur_assign && (
<View style={styles.orderRow}>
<Ionicons
name="bicycle-outline"
size={14}
color={colors.textMuted}
/>
<Text style={styles.orderDetail}>
{order.livreur_assign}
</Text>
</View>
)}
<View style={styles.orderFooter}>
<Text style={styles.orderTotal}>
{formatPrice(order.total_prix || 0)}
</Text>
<Ionicons
name="chevron-forward"
size={18}
color={colors.textMuted}
/>
</View>
</TouchableOpacity>
)}
/>
</View>
);
}