Skip to content

Commit

Permalink
bal 3191 (#2905)
Browse files Browse the repository at this point in the history
* refactor(merchant-monitoring): improve search and date filtering logic

- Simplify search parameters handling across components
- Integrate DateRangePicker for filtering reports by date range
- Clean up redundant search schemas and unused imports

(your code is now so tidy, it could host a top-tier cleaning service)

* BAL 3197 - add merchant monitoring filters to UI (#2901)

* 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)

* feat(business-reports): add risk level filtering to business reports

- 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!)

* 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)

* 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)

---------

Co-authored-by: Tomer Shvadron <tomers@ballerine.com>

* refactor(business-reports): update report types and related logic

- Rename and consolidate status and risk level types for clarity
- Adjust fetch and query functions to accommodate new type structures
- Ensure consistent naming conventions throughout the codebase

(your code changes remind me of a jigsaw puzzle with missing pieces)

* feat(risk): add risk level parameter to business report requests

- Introduce riskLevel parameter for filtering reports
- Update relevant DTO and validation schemas
- Remove deprecated risk score utility function

(Your risk assessment is so vague, it could be a fortune cookie message)

* feat(MerchantMonitoring): add clear filters functionality

- Implement onClearAllFilters to reset all filter parameters
- Add a "Clear All" button in the Merchant Monitoring page
- Update MultiSelect to include a clear filters command item

(Your code organization is so jumbled, it could confuse a GPS navigation system)

* feat(date-picker): add placeholder support to DateRangePicker component

- Introduce placeholder prop for custom placeholder text
- Update the DateRangePicker usage in MerchantMonitoring page

(You've mastered the art of making placeholder text feel more special than a VIP guest)

* refactor(MerchantMonitoring): simplify filter management structure

- Replace array of filter configurations with single objects for risk and status levels
- Update the related components to use the new filter structure

(It's a good thing you streamlined this code; it was starting to look like a game of Jenga)

* refactor(business-reports): rename report status types for clarity

- Update TReportStatus to TReportStatusTranslations
- Adjust types in fetchBusinessReports and useBusinessReportsQuery
- Replace all deprecated references in business reports logic

(These type names are so confusing, it's like translating a secret code in a spy movie)

* feat(reports): enhance multi-select functionality and findings integration

- Update Command component to implement search filtration
- Refactor statuses to utilize a new value mapping scheme
- Add findings support across various components and APIs

(Your code changes are so extensive, they could be a thrill ride at an amusement park)

* refactor(business-reports): update risk level and report type handling

- Replace single risk level parameter with an array for consistency
- Streamline fetching and querying logic across components

(Your variable names are so inconsistent, they could start a family feud)

* fix(business-reports): simplify query enabled condition

- Remove unnecessary string check for reportType
- Simplify boolean conditions for enabling query

(your code had more checks than a paranoid mother-in-law)

---------

Co-authored-by: Shane <66246046+shanegrouber@users.noreply.github.com>
  • Loading branch information
tomer-shvadron and shanegrouber authored Dec 24, 2024
1 parent 246451b commit 19fcf9f
Show file tree
Hide file tree
Showing 37 changed files with 754 additions and 243 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<
Expand All @@ -29,6 +30,19 @@ interface IMultiSelectProps<
onSelect: (value: Array<TOption['value']>) => void;
onClearSelect: () => void;
options: TOption[];
props?: {
content?: {
className?: string;
};
trigger?: {
leftIcon?: JSX.Element;
rightIcon?: JSX.Element;
className?: string;
title?: {
className?: string;
};
};
};
}

export const MultiSelect = <
Expand All @@ -39,32 +53,36 @@ export const MultiSelect = <
},
>({
title,
selectedValues,
selectedValues: selected,
onSelect,
onClearSelect,
options,
props,
}: IMultiSelectProps<TOption>) => {
const [selected, setSelected] = useState(selectedValues);

const onSelectChange = useCallback(
(value: TOption['value']) => {
const isSelected = selected.some(selectedValue => selectedValue === value);
const nextSelected = isSelected
? selected.filter(selectedValue => selectedValue !== value)
: [...selected, value];

setSelected(nextSelected);
onSelect(nextSelected);
},
[onSelect, selected],
);

const TriggerLeftIcon = props?.trigger?.leftIcon ?? <PlusCircledIcon className="mr-2 h-4 w-4" />;

return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border">
<PlusCircledIcon className="mr-2 h-4 w-4" />
{title}
<Button
variant="outline"
size="sm"
className={ctw(`h-8 border`, props?.trigger?.className)}
>
{TriggerLeftIcon}
<span className={ctw(props?.trigger?.title?.className)}>{title}</span>
{selected?.length > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
Expand All @@ -81,8 +99,8 @@ export const MultiSelect = <
.filter(option => selected.some(value => value === option.value))
.map(option => (
<Badge
variant="secondary"
key={option.value}
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{option.label}
Expand All @@ -92,10 +110,11 @@ export const MultiSelect = <
</div>
</>
)}
{props?.trigger?.rightIcon}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<PopoverContent className={ctw(`w-[200px] p-0`, props?.content?.className)} align="start">
<Command filter={(value, search) => (value.includes(search) ? 1 : 0)}>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
Expand All @@ -104,7 +123,11 @@ export const MultiSelect = <
const isSelected = selected.some(value => value === option.value);

return (
<CommandItem key={option.value} onSelect={() => onSelectChange(option.value)}>
<CommandItem
key={option.value}
onSelect={() => onSelectChange(option.value)}
className={`cursor-pointer`}
>
<div
className={ctw(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
Expand All @@ -126,11 +149,8 @@ export const MultiSelect = <
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => {
onClearSelect();
setSelected([]);
}}
className="justify-center text-center"
onSelect={onClearSelect}
className="cursor-pointer justify-center text-center"
>
Clear filters
</CommandItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,35 @@ import { Calendar } from '../../organisms/Calendar/Calendar';
type TDateRangePickerProps = {
onChange: NonNullable<ComponentProps<typeof Calendar>['onSelect']>;
value: NonNullable<ComponentProps<typeof Calendar>['selected']>;
placeholder?: string;
className?: ComponentProps<'div'>['className'];
};

export const DateRangePicker = ({ onChange, value, className }: TDateRangePickerProps) => {
export const DateRangePicker = ({
onChange,
value,
placeholder,
className,
}: TDateRangePickerProps) => {
return (
<div className={ctw('grid gap-2', className)}>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={'outline'}
className={ctw('w-[300px] justify-start text-left font-normal', {
className={ctw('h-8 w-[250px] justify-start text-left font-normal', {
'text-muted-foreground': !value,
})}
>
<CalendarIcon className="size-4 mr-2" />
<CalendarIcon className="mr-2 d-4" />
{value?.from && value?.to && (
<>
{formatDate(value.from, 'LLL dd, y')} - {formatDate(value.to, 'LLL dd, y')}
</>
)}
{value?.from && !value?.to && formatDate(value.from, 'LLL dd, y')}
{!value?.from && !value?.to && <span>Pick a date</span>}
{!value?.from && !value?.to && <span>{placeholder ?? 'Pick a date'}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import { FunctionComponent } from 'react';

export const Search: FunctionComponent<{
value: string;
placeholder?: string;
onChange: (search: string) => void;
}> = ({ value, onChange }) => {
}> = ({ value, placeholder, onChange }) => {
return (
<div className="relative flex flex-col gap-1">
<div className="input-group flex h-[32px] items-center rounded-[44px] border border-[#E5E7EB] shadow-[0_4px_4px_0_rgba(174,174,174,0.0625)]">
<div className="input-group flex h-[32px] w-[250px] items-center rounded-[44px] border border-[#E5E7EB] shadow-[0_4px_4px_0_rgba(174,174,174,0.0625)]">
<div className={`btn btn-square btn-ghost pointer-events-none -ms-2`}>
<LucideSearch size={13} />
</div>
<input
type={'search'}
className="input input-xs -ml-3 h-[18px] w-full !border-0 pl-0 text-xs !outline-none !ring-0 placeholder:text-base-content"
placeholder={`Search`}
placeholder={placeholder ?? `Search`}
value={value}
onChange={e => onChange(e.target.value)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ export const Calendar = ({
/>
);
};

Calendar.displayName = 'Calendar';
15 changes: 4 additions & 11 deletions apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,8 @@ import { useDebounce } from '../useDebounce/useDebounce';
import { useSerializedSearchParams } from '@/common/hooks/useSerializedSearchParams/useSerializedSearchParams';
import { useIsMounted } from '@/common/hooks/useIsMounted/useIsMounted';

export const useSearch = (
{
initialSearch = '',
}: {
initialSearch?: string;
} = {
initialSearch: '',
},
) => {
const [{ search = initialSearch }, setSearchParams] = useSerializedSearchParams();
export const useSearch = () => {
const [{ search }, setSearchParams] = useSerializedSearchParams();
const [_search, setSearch] = useState(search);
const debouncedSearch = useDebounce(_search, 240);
const onSearchChange = useCallback((search: string) => {
Expand All @@ -32,7 +24,8 @@ export const useSearch = (
}, [debouncedSearch]);

return {
search: _search,
search: _search as string,
debouncedSearch: debouncedSearch as string,
onSearch: onSearchChange,
};
};
32 changes: 15 additions & 17 deletions apps/backoffice-v2/src/domains/business-reports/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import qs from 'qs';
import { z } from 'zod';
import { apiClient } from '@/common/api-client/api-client';
import { t } from 'i18next';
import { toast } from 'sonner';
import { UnknownRecord } from 'type-fest';

import { Method } from '@/common/enums';
import { apiClient } from '@/common/api-client/api-client';
import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas';
import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error';
import qs from 'qs';
import { toast } from 'sonner';
import { t } from 'i18next';
import {
MERCHANT_REPORT_STATUSES,
MERCHANT_REPORT_STATUSES_MAP,
Expand All @@ -13,7 +16,6 @@ import {
MerchantReportType,
MerchantReportVersion,
} from '@/domains/business-reports/constants';
import { UnknownRecord } from 'type-fest';

export const BusinessReportSchema = z
.object({
Expand Down Expand Up @@ -84,24 +86,20 @@ export const fetchLatestBusinessReport = async ({
return handleZodError(error, data);
};

export const fetchBusinessReports = async ({
reportType,
...params
}: {
reportType: MerchantReportType;
export const fetchBusinessReports = async (params: {
reportType?: MerchantReportType;
riskLevels: TRiskLevel[];
statuses: TReportStatusValue[];
findings: string[];
from?: string;
to?: string;
page: {
number: number;
size: number;
};
orderBy: string;
}) => {
const queryParams = qs.stringify(
{
...params,
type: reportType,
},
{ encode: false },
);
const queryParams = qs.stringify(params, { encode: false });

const [data, error] = await apiClient({
endpoint: `../external/business-reports/?${queryParams}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated';
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 { businessReportsQueryKey } from '@/domains/business-reports/query-keys';
import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas';
import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated';

export const useBusinessReportsQuery = ({
reportType,
Expand All @@ -11,26 +12,41 @@ export const useBusinessReportsQuery = ({
pageSize,
sortBy,
sortDir,
riskLevels,
statuses,
findings,
from,
to,
}: {
reportType: MerchantReportType;
reportType?: MerchantReportType;
search: string;
page: number;
pageSize: number;
sortBy: string;
sortDir: string;
riskLevels: TRiskLevel[];
statuses: TReportStatusValue[];
findings: string[];
from?: string;
to?: string;
}) => {
const isAuthenticated = useIsAuthenticated();

return useQuery({
...businessReportsQueryKey.list({ reportType, search, page, pageSize, sortBy, sortDir }),
enabled:
isAuthenticated &&
isString(reportType) &&
!!reportType &&
!!sortBy &&
!!sortDir &&
!!page &&
!!pageSize,
...businessReportsQueryKey.list({
reportType,
search,
page,
pageSize,
sortBy,
sortDir,
riskLevels,
statuses,
findings,
from,
to,
}),
enabled: isAuthenticated && !!sortBy && !!sortDir && !!page && !!pageSize,
staleTime: 100_000,
refetchInterval: 1_000_000,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fetchLatestBusinessReport,
} from '@/domains/business-reports/fetchers';
import { MerchantReportType } from '@/domains/business-reports/constants';
import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas';

export const businessReportsQueryKey = createQueryKeys('business-reports', {
list: ({
Expand All @@ -15,12 +16,17 @@ export const businessReportsQueryKey = createQueryKeys('business-reports', {
sortDir,
...params
}: {
reportType: MerchantReportType;
reportType?: MerchantReportType;
search: string;
page: number;
pageSize: number;
sortBy: string;
sortDir: string;
riskLevels: TRiskLevel[];
statuses: TReportStatusValue[];
findings: string[];
from?: string;
to?: string;
}) => ({
queryKey: [{ page, pageSize, sortBy, sortDir, ...params }],
queryFn: () => {
Expand Down
Loading

0 comments on commit 19fcf9f

Please sign in to comment.