Commit dfdb5556 by G

feat: Transaction history screen with filters and performant scrollView using Flashlist.

parent 28d87f82
...@@ -5,6 +5,7 @@ import type { ...@@ -5,6 +5,7 @@ import type {
OmInitializationPayload as OmTransactionInitializationPayload, OmInitializationPayload as OmTransactionInitializationPayload,
OmInitializationResponse as OmTransactionInitializationResponse, OmInitializationResponse as OmTransactionInitializationResponse,
PaymentType, PaymentType,
Transaction,
WaveInitializationPayload, WaveInitializationPayload,
WaveTransactionInitilizationResponse, WaveTransactionInitilizationResponse,
} from "./types"; } from "./types";
...@@ -54,3 +55,7 @@ export const waveInitializeTransaction = (payload: WaveInitializationPayload) => ...@@ -54,3 +55,7 @@ export const waveInitializeTransaction = (payload: WaveInitializationPayload) =>
export const waveGetTransactionStatus = (id: string) => { export const waveGetTransactionStatus = (id: string) => {
return axiosInstance.get<WaveTransactionInitilizationResponse>(`/wave-session/${id}/`); return axiosInstance.get<WaveTransactionInitilizationResponse>(`/wave-session/${id}/`);
}; };
export const getTransactions = () => {
return axiosInstance.get<DjangoPaginated<Transaction[]>>("/transactions/");
};
import moment from "moment";
import "moment/locale/fr";
import { asp as g } from "@asp/asp";
import type { FC } from "react";
import { Text, View } from "react-native";
import type { Transaction } from "../types";
import PaymentType from "./PaymentType";
moment.locale("fr");
export const TransactionItem: FC<{ transaction: Transaction }> = ({ transaction }) => {
const { type_paiement_label, reference, date, montant, status } = transaction;
const dateObject = Date.parse(date);
const color = status === "SUCCESS" ? "green" : status === "INITIATED" ? "orange" : "red";
return (
<View
style={[
g.w_full,
g.p_sm,
g.flex_row,
g.gap_sm,
g.rounded_sm,
g.shadow_elevated,
g.justify_between,
{ backgroundColor: "white" },
]}
>
<View style={[g.flex_row, g.flex_1, g.gap_sm]}>
<View style={[g.rounded_md, g.overflow_hidden, { height: 50, width: 50 }]}>
<PaymentType
style={{ height: "100%", aspectRatio: 1 }}
type={type_paiement_label}
/>
</View>
<View style={[g.flex_1, { height: 50 }]}>
<Text>{reference}</Text>
<Text>{moment(dateObject).fromNow()}</Text>
</View>
</View>
<View style={{ height: 50 }}>
<Text style={{ color: color }}>{montant}</Text>
</View>
</View>
);
};
...@@ -89,3 +89,19 @@ export interface WaveTransactionInitilizationResponse { ...@@ -89,3 +89,19 @@ export interface WaveTransactionInitilizationResponse {
when_created: string; when_created: string;
when_expires: string; when_expires: string;
} }
export type Transaction = {
type_paiement: number;
type_paiement_label: PaymentTypeCode;
marchand: string;
marchand_name: string;
service: string;
montant: number;
date: string;
commentaire: string;
etat: boolean;
status: "SUCCESS" | "INITIATED" | "FAILED";
reference: string;
transaction_id: number;
marchand_code: string;
};
import { useModalsManagerContext } from "@/contexts/ModalsManagerContext"; /** biome-ignore-all lint/style/useNamingConvention: <explanation> */
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon";
import Button from "@components/Button"; import { asp as g } from "@asp/asp";
import Input from "@components/Input"; import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import TransactionInformationsItem from "@components/TransactionInformationsItem"; import BeasyLogoIcon from "@components/BeasyLogoIcon";
import Box from "@components/bases/Box"; import * as Button from "@components/ButtonNew";
import Text from "@components/bases/Text"; import * as Input from "@components/InputNew";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight"; import * as Modal from "@components/Modal";
import useTransactionsHistory, { type OperatorsFilter } from "@hooks/useTransactionsHistory"; import Entypo from "@expo/vector-icons/Entypo";
import Ionicons from "@expo/vector-icons/Ionicons";
import { LOG } from "@logger"; import { LOG } from "@logger";
import Card from "@re-card"; import { FlashList } from "@shopify/flash-list";
import theme from "@themes/Theme"; import { useQuery } from "@tanstack/react-query";
import { useCallback, useState } from "react"; import { useState } from "react";
import { RefreshControl, ScrollView, Switch } from "react-native"; import { Switch, Text, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/Ionicons"; import { getTransactions } from "@/features/pay/api";
import { TransactionItem } from "@/features/pay/components/TransactionItem";
import type { PaymentTypeCode } from "@/features/pay/types";
const log = LOG.extend("TransactionHistoryScreen"); const log = LOG.extend("TransactionHistoryScreen");
const TransactionHistoryScreen = () => { const TransactionHistoryScreen = () => {
log.verbose("TransactionHistoryScreen"); log.verbose("TransactionHistoryScreen");
const [showFilterModal, setShowFilterModal] = useState(false);
const [filters, setFilters] = useState<Record<PaymentTypeCode, boolean>>({
CB: true,
FLOOZ: true,
MTN: true,
WAVE: true,
OM: true,
});
const [referenceFilter, setReferenceFilter] = useState("");
const { const transactionHistoryQuery = useQuery({
transactionsHistory: data, queryKey: ["transactionsHistory"],
isLoading, queryFn: getTransactions,
error, });
refetch,
setReferenceFilter,
operatorsFilter,
setOperatorsFilter,
} = useTransactionsHistory();
const { showModal } = useModalsManagerContext(); const transactions = transactionHistoryQuery.data?.data.results
? transactionHistoryQuery.data?.data?.results.filter(
(transaction) =>
filters[transaction.type_paiement_label] &&
transaction.reference.includes(referenceFilter),
)
: [];
return ( return (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight> <BarnoinPayBackground style={[g.gap_5xl]}>
<BarWithBeasyAndNotificationsIcon /> <View style={[g.px_lg, g.flex_row, g.justify_between]}>
<BeasyLogoIcon />
<Ionicons name="notifications" size={24} color="black" />
</View>
<Card variant={"curvedTopContainer"} padding={"s"} height={"100%"} mt={"m"}> <View
<Box px={"m"} flexDirection={"row"} gap={"s"} alignItems={"center"}> style={[
<Box flex={1}> g.px_xl,
<Input placeholder="Reference" onChangeText={setReferenceFilter} /> g.pt_xl,
</Box> g.flex_1,
<Box g.gap_lg,
height={50} { backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
backgroundColor={"lightGray"} ]}
borderRadius={10} >
width={50} <View style={[g.flex_row, g.gap_sm, g.align_center, g.w_full]}>
// justifyContent={"center"} <Input.Container style={[g.flex_1]}>
alignItems={"center"} <Input.FieldContainer>
justifyContent={"center"} <Input.Field
placeholder="Recherche par référence"
placeholderTextColor={"gray"}
onChangeText={setReferenceFilter}
/>
</Input.FieldContainer>
</Input.Container>
<TouchableOpacity
onPress={() => setShowFilterModal(true)}
style={[
g.justify_center,
g.align_center,
g.p_md,
g.rounded_md,
{ backgroundColor: "#e8e8e9ff" },
]}
> >
<Icon <Ionicons name="filter" size={24} color="black" />
name="filter" </TouchableOpacity>
size={30} </View>
color="black"
// onPress={() => setShowFiltersModal(true)}
onPress={() =>
showModal(
<FiltersModal
setOperatorsFilter={setOperatorsFilter}
operatorsFilter={operatorsFilter}
/>,
)
}
/>
</Box>
</Box>
<ScrollView
// style={{ backgroundColor: "red" }}
refreshControl={<RefreshControl refreshing={isLoading} onRefresh={refetch} />}
contentContainerStyle={{
gap: 10,
padding: 5,
// marginTop: 10,
paddingBottom: 30,
flexDirection: "column",
}}
showsVerticalScrollIndicator={false}
>
{data?.map((transaction) => (
<TransactionInformationsItem
key={transaction.transaction_id}
paymentType={transaction.type_paiement_label}
reference={transaction.reference}
amount={transaction.montant}
date={transaction.date}
status={transaction.status}
/>
))}
</ScrollView>
</Card>
</WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
);
};
export default TransactionHistoryScreen; {!transactions ? (
<Text style={[g.text_center]}>Aucune transaction n'a été trouvée</Text>
interface FiltersModalProps { ) : (
// biome-ignore lint/style/useNamingConvention: <explanation> <FlashList
operatorsFilter: OperatorsFilter; data={transactions}
// biome-ignore lint/style/useNamingConvention: <explanation> keyExtractor={(item) => item.reference}
setOperatorsFilter: React.Dispatch<React.SetStateAction<OperatorsFilter>>; estimatedItemSize={75}
} bounces={false}
const FiltersModal: React.FC<FiltersModalProps> = ({ operatorsFilter, setOperatorsFilter }) => { renderItem={({ item }) => (
const [filterOm, setFilterOm] = useState(operatorsFilter.OM); <View style={[g.p_sm]} key={item.reference}>
const [filterMtn, setFilterMtn] = useState(operatorsFilter.MTN); <TransactionItem transaction={item} />
const [filterFlooz, setFilterFlooz] = useState(operatorsFilter.FLOOZ); </View>
const [filterWave, setFilterWave] = useState(operatorsFilter.WAVE); )}
const [filterCb, setFilterCb] = useState(operatorsFilter.CB); />
)}
const saveOperatorsFilters = useCallback(() => { </View>
setOperatorsFilter({
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
OM: filterOm,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
MTN: filterMtn,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
FLOOZ: filterFlooz,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
WAVE: filterWave,
// biome-ignore lint/style/useNamingConvention: <PaymentCode Type requirement>
CB: filterCb,
});
}, [filterOm, filterMtn, filterFlooz, filterWave, filterCb, setOperatorsFilter]);
const { closeModal } = useModalsManagerContext();
const saveFilters = useCallback(() => { {/* FILTER MODAL */}
saveOperatorsFilters();
closeModal();
}, [saveOperatorsFilters, closeModal]);
return ( <Modal.RnModal
<Card variant={"absoluteForegroundScreenSizedTransparentCard"}> visible={showFilterModal}
<Box style={{ backgroundColor: "rgba(0, 0, 0, 0.25)", flex: 1 }}
backgroundColor={"white"}
width={"90%"}
// height={"70%"}
maxHeight={"80%"}
borderRadius={10}
p={"m"}
flexDirection={"column"}
style={{ margin: "auto" }}
> >
<Box <Modal.OuterView
justifyContent={"space-between"} style={[
alignItems={"center"} g.p_md,
flexDirection={"row"} g.shadow_elevated,
width={"100%"} {
backgroundColor: "white",
width: "90%",
height: "60%",
},
]}
> >
<Text fontSize={20} variant={"black"} fontWeight={"bold"}> <View style={[g.p_md, g.flex_1, g.gap_sm]}>
Paramétrage filtre <View style={[g.flex_row, g.justify_between, g.align_center]}>
</Text> <Text style={[g.font_bold, g.text_2xl]}>Paramétrage du filtre</Text>
<Icon <Entypo name="cross" size={24} color="black" />
name="close-outline" </View>
size={30} <Text style={[g.font_bold, g.text_lg]}>Opétareurs</Text>
color="black" <View style={[g.px_md, { backgroundColor: "#e8e8e9ff" }]}>
onPress={() => { <View
closeModal(); style={[
}} g.flex_row,
/> g.justify_between,
</Box> g.align_center,
g.py_md,
<Text fontSize={20} fontWeight={"bold"} my={"s"}> { borderBottomColor: "white", borderBottomWidth: 1 },
Opérateurs ]}
</Text> >
<Box <Text style={[g.font_bold]}>Orange Money</Text>
width={"100%"} <Switch
backgroundColor={"lightGray"} trackColor={{ false: "#767577", true: "#167a00ff" }}
borderRadius={10} value={filters.OM}
p={"s"} onValueChange={(checked) =>
gap={"s"} setFilters((prev) => ({ ...prev, OM: checked }))
> }
<Box />
flexDirection={"row"} </View>
alignItems={"center"} <View
justifyContent={"space-between"} style={[
borderBottomColor={"white"} g.flex_row,
borderBottomWidth={1} g.justify_between,
py={"s"} g.align_center,
> g.py_md,
<Text fontWeight={"bold"}>Orange Money</Text> { borderBottomColor: "white", borderBottomWidth: 1 },
<Switch ]}
trackColor={{ false: "#767577", true: theme.colors.secondary }} >
value={filterOm} <Text style={[g.font_bold]}>MTN Money</Text>
onValueChange={setFilterOm} <Switch
/> trackColor={{ false: "#767577", true: "#167a00ff" }}
</Box> value={filters.MTN}
<Box onValueChange={(checked) =>
flexDirection={"row"} setFilters((prev) => ({ ...prev, MTN: checked }))
alignItems={"center"} }
justifyContent={"space-between"} />
borderBottomColor={"white"} </View>
borderBottomWidth={1} <View
py={"s"} style={[
> g.flex_row,
<Text fontWeight={"bold"}>MTN Money</Text> g.justify_between,
<Switch g.align_center,
trackColor={{ false: "#767577", true: theme.colors.secondary }} g.py_md,
value={filterMtn} { borderBottomColor: "white", borderBottomWidth: 1 },
onValueChange={setFilterMtn} ]}
/> >
</Box> <Text style={[g.font_bold]}>Flooz Money</Text>
<Box <Switch
flexDirection={"row"} trackColor={{ false: "#767577", true: "#167a00ff" }}
alignItems={"center"} value={filters.FLOOZ}
justifyContent={"space-between"} onValueChange={(checked) =>
borderBottomColor={"white"} setFilters((prev) => ({ ...prev, FLOOZ: checked }))
borderBottomWidth={1} }
py={"s"} />
> </View>
<Text fontWeight={"bold"}>Flooz Money</Text> <View
<Switch style={[
trackColor={{ false: "#767577", true: theme.colors.secondary }} g.flex_row,
value={filterFlooz} g.justify_between,
onValueChange={setFilterFlooz} g.align_center,
/> g.py_md,
</Box> { borderBottomColor: "white", borderBottomWidth: 1 },
<Box ]}
flexDirection={"row"} >
alignItems={"center"} <Text style={[g.font_bold]}>Wave</Text>
justifyContent={"space-between"} <Switch
borderBottomColor={"white"} trackColor={{ false: "#767577", true: "#167a00ff" }}
borderBottomWidth={1} value={filters.WAVE}
py={"s"} onValueChange={(checked) =>
> setFilters((prev) => ({ ...prev, WAVE: checked }))
<Text fontWeight={"bold"}>Wave</Text> }
<Switch />
trackColor={{ false: "#767577", true: theme.colors.secondary }} </View>
value={filterWave} <View style={[g.flex_row, g.justify_between, g.align_center, g.py_md]}>
onValueChange={setFilterWave} <Text style={[g.font_bold]}>Carte Bancaire</Text>
/> <Switch
</Box> trackColor={{ false: "#767577", true: "#167a00ff" }}
<Box value={filters.CB}
flexDirection={"row"} onValueChange={(checked) =>
alignItems={"center"} setFilters((prev) => ({ ...prev, CB: checked }))
justifyContent={"space-between"} }
borderBottomColor={"white"} />
py={"s"} </View>
> </View>
<Text fontWeight={"bold"}>Carte Bancaire</Text> <Button.Container onPress={() => setShowFilterModal(false)}>
<Switch <Button.Label>Valider</Button.Label>
trackColor={{ false: "#767577", true: theme.colors.secondary }} </Button.Container>
value={filterCb} </View>
onValueChange={setFilterCb} </Modal.OuterView>
/> </Modal.RnModal>
</Box> </BarnoinPayBackground>
</Box>
<Box style={{ marginTop: 20 }}>
<Button
variant={"full"}
label={"Valider"}
textVariants={"white"}
onPress={saveFilters}
/>
</Box>
</Box>
</Card>
); );
}; };
export default TransactionHistoryScreen;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment