449 lines
20 KiB
TypeScript
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>
|
|
);
|
|
}
|