From 332dadc1909548d523def774c8360ce3be34f05c Mon Sep 17 00:00:00 2001 From: Thomas Lathuiliere <40292402+balzdur@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:40:52 +0200 Subject: [PATCH] feat(transfer-alerts): list and detail pages (#485) * feat(transfer): add sender_account_type to transfer data * feat(transfer-alerts): list and detail pages --- .../public/locales/en/navigation.json | 3 +- .../public/locales/en/transfercheck.json | 22 ++- .../app-builder/src/components/HelpCenter.tsx | 2 +- .../app-builder/src/components/Navigation.tsx | 1 + .../components/TransferAlerts/AlertData.tsx | 148 ++++++++++++++++++ .../components/TransferAlerts/AlertsList.tsx | 105 +++++++++++++ .../components/TransferAlerts/alerts-i18n.ts | 3 + .../src/components/Transfers/TransferData.tsx | 71 ++++++--- packages/app-builder/src/models/transfer.ts | 2 + .../src/routes/_builder+/_layout.tsx | 2 +- .../_builder+/workflows+/$scenarioId.tsx | 2 +- .../src/routes/transfercheck+/_layout.tsx | 7 + .../transfercheck+/alerts+/$alertId.tsx | 105 +++++++++++++ .../routes/transfercheck+/alerts+/_index.tsx | 56 +++++++ .../transfercheck+/transfers+/$transferId.tsx | 2 +- .../app-builder/src/utils/routes/routes.ts | 35 +++-- .../app-builder/src/utils/routes/types.ts | 7 +- .../openapis/transfercheck-api.yaml | 4 + .../src/generated/transfercheck-api.ts | 1 + packages/ui-icons/src/generated/icon-names.ts | 1 + .../src/generated/icons-svg-sprite.svg | 2 +- .../ui-icons/svgs/icons/notifications.svg | 1 + 22 files changed, 543 insertions(+), 39 deletions(-) create mode 100644 packages/app-builder/src/components/TransferAlerts/AlertData.tsx create mode 100644 packages/app-builder/src/components/TransferAlerts/AlertsList.tsx create mode 100644 packages/app-builder/src/components/TransferAlerts/alerts-i18n.ts create mode 100644 packages/app-builder/src/routes/transfercheck+/alerts+/$alertId.tsx create mode 100644 packages/app-builder/src/routes/transfercheck+/alerts+/_index.tsx create mode 100644 packages/ui-icons/svgs/icons/notifications.svg diff --git a/packages/app-builder/public/locales/en/navigation.json b/packages/app-builder/public/locales/en/navigation.json index d57b0bad..c5b8f5c0 100644 --- a/packages/app-builder/public/locales/en/navigation.json +++ b/packages/app-builder/public/locales/en/navigation.json @@ -24,5 +24,6 @@ "settings.case_manager": "Case Manager", "settings.inboxes": "Inboxes", "settings.tags": "Tags", - "transfercheck.transfers": "Transfers" + "transfercheck.transfers": "Transfers", + "transfercheck.alerts": "Alerts" } diff --git a/packages/app-builder/public/locales/en/transfercheck.json b/packages/app-builder/public/locales/en/transfercheck.json index d5429782..b30f9b9d 100644 --- a/packages/app-builder/public/locales/en/transfercheck.json +++ b/packages/app-builder/public/locales/en/transfercheck.json @@ -6,6 +6,26 @@ "transfer_detail.status.suspected_fraud": "Suspected fraud", "transfer_detail.status.confirmed_fraud": "Confirmed fraud", "transfer_detail.title": "Transfer details", + "transfer_detail.transfer_status.title": "Transfer status", "transfer_detail.transfer_data.title": "Transfer data", - "transfer_detail.transfer_data.description": "Here is the transfer id associated to the source information system transfer_id: {{transferId}}" + "transfer_detail.transfer_data.description": "Here is the transfer id associated to the source information system transfer_id: {{transferId}}", + "transfer_detail.transfer_data.sender": "Sender", + "transfer_detail.transfer_data.beneficiary": "Beneficiary", + "transfer_detail.transfer_data.label": "Label", + "transfer_detail.transfer_data.currency": "Currency", + "transfer_detail.transfer_data.value": "Value", + "transfer_detail.transfer_data.requested_at": "Requested at", + "transfer_detail.transfer_data.created_at": "Created at", + "transfer_detail.transfer_data.updated_at": "Updated at", + "transfer_detail.transfer_data.account_id": "Account id", + "transfer_detail.transfer_data.account_type": "Account type", + "transfer_detail.transfer_data.bic": "BIC", + "transfer_detail.transfer_data.device": "Device", + "transfer_detail.transfer_data.ip": "IP", + "transfer_detail.transfer_data.name": "Name", + "alerts.empty": "You have no alerts", + "alerts.search.empty": "No alert found", + "alert_detail.title": "Alert details", + "alert_detail.alert_data.title": "Alert data", + "alert_detail.alert_data.transfer_id": "Here is the transfer id associated to this alert transfer_id: {{transferId}}" } diff --git a/packages/app-builder/src/components/HelpCenter.tsx b/packages/app-builder/src/components/HelpCenter.tsx index b2133ca6..dee969c1 100644 --- a/packages/app-builder/src/components/HelpCenter.tsx +++ b/packages/app-builder/src/components/HelpCenter.tsx @@ -234,7 +234,7 @@ export function useMarbleCoreResources() { return t('navigation:scenarios'); if (location.pathname.startsWith(getRoute('/lists/'))) return t('navigation:lists'); - if (location.pathname.startsWith(getRoute('/workflows/'))) + if (location.pathname.startsWith(getRoute('/workflows'))) return t('navigation:workflows'); if (location.pathname.startsWith(getRoute('/data'))) return t('navigation:data'); diff --git a/packages/app-builder/src/components/Navigation.tsx b/packages/app-builder/src/components/Navigation.tsx index caee16cb..c6ce257c 100644 --- a/packages/app-builder/src/components/Navigation.tsx +++ b/packages/app-builder/src/components/Navigation.tsx @@ -6,6 +6,7 @@ import { type IconProps } from 'packages/ui-icons/src/Icon'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; +//TODO(split apps): refactor this to be translation agnostic: directly pass the translated string (it will help separate the navigation.json file per "app") export const navigationI18n = ['navigation'] satisfies Namespace; export interface SidebarLinkProps { diff --git a/packages/app-builder/src/components/TransferAlerts/AlertData.tsx b/packages/app-builder/src/components/TransferAlerts/AlertData.tsx new file mode 100644 index 00000000..3211cba7 --- /dev/null +++ b/packages/app-builder/src/components/TransferAlerts/AlertData.tsx @@ -0,0 +1,148 @@ +import { type TransferAlert } from '@app-builder/models/transfer-alert'; +import { + formatDateRelative, + formatDateTime, + useFormatLanguage, +} from '@app-builder/utils/format'; +import { useGetCopyToClipboard } from '@app-builder/utils/use-get-copy-to-clipboard'; +import { Trans, useTranslation } from 'react-i18next'; +import { Tooltip } from 'ui-design-system'; + +import { Callout } from '../Callout'; +import { alertsI18n } from './alerts-i18n'; + +interface AlertDataProps { + alert: TransferAlert; +} + +export function AlertData({ alert }: AlertDataProps) { + const { t } = useTranslation(alertsI18n); + const language = useFormatLanguage(); + const getCopyToClipboardProps = useGetCopyToClipboard(); + + return ( +
+ +
+ + {formatDateTime(alert.createdAt, { + language, + })} + + } + > + + {formatDateRelative(alert.createdAt, { + language, + })} + + + + {alert.message} +
+
+ + + + + + + + + + + + + +
+ Sender +
+ IBAN + + {alert.beneficiaryIban} +
+ + + + + + + + + + + + + +
+ Beneficiary +
+ IBAN + + {alert.beneficiaryIban} +
+ +

+ , + TransferIdValue: ( + + ), + }} + values={{ + transferId: alert.transferEndToEndId, + }} + /> +

+
+ ); +} + +export function AlertData2({ alert }: AlertDataProps) { + const language = useFormatLanguage(); + + return ( +
+ +
+ + {formatDateTime(alert.createdAt, { + language, + })} + + } + > + + {formatDateRelative(alert.createdAt, { + language, + })} + + + + {alert.message} +
+
+ +
+ Transfer ID + {alert.transferEndToEndId} + + Sender IBAN + {alert.senderIban} + + Beneficiary IBAN + {alert.beneficiaryIban} +
+
+ ); +} diff --git a/packages/app-builder/src/components/TransferAlerts/AlertsList.tsx b/packages/app-builder/src/components/TransferAlerts/AlertsList.tsx new file mode 100644 index 00000000..2d96a7b8 --- /dev/null +++ b/packages/app-builder/src/components/TransferAlerts/AlertsList.tsx @@ -0,0 +1,105 @@ +import { type TransferAlert } from '@app-builder/models/transfer-alert'; +import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format'; +import { getRoute } from '@app-builder/utils/routes'; +import { fromUUID } from '@app-builder/utils/short-uuid'; +import { Link } from '@remix-run/react'; +import { createColumnHelper, getCoreRowModel } from '@tanstack/react-table'; +import clsx from 'clsx'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Table, useTable } from 'ui-design-system'; + +import { alertsI18n } from './alerts-i18n'; + +interface AlertsListProps { + alerts: TransferAlert[]; +} + +const columnHelper = createColumnHelper(); + +export function AlertsList({ alerts }: AlertsListProps) { + const { t } = useTranslation(alertsI18n); + const language = useFormatLanguage(); + + const columns = React.useMemo( + () => [ + columnHelper.accessor((row) => row.status, { + id: 'status', + header: 'Status', + size: 50, + }), + columnHelper.accessor((row) => row.message, { + id: 'message', + header: 'Message', + size: 200, + cell: ({ getValue }) => { + const message = getValue(); + return
{message}
; + }, + }), + columnHelper.accessor((row) => row.createdAt, { + id: 'createdAt', + header: 'Created At', + size: 100, + cell: ({ getValue }) => { + const dateTime = getValue(); + return ( + + ); + }, + }), + ], + [language], + ); + + const { rows, table, getBodyProps, getContainerProps } = useTable({ + data: alerts, + columns, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + rowLink: (alert) => ( + + ), + }); + + if (rows.length === 0 || alerts.length === 0) { + const emptyMessage = + alerts.length === 0 + ? t('transfercheck:alerts.empty') + : t('transfercheck:alerts.search.empty'); + return ( +
+

{emptyMessage}

+
+ ); + } + + return ( + + + + {rows.map((row) => { + const bgClassName = + row.original.status === 'unread' ? 'bg-grey-00' : 'transparent'; + return ( + + ); + })} + + + ); +} diff --git a/packages/app-builder/src/components/TransferAlerts/alerts-i18n.ts b/packages/app-builder/src/components/TransferAlerts/alerts-i18n.ts new file mode 100644 index 00000000..8717c61f --- /dev/null +++ b/packages/app-builder/src/components/TransferAlerts/alerts-i18n.ts @@ -0,0 +1,3 @@ +import { type Namespace } from 'i18next'; + +export const alertsI18n = ['common', 'transfercheck'] satisfies Namespace; diff --git a/packages/app-builder/src/components/Transfers/TransferData.tsx b/packages/app-builder/src/components/Transfers/TransferData.tsx index 1b310a3b..58405d28 100644 --- a/packages/app-builder/src/components/Transfers/TransferData.tsx +++ b/packages/app-builder/src/components/Transfers/TransferData.tsx @@ -17,6 +17,7 @@ interface TransferDataProps { currency: string; label: string; senderAccountId: string; + senderAccountType: string; senderBic: string; senderDevice: string; senderIp: string; @@ -55,20 +56,28 @@ export function TransferData(props: TransferDataProps) {
- Label + + {t('transfercheck:transfer_detail.transfer_data.label')} + {props.label} - Currency + + {t('transfercheck:transfer_detail.transfer_data.currency')} + {props.currency} - Value + + {t('transfercheck:transfer_detail.transfer_data.value')} + {formatNumber(props.value, { language })}
- requested at + + {t('transfercheck:transfer_detail.transfer_data.requested_at')} + {formatDateTime(props.transferRequestedAt, { language, @@ -76,7 +85,9 @@ export function TransferData(props: TransferDataProps) { })} - created at + + {t('transfercheck:transfer_detail.transfer_data.created_at')} + {formatDateTime(props.createdAt, { language, @@ -84,7 +95,9 @@ export function TransferData(props: TransferDataProps) { })} - updated at + + {t('transfercheck:transfer_detail.transfer_data.updated_at')} + {formatDateTime(props.updatedAt, { language, @@ -96,39 +109,50 @@ export function TransferData(props: TransferDataProps) { - - - + + + + - -
- Sender + + {t('transfercheck:transfer_detail.transfer_data.sender')}
- Account ID + + {t('transfercheck:transfer_detail.transfer_data.account_id')} {props.senderAccountId}
- BIC + + {t('transfercheck:transfer_detail.transfer_data.account_type')} + + {props.senderAccountType} +
+ {t('transfercheck:transfer_detail.transfer_data.bic')} {props.senderBic}
- Device + + {t('transfercheck:transfer_detail.transfer_data.device')} {props.senderDevice}
- IP + + {t('transfercheck:transfer_detail.transfer_data.ip')} {props.senderIp} @@ -140,23 +164,26 @@ export function TransferData(props: TransferDataProps) { - - -
- Beneficiary + + {t('transfercheck:transfer_detail.transfer_data.beneficiary')}
- BIC + + {t('transfercheck:transfer_detail.transfer_data.bic')} {props.beneficiaryBic}
- Name + + {t('transfercheck:transfer_detail.transfer_data.name')} {props.beneficiaryName} diff --git a/packages/app-builder/src/models/transfer.ts b/packages/app-builder/src/models/transfer.ts index 5d842c96..cd3cacd0 100644 --- a/packages/app-builder/src/models/transfer.ts +++ b/packages/app-builder/src/models/transfer.ts @@ -13,6 +13,7 @@ export interface TransferData { currency: string; label: string; senderAccountId: string; + senderAccountType: 'physical_person' | 'moral_person'; senderBic: string; senderDevice: string; senderIp: string; @@ -35,6 +36,7 @@ export function adaptTransferData( currency: transferDataDto.currency, label: transferDataDto.label, senderAccountId: transferDataDto.sender_account_id, + senderAccountType: transferDataDto.sender_account_type, senderBic: transferDataDto.sender_bic, senderDevice: transferDataDto.sender_device, senderIp: transferDataDto.sender_ip, diff --git a/packages/app-builder/src/routes/_builder+/_layout.tsx b/packages/app-builder/src/routes/_builder+/_layout.tsx index 87289e36..39e6082d 100644 --- a/packages/app-builder/src/routes/_builder+/_layout.tsx +++ b/packages/app-builder/src/routes/_builder+/_layout.tsx @@ -168,7 +168,7 @@ export default function Builder() {
  • ( )} diff --git a/packages/app-builder/src/routes/_builder+/workflows+/$scenarioId.tsx b/packages/app-builder/src/routes/_builder+/workflows+/$scenarioId.tsx index f7e67eee..a3e8e174 100644 --- a/packages/app-builder/src/routes/_builder+/workflows+/$scenarioId.tsx +++ b/packages/app-builder/src/routes/_builder+/workflows+/$scenarioId.tsx @@ -102,7 +102,7 @@ export async function action({ request, params }: LoaderFunctionArgs) { : t('workflows:toast.success.create_workflow'), }); - return redirect(getRoute('/workflows/'), { + return redirect(getRoute('/workflows'), { headers: { 'Set-Cookie': await commitSession(session) }, }); } diff --git a/packages/app-builder/src/routes/transfercheck+/_layout.tsx b/packages/app-builder/src/routes/transfercheck+/_layout.tsx index 7d200bbf..c0647fa2 100644 --- a/packages/app-builder/src/routes/transfercheck+/_layout.tsx +++ b/packages/app-builder/src/routes/transfercheck+/_layout.tsx @@ -109,6 +109,13 @@ export default function Builder() { )} />
  • +
  • + } + /> +
  • diff --git a/packages/app-builder/src/routes/transfercheck+/alerts+/$alertId.tsx b/packages/app-builder/src/routes/transfercheck+/alerts+/$alertId.tsx new file mode 100644 index 00000000..0d5e0840 --- /dev/null +++ b/packages/app-builder/src/routes/transfercheck+/alerts+/$alertId.tsx @@ -0,0 +1,105 @@ +import { ErrorComponent, Page } from '@app-builder/components'; +import { + AlertData, + AlertData2, +} from '@app-builder/components/TransferAlerts/AlertData'; +import { alertsI18n } from '@app-builder/components/TransferAlerts/alerts-i18n'; +import { isNotFoundHttpError } from '@app-builder/models'; +import { serverServices } from '@app-builder/services/init.server'; +import { handleParseParamError } from '@app-builder/utils/http/handle-errors'; +import { notFound } from '@app-builder/utils/http/http-responses'; +import { parseParamsSafe } from '@app-builder/utils/input-validation'; +import { getRoute } from '@app-builder/utils/routes'; +import { shortUUIDSchema } from '@app-builder/utils/schema/shortUUIDSchema'; +import { json, type LoaderFunctionArgs } from '@remix-run/node'; +import { useLoaderData, useRouteError } from '@remix-run/react'; +import { captureRemixErrorBoundaryError } from '@sentry/remix'; +import { type Namespace } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { Collapsible } from 'ui-design-system'; +import { z } from 'zod'; + +export const handle = { + i18n: ['common', 'navigation', ...alertsI18n] satisfies Namespace, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { authService } = serverServices; + const { transferAlertRepository } = await authService.isAuthenticated( + request, + { + failureRedirect: getRoute('/sign-in'), + }, + ); + + const parsedParam = await parseParamsSafe( + params, + z.object({ alertId: shortUUIDSchema }), + ); + if (!parsedParam.success) { + return handleParseParamError(request, parsedParam.error); + } + const { alertId } = parsedParam.data; + + try { + const alert = await transferAlertRepository.getAlert({ + alertId, + }); + + return json({ + alert, + }); + } catch (error) { + if (isNotFoundHttpError(error)) { + return notFound(null); + } else { + throw error; + } + } +} + +export default function AlertDetailPage() { + const { t } = useTranslation(handle.i18n); + const { alert } = useLoaderData(); + + return ( + + +
    + + + {t('transfercheck:alert_detail.title')} + +
    +
    + + +
    + + + {t('transfercheck:alert_detail.alert_data.title')} + + + + + + + + {t('transfercheck:alert_detail.alert_data.title')} + + + + + +
    +
    +
    + ); +} + +export function ErrorBoundary() { + const error = useRouteError(); + captureRemixErrorBoundaryError(error); + + return ; +} diff --git a/packages/app-builder/src/routes/transfercheck+/alerts+/_index.tsx b/packages/app-builder/src/routes/transfercheck+/alerts+/_index.tsx new file mode 100644 index 00000000..0d47c5db --- /dev/null +++ b/packages/app-builder/src/routes/transfercheck+/alerts+/_index.tsx @@ -0,0 +1,56 @@ +import { Page } from '@app-builder/components'; +import { alertsI18n } from '@app-builder/components/TransferAlerts/alerts-i18n'; +import { AlertsList } from '@app-builder/components/TransferAlerts/AlertsList'; +import { serverServices } from '@app-builder/services/init.server'; +import { getRoute } from '@app-builder/utils/routes'; +import { json, type LoaderFunctionArgs } from '@remix-run/node'; +import { useLoaderData } from '@remix-run/react'; +import { type Namespace } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { Icon } from 'ui-icons'; + +export const handle = { + i18n: ['common', 'navigation', ...alertsI18n] satisfies Namespace, +}; + +export async function loader({ request }: LoaderFunctionArgs) { + const { authService } = serverServices; + const { transferAlertRepository } = await authService.isAuthenticated( + request, + { + failureRedirect: getRoute('/sign-in'), + }, + ); + + const alerts = await transferAlertRepository.listAlerts(); + + return json({ + alerts, + }); +} + +export default function AlertsPage() { + const { t } = useTranslation(handle.i18n); + const { alerts } = useLoaderData(); + + return ( + + + + {t('navigation:transfercheck.alerts')} + + + + {/* + TODO: + - think about split between received and sent alerts + - add filters (local at least) + - add pagination (local at least) + - add sorting (local at least) + - add search (local at least) + */} + + + + ); +} diff --git a/packages/app-builder/src/routes/transfercheck+/transfers+/$transferId.tsx b/packages/app-builder/src/routes/transfercheck+/transfers+/$transferId.tsx index a4943d45..7f3fb5b9 100644 --- a/packages/app-builder/src/routes/transfercheck+/transfers+/$transferId.tsx +++ b/packages/app-builder/src/routes/transfercheck+/transfers+/$transferId.tsx @@ -141,7 +141,7 @@ export default function TransferDetailPage() {
    - {t('transfercheck:transfer_detail.transfer_data.title')} + {t('transfercheck:transfer_detail.transfer_status.title')} \ No newline at end of file + \ No newline at end of file diff --git a/packages/ui-icons/svgs/icons/notifications.svg b/packages/ui-icons/svgs/icons/notifications.svg new file mode 100644 index 00000000..83b3d6e1 --- /dev/null +++ b/packages/ui-icons/svgs/icons/notifications.svg @@ -0,0 +1 @@ + \ No newline at end of file