From 25df8901faedcffa2e0469fc1a73f35c6b2331c7 Mon Sep 17 00:00:00 2001 From: Shane <66246046+shanegrouber@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:32:23 +0200 Subject: [PATCH 1/4] feat(business-reports): add UI for filtering by merchant type - Update reportType to accept 'All' alongside existing options - Modify query parameters to exclude type when 'All' is selected - Integrate a dropdown for selecting report types in the Merchant Monitoring page (Your dropdown is so user-friendly, it practically holds their hand through the process) --- .../src/domains/business-reports/fetchers.ts | 4 +- .../useBusinessReportsQuery.tsx | 2 +- .../domains/business-reports/query-keys.ts | 2 +- .../MerchantMonitoring.page.tsx | 37 +++++++++++++++++-- .../get-merchant-monitoring-search-schema.ts | 12 +++++- .../useMerchantMonitoringLogic.tsx | 31 ++++++++++++++-- .../workflows-service/prisma/data-migrations | 2 +- 7 files changed, 77 insertions(+), 13 deletions(-) diff --git a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts index 78d5a3f251..282083eeb7 100644 --- a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts +++ b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts @@ -88,7 +88,7 @@ export const fetchBusinessReports = async ({ reportType, ...params }: { - reportType: MerchantReportType; + reportType: MerchantReportType | 'All'; page: { number: number; size: number; @@ -98,7 +98,7 @@ export const fetchBusinessReports = async ({ const queryParams = qs.stringify( { ...params, - type: reportType, + ...(reportType !== 'All' && { type: reportType }), }, { encode: false }, ); diff --git a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx index 35ccbad732..47002e099c 100644 --- a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx +++ b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx @@ -12,7 +12,7 @@ export const useBusinessReportsQuery = ({ sortBy, sortDir, }: { - reportType: MerchantReportType; + reportType: MerchantReportType | 'All'; search: string; page: number; pageSize: number; diff --git a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts index f7c417ee52..72d5d3389f 100644 --- a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts +++ b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts @@ -15,7 +15,7 @@ export const businessReportsQueryKey = createQueryKeys('business-reports', { sortDir, ...params }: { - reportType: MerchantReportType; + reportType: MerchantReportType | 'All'; search: string; page: number; pageSize: number; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx index 77ef7582a3..ed2d831620 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx @@ -5,15 +5,23 @@ import { useMerchantMonitoringLogic } from '@/pages/MerchantMonitoring/hooks/use import { NoBusinessReports } from '@/pages/MerchantMonitoring/components/NoBusinessReports/NoBusinessReports'; import { MerchantMonitoringTable } from '@/pages/MerchantMonitoring/components/MerchantMonitoringTable/MerchantMonitoringTable'; import { buttonVariants } from '@/common/components/atoms/Button/Button'; -import { Plus, Table2 } from 'lucide-react'; +import { Plus, SlidersHorizontal, Table2 } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Search } from '@/common/components/molecules/Search'; -import { Skeleton } from '@ballerine/ui'; +import { + Button, + DropdownMenuTrigger, + DropdownMenu, + Skeleton, + DropdownMenuContent, + DropdownMenuCheckboxItem, +} from '@ballerine/ui'; import { TooltipProvider } from '@/common/components/atoms/Tooltip/Tooltip.Provider'; import { Tooltip } from '@/common/components/atoms/Tooltip/Tooltip'; import { TooltipTrigger } from '@/common/components/atoms/Tooltip/Tooltip.Trigger'; import { TooltipContent } from '@/common/components/atoms/Tooltip/Tooltip.Content'; import { t } from 'i18next'; +import { titleCase } from 'string-ts'; export const MerchantMonitoring: FunctionComponent = () => { const { @@ -31,6 +39,9 @@ export const MerchantMonitoring: FunctionComponent = () => { locale, createBusinessReport, createBusinessReportBatch, + reportType, + onReportTypeChange, + REPORT_TYPE_TO_DISPLAY_TEXT, } = useMerchantMonitoringLogic(); return ( @@ -90,8 +101,28 @@ export const MerchantMonitoring: FunctionComponent = () => { -
+
+ + + + + + {Object.entries(REPORT_TYPE_TO_DISPLAY_TEXT).map(([type, displayText]) => ( + + onReportTypeChange(type as keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) + } + > + {displayText} + + ))} + +
{isNonEmptyArray(businessReports) && } diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts index 2671b17e1c..d42eb90689 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts @@ -2,6 +2,7 @@ import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validat import { z } from 'zod'; import { TBusinessReport } from '@/domains/business-reports/fetchers'; import { BooleanishRecordSchema } from '@ballerine/ui'; +import { REPORT_TYPE_TO_DISPLAY_TEXT } from './hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const getMerchantMonitoringSearchSchema = () => BaseSearchSchema.extend({ @@ -14,15 +15,24 @@ export const getMerchantMonitoringSearchSchema = () => 'business.country', 'riskScore', 'status', + 'reportType', ] as const satisfies ReadonlyArray< | Extract< keyof NonNullable, - 'createdAt' | 'updatedAt' | 'riskScore' | 'status' + 'createdAt' | 'updatedAt' | 'riskScore' | 'status' | 'reportType' > | 'business.website' | 'business.companyName' | 'business.country' >) .catch('createdAt'), + reportType: z + .enum([ + ...(Object.values(REPORT_TYPE_TO_DISPLAY_TEXT) as [ + (typeof REPORT_TYPE_TO_DISPLAY_TEXT)['All'], + ...Array<(typeof REPORT_TYPE_TO_DISPLAY_TEXT)[keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT]>, + ]), + ]) + .catch('All'), selected: BooleanishRecordSchema.optional(), }); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index b0392aecdf..563a34f507 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -6,15 +6,28 @@ import { useLocale } from '@/common/hooks/useLocale/useLocale'; import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; import { useSearch } from '@/common/hooks/useSearch/useSearch'; +export const REPORT_TYPE_TO_DISPLAY_TEXT = { + All: 'All', + MERCHANT_REPORT_T1: 'Onboarding', + ONGOING_MERCHANT_REPORT_T1: 'Monitoring', +} as const; + +const DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE = { + All: 'All', + MERCHANT_REPORT_T1: 'MERCHANT_REPORT_T1', + ONGOING_MERCHANT_REPORT_T1: 'ONGOING_MERCHANT_REPORT_T1', +} as const; + export const useMerchantMonitoringLogic = () => { const locale = useLocale(); const { data: customer } = useCustomerQuery(); const MerchantMonitoringSearchSchema = getMerchantMonitoringSearchSchema(); - const [{ page, pageSize, sortBy, sortDir, search: searchParamValue }] = useZodSearchParams( - MerchantMonitoringSearchSchema, - ); + const [ + { page, pageSize, sortBy, sortDir, search: searchParamValue, reportType }, + setSearchParams, + ] = useZodSearchParams(MerchantMonitoringSearchSchema); const { search: searchTerm, onSearch } = useSearch({ initialSearch: searchParamValue, @@ -23,7 +36,10 @@ export const useMerchantMonitoringLogic = () => { const search = searchTerm as string; const { data, isLoading: isLoadingBusinessReports } = useBusinessReportsQuery({ - reportType: 'MERCHANT_REPORT_T1', + reportType: + DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE[ + reportType as keyof typeof DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE + ], search, page, pageSize, @@ -31,6 +47,10 @@ export const useMerchantMonitoringLogic = () => { sortDir, }); + const onReportTypeChange = (reportType: keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) => { + setSearchParams({ reportType: REPORT_TYPE_TO_DISPLAY_TEXT[reportType] }); + }; + const { onPaginate, onPrevPage, onNextPage, onLastPage, isLastPage } = usePagination({ totalPages: data?.totalPages ?? 0, }); @@ -51,5 +71,8 @@ export const useMerchantMonitoringLogic = () => { onPaginate, isLastPage, locale, + reportType, + onReportTypeChange, + REPORT_TYPE_TO_DISPLAY_TEXT, }; }; diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index f7a9926755..bfc772b0ad 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit f7a9926755b98bc9e1c908a81b86baf9824bc7f1 +Subproject commit bfc772b0ade3ae49465629d6c85ac26aac3796ab From 0d282c439acaa1eadafccde26be419a7b470b37f Mon Sep 17 00:00:00 2001 From: Shane <66246046+shanegrouber@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:59:06 +0200 Subject: [PATCH 2/4] feat(business-reports): add risk level filtering to business reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate risk level filters in the business reports fetching logic - Update the MerchantMonitoring component to support risk level selection - Enhance search schema to include risk level as an optional filter (You've just added complexity like it's a family reunion—everyone’s confused!) --- .../src/domains/business-reports/fetchers.ts | 4 +++ .../useBusinessReportsQuery.tsx | 13 ++++++- .../domains/business-reports/query-keys.ts | 2 ++ .../MerchantMonitoring.page.tsx | 18 +++++++++- .../get-merchant-monitoring-search-schema.ts | 15 +++++++- .../useMerchantMonitoringLogic.tsx | 34 ++++++++++++++++++- 6 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts index 282083eeb7..9d82771336 100644 --- a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts +++ b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts @@ -14,6 +14,7 @@ import { MerchantReportVersion, } from '@/domains/business-reports/constants'; import { UnknownRecord } from 'type-fest'; +import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const BusinessReportSchema = z .object({ @@ -86,9 +87,11 @@ export const fetchLatestBusinessReport = async ({ export const fetchBusinessReports = async ({ reportType, + riskLevel, ...params }: { reportType: MerchantReportType | 'All'; + riskLevel: Array<(typeof RISK_LEVELS)[number]>; page: { number: number; size: number; @@ -99,6 +102,7 @@ export const fetchBusinessReports = async ({ { ...params, ...(reportType !== 'All' && { type: reportType }), + riskLevel: riskLevel, }, { encode: false }, ); diff --git a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx index 47002e099c..57de088021 100644 --- a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx +++ b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { businessReportsQueryKey } from '@/domains/business-reports/query-keys'; import { isString } from '@/common/utils/is-string/is-string'; import { MerchantReportType } from '@/domains/business-reports/constants'; +import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const useBusinessReportsQuery = ({ reportType, @@ -11,6 +12,7 @@ export const useBusinessReportsQuery = ({ pageSize, sortBy, sortDir, + riskLevel, }: { reportType: MerchantReportType | 'All'; search: string; @@ -18,11 +20,20 @@ export const useBusinessReportsQuery = ({ pageSize: number; sortBy: string; sortDir: string; + riskLevel: Array<(typeof RISK_LEVELS)[number]>; }) => { const isAuthenticated = useIsAuthenticated(); return useQuery({ - ...businessReportsQueryKey.list({ reportType, search, page, pageSize, sortBy, sortDir }), + ...businessReportsQueryKey.list({ + reportType, + search, + page, + pageSize, + sortBy, + sortDir, + riskLevel, + }), enabled: isAuthenticated && isString(reportType) && diff --git a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts index 72d5d3389f..07f4a2c8fa 100644 --- a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts +++ b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts @@ -6,6 +6,7 @@ import { fetchLatestBusinessReport, } from '@/domains/business-reports/fetchers'; import { MerchantReportType } from '@/domains/business-reports/constants'; +import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const businessReportsQueryKey = createQueryKeys('business-reports', { list: ({ @@ -21,6 +22,7 @@ export const businessReportsQueryKey = createQueryKeys('business-reports', { pageSize: number; sortBy: string; sortDir: string; + riskLevel: Array<(typeof RISK_LEVELS)[number]>; }) => ({ queryKey: [{ page, pageSize, sortBy, sortDir, ...params }], queryFn: () => { diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx index ed2d831620..dc20d50899 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx @@ -21,7 +21,7 @@ import { Tooltip } from '@/common/components/atoms/Tooltip/Tooltip'; import { TooltipTrigger } from '@/common/components/atoms/Tooltip/Tooltip.Trigger'; import { TooltipContent } from '@/common/components/atoms/Tooltip/Tooltip.Content'; import { t } from 'i18next'; -import { titleCase } from 'string-ts'; +import { MultiSelect } from '@/common/components/atoms/MultiSelect/MultiSelect'; export const MerchantMonitoring: FunctionComponent = () => { const { @@ -42,6 +42,9 @@ export const MerchantMonitoring: FunctionComponent = () => { reportType, onReportTypeChange, REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTERS, + onRiskLevelChange, + onClearSelect, } = useMerchantMonitoringLogic(); return ( @@ -123,6 +126,19 @@ export const MerchantMonitoring: FunctionComponent = () => { ))} + +
+ {RISK_LEVEL_FILTERS.map(({ title, options }) => ( + + ))} +
{isNonEmptyArray(businessReports) && } diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts index d42eb90689..39dc8dea04 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts @@ -2,7 +2,10 @@ import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validat import { z } from 'zod'; import { TBusinessReport } from '@/domains/business-reports/fetchers'; import { BooleanishRecordSchema } from '@ballerine/ui'; -import { REPORT_TYPE_TO_DISPLAY_TEXT } from './hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; +import { + REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVELS, +} from './hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const getMerchantMonitoringSearchSchema = () => BaseSearchSchema.extend({ @@ -35,4 +38,14 @@ export const getMerchantMonitoringSearchSchema = () => ]) .catch('All'), selected: BooleanishRecordSchema.optional(), + riskLevel: z + .array( + z.enum( + RISK_LEVELS.map(riskLevel => riskLevel.toLowerCase()) as [ + (typeof RISK_LEVELS)[number], + ...Array<(typeof RISK_LEVELS)[number]>, + ], + ), + ) + .optional(), }); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index 563a34f507..92eeb6fd03 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -5,6 +5,7 @@ import { usePagination } from '@/common/hooks/usePagination/usePagination'; import { useLocale } from '@/common/hooks/useLocale/useLocale'; import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; import { useSearch } from '@/common/hooks/useSearch/useSearch'; +import { useCallback } from 'react'; export const REPORT_TYPE_TO_DISPLAY_TEXT = { All: 'All', @@ -18,6 +19,18 @@ const DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE = { ONGOING_MERCHANT_REPORT_T1: 'ONGOING_MERCHANT_REPORT_T1', } as const; +export const RISK_LEVELS = ['Critical', 'High', 'Medium', 'Low'] as const; + +const RISK_LEVEL_FILTERS = [ + { + title: 'Risk Level', + options: RISK_LEVELS.map(riskLevel => ({ + label: riskLevel, + value: riskLevel.toLowerCase(), + })), + }, +]; + export const useMerchantMonitoringLogic = () => { const locale = useLocale(); const { data: customer } = useCustomerQuery(); @@ -25,7 +38,7 @@ export const useMerchantMonitoringLogic = () => { const MerchantMonitoringSearchSchema = getMerchantMonitoringSearchSchema(); const [ - { page, pageSize, sortBy, sortDir, search: searchParamValue, reportType }, + { page, pageSize, sortBy, sortDir, search: searchParamValue, reportType, riskLevel }, setSearchParams, ] = useZodSearchParams(MerchantMonitoringSearchSchema); @@ -45,12 +58,28 @@ export const useMerchantMonitoringLogic = () => { pageSize, sortBy, sortDir, + riskLevel: riskLevel ?? [], }); const onReportTypeChange = (reportType: keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) => { setSearchParams({ reportType: REPORT_TYPE_TO_DISPLAY_TEXT[reportType] }); }; + const onRiskLevelChange = useCallback( + (selected: unknown[]) => { + const selectedRiskLevels = selected as Array<(typeof RISK_LEVELS)[number]>; + + setSearchParams({ + riskLevel: selectedRiskLevels, + }); + }, + [setSearchParams], + ); + + const onClearSelect = useCallback(() => { + setSearchParams({ riskLevel: [] }); + }, [setSearchParams]); + const { onPaginate, onPrevPage, onNextPage, onLastPage, isLastPage } = usePagination({ totalPages: data?.totalPages ?? 0, }); @@ -74,5 +103,8 @@ export const useMerchantMonitoringLogic = () => { reportType, onReportTypeChange, REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTERS, + onRiskLevelChange, + onClearSelect, }; }; From 1ab4ea509e8e2742afeceb091d1a2641358569e9 Mon Sep 17 00:00:00 2001 From: Shane <66246046+shanegrouber@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:23:27 +0200 Subject: [PATCH 3/4] feat(MerchantMonitoring): add status filters to reports - Refactor MultiSelect components to include status filters - Update handling functions for new status parameter (your code is now as organized as a folder full of junk drawers) --- .../atoms/MultiSelect/MultiSelect.tsx | 13 +----- .../src/domains/business-reports/fetchers.ts | 10 ++++- .../useBusinessReportsQuery.tsx | 8 +++- .../domains/business-reports/query-keys.ts | 6 ++- .../MerchantMonitoring.page.tsx | 39 ++++++++++------ .../get-merchant-monitoring-search-schema.ts | 13 +++++- .../useMerchantMonitoringLogic.tsx | 45 ++++++++++++++----- .../AlertsFilters/AlertsFilters.tsx | 3 +- 8 files changed, 94 insertions(+), 43 deletions(-) diff --git a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx index 02c64a0242..78d5bdbaac 100644 --- a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx +++ b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx @@ -39,13 +39,11 @@ export const MultiSelect = < }, >({ title, - selectedValues, + selectedValues: selected, onSelect, onClearSelect, options, }: IMultiSelectProps) => { - const [selected, setSelected] = useState(selectedValues); - const onSelectChange = useCallback( (value: TOption['value']) => { const isSelected = selected.some(selectedValue => selectedValue === value); @@ -53,7 +51,6 @@ export const MultiSelect = < ? selected.filter(selectedValue => selectedValue !== value) : [...selected, value]; - setSelected(nextSelected); onSelect(nextSelected); }, [onSelect, selected], @@ -125,13 +122,7 @@ export const MultiSelect = < <> - { - onClearSelect(); - setSelected([]); - }} - className="justify-center text-center" - > + Clear filters diff --git a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts index 9d82771336..eb923fe846 100644 --- a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts +++ b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts @@ -14,7 +14,10 @@ import { MerchantReportVersion, } from '@/domains/business-reports/constants'; import { UnknownRecord } from 'type-fest'; -import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; +import { + RISK_LEVELS, + STATUS_OPTIONS, +} from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const BusinessReportSchema = z .object({ @@ -88,10 +91,12 @@ export const fetchLatestBusinessReport = async ({ export const fetchBusinessReports = async ({ reportType, riskLevel, + status, ...params }: { reportType: MerchantReportType | 'All'; riskLevel: Array<(typeof RISK_LEVELS)[number]>; + status: Array<(typeof STATUS_OPTIONS)[number]>; page: { number: number; size: number; @@ -102,7 +107,8 @@ export const fetchBusinessReports = async ({ { ...params, ...(reportType !== 'All' && { type: reportType }), - riskLevel: riskLevel, + riskLevel, + status, }, { encode: false }, ); diff --git a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx index 57de088021..0f0cbd232d 100644 --- a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx +++ b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx @@ -3,7 +3,10 @@ import { useQuery } from '@tanstack/react-query'; import { businessReportsQueryKey } from '@/domains/business-reports/query-keys'; import { isString } from '@/common/utils/is-string/is-string'; import { MerchantReportType } from '@/domains/business-reports/constants'; -import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; +import { + RISK_LEVELS, + STATUS_OPTIONS, +} from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const useBusinessReportsQuery = ({ reportType, @@ -13,6 +16,7 @@ export const useBusinessReportsQuery = ({ sortBy, sortDir, riskLevel, + status, }: { reportType: MerchantReportType | 'All'; search: string; @@ -21,6 +25,7 @@ export const useBusinessReportsQuery = ({ sortBy: string; sortDir: string; riskLevel: Array<(typeof RISK_LEVELS)[number]>; + status: Array<(typeof STATUS_OPTIONS)[number]>; }) => { const isAuthenticated = useIsAuthenticated(); @@ -33,6 +38,7 @@ export const useBusinessReportsQuery = ({ sortBy, sortDir, riskLevel, + status, }), enabled: isAuthenticated && diff --git a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts index 07f4a2c8fa..6b192ad952 100644 --- a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts +++ b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts @@ -6,7 +6,10 @@ import { fetchLatestBusinessReport, } from '@/domains/business-reports/fetchers'; import { MerchantReportType } from '@/domains/business-reports/constants'; -import { RISK_LEVELS } from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; +import { + RISK_LEVELS, + STATUS_OPTIONS, +} from '@/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const businessReportsQueryKey = createQueryKeys('business-reports', { list: ({ @@ -23,6 +26,7 @@ export const businessReportsQueryKey = createQueryKeys('business-reports', { sortBy: string; sortDir: string; riskLevel: Array<(typeof RISK_LEVELS)[number]>; + status: Array<(typeof STATUS_OPTIONS)[number]>; }) => ({ queryKey: [{ page, pageSize, sortBy, sortDir, ...params }], queryFn: () => { diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx index dc20d50899..fdc2691786 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx @@ -43,8 +43,11 @@ export const MerchantMonitoring: FunctionComponent = () => { onReportTypeChange, REPORT_TYPE_TO_DISPLAY_TEXT, RISK_LEVEL_FILTERS, - onRiskLevelChange, - onClearSelect, + STATUS_LEVEL_FILTERS, + handleFilterChange, + handleFilterClear, + riskLevel, + status, } = useMerchantMonitoringLogic(); return ( @@ -127,18 +130,26 @@ export const MerchantMonitoring: FunctionComponent = () => { -
- {RISK_LEVEL_FILTERS.map(({ title, options }) => ( - - ))} -
+ {RISK_LEVEL_FILTERS.map(({ title, accessor, options }) => ( + + ))} + {STATUS_LEVEL_FILTERS.map(({ title, accessor, options }) => ( + + ))}
{isNonEmptyArray(businessReports) && } diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts index 39dc8dea04..ea1453a6c9 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts @@ -5,6 +5,7 @@ import { BooleanishRecordSchema } from '@ballerine/ui'; import { REPORT_TYPE_TO_DISPLAY_TEXT, RISK_LEVELS, + STATUS_OPTIONS, } from './hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic'; export const getMerchantMonitoringSearchSchema = () => @@ -47,5 +48,15 @@ export const getMerchantMonitoringSearchSchema = () => ], ), ) - .optional(), + .catch([]), + status: z + .array( + z.enum( + STATUS_OPTIONS.map(status => status.toLowerCase()) as [ + (typeof STATUS_OPTIONS)[number], + ...Array<(typeof STATUS_OPTIONS)[number]>, + ], + ), + ) + .catch([]), }); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index 92eeb6fd03..53c85af6de 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -24,6 +24,7 @@ export const RISK_LEVELS = ['Critical', 'High', 'Medium', 'Low'] as const; const RISK_LEVEL_FILTERS = [ { title: 'Risk Level', + accessor: 'riskLevel', options: RISK_LEVELS.map(riskLevel => ({ label: riskLevel, value: riskLevel.toLowerCase(), @@ -31,6 +32,19 @@ const RISK_LEVEL_FILTERS = [ }, ]; +export const STATUS_OPTIONS = ['In Progress', 'Quality Control', 'Manual Review'] as const; + +const STATUS_LEVEL_FILTERS = [ + { + title: 'Status', + accessor: 'status', + options: STATUS_OPTIONS.map(status => ({ + label: status, + value: status.toLowerCase(), + })), + }, +]; + export const useMerchantMonitoringLogic = () => { const locale = useLocale(); const { data: customer } = useCustomerQuery(); @@ -38,7 +52,7 @@ export const useMerchantMonitoringLogic = () => { const MerchantMonitoringSearchSchema = getMerchantMonitoringSearchSchema(); const [ - { page, pageSize, sortBy, sortDir, search: searchParamValue, reportType, riskLevel }, + { page, pageSize, sortBy, sortDir, search: searchParamValue, reportType, riskLevel, status }, setSearchParams, ] = useZodSearchParams(MerchantMonitoringSearchSchema); @@ -59,26 +73,32 @@ export const useMerchantMonitoringLogic = () => { sortBy, sortDir, riskLevel: riskLevel ?? [], + status: status ?? [], }); const onReportTypeChange = (reportType: keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) => { setSearchParams({ reportType: REPORT_TYPE_TO_DISPLAY_TEXT[reportType] }); }; - const onRiskLevelChange = useCallback( - (selected: unknown[]) => { - const selectedRiskLevels = selected as Array<(typeof RISK_LEVELS)[number]>; - + const handleFilterChange = useCallback( + (filterKey: string) => (selected: unknown) => { setSearchParams({ - riskLevel: selectedRiskLevels, + [filterKey]: Array.isArray(selected) ? selected : [selected], + page: '1', }); }, [setSearchParams], ); - const onClearSelect = useCallback(() => { - setSearchParams({ riskLevel: [] }); - }, [setSearchParams]); + const handleFilterClear = useCallback( + (filterKey: string) => () => { + setSearchParams({ + [filterKey]: [], + page: '1', + }); + }, + [setSearchParams], + ); const { onPaginate, onPrevPage, onNextPage, onLastPage, isLastPage } = usePagination({ totalPages: data?.totalPages ?? 0, @@ -104,7 +124,10 @@ export const useMerchantMonitoringLogic = () => { onReportTypeChange, REPORT_TYPE_TO_DISPLAY_TEXT, RISK_LEVEL_FILTERS, - onRiskLevelChange, - onClearSelect, + STATUS_LEVEL_FILTERS, + handleFilterChange, + handleFilterClear, + riskLevel, + status, }; }; diff --git a/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx b/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx index 9258e26d9d..d01798d4f6 100644 --- a/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx +++ b/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx @@ -4,7 +4,6 @@ import { MultiSelect } from '@/common/components/atoms/MultiSelect/MultiSelect'; import { useFilter } from '@/common/hooks/useFilter/useFilter'; import { AlertStatuses } from '@/domains/alerts/fetchers'; import { titleCase } from 'string-ts'; -import { keyFactory } from '@/common/utils/key-factory/key-factory'; export const AlertsFilters: FunctionComponent<{ assignees: TUsers; @@ -79,7 +78,7 @@ export const AlertsFilters: FunctionComponent<{
{filters.map(({ title, accessor, options }) => ( Date: Sun, 22 Dec 2024 12:53:29 +0200 Subject: [PATCH 4/4] feat(multi-select): enhance multi-select component with optional props - Add support for left and right icons in multi-select trigger - Refactor button styling in multi-select to accommodate new props - Modify multi-select usage in MerchantMonitoring to utilize new features (Your multi-select options are so numerous, I'm surprised it's not a buffet) --- .../atoms/MultiSelect/MultiSelect.tsx | 31 +++++++++++++++---- .../DateRangePicker/DateRangePicker.tsx | 2 +- .../components/molecules/Search/Search.tsx | 2 +- .../MerchantMonitoring.page.tsx | 12 ++++--- .../MerchantMonitoringTable/columns.tsx | 21 ++++++++----- .../useMerchantMonitoringLogic.tsx | 26 ++++++++++++---- 6 files changed, 68 insertions(+), 26 deletions(-) diff --git a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx index 78d5bdbaac..fc54f15ee5 100644 --- a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx +++ b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx @@ -1,4 +1,5 @@ -import { ReactNode, useCallback, useState } from 'react'; +import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'; +import { ReactNode, useCallback } from 'react'; import { Badge, Button, @@ -14,7 +15,7 @@ import { PopoverContent, PopoverTrigger, } from '@ballerine/ui'; -import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'; + import { Separator } from '@/common/components/atoms/Separator/Separator'; interface IMultiSelectProps< @@ -29,6 +30,16 @@ interface IMultiSelectProps< onSelect: (value: Array) => void; onClearSelect: () => void; options: TOption[]; + props?: { + trigger?: { + leftIcon?: JSX.Element; + rightIcon?: JSX.Element; + className?: string; + title?: { + className?: string; + }; + }; + }; } export const MultiSelect = < @@ -43,6 +54,7 @@ export const MultiSelect = < onSelect, onClearSelect, options, + props, }: IMultiSelectProps) => { const onSelectChange = useCallback( (value: TOption['value']) => { @@ -56,12 +68,18 @@ export const MultiSelect = < [onSelect, selected], ); + const TriggerLeftIcon = props?.trigger?.leftIcon ?? ; + return ( -
)} + {props?.trigger?.rightIcon} diff --git a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx index def20aa47e..1441864d4f 100644 --- a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx @@ -19,7 +19,7 @@ export const DateRangePicker = ({ onChange, value, className }: TDateRangePicker @@ -143,20 +145,22 @@ export const MerchantMonitoring: FunctionComponent = () => { ))} {STATUS_LEVEL_FILTERS.map(({ title, accessor, options }) => ( ))}
diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx index 3ba4b15a7b..97d532ac93 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx @@ -26,7 +26,12 @@ const columnHelper = createColumnHelper(); const SCAN_TYPES = { ONBOARDING: 'Onboarding', MONITORING: 'Monitoring', -}; +} as const; + +const REPORT_STATUS_TO_DISPLAY_STATUS = { + [MERCHANT_REPORT_STATUSES_MAP.completed]: 'Manual Review', + [MERCHANT_REPORT_STATUSES_MAP['quality-control']]: 'Quality Control', +} as const; const REPORT_TYPE_TO_SCAN_TYPE = { [MERCHANT_REPORT_TYPES_MAP.MERCHANT_REPORT_T1]: SCAN_TYPES.ONBOARDING, @@ -152,24 +157,24 @@ export const columns = [
); }, - header: 'Risk Score', + header: 'Risk Level', }), columnHelper.accessor('status', { cell: info => { const status = info.getValue(); - const statusToDisplayStatus = { - [MERCHANT_REPORT_STATUSES_MAP.completed]: 'Manual Review', - [MERCHANT_REPORT_STATUSES_MAP['quality-control']]: 'Quality Control', - } as const; return ( - {titleCase(statusToDisplayStatus[status as keyof typeof statusToDisplayStatus] ?? status)} + {titleCase( + REPORT_STATUS_TO_DISPLAY_STATUS[ + status as keyof typeof REPORT_STATUS_TO_DISPLAY_STATUS + ] ?? status, + )} ); }, diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index c80c231c70..f3b4d0cbdb 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -1,9 +1,15 @@ import dayjs from 'dayjs'; -import { useCallback, ComponentProps } from 'react'; +import { SlidersHorizontal } from 'lucide-react'; +import React, { useCallback, ComponentProps, useMemo } from 'react'; import { useLocale } from '@/common/hooks/useLocale/useLocale'; import { useSearch } from '@/common/hooks/useSearch/useSearch'; import { usePagination } from '@/common/hooks/usePagination/usePagination'; +import { useFindings } from '@/pages/MerchantMonitoring/hooks/useFindings/useFindings'; +import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams'; +import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; +import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; +import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; import { DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE, MerchantMonitoringSearchSchema, @@ -11,11 +17,6 @@ import { RISK_LEVEL_FILTERS, STATUS_LEVEL_FILTERS, } from '@/pages/MerchantMonitoring/schemas'; -import { useFindings } from '@/pages/MerchantMonitoring/hooks/useFindings/useFindings'; -import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams'; -import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; -import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; -import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; export const useMerchantMonitoringLogic = () => { const locale = useLocale(); @@ -81,6 +82,18 @@ export const useMerchantMonitoringLogic = () => { setSearchParams({ from, to }); }; + const multiselectProps = useMemo( + () => ({ + trigger: { + leftIcon: , + title: { + className: `font-normal text-sm`, + }, + }, + }), + [], + ); + return { totalPages: data?.totalPages || 0, totalItems: data?.totalItems || 0, @@ -99,6 +112,7 @@ export const useMerchantMonitoringLogic = () => { locale, reportType, onReportTypeChange, + multiselectProps, REPORT_TYPE_TO_DISPLAY_TEXT, RISK_LEVEL_FILTERS, STATUS_LEVEL_FILTERS,