diff --git a/locales/en/index.yml b/locales/en/index.yml index 03d4eb990c8..2efc4b2d729 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3212,6 +3212,15 @@ features: banner: label: "The loading of the receipts failed." retryButton: "Retry" + filters: + tabs: + all: Tutte + payer: Pagate da me + debtor: Intestate a me + list: + empty: + title: Nessuna ricevuta trovata + subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. details: payPal: banner: diff --git a/locales/it/index.yml b/locales/it/index.yml index 2a7391e2fdc..a1c36642162 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3212,6 +3212,15 @@ features: banner: label: "Il caricamento delle ricevute è fallito." retryButton: "Prova di nuovo" + filters: + tabs: + all: Tutte + payer: Pagate da me + debtor: Intestate a me + list: + empty: + title: Nessuna ricevuta trovata + subtitle: Se stai cercando la ricevuta di un avviso pagoPA che hai pagato in passato, rivolgiti all’ente creditore. details: payPal: banner: diff --git a/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFadeInOutAnimationView.tsx b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFadeInOutAnimationView.tsx new file mode 100644 index 00000000000..4c1087d6708 --- /dev/null +++ b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFadeInOutAnimationView.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { IOStyles } from "@pagopa/io-app-design-system"; +import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; + +export const PaymentsBizEventsFadeInOutAnimationView = React.memo( + ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + () => true +); diff --git a/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFilterTabs.tsx b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFilterTabs.tsx new file mode 100644 index 00000000000..b8f9d2da1e5 --- /dev/null +++ b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsFilterTabs.tsx @@ -0,0 +1,64 @@ +import { + IOVisualCostants, + TabItem, + TabNavigation, + VSpacer +} from "@pagopa/io-app-design-system"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { + PaymentBizEventsCategoryFilter, + paymentsBizEventsCategoryFilters +} from "../types"; +import I18n from "../../../../i18n"; + +type PaymentsBizEventsFilterTabsProps = { + selectedCategory: PaymentBizEventsCategoryFilter; + onCategorySelected?: (category: PaymentBizEventsCategoryFilter) => void; +}; + +const PaymentsBizEventsFilterTabs = ({ + selectedCategory, + onCategorySelected +}: PaymentsBizEventsFilterTabsProps) => { + const selectedIndexOfCategory = + paymentsBizEventsCategoryFilters.indexOf(selectedCategory); + + const handleFilterSelected = (index: number) => { + const categoryByIndex = paymentsBizEventsCategoryFilters[index]; + onCategorySelected?.(categoryByIndex); + }; + + return ( + + + {paymentsBizEventsCategoryFilters.map(category => ( + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: -IOVisualCostants.appMarginDefault * 2, + paddingHorizontal: IOVisualCostants.appMarginDefault + } +}); + +export { PaymentsBizEventsFilterTabs }; diff --git a/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsListItemTransaction.tsx b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsListItemTransaction.tsx index c59887a5225..2320acb3112 100644 --- a/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsListItemTransaction.tsx +++ b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsListItemTransaction.tsx @@ -1,7 +1,7 @@ import { Avatar, ListItemTransaction } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import React from "react"; +import React, { useMemo } from "react"; import { getAccessibleAmountText } from "../../../../utils/accessibility"; import { format } from "../../../../utils/dates"; import { getTransactionLogo } from "../../common/utils"; @@ -14,73 +14,73 @@ type Props = { onPress?: () => void; }; -const PaymentsBizEventsListItemTransaction = ({ - transaction, - onPress -}: Props) => { - const recipient = transaction.payeeName || ""; +const PaymentsBizEventsListItemTransaction = React.memo( + ({ transaction, onPress }: Props) => { + const recipient = transaction.payeeName ?? ""; - const amountText = pipe( - transaction.amount, - O.fromNullable, - O.map(amount => formatAmountText(amount)), - O.getOrElse(() => "") - ); + const amountText = pipe( + transaction.amount, + O.fromNullable, + O.map(amount => formatAmountText(amount)), + O.getOrElse(() => "") + ); - const datetime: string = pipe( - transaction.noticeDate, - O.fromNullable, - O.map(transactionDate => format(transactionDate, "DD MMM YYYY, HH:mm")), - O.getOrElse(() => "") - ); + const datetime: string = pipe( + transaction.noticeDate, + O.fromNullable, + O.map(transactionDate => format(transactionDate, "DD MMM YYYY, HH:mm")), + O.getOrElse(() => "") + ); - const accessibleDatetime: string = pipe( - transaction.noticeDate, - O.fromNullable, - O.map(transactionDate => format(transactionDate, "DD MMMM YYYY, HH:mm")), - O.getOrElse(() => "") - ); + const accessibleDatetime: string = pipe( + transaction.noticeDate, + O.fromNullable, + O.map(transactionDate => format(transactionDate, "DD MMMM YYYY, HH:mm")), + O.getOrElse(() => "") + ); - const transactionPayeeLogoUri = getTransactionLogo(transaction); + const transactionPayeeLogoUri = getTransactionLogo(transaction); - const accessibleAmountText = getAccessibleAmountText(amountText); - const accessibilityLabel = `${recipient}; ${accessibleDatetime}; ${accessibleAmountText}`; + const accessibleAmountText = getAccessibleAmountText(amountText); + const accessibilityLabel = `${recipient}; ${accessibleDatetime}; ${accessibleAmountText}`; - const TransactionEmptyIcon = () => ; + const TransactionEmptyIcon = useMemo(() => , []); - const transactionLogo = pipe( - transactionPayeeLogoUri, - O.map(uri => ({ uri })), - O.getOrElseW(() => ) - ); + const transactionLogo = pipe( + transactionPayeeLogoUri, + O.map(uri => ({ uri })), + O.getOrElseW(() => TransactionEmptyIcon) + ); + + if (transaction.isCart) { + return ( + + ); + } - if (transaction.isCart) { return ( } + paymentLogoIcon={transactionLogo} onPress={onPress} accessible={true} - title={I18n.t("features.payments.transactions.multiplePayment")} + title={recipient} subtitle={datetime} transactionAmount={amountText} accessibilityLabel={accessibilityLabel} transactionStatus="success" /> ); - } - - return ( - - ); -}; + }, + (prevProps, nextProps) => prevProps.transaction === nextProps.transaction +); export { PaymentsBizEventsListItemTransaction }; diff --git a/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsTransactionLoadingList.tsx b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsTransactionLoadingList.tsx new file mode 100644 index 00000000000..66fed3e5aee --- /dev/null +++ b/ts/features/payments/bizEventsTransaction/components/PaymentsBizEventsTransactionLoadingList.tsx @@ -0,0 +1,34 @@ +import { ListItemTransaction, VSpacer } from "@pagopa/io-app-design-system"; +import * as React from "react"; +import Placeholder from "rn-placeholder"; +import { PaymentsBizEventsFadeInOutAnimationView } from "./PaymentsBizEventsFadeInOutAnimationView"; + +export type PaymentsBizEventsTransactionLoadingListProps = { + showSectionTitleSkeleton?: boolean; +}; + +export const PaymentsBizEventsTransactionLoadingList = ({ + showSectionTitleSkeleton +}: PaymentsBizEventsTransactionLoadingListProps) => ( + <> + {showSectionTitleSkeleton && ( + <> + + + + + )} + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + +); diff --git a/ts/features/payments/bizEventsTransaction/saga/handleGetBizEventsTransactions.ts b/ts/features/payments/bizEventsTransaction/saga/handleGetBizEventsTransactions.ts index 8fd748714c1..34454505a18 100644 --- a/ts/features/payments/bizEventsTransaction/saga/handleGetBizEventsTransactions.ts +++ b/ts/features/payments/bizEventsTransaction/saga/handleGetBizEventsTransactions.ts @@ -21,7 +21,9 @@ export function* handleGetBizEventsTransactions( action, { size: action.payload.size || DEFAULT_TRANSACTION_LIST_SIZE, - "x-continuation-token": action.payload.continuationToken + "x-continuation-token": action.payload.continuationToken, + is_debtor: action.payload.noticeCategory === "debtor" || undefined, + is_payer: action.payload.noticeCategory === "payer" || undefined }, "Authorization" ); diff --git a/ts/features/payments/bizEventsTransaction/screens/PaymentsTransactionBizEventsListScreen.tsx b/ts/features/payments/bizEventsTransaction/screens/PaymentsTransactionBizEventsListScreen.tsx index 41acb4548b8..7eb39287251 100644 --- a/ts/features/payments/bizEventsTransaction/screens/PaymentsTransactionBizEventsListScreen.tsx +++ b/ts/features/payments/bizEventsTransaction/screens/PaymentsTransactionBizEventsListScreen.tsx @@ -4,7 +4,6 @@ import { H2, IOStyles, ListItemHeader, - ListItemTransaction, VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; @@ -28,7 +27,6 @@ import { walletTransactionBizEventsListPotSelector } from "../store/selectors"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { isPaymentsTransactionsEmptySelector } from "../../home/store/selectors"; import { PaymentsBizEventsListItemTransaction } from "../components/PaymentsBizEventsListItemTransaction"; -import { PaymentsHomeEmptyScreenContent } from "../../home/components/PaymentsHomeEmptyScreenContent"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; import { groupTransactionsByMonth } from "../utils"; @@ -37,6 +35,11 @@ import { PaymentsTransactionBizEventsRoutes } from "../navigation/routes"; import { PaymentsTransactionRoutes } from "../../transaction/navigation/routes"; import { NoticeListItem } from "../../../../../definitions/pagopa/biz-events/NoticeListItem"; import * as analytics from "../analytics"; +import { PaymentsBizEventsFilterTabs } from "../components/PaymentsBizEventsFilterTabs"; +import { PaymentBizEventsCategoryFilter } from "../types"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import { PaymentsBizEventsFadeInOutAnimationView } from "../components/PaymentsBizEventsFadeInOutAnimationView"; +import { PaymentsBizEventsTransactionLoadingList } from "../components/PaymentsBizEventsTransactionLoadingList"; export type PaymentsTransactionBizEventsListScreenProps = RouteProp< PaymentsTransactionBizEventsParamsList, @@ -57,6 +60,8 @@ const PaymentsTransactionBizEventsListScreen = () => { const [continuationToken, setContinuationToken] = React.useState< string | undefined >(); + const [noticeCategory, setNoticeCategory] = + React.useState("all"); const [groupedTransactions, setGroupedTransactions] = React.useState>>(); const insets = useSafeAreaInsets(); @@ -105,6 +110,19 @@ const PaymentsTransactionBizEventsListScreen = () => { setTitleHeight(height); }; + const fetchNextPage = () => { + if (!continuationToken || isLoading) { + return; + } + dispatch( + getPaymentsBizEventsTransactionsAction.request({ + noticeCategory, + continuationToken, + onSuccess: handleOnSuccess + }) + ); + }; + const handleOnSuccess = (paginationToken?: string) => { setContinuationToken(paginationToken); setIsRefreshing(false); @@ -115,6 +133,18 @@ const PaymentsTransactionBizEventsListScreen = () => { dispatch( getPaymentsBizEventsTransactionsAction.request({ firstLoad: true, + noticeCategory, + onSuccess: handleOnSuccess + }) + ); + }; + + const handleCategorySelected = (category: PaymentBizEventsCategoryFilter) => { + setNoticeCategory(category); + dispatch( + getPaymentsBizEventsTransactionsAction.request({ + firstLoad: true, + noticeCategory: category, onSuccess: handleOnSuccess }) ); @@ -132,12 +162,6 @@ const PaymentsTransactionBizEventsListScreen = () => { }, [dispatch]) ); - React.useEffect(() => { - if (pot.isSome(transactionsPot)) { - setGroupedTransactions(groupTransactionsByMonth(transactionsPot.value)); - } - }, [transactionsPot]); - useHeaderSecondLevel({ title: I18n.t("features.payments.transactions.title"), supportRequest: true, @@ -147,6 +171,12 @@ const PaymentsTransactionBizEventsListScreen = () => { } }); + React.useEffect(() => { + if (pot.isSome(transactionsPot)) { + setGroupedTransactions(groupTransactionsByMonth(transactionsPot.value)); + } + }, [transactionsPot]); + const SectionListHeaderTitle = (

{ > {I18n.t("features.payments.transactions.title")}

+ +
); @@ -171,36 +206,27 @@ const PaymentsTransactionBizEventsListScreen = () => { const renderLoadingFooter = () => ( <> - {isLoading && - Array.from({ length: 5 }).map((_, index) => ( - - ))} - {!isLoading && !continuationToken && } + {isLoading && ( + + )} + {!isLoading && !continuationToken && noticeCategory === "all" && ( + + )} ); - if (isEmpty) { - return ; - } - - const fetchNextPage = () => { - if (!continuationToken || isLoading) { - return; - } - dispatch( - getPaymentsBizEventsTransactionsAction.request({ - continuationToken, - onSuccess: handleOnSuccess - }) - ); - }; + const EmptyStateList = isEmpty ? ( + + + + ) : undefined; return ( { scrollIndicatorInsets={{ right: 0 }} contentContainerStyle={{ ...IOStyles.horizontalContentPadding, + minHeight: isEmpty ? "100%" : undefined, paddingBottom: insets.bottom + 24 }} onEndReached={fetchNextPage} @@ -226,13 +253,16 @@ const PaymentsTransactionBizEventsListScreen = () => { renderSectionHeader={({ section }) => ( )} + ListEmptyComponent={EmptyStateList} ListFooterComponent={renderLoadingFooter} keyExtractor={item => `transaction_${item.eventId}`} renderItem={({ item }) => ( - handleNavigateToTransactionDetails(item)} - transaction={item} - /> + + handleNavigateToTransactionDetails(item)} + transaction={item} + /> + )} /> ); diff --git a/ts/features/payments/bizEventsTransaction/store/actions/index.ts b/ts/features/payments/bizEventsTransaction/store/actions/index.ts index 64eb3810229..e05f953f989 100644 --- a/ts/features/payments/bizEventsTransaction/store/actions/index.ts +++ b/ts/features/payments/bizEventsTransaction/store/actions/index.ts @@ -2,9 +2,11 @@ import { ActionType, createAsyncAction } from "typesafe-actions"; import { NetworkError } from "../../../../../utils/errors"; import { NoticeListWrapResponse } from "../../../../../../definitions/pagopa/biz-events/NoticeListWrapResponse"; import { NoticeDetailResponse } from "../../../../../../definitions/pagopa/biz-events/NoticeDetailResponse"; +import { PaymentBizEventsCategoryFilter } from "../../types"; export type PaymentsGetBizEventsTransactionPayload = { firstLoad?: boolean; + noticeCategory?: PaymentBizEventsCategoryFilter; size?: number; continuationToken?: string; onSuccess?: (continuationToken?: string) => void; diff --git a/ts/features/payments/bizEventsTransaction/store/reducers/index.ts b/ts/features/payments/bizEventsTransaction/store/reducers/index.ts index f41c1750685..6b5a415b319 100644 --- a/ts/features/payments/bizEventsTransaction/store/reducers/index.ts +++ b/ts/features/payments/bizEventsTransaction/store/reducers/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; import { Action } from "../../../../../store/actions/types"; @@ -58,9 +59,12 @@ const reducer = ( }; // GET TRANSACTIONS LIST case getType(getPaymentsBizEventsTransactionsAction.request): + const transactions = action.payload.firstLoad + ? pot.noneLoading + : pot.toLoading(state.transactions); return { ...state, - transactions: pot.toLoading(state.transactions) + transactions }; case getType(getPaymentsBizEventsTransactionsAction.success): const previousTransactions = pot.getOrElse(state.transactions, []); diff --git a/ts/features/payments/bizEventsTransaction/types/index.ts b/ts/features/payments/bizEventsTransaction/types/index.ts new file mode 100644 index 00000000000..6c90e0004b5 --- /dev/null +++ b/ts/features/payments/bizEventsTransaction/types/index.ts @@ -0,0 +1,7 @@ +export const paymentsBizEventsCategoryFilters = [ + "all", + "payer", + "debtor" +] as const; +export type PaymentBizEventsCategoryFilter = + (typeof paymentsBizEventsCategoryFilters)[number]; diff --git a/ts/features/payments/bizEventsTransaction/utils/types.ts b/ts/features/payments/bizEventsTransaction/utils/types.ts index 3466b0794b9..5cccf916caa 100644 --- a/ts/features/payments/bizEventsTransaction/utils/types.ts +++ b/ts/features/payments/bizEventsTransaction/utils/types.ts @@ -7,8 +7,7 @@ import * as t from "io-ts"; */ export const BizEventsHeaders = t.type({ map: t.type({ - "x-continuation-token": t.string, - "content-disposition": t.string + "x-continuation-token": t.string }) });