Commit dfdb5556 by G

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

parent 28d87f82
......@@ -5,6 +5,7 @@ import type {
OmInitializationPayload as OmTransactionInitializationPayload,
OmInitializationResponse as OmTransactionInitializationResponse,
PaymentType,
Transaction,
WaveInitializationPayload,
WaveTransactionInitilizationResponse,
} from "./types";
......@@ -54,3 +55,7 @@ export const waveInitializeTransaction = (payload: WaveInitializationPayload) =>
export const waveGetTransactionStatus = (id: string) => {
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 {
when_created: 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";
import BarWithBeasyAndNotificationsIcon from "@components/BarWithBeasyAndNotificationsIcon";
import Button from "@components/Button";
import Input from "@components/Input";
import TransactionInformationsItem from "@components/TransactionInformationsItem";
import Box from "@components/bases/Box";
import Text from "@components/bases/Text";
import WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight from "@components/wrappers/WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight";
import useTransactionsHistory, { type OperatorsFilter } from "@hooks/useTransactionsHistory";
/** biome-ignore-all lint/style/useNamingConvention: <explanation> */
import { asp as g } from "@asp/asp";
import { BarnoinPayBackground } from "@components/BarnoinPayBackground";
import BeasyLogoIcon from "@components/BeasyLogoIcon";
import * as Button from "@components/ButtonNew";
import * as Input from "@components/InputNew";
import * as Modal from "@components/Modal";
import Entypo from "@expo/vector-icons/Entypo";
import Ionicons from "@expo/vector-icons/Ionicons";
import { LOG } from "@logger";
import Card from "@re-card";
import theme from "@themes/Theme";
import { useCallback, useState } from "react";
import { RefreshControl, ScrollView, Switch } from "react-native";
import Icon from "react-native-vector-icons/Ionicons";
import { FlashList } from "@shopify/flash-list";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { Switch, Text, TouchableOpacity, View } from "react-native";
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 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 {
transactionsHistory: data,
isLoading,
error,
refetch,
setReferenceFilter,
operatorsFilter,
setOperatorsFilter,
} = useTransactionsHistory();
const transactionHistoryQuery = useQuery({
queryKey: ["transactionsHistory"],
queryFn: getTransactions,
});
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 (
<WrapperWithDefaultBeasyBackgroundAndSafeAreaTopLeftRight>
<BarWithBeasyAndNotificationsIcon />
<BarnoinPayBackground style={[g.gap_5xl]}>
<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"}>
<Box px={"m"} flexDirection={"row"} gap={"s"} alignItems={"center"}>
<Box flex={1}>
<Input placeholder="Reference" onChangeText={setReferenceFilter} />
</Box>
<Box
height={50}
backgroundColor={"lightGray"}
borderRadius={10}
width={50}
// justifyContent={"center"}
alignItems={"center"}
justifyContent={"center"}
<View
style={[
g.px_xl,
g.pt_xl,
g.flex_1,
g.gap_lg,
{ backgroundColor: "white", borderTopLeftRadius: 20, borderTopRightRadius: 20 },
]}
>
<View style={[g.flex_row, g.gap_sm, g.align_center, g.w_full]}>
<Input.Container style={[g.flex_1]}>
<Input.FieldContainer>
<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
name="filter"
size={30}
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>
);
};
<Ionicons name="filter" size={24} color="black" />
</TouchableOpacity>
</View>
export default TransactionHistoryScreen;
interface FiltersModalProps {
// biome-ignore lint/style/useNamingConvention: <explanation>
operatorsFilter: OperatorsFilter;
// biome-ignore lint/style/useNamingConvention: <explanation>
setOperatorsFilter: React.Dispatch<React.SetStateAction<OperatorsFilter>>;
}
const FiltersModal: React.FC<FiltersModalProps> = ({ operatorsFilter, setOperatorsFilter }) => {
const [filterOm, setFilterOm] = useState(operatorsFilter.OM);
const [filterMtn, setFilterMtn] = useState(operatorsFilter.MTN);
const [filterFlooz, setFilterFlooz] = useState(operatorsFilter.FLOOZ);
const [filterWave, setFilterWave] = useState(operatorsFilter.WAVE);
const [filterCb, setFilterCb] = useState(operatorsFilter.CB);
const saveOperatorsFilters = useCallback(() => {
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();
{!transactions ? (
<Text style={[g.text_center]}>Aucune transaction n'a été trouvée</Text>
) : (
<FlashList
data={transactions}
keyExtractor={(item) => item.reference}
estimatedItemSize={75}
bounces={false}
renderItem={({ item }) => (
<View style={[g.p_sm]} key={item.reference}>
<TransactionItem transaction={item} />
</View>
)}
/>
)}
</View>
const saveFilters = useCallback(() => {
saveOperatorsFilters();
closeModal();
}, [saveOperatorsFilters, closeModal]);
{/* FILTER MODAL */}
return (
<Card variant={"absoluteForegroundScreenSizedTransparentCard"}>
<Box
backgroundColor={"white"}
width={"90%"}
// height={"70%"}
maxHeight={"80%"}
borderRadius={10}
p={"m"}
flexDirection={"column"}
style={{ margin: "auto" }}
<Modal.RnModal
visible={showFilterModal}
style={{ backgroundColor: "rgba(0, 0, 0, 0.25)", flex: 1 }}
>
<Box
justifyContent={"space-between"}
alignItems={"center"}
flexDirection={"row"}
width={"100%"}
<Modal.OuterView
style={[
g.p_md,
g.shadow_elevated,
{
backgroundColor: "white",
width: "90%",
height: "60%",
},
]}
>
<Text fontSize={20} variant={"black"} fontWeight={"bold"}>
Paramétrage filtre
</Text>
<Icon
name="close-outline"
size={30}
color="black"
onPress={() => {
closeModal();
}}
/>
</Box>
<Text fontSize={20} fontWeight={"bold"} my={"s"}>
Opérateurs
</Text>
<Box
width={"100%"}
backgroundColor={"lightGray"}
borderRadius={10}
p={"s"}
gap={"s"}
>
<Box
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
borderBottomWidth={1}
py={"s"}
>
<Text fontWeight={"bold"}>Orange Money</Text>
<Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }}
value={filterOm}
onValueChange={setFilterOm}
/>
</Box>
<Box
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
borderBottomWidth={1}
py={"s"}
>
<Text fontWeight={"bold"}>MTN Money</Text>
<Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }}
value={filterMtn}
onValueChange={setFilterMtn}
/>
</Box>
<Box
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
borderBottomWidth={1}
py={"s"}
>
<Text fontWeight={"bold"}>Flooz Money</Text>
<Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }}
value={filterFlooz}
onValueChange={setFilterFlooz}
/>
</Box>
<Box
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
borderBottomWidth={1}
py={"s"}
>
<Text fontWeight={"bold"}>Wave</Text>
<Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }}
value={filterWave}
onValueChange={setFilterWave}
/>
</Box>
<Box
flexDirection={"row"}
alignItems={"center"}
justifyContent={"space-between"}
borderBottomColor={"white"}
py={"s"}
>
<Text fontWeight={"bold"}>Carte Bancaire</Text>
<Switch
trackColor={{ false: "#767577", true: theme.colors.secondary }}
value={filterCb}
onValueChange={setFilterCb}
/>
</Box>
</Box>
<Box style={{ marginTop: 20 }}>
<Button
variant={"full"}
label={"Valider"}
textVariants={"white"}
onPress={saveFilters}
/>
</Box>
</Box>
</Card>
<View style={[g.p_md, g.flex_1, g.gap_sm]}>
<View style={[g.flex_row, g.justify_between, g.align_center]}>
<Text style={[g.font_bold, g.text_2xl]}>Paramétrage du filtre</Text>
<Entypo name="cross" size={24} color="black" />
</View>
<Text style={[g.font_bold, g.text_lg]}>Opétareurs</Text>
<View style={[g.px_md, { backgroundColor: "#e8e8e9ff" }]}>
<View
style={[
g.flex_row,
g.justify_between,
g.align_center,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text style={[g.font_bold]}>Orange Money</Text>
<Switch
trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filters.OM}
onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, OM: checked }))
}
/>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.align_center,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text style={[g.font_bold]}>MTN Money</Text>
<Switch
trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filters.MTN}
onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, MTN: checked }))
}
/>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.align_center,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text style={[g.font_bold]}>Flooz Money</Text>
<Switch
trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filters.FLOOZ}
onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, FLOOZ: checked }))
}
/>
</View>
<View
style={[
g.flex_row,
g.justify_between,
g.align_center,
g.py_md,
{ borderBottomColor: "white", borderBottomWidth: 1 },
]}
>
<Text style={[g.font_bold]}>Wave</Text>
<Switch
trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filters.WAVE}
onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, WAVE: checked }))
}
/>
</View>
<View style={[g.flex_row, g.justify_between, g.align_center, g.py_md]}>
<Text style={[g.font_bold]}>Carte Bancaire</Text>
<Switch
trackColor={{ false: "#767577", true: "#167a00ff" }}
value={filters.CB}
onValueChange={(checked) =>
setFilters((prev) => ({ ...prev, CB: checked }))
}
/>
</View>
</View>
<Button.Container onPress={() => setShowFilterModal(false)}>
<Button.Label>Valider</Button.Label>
</Button.Container>
</View>
</Modal.OuterView>
</Modal.RnModal>
</BarnoinPayBackground>
);
};
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