352 lines
8.6 KiB
JavaScript
352 lines
8.6 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
FlatList,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
|
|
import firebase from "../../config/firebase";
|
|
import { background } from "../../assets";
|
|
import Page from "../../layouts/Page";
|
|
import { useUser } from "../../providers/UserDataProvider";
|
|
import { gutters, Palette } from "../../styles";
|
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
|
|
|
const ORDER_TYPE_LABELS = {
|
|
GIFT: "Crédit offert",
|
|
COINS: "Achat de coins",
|
|
SONG: "Génération de musique",
|
|
};
|
|
|
|
const STATUS_LABELS = {
|
|
PENDING: "En cours",
|
|
APPLIED: "Confirmée",
|
|
REJECTED: "Refusée",
|
|
};
|
|
|
|
const formatCoins = (amount) => {
|
|
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
return "0";
|
|
}
|
|
try {
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
maximumFractionDigits: 0,
|
|
}).format(amount);
|
|
} catch (_error) {
|
|
return `${amount}`;
|
|
}
|
|
};
|
|
|
|
const formatDate = (date) => {
|
|
if (!date) {
|
|
return "En attente de confirmation";
|
|
}
|
|
try {
|
|
return date.toLocaleString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "short",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
} catch (_error) {
|
|
return date.toString();
|
|
}
|
|
};
|
|
|
|
const mapOrderType = (type) => ORDER_TYPE_LABELS[type] || "Opération";
|
|
|
|
const mapStatus = (status) => STATUS_LABELS[status] || "Inconnue";
|
|
|
|
const OrderHistory = () => {
|
|
const { currentUID } = useUser() || {};
|
|
const [orders, setOrders] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
if (!currentUID) {
|
|
setOrders([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const unsubscribe = firebase
|
|
.firestore()
|
|
.collection("orders")
|
|
.where("userId", "==", currentUID)
|
|
.orderBy("createdAt", "desc")
|
|
.onSnapshot(
|
|
(snapshot) => {
|
|
const nextOrders = snapshot.docs.map((doc) => {
|
|
const rawData = doc.data() || {};
|
|
const createdAt =
|
|
typeof rawData.createdAt?.toDate === "function"
|
|
? rawData.createdAt.toDate()
|
|
: null;
|
|
return {
|
|
id: doc.id,
|
|
...rawData,
|
|
createdAt,
|
|
};
|
|
});
|
|
|
|
setOrders(nextOrders);
|
|
setLoading(false);
|
|
},
|
|
(firestoreError) => {
|
|
console.warn(
|
|
"[OrderHistory] Unable to load orders",
|
|
firestoreError?.message || firestoreError,
|
|
);
|
|
setError(
|
|
firestoreError?.message ||
|
|
"Impossible de récupérer l'historique des commandes.",
|
|
);
|
|
setLoading(false);
|
|
},
|
|
);
|
|
|
|
return () => {
|
|
if (typeof unsubscribe === "function") {
|
|
unsubscribe();
|
|
}
|
|
};
|
|
}, [currentUID]);
|
|
|
|
const renderOrder = useCallback(({ item }) => {
|
|
const amountValue =
|
|
typeof item.amount === "number" && Number.isFinite(item.amount)
|
|
? item.amount
|
|
: 0;
|
|
const isPositive = amountValue > 0;
|
|
const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${
|
|
amountValue === 1 ? "coin" : "coins"
|
|
}`;
|
|
|
|
const status = item.status || "PENDING";
|
|
|
|
const details = (() => {
|
|
if (item.type === "GIFT" && item.metadata?.reason) {
|
|
switch (item.metadata.reason) {
|
|
case "WELCOME_BONUS":
|
|
return "Crédit de bienvenue";
|
|
default:
|
|
return item.metadata.reason;
|
|
}
|
|
}
|
|
if (item.type === "SONG" && item.songId) {
|
|
return `Musique ${item.songId}`;
|
|
}
|
|
if (item.type === "COINS" && item.metadata?.paymentId) {
|
|
return `Paiement ${item.metadata.paymentId}`;
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
return (
|
|
<View style={styles.orderCard}>
|
|
<View style={styles.orderHeader}>
|
|
<Text style={styles.orderTitle}>{mapOrderType(item.type)}</Text>
|
|
<Text
|
|
style={[
|
|
styles.orderAmount,
|
|
isPositive ? styles.amountPositive : styles.amountNegative,
|
|
]}
|
|
>
|
|
{amountLabel}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.orderMetaRow}>
|
|
<Text style={styles.orderDate}>{formatDate(item.createdAt)}</Text>
|
|
<View
|
|
style={[
|
|
styles.statusBadge,
|
|
status === "APPLIED"
|
|
? styles.statusApplied
|
|
: status === "REJECTED"
|
|
? styles.statusRejected
|
|
: styles.statusPending,
|
|
]}
|
|
>
|
|
<Text style={styles.statusText}>{mapStatus(status)}</Text>
|
|
</View>
|
|
</View>
|
|
{details ? <Text style={styles.orderDetails}>{details}</Text> : null}
|
|
{typeof item.balanceAfter === "number" &&
|
|
Number.isFinite(item.balanceAfter) ? (
|
|
<Text style={styles.orderBalance}>
|
|
Solde après opération: {formatCoins(item.balanceAfter)} coins
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}, []);
|
|
|
|
const keyExtractor = useCallback((item) => item.id, []);
|
|
|
|
const listEmptyComponent = useMemo(() => {
|
|
if (loading) {
|
|
return null;
|
|
}
|
|
if (!currentUID) {
|
|
return (
|
|
<Text style={styles.emptyText}>
|
|
Connecte-toi pour consulter l'historique de tes commandes.
|
|
</Text>
|
|
);
|
|
}
|
|
if (error) {
|
|
return <Text style={styles.errorText}>{error}</Text>;
|
|
}
|
|
return (
|
|
<Text style={styles.emptyText}>
|
|
Aucune opération de coins pour le moment.
|
|
</Text>
|
|
);
|
|
}, [loading, currentUID, error]);
|
|
|
|
return (
|
|
<Page
|
|
headerType="NAVIGATION"
|
|
title="Historique de commandes"
|
|
backgroundImg={background.profileBG}
|
|
contentContainerStyle={{ paddingBottom: gutters * 2 }}
|
|
containerStyle={{
|
|
backgroundColor: "rgba(0,0,0,0.4)",
|
|
}}
|
|
>
|
|
<View style={styles.container}>
|
|
{loading ? (
|
|
<View style={styles.loaderContainer}>
|
|
<ActivityIndicator color={Palette.white} size="large" />
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={orders}
|
|
keyExtractor={keyExtractor}
|
|
renderItem={renderOrder}
|
|
contentContainerStyle={[
|
|
styles.listContent,
|
|
orders.length === 0 && styles.emptyListContent,
|
|
]}
|
|
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
|
|
ListEmptyComponent={listEmptyComponent}
|
|
/>
|
|
)}
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
paddingHorizontal: gutters,
|
|
paddingTop: gutters,
|
|
},
|
|
loaderContainer: {
|
|
flex: 1,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
listContent: {
|
|
paddingBottom: gutters * 2,
|
|
},
|
|
emptyListContent: {
|
|
flexGrow: 1,
|
|
justifyContent: "center",
|
|
},
|
|
orderCard: {
|
|
padding: 16,
|
|
borderRadius: 16,
|
|
backgroundColor: Palette.ultraLightWhite,
|
|
borderWidth: 1,
|
|
borderColor: "rgba(255, 255, 255, 0.08)",
|
|
},
|
|
orderHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
justifyContent: "space-between",
|
|
marginBottom: 8,
|
|
gap: 12,
|
|
},
|
|
orderTitle: {
|
|
flex: 1,
|
|
fontSize: 16,
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
color: Palette.white,
|
|
},
|
|
orderAmount: {
|
|
fontSize: 16,
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
},
|
|
amountPositive: {
|
|
color: Palette.green,
|
|
},
|
|
amountNegative: {
|
|
color: Palette.red,
|
|
},
|
|
orderMetaRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginBottom: 6,
|
|
gap: 12,
|
|
},
|
|
orderDate: {
|
|
fontSize: 13,
|
|
color: Palette.grayMid,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
},
|
|
statusBadge: {
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderRadius: 999,
|
|
},
|
|
statusApplied: {
|
|
backgroundColor: Palette.transparentGreen,
|
|
},
|
|
statusPending: {
|
|
backgroundColor: Palette.transparentOrange,
|
|
},
|
|
statusRejected: {
|
|
backgroundColor: Palette.transparentRed,
|
|
},
|
|
statusText: {
|
|
fontSize: 12,
|
|
fontFamily: FONT_FAMILY.InterMedium,
|
|
color: Palette.white,
|
|
},
|
|
orderDetails: {
|
|
marginBottom: 6,
|
|
fontSize: 14,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
color: Palette.white,
|
|
},
|
|
orderBalance: {
|
|
fontSize: 13,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
color: Palette.grayMid,
|
|
},
|
|
emptyText: {
|
|
textAlign: "center",
|
|
color: Palette.grayMid,
|
|
fontSize: 15,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
},
|
|
errorText: {
|
|
textAlign: "center",
|
|
color: Palette.red,
|
|
fontSize: 15,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
},
|
|
});
|
|
|
|
export default OrderHistory;
|