Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Epic/feat/documents tracker #3022

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/backoffice-v2/public/locales/en/toast.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,9 @@
"ubo_deleted": {
"success": "UBO successfully removed",
"error": "Error removing UBO"
},
"request_documents": {
"success": "Documents requested successfully.",
"error": "Error occurred while requesting documents."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {
AccordionCard,
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@ballerine/ui';
import { HelpCircle, Loader2, SendIcon } from 'lucide-react';
import { FunctionComponent, memo, useMemo } from 'react';

import { DocumentsTrackerData, TrackedDocument } from '@/domains/documents/hooks/schemas/document';
import { Icon } from './constants';
import { useDocumentTracker } from './hooks/useDocumentTracker';

export const DocumentTracker: FunctionComponent<{ workflowId: string }> = ({ workflowId }) => {
const {
documentTrackerItems,
isLoadingDocuments,
getSubItems,
selectedIdsToRequest,
onRequestDocuments,
open,
onOpenChange,
isRequestButtonDisabled,
} = useDocumentTracker({ workflowId });

return (
<div className={`max-w-xs`}>
<AccordionCard className={`h-full`}>
<AccordionCard.Title
className={`flex-row items-center justify-between`}
rightChildren={
selectedIdsToRequest.length > 0 ? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>
Comment on lines +42 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add aria-label to request button.

The button lacks an accessible label for screen readers.

Apply this improvement:

  <Button
    className="h-7 bg-warning px-2 text-sm"
    disabled={isRequestButtonDisabled}
+   aria-label={`Request ${selectedIdsToRequest.length} documents`}
  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
aria-label={`Request ${selectedIdsToRequest.length} documents`}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>


<DialogContent
onPointerDownOutside={e => e.preventDefault()}
className="px-20 py-12 sm:max-w-2xl"
>
<DialogHeader>
<DialogTitle className="text-4xl">Ask for all requests</DialogTitle>
</DialogHeader>

<DialogDescription>
By clicking the button below, an email with a link will be sent to the customer,
directing them to upload the documents you have marked as requested. The case’s
status will then change to “Revisions” until the customer will provide the
needed documents and fixes.
</DialogDescription>

<DialogFooter>
<Button type="button" onClick={onRequestDocuments}>
Send email
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Comment on lines +41 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance dialog accessibility.

The dialog component is missing important accessibility attributes.

Apply these improvements:

- <Dialog open={open} onOpenChange={onOpenChange}>
+ <Dialog 
+   open={open} 
+   onOpenChange={onOpenChange}
+   aria-label="Request documents dialog"
+ >

Also, ensure keyboard navigation is handled properly:

  <DialogContent
    onPointerDownOutside={e => e.preventDefault()}
+   onEscapeKeyDown={e => onOpenChange(false)}
    className="px-20 py-12 sm:max-w-2xl"
  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>
<DialogContent
onPointerDownOutside={e => e.preventDefault()}
className="px-20 py-12 sm:max-w-2xl"
>
<DialogHeader>
<DialogTitle className="text-4xl">Ask for all requests</DialogTitle>
</DialogHeader>
<DialogDescription>
By clicking the button below, an email with a link will be sent to the customer,
directing them to upload the documents you have marked as requested. The case’s
status will then change to “Revisions” until the customer will provide the
needed documents and fixes.
</DialogDescription>
<DialogFooter>
<Button type="button" onClick={onRequestDocuments}>
Send email
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
open={open}
onOpenChange={onOpenChange}
aria-label="Request documents dialog"
>
<DialogTrigger disabled={isRequestButtonDisabled}>
<Button
className="h-7 bg-warning px-2 text-sm"
disabled={isRequestButtonDisabled}
>
<SendIcon className="mr-1.5 d-4" />
<span className="whitespace-nowrap">
Request{' '}
<span className="text-xs font-bold">{selectedIdsToRequest.length}</span>
</span>
</Button>
</DialogTrigger>
<DialogContent
onPointerDownOutside={e => e.preventDefault()}
onEscapeKeyDown={e => onOpenChange(false)}
className="px-20 py-12 sm:max-w-2xl"
>
<DialogHeader>
<DialogTitle className="text-4xl">Ask for all requests</DialogTitle>
</DialogHeader>
<DialogDescription>
By clicking the button below, an email with a link will be sent to the customer,
directing them to upload the documents you have marked as requested. The case’s
status will then change to “Revisions” until the customer will provide the
needed documents and fixes.
</DialogDescription>
<DialogFooter>
<Button type="button" onClick={onRequestDocuments}>
Send email
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

) : (
<HoverCard openDelay={0}>
<HoverCardTrigger className={`pb-1.5 pt-1`}>
<HelpCircle size={18} className={`stroke-slate-400/70`} />
</HoverCardTrigger>
<HoverCardContent side={'top'} align={'start'}>
<ul className={`flex flex-col space-y-2`}>
<li className={`flex items-center gap-x-2`}>
{Icon.INDICATOR}
Not yet provided
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.CHECK}
Provided
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.MARKED}
Marked as requested
</li>
<li className={`flex items-center gap-x-2`}>
{Icon.REQUESTED}
Requested
</li>
</ul>
</HoverCardContent>
</HoverCard>
)
}
>
Documents
</AccordionCard.Title>
<AccordionCard.Content>
<DocumentTrackerItems
documentTrackerItems={documentTrackerItems}
isLoading={isLoadingDocuments}
getSubItems={getSubItems}
/>
</AccordionCard.Content>
</AccordionCard>
</div>
);
};

type AccordionContentProps = {
documentTrackerItems: DocumentsTrackerData | null | undefined;
isLoading: boolean;
getSubItems: (
doc: TrackedDocument,
) => Parameters<typeof AccordionCard.Item>[number]['subitems'][number];
};
const DocumentTrackerItems = memo(
({ documentTrackerItems, isLoading, getSubItems }: AccordionContentProps) => {
const businessSubitems = useMemo(
() => documentTrackerItems?.business.map(getSubItems).filter(Boolean) ?? [],
[documentTrackerItems?.business, getSubItems],
);
const individualsSubitems = useMemo(
() =>
[
...(documentTrackerItems?.individuals.ubos ?? []),
...(documentTrackerItems?.individuals.directors ?? []),
]
.map(getSubItems)
.filter(Boolean),
[
documentTrackerItems?.individuals.ubos,
documentTrackerItems?.individuals.directors,
getSubItems,
],
);

if (isLoading) {
return (
<div className="flex h-20 animate-spin items-center justify-center">
<Loader2 className="d-6" />
</div>
);
}

if (
!documentTrackerItems ||
[
!documentTrackerItems.business.length,
!documentTrackerItems.individuals.ubos.length,
!documentTrackerItems.individuals.directors.length,
].every(Boolean)
) {
return (
<div className="flex h-20 items-center justify-center text-sm">No documents available</div>
);
}

return (
<>
<AccordionCard.Item
title="Company documents"
value="company-documents"
ulProps={{ className: '[&>li]:py-0' }}
subitems={businessSubitems}
/>

<AccordionCard.Item
title="Individual's documents"
value="individual-documents"
subitems={individualsSubitems}
/>
</>
);
},
);
DocumentTrackerItems.displayName = 'DocumentTrackerItems';
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { CheckCircle } from '@ballerine/ui';
import { FilePlus2Icon } from 'lucide-react';
import { ReactNode } from 'react';

import { ClockCircle } from '@/common/components/atoms/ClockCircle/ClockCircle';
import { IndicatorCircle } from '@/common/components/atoms/IndicatorCircle/IndicatorCircle';
import { XCircle } from '@/common/components/atoms/XCircle/XCircle';
import { TrackedDocument } from '@/domains/documents/hooks/schemas/document';

export const Icon = {
CHECK: (
<CheckCircle
size={18}
className={`stroke-success`}
containerProps={{
className: 'bg-success/20',
}}
/>
),
X: (
<XCircle
size={18}
className={`stroke-destructive`}
containerProps={{
className: 'bg-destructive/20',
}}
/>
),
INDICATOR: (
<IndicatorCircle
size={18}
className={`stroke-transparent`}
containerProps={{
className: 'bg-slate-500/20',
}}
/>
),
REQUESTED: (
<ClockCircle
size={18}
className={`fill-violet-500 stroke-white`}
containerProps={{
className: 'bg-violet-500/20',
}}
/>
),
MARKED: <FilePlus2Icon className="stroke-warning" size={16.5} />,
} as const;

export const documentStatusToIcon: Record<TrackedDocument['status'], ReactNode> = {
unprovided: Icon.INDICATOR,
provided: Icon.CHECK,
requested: Icon.REQUESTED,
} as const;
Loading
Loading