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..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 = < @@ -39,13 +50,12 @@ export const MultiSelect = < }, >({ title, - selectedValues, + selectedValues: selected, onSelect, onClearSelect, options, + props, }: IMultiSelectProps) => { - const [selected, setSelected] = useState(selectedValues); - const onSelectChange = useCallback( (value: TOption['value']) => { const isSelected = selected.some(selectedValue => selectedValue === value); @@ -53,18 +63,23 @@ export const MultiSelect = < ? selected.filter(selectedValue => selectedValue !== value) : [...selected, value]; - setSelected(nextSelected); onSelect(nextSelected); }, [onSelect, selected], ); + const TriggerLeftIcon = props?.trigger?.leftIcon ?? ; + return ( - @@ -125,13 +141,7 @@ export const MultiSelect = < <> - { - onClearSelect(); - setSelected([]); - }} - className="justify-center text-center" - > + Clear filters 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 + + + {Object.entries(REPORT_TYPE_TO_DISPLAY_TEXT).map(([type, displayText]) => ( + + onReportTypeChange(type as keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) + } + > + {displayText} + + ))} + + + {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/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 ea6049ece5..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,37 +1,76 @@ +import dayjs from 'dayjs'; +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 { MerchantMonitoringSearchSchema } from '@/pages/MerchantMonitoring/merchant-monitoring-search-schema'; import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; -import { ComponentProps } from 'react'; -import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; -import dayjs from 'dayjs'; +import { + DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE, + MerchantMonitoringSearchSchema, + REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTERS, + STATUS_LEVEL_FILTERS, +} from '@/pages/MerchantMonitoring/schemas'; export const useMerchantMonitoringLogic = () => { const locale = useLocale(); const { data: customer } = useCustomerQuery(); const { search, debouncedSearch, onSearch } = useSearch(); - const [{ page, pageSize, sortBy, sortDir, from, to }, setSearchParams] = useZodSearchParams( - MerchantMonitoringSearchSchema, - ); + + const [ + { page, pageSize, sortBy, sortDir, reportType, riskLevel, status, from, to }, + setSearchParams, + ] = useZodSearchParams(MerchantMonitoringSearchSchema); const { findings } = useFindings(); 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: debouncedSearch, page, pageSize, sortBy, sortDir, + riskLevel: riskLevel ?? [], + status: status ?? [], from, to, }); + const onReportTypeChange = (reportType: keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) => { + setSearchParams({ reportType: REPORT_TYPE_TO_DISPLAY_TEXT[reportType] }); + }; + + const handleFilterChange = useCallback( + (filterKey: string) => (selected: unknown) => { + setSearchParams({ + [filterKey]: Array.isArray(selected) ? selected : [selected], + page: '1', + }); + }, + [setSearchParams], + ); + + const handleFilterClear = useCallback( + (filterKey: string) => () => { + setSearchParams({ + [filterKey]: [], + page: '1', + }); + }, + [setSearchParams], + ); + const { onPaginate, onPrevPage, onNextPage, onLastPage, isLastPage } = usePagination({ totalPages: data?.totalPages ?? 0, }); @@ -43,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, @@ -59,6 +110,16 @@ export const useMerchantMonitoringLogic = () => { onPaginate, isLastPage, locale, + reportType, + onReportTypeChange, + multiselectProps, + REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTERS, + STATUS_LEVEL_FILTERS, + handleFilterChange, + handleFilterClear, + riskLevel, + status, dates: { from, to }, onDatesChange, }; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/merchant-monitoring-search-schema.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/merchant-monitoring-search-schema.ts deleted file mode 100644 index 63f96b940c..0000000000 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/merchant-monitoring-search-schema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validation-schemas'; -import { z } from 'zod'; -import { TBusinessReport } from '@/domains/business-reports/fetchers'; -import { BooleanishRecordSchema } from '@ballerine/ui'; - -export const MerchantMonitoringSearchSchema = BaseSearchSchema.extend({ - sortBy: z - .enum([ - 'createdAt', - 'updatedAt', - 'business.website', - 'business.companyName', - 'business.country', - 'riskScore', - 'status', - ] as const satisfies ReadonlyArray< - | Extract< - keyof NonNullable, - 'createdAt' | 'updatedAt' | 'riskScore' | 'status' - > - | 'business.website' - | 'business.companyName' - | 'business.country' - >) - .catch('createdAt'), - selected: BooleanishRecordSchema.optional(), - from: z.string().date().optional(), - to: z.string().date().optional(), -}); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts index 40fd335b03..5b64ed23e0 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts @@ -1,3 +1,98 @@ import { z } from 'zod'; +import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validation-schemas'; +import { TBusinessReport } from '@/domains/business-reports/fetchers'; +import { BooleanishRecordSchema } from '@ballerine/ui'; + +export const REPORT_TYPE_TO_DISPLAY_TEXT = { + All: 'All', + MERCHANT_REPORT_T1: 'Onboarding', + ONGOING_MERCHANT_REPORT_T1: 'Monitoring', +} as const; + +export 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 RISK_LEVELS = ['Critical', 'High', 'Medium', 'Low'] as const; + +export const RISK_LEVEL_FILTERS = [ + { + title: 'Risk Level', + accessor: 'riskLevel', + options: RISK_LEVELS.map(riskLevel => ({ + label: riskLevel, + value: riskLevel.toLowerCase(), + })), + }, +]; + +export const STATUS_OPTIONS = ['In Progress', 'Quality Control', 'Manual Review'] as const; + +export const STATUS_LEVEL_FILTERS = [ + { + title: 'Status', + accessor: 'status', + options: STATUS_OPTIONS.map(status => ({ + label: status, + value: status.toLowerCase(), + })), + }, +]; export const FindingsSchema = z.array(z.object({ value: z.string(), title: z.string() })); + +export const MerchantMonitoringSearchSchema = BaseSearchSchema.extend({ + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'business.website', + 'business.companyName', + 'business.country', + 'riskScore', + 'status', + 'reportType', + ] as const satisfies ReadonlyArray< + | Extract< + keyof NonNullable, + 'createdAt' | 'updatedAt' | 'riskScore' | 'status' | 'reportType' + > + | 'business.website' + | 'business.companyName' + | 'business.country' + >) + .catch('createdAt'), + selected: BooleanishRecordSchema.optional(), + 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'), + riskLevel: z + .array( + z.enum( + RISK_LEVELS.map(riskLevel => riskLevel.toLowerCase()) as [ + (typeof RISK_LEVELS)[number], + ...Array<(typeof RISK_LEVELS)[number]>, + ], + ), + ) + .catch([]), + status: z + .array( + z.enum( + STATUS_OPTIONS.map(status => status.toLowerCase()) as [ + (typeof STATUS_OPTIONS)[number], + ...Array<(typeof STATUS_OPTIONS)[number]>, + ], + ), + ) + .catch([]), + from: z.string().date().optional(), + to: z.string().date().optional(), +}); 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 }) => (