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

feat(ui): enhance merchant monitoring report status component #3061

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -105,6 +105,10 @@
"error": "Error occurred while creating merchant checks.",
"is_example": "Please contact Ballerine at [email protected] for access to this feature."
},
"business_report_status_update": {
"success": "Merchant check status updated successfully.",
"error": "Error occurred while updating merchant check status."
},
"note_created": {
"success": "Note added successfully.",
"error": "Error occurred while adding note."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const buttonVariants = cva(
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
status: 'focus-visible:ring-0 focus-visible:ring-offset-0 focus:!bg-[#F4F6FD] bg-[#F4F6FD]',
},
size: {
default: 'h-10 py-2 px-4',
Expand Down
4 changes: 0 additions & 4 deletions apps/backoffice-v2/src/domains/business-reports/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import qs from 'qs';
import { z } from 'zod';
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 {
MERCHANT_REPORT_STATUSES,
MERCHANT_REPORT_STATUSES_MAP,
MERCHANT_REPORT_TYPES,
MERCHANT_REPORT_VERSIONS,
MerchantReportStatus,
MerchantReportType,
MerchantReportVersion,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { t } from 'i18next';
import { toast } from 'sonner';
import { isObject, MerchantReportType } from '@ballerine/common';
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { HttpError } from '@/common/errors/http-error';
import { createBusinessReportBatch } from '@/domains/business-reports/fetchers';
import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery';
import { isObject } from '@ballerine/common';
import { MerchantReportType } from '@/domains/business-reports/constants';

export const useCreateBusinessReportBatchMutation = ({
reportType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { MerchantReportType } from '@ballerine/common';

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';
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 { MerchantReportType } from '@ballerine/common';

import { isString } from '@/common/utils/is-string/is-string';
import { businessReportsQueryKey } from '@/domains/business-reports/query-keys';
import { MerchantReportType } from '@/domains/business-reports/constants';
import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated';

export const useLatestBusinessReportQuery = ({
businessId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { MerchantReportType } from '@ballerine/common';
import { createQueryKeys } from '@lukemorales/query-key-factory';

import {
fetchBusinessReportById,
fetchBusinessReports,
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', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import { fetchWorkflowDocumentOCRResult } from '@/domains/workflows/fetchers';
import { toast } from 'sonner';
import { t } from 'i18next';
import { workflowsQueryKeys } from '@/domains/workflows/query-keys';
import { useFilterId } from '@/common/hooks/useFilterId/useFilterId';
import { isEmptyObject } from '@ballerine/common';

export const useDocumentOcr = ({ workflowId }: { workflowId: string }) => {
const filterId = useFilterId();
const workflowById = workflowsQueryKeys.byId({ workflowId, filterId });
const queryClient = useQueryClient();

return useMutation({
Expand All @@ -18,7 +15,7 @@ export const useDocumentOcr = ({ workflowId }: { workflowId: string }) => {
documentId,
});
},
onSuccess: (data, variables) => {
onSuccess: data => {
void queryClient.invalidateQueries(workflowsQueryKeys._def);

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const useDefaultBlocksLogic = () => {
omitPropsFromObjectWhitelist({
object: workflow?.context?.pluginsOutput,
whitelist: registryInfoWhitelist,
}),
}) ?? {},
[workflow?.context?.pluginsOutput],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { z } from 'zod';
import React, { ComponentProps } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { SubmitHandler, useForm } from 'react-hook-form';
import { MERCHANT_REPORT_STATUSES_MAP } from '@ballerine/common';
import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
TextArea,
} from '@ballerine/ui';

import { Form } from '@/common/components/organisms/Form/Form';
import { Button } from '@/common/components/atoms/Button/Button';
import { FormItem } from '@/common/components/organisms/Form/Form.Item';
import { FormField } from '@/common/components/organisms/Form/Form.Field';
import { FormLabel } from '@/common/components/organisms/Form/Form.Label';
import { FormControl } from '@/common/components/organisms/Form/Form.Control';
import { FormMessage } from '@/common/components/organisms/Form/Form.Message';
import { useUpdateReportStatusMutation } from '@/pages/MerchantMonitoring/components/MerchantMonitoringReportStatus/hooks/useUpdateReportStatusMutation/useUpdateReportStatusMutation';
import {
MerchantMonitoringStatusBadge,
statusToData,
} from '@/pages/MerchantMonitoring/components/MerchantMonitoringReportStatus/MerchantMonitoringStatusBadge';
import { useToggle } from '@/common/hooks/useToggle/useToggle';
import { useCreateNoteMutation } from '@/domains/notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation';
import { DialogDropdownItem } from '@/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page';
import { MerchantMonitoringStatusButton } from './MerchantMonitoringReportStatusButton';

const selectableStatuses = [
MERCHANT_REPORT_STATUSES_MAP['pending-review'],
MERCHANT_REPORT_STATUSES_MAP['under-review'],
MERCHANT_REPORT_STATUSES_MAP.completed,
];
Comment on lines +35 to +39
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

Move selectableStatuses to @ballerine/common package.

The selectableStatuses array should be moved to the @ballerine/common package to maintain consistency and reusability across the codebase.


const MerchantMonitoringCompletedStatusFormSchema = z.object({
text: z.string().optional(),
});

export const MerchantMonitoringReportStatus = ({
status,
reportId,
businessId,
onClick,
}: {
reportId?: string;
businessId?: string;
status?: keyof typeof statusToData;
onClick?: ComponentProps<typeof Button>['onClick'];
}) => {
const { mutateAsync: mutateCreateNote } = useCreateNoteMutation({ disableToast: true });

const { mutate: mutateUpdateReportStatus, isLoading } = useUpdateReportStatusMutation();

const formDefaultValues = {
text: '',
} satisfies z.infer<typeof MerchantMonitoringCompletedStatusFormSchema>;

const form = useForm({
resolver: zodResolver(MerchantMonitoringCompletedStatusFormSchema),
defaultValues: formDefaultValues,
});

const [isCompleteReviewModalOpen, setIsCompleteReviewModalOpen] = useToggle(false);

const onSubmit: SubmitHandler<
z.infer<typeof MerchantMonitoringCompletedStatusFormSchema>
> = async ({ text }) => {
mutateUpdateReportStatus({ reportId, status: MERCHANT_REPORT_STATUSES_MAP.completed, text });

const content = `Status changed to 'Review Completed' ${text ? ` with details: ${text}` : ''}`;

void mutateCreateNote({
content,
entityId: businessId ?? '',
entityType: 'Business',
noteableId: reportId ?? '',
noteableType: 'Report',
parentNoteId: null,
});
Comment on lines +78 to +85
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for mutateCreateNote.

The mutateCreateNote call is wrapped in a void operator without proper error handling. Consider handling potential errors to ensure data consistency.

-    void mutateCreateNote({
+    try {
+      await mutateCreateNote({
       content,
       entityId: businessId ?? '',
       entityType: 'Business',
       noteableId: reportId ?? '',
       noteableType: 'Report',
       parentNoteId: null,
-    });
+      });
+    } catch (error) {
+      // Handle error appropriately
+      console.error('Failed to create note:', error);
+      // Consider showing an error toast or rolling back the status update
+    }
📝 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
void mutateCreateNote({
content,
entityId: businessId ?? '',
entityType: 'Business',
noteableId: reportId ?? '',
noteableType: 'Report',
parentNoteId: null,
});
try {
await mutateCreateNote({
content,
entityId: businessId ?? '',
entityType: 'Business',
noteableId: reportId ?? '',
noteableType: 'Report',
parentNoteId: null,
});
} catch (error) {
// Handle error appropriately
console.error('Failed to create note:', error);
// Consider showing an error toast or rolling back the status update
}


setIsCompleteReviewModalOpen(false);
form.reset();
};

if (!status || !reportId) {
return null;
}

return (
<>
<DropdownMenu>
<DropdownMenuTrigger
className={`flex items-center focus-visible:outline-none`}
disabled={
isLoading ||
[
MERCHANT_REPORT_STATUSES_MAP['in-progress'],
MERCHANT_REPORT_STATUSES_MAP['quality-control'],
MERCHANT_REPORT_STATUSES_MAP['completed'],
].includes(status)
}
>
<MerchantMonitoringStatusBadge disabled={isLoading} status={status} />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className={`space-y-2 p-4`}
onEscapeKeyDown={e => {
if (isCompleteReviewModalOpen) {
e.preventDefault();
}

setIsCompleteReviewModalOpen(false);
}}
>
{selectableStatuses.map(selectableStatus =>
selectableStatus === MERCHANT_REPORT_STATUSES_MAP.completed ? (
<DialogDropdownItem
key={selectableStatus}
className="flex w-full cursor-pointer items-center p-0"
triggerChildren={
<MerchantMonitoringStatusButton disabled={isLoading} status={selectableStatus} />
}
open={isCompleteReviewModalOpen}
onOpenChange={() => {
const activeElement = document.activeElement as HTMLElement;

if (activeElement) {
activeElement.blur();
}

setIsCompleteReviewModalOpen();
}}
>
<DialogHeader>
<DialogTitle>Confirm Review Completion</DialogTitle>
<DialogDescription>
Please provide any relevant details or findings regarding the review. This can
include notes or conclusions drawn from the investigation.
</DialogDescription>
</DialogHeader>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
name="text"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Additional details</FormLabel>

<FormControl>
<TextArea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<DialogFooter className="mt-6 flex justify-end space-x-4">
<Button
type="button"
onClick={() => {
setIsCompleteReviewModalOpen(false);
}}
variant="ghost"
>
Cancel
</Button>
<Button type="submit" variant="destructive">
Complete Review
</Button>
</DialogFooter>
</form>
</Form>
</DialogDropdownItem>
) : (
<DropdownMenuItem
key={selectableStatus}
className="flex w-full cursor-pointer items-center p-0"
>
<MerchantMonitoringStatusButton
status={selectableStatus}
disabled={selectableStatus === status || isLoading}
onClick={e => {
e.preventDefault();
e.stopPropagation();

mutateUpdateReportStatus({ reportId, status: selectableStatus });
}}
/>
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ctw } from '@ballerine/ui';
import React, { ComponentProps } from 'react';

import { Button } from '@/common/components/atoms/Button/Button';
import {
statusToData,
MerchantMonitoringStatusBadge,
} from '@/pages/MerchantMonitoring/components/MerchantMonitoringReportStatus/MerchantMonitoringStatusBadge';

export const MerchantMonitoringStatusButton = ({
status,
onClick,
disabled = false,
}: ComponentProps<typeof Button> & { disabled?: boolean; status: keyof typeof statusToData }) => (
<Button
onClick={e => {
if (disabled) {
e.stopPropagation();

return;
}

onClick?.(e);
}}
variant={'status'}
className={ctw(`flex h-16 w-80 flex-col items-start justify-center space-y-1 px-4 py-2`, {
'!cursor-not-allowed': disabled,
})}
>
<MerchantMonitoringStatusBadge status={status} disabled={disabled} />
<span className={`text-xs font-semibold leading-5 text-[#94A3B8]`}>
{statusToData[status].text}
</span>
</Button>
);
Loading