Skip to content

Commit

Permalink
Individuals profiles page (#2338)
Browse files Browse the repository at this point in the history
* feat(backoffice-v2): added a view for individuals profiles

* refactor(*): moved mock data to server

* made changes to ensure the business column is visible and looks okay

* feat(backoffice-v2): updated ui to reduce task scope

* fix(*): fixed mistakes found in pr

* fix(*): fixed issues found in pr

* refactor(backoffice-v2): renamed no profiles phrasing

* feat(*): added sanctions column

* revert(workflows-service): removed pluralize

* feat(*): updated packages

* feat(*): made changes request by product

* fix(*): improved tooltip, copy component, and fixed correlationId
  • Loading branch information
Omri-Levy committed May 6, 2024
1 parent a1804fc commit 5f4cac1
Show file tree
Hide file tree
Showing 61 changed files with 1,283 additions and 72 deletions.
9 changes: 9 additions & 0 deletions apps/backoffice-v2/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @ballerine/backoffice-v2

## 0.7.7

### Patch Changes

- Updated dependencies
- @ballerine/common@0.9.5
- @ballerine/workflow-browser-sdk@0.6.8
- @ballerine/workflow-node-sdk@0.6.8

## 0.7.6

### Patch Changes
Expand Down
9 changes: 5 additions & 4 deletions apps/backoffice-v2/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ballerine/backoffice-v2",
"version": "0.7.6",
"version": "0.7.7",
"description": "Ballerine - Backoffice",
"homepage": "https://github.com/ballerine-io/ballerine",
"repository": {
Expand Down Expand Up @@ -51,10 +51,10 @@
},
"dependencies": {
"@ballerine/blocks": "0.2.2",
"@ballerine/common": "0.9.4",
"@ballerine/common": "0.9.5",
"@ballerine/ui": "^0.5.1",
"@ballerine/workflow-browser-sdk": "0.6.7",
"@ballerine/workflow-node-sdk": "0.6.7",
"@ballerine/workflow-browser-sdk": "0.6.8",
"@ballerine/workflow-node-sdk": "0.6.8",
"@fontsource/inter": "^4.5.15",
"@formkit/auto-animate": "1.0.0-beta.5",
"@hookform/resolvers": "^3.1.0",
Expand All @@ -74,6 +74,7 @@
"@radix-ui/react-slot": "^1.0.1",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@rjsf/utils": "^5.9.0",
"@tanstack/react-query": "^4.19.1",
"@tanstack/react-table": "^8.9.2",
Expand Down
14 changes: 14 additions & 0 deletions apps/backoffice-v2/src/Router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { NotFoundRedirect } from '@/pages/NotFound/NotFound';
import { TransactionMonitoringAlerts } from '@/pages/TransactionMonitoringAlerts/TransactionMonitoringAlerts.page';
import { TransactionMonitoring } from '@/pages/TransactionMonitoring/TransactionMonitoring';
import { TransactionMonitoringAlertsAnalysisPage } from '@/pages/TransactionMonitoringAlertsAnalysis/TransactionMonitoringAlertsAnalysis.page';
import { Profiles } from '@/pages/Profiles/Profiles.page';
import { Individuals } from '@/pages/Profiles/Individuals/Individuals.page';

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -88,6 +90,18 @@ const router = createBrowserRouter([
},
],
},
{
path: '/:locale/profiles',
element: <Profiles />,
errorElement: <RouteError />,
children: [
{
path: '/:locale/profiles/individuals',
element: <Individuals />,
errorElement: <RouteError />,
},
],
},
{
path: '/:locale/transaction-monitoring',
element: <TransactionMonitoring />,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CopySvg } from '@/common/components/atoms/icons';
import { Button } from '@/common/components/atoms/Button/Button';
import { ComponentProps, FunctionComponent } from 'react';
import { copyToClipboard } from '@/common/utils/copy-to-clipboard/copy-to-clipboard';
import { ctw } from '@ballerine/ui';

interface ICopyToClipboardProps extends ComponentProps<typeof Button> {
textToCopy: string;
}

export const CopyToClipboard: FunctionComponent<ICopyToClipboardProps> = ({
textToCopy,
className,
disabled,
...props
}) => {
return (
<Button
variant={'ghost'}
size={'icon'}
onClick={copyToClipboard(textToCopy)}
className={ctw(
`h-[unset] w-[unset] p-1 opacity-80 hover:bg-transparent hover:opacity-100`,
{
'!bg-transparent opacity-50': disabled,
},
className,
)}
disabled={disabled}
{...props}
>
<CopySvg className={`d-4`} />
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { ctw } from '@ballerine/ui';

export const TooltipContent = forwardRef<
ElementRef<typeof TooltipPrimitive.Content>,
ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={ctw(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
));

TooltipContent.displayName = TooltipPrimitive.Content.displayName;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';

export const TooltipProvider = TooltipPrimitive.Provider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';

export const TooltipTrigger = TooltipPrimitive.Trigger;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';

export const Tooltip = TooltipPrimitive.Root;
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ export const useNavbarLogic = () => {
{
text: 'Individuals',
icon: <Users size={20} />,
children:
individualsFilters?.map(({ id, name }) => ({
children: [
{
text: 'Profiles',
href: `/en/profiles/individuals`,
key: 'nav-item-profile-individuals',
},
...(individualsFilters?.map(({ id, name }) => ({
filterId: id,
text: name,
href: `/en/case-management/entities?filterId=${id}`,
key: `nav-item-${id}`,
})) ?? [],
})) ?? []),
],
key: 'nav-item-individuals',
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { toast } from 'sonner';
import { t } from 'i18next';

export const copyToClipboard = (text: string) => async () => {
await navigator.clipboard.writeText(text);

toast.success(t(`toast:copy_to_clipboard`, { text }));
};
48 changes: 48 additions & 0 deletions apps/backoffice-v2/src/domains/profiles/fetchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { apiClient } from '../../common/api-client/api-client';
import { z } from 'zod';
import { handleZodError } from '../../common/utils/handle-zod-error/handle-zod-error';
import { Method } from '../../common/enums';
import qs from 'qs';
import { getOriginUrl } from '@/common/utils/get-origin-url/get-url-origin';
import { env } from '@/common/env/env';
import { KYCs, Roles } from '@/pages/Profiles/Individuals/components/ProfilesTable/columns';

export const IndividualProfileSchema = z.object({
correlationId: z.string().nullable().optional(),
createdAt: z.string(),
name: z.string(),
businesses: z.string().optional(),
role: z.enum(Roles),
kyc: z.enum(KYCs).or(z.undefined()),
isMonitored: z.boolean(),
matches: z.string(),
alerts: z.number(),
updatedAt: z.string().datetime(),
});

export const IndividualsProfilesSchema = z.array(IndividualProfileSchema);

export type TIndividualProfile = z.infer<typeof IndividualProfileSchema>;

export type TIndividualsProfiles = z.infer<typeof IndividualsProfilesSchema>;

export const fetchIndividualsProfiles = async (params: {
orderBy: string;
page: {
number: number;
size: number;
};
filter: Record<string, unknown>;
}) => {
const queryParams = qs.stringify(params, { encode: false });

const [individualsProfiles, error] = await apiClient({
url: `${getOriginUrl(
env.VITE_API_URL,
)}/api/v1/case-management/profiles/individuals?${queryParams}`,
method: Method.GET,
schema: IndividualsProfilesSchema,
});

return handleZodError(error, individualsProfiles);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { individualsProfilesQueryKeys } from '../../../query-keys';
import { useQuery } from '@tanstack/react-query';

export const useIndividualsProfilesQuery = ({
sortBy,
sortDir,
page,
pageSize,
search,
filter,
}: {
sortBy: string;
sortDir: string;
page: number;
pageSize: number;
search: string;
filter: Record<string, unknown>;
}) => {
return useQuery({
...individualsProfilesQueryKeys.list({
sortBy,
sortDir,
page,
pageSize,
search,
filter,
}),
staleTime: 1_000_000,
refetchInterval: 1_000_000,
});
};
28 changes: 28 additions & 0 deletions apps/backoffice-v2/src/domains/profiles/query-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createQueryKeys } from '@lukemorales/query-key-factory';
import { fetchIndividualsProfiles } from './fetchers';

export const individualsProfilesQueryKeys = createQueryKeys('individuals-profiles', {
list: ({ sortBy, sortDir, page, pageSize, ...params }) => {
const data = {
...params,
orderBy: `${sortBy}:${sortDir}`,
page: {
number: Number(page),
size: Number(pageSize),
},
};

return {
queryKey: [
{
...params,
sortBy,
sortDir,
page,
pageSize,
},
],
queryFn: () => fetchIndividualsProfiles(data),
};
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useFilterId } from '@/common/hooks/useFilterId/useFilterId';
import { useTesseract } from '@/common/hooks/useTesseract/useTesseract';
import { createArrayOfNumbers } from '@/common/utils/create-array-of-numbers/create-array-of-numbers';
import { useStorageFileByIdQuery } from '@/domains/storage/hooks/queries/useStorageFileByIdQuery/useStorageFileByIdQuery';
import { copyToClipboard } from '@/common/utils/copy-to-clipboard/copy-to-clipboard';

export const useDocuments = (documents: IDocumentsProps['documents']) => {
const initialImage = documents?.[0];
Expand Down Expand Up @@ -45,9 +46,7 @@ export const useDocuments = (documents: IDocumentsProps['documents']) => {
throw new Error('No document OCR text found');
}

await navigator.clipboard.writeText(text);

toast.success(t('toast:copy_to_clipboard', { text }));
await copyToClipboard(text)();
} catch (err) {
console.error(err);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { isNonEmptyArray } from '@ballerine/common';
import { useIndividualsLogic } from '@/pages/Profiles/Individuals/hooks/useIndividualsLogic/useIndividualsLogic';
import { ProfilesTable } from '@/pages/Profiles/Individuals/components/ProfilesTable';
import { NoProfiles } from '@/pages/Profiles/Individuals/components/NoProfiles/NoProfiles';
import { ProfilesPagination } from '@/pages/Profiles/Individuals/components/ProfilesPagination/ProfilesPagination';
import { ProfilesHeader } from './components/ProfilesHeader';

export const Individuals = () => {
const {
isLoadingIndividualsProfiles,
individualsProfiles,
page,
onPrevPage,
onNextPage,
onPaginate,
isLastPage,
search,
onSearch,
} = useIndividualsLogic();

return (
<div className="flex h-full flex-col px-6 pb-6 pt-10">
<h1 className="pb-5 text-2xl font-bold">Individuals Profiles</h1>
<div className="flex flex-1 flex-col gap-6 overflow-auto">
<ProfilesHeader search={search} onSearch={onSearch} />
{isNonEmptyArray(individualsProfiles) && <ProfilesTable data={individualsProfiles ?? []} />}
{Array.isArray(individualsProfiles) &&
!individualsProfiles.length &&
!isLoadingIndividualsProfiles && <NoProfiles />}
<div className={`flex items-center gap-x-2`}>
<ProfilesPagination
page={page}
onPrevPage={onPrevPage}
onNextPage={onNextPage}
onPaginate={onPaginate}
isLastPage={isLastPage}
/>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NoCasesSvg } from '@/common/components/atoms/icons';
import { FunctionComponent } from 'react';

export const NoProfiles: FunctionComponent = () => {
return (
<div className="flex items-center justify-center p-4 pb-72">
<div className="inline-flex flex-col items-start gap-4 rounded-md border-[1px] border-[#CBD5E1] p-6">
<div className="flex w-[464px] items-center justify-center">
<NoCasesSvg width={96} height={81} />
</div>

<div className="flex w-[464px] flex-col items-start gap-2">
<h2 className="text-lg font-[600]">No profiles found</h2>

<div className="text-sm leading-[20px]">
<p className="font-[400]">
It looks like there aren&apos;t any profiles in your system right now.
</p>

<div className="mt-[20px] flex flex-col">
<span className="font-[700]">What can you do now?</span>

<ul className="list-disc pl-6 pr-2">
<li>Make sure to refresh or check back often for new profiles.</li>
<li>Ensure that your filters aren&apos;t too narrow.</li>
<li>
If you suspect a technical issue, reach out to your technical team to diagnose the
issue.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
};

0 comments on commit 5f4cac1

Please sign in to comment.