Skip to content

Commit

Permalink
ongoing monitoring turning on and off (#2941)
Browse files Browse the repository at this point in the history
* feat(monitoring): implement business monitoring feature

- Add success and error messages for turning monitoring on/off
- Update API endpoints to manage ongoing monitoring status
- Integrate tooltip UI for monitoring status display

(Your code is so reactive that I'm surprised it doesn't require a safe word)

* chore(business-report): remove unused import for BusinessReportMetricsDto

- Eliminate unnecessary import to clean up the code
- Reduces clutter and potential confusion in the module

(your code is so tidy now, it could be a minimalist's dream home)

* fix(report): optimize business report fetching logic

- Simplify monitoring mutation definitions
- Consolidate business fetching to reduce database calls

(Your database queries are so chatty, they could use a good night's sleep)
  • Loading branch information
tomer-shvadron authored Jan 8, 2025
1 parent cb091ec commit 8b6eb3f
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 93 deletions.
8 changes: 8 additions & 0 deletions apps/backoffice-v2/public/locales/en/toast.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@
"empty_extraction": "Unable to extract the document's relevant fields.",
"error": "Failed to perform OCR on the document."
},
"business_monitoring_off": {
"success": "Merchant monitoring has been turned off successfully.",
"error": "Error occurred while turning merchant monitoring off."
},
"business_monitoring_on": {
"success": "Merchant monitoring has been turned on successfully.",
"error": "Error occurred while turning merchant monitoring on."
},
"business_report_creation": {
"success": "Merchant check created successfully.",
"error": "Error occurred while creating a merchant check.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { FunctionComponent, PropsWithChildren } from 'react';

import { queryClient } from '@/lib/react-query/query-client';
import { TooltipProvider } from '../../atoms/Tooltip/Tooltip.Provider';
import { AuthProvider } from '@/domains/auth/context/AuthProvider/AuthProvider';

export const Providers: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
<AuthProvider>
<TooltipProvider>{children}</TooltipProvider>
</AuthProvider>
</QueryClientProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const BusinessReportSchema = z
workflowVersion: z.enum([MERCHANT_REPORT_VERSIONS[0]!, ...MERCHANT_REPORT_VERSIONS.slice(1)]),
isAlert: z.boolean().nullable(),
companyName: z.string().nullish(),
monitoringStatus: z.boolean(),
website: z.object({
id: z.string(),
url: z.string().url(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,25 @@ import { titleCase } from 'string-ts';

import { ctw } from '@/common/utils/ctw/ctw';
import { getSeverityFromRiskScore } from '@ballerine/common';
import { Badge, severityToClassName, TextWithNAFallback, WarningFilledSvg } from '@ballerine/ui';
import {
Badge,
CheckCircle,
severityToClassName,
TextWithNAFallback,
WarningFilledSvg,
} from '@ballerine/ui';
import { useEllipsesWithTitle } from '@/common/hooks/useEllipsesWithTitle/useEllipsesWithTitle';
import { CopyToClipboardButton } from '@/common/components/atoms/CopyToClipboardButton/CopyToClipboardButton';
import { Minus } from 'lucide-react';
import {
MERCHANT_REPORT_STATUSES_MAP,
MERCHANT_REPORT_TYPES_MAP,
} from '@/domains/business-reports/constants';
import React from 'react';
import { IndicatorCircle } from '@/common/components/atoms/IndicatorCircle/IndicatorCircle';
import { TooltipTrigger } from '@/common/components/atoms/Tooltip/Tooltip.Trigger';
import { TooltipContent } from '@/common/components/atoms/Tooltip/Tooltip.Content';
import { Tooltip } from '@/common/components/atoms/Tooltip/Tooltip';

const columnHelper = createColumnHelper<TBusinessReport>();

Expand All @@ -39,15 +50,93 @@ const REPORT_TYPE_TO_SCAN_TYPE = {
} as const;

export const columns = [
columnHelper.accessor('isAlert', {
columnHelper.accessor('companyName', {
cell: info => {
const companyName = info.getValue();

return (
<TextWithNAFallback>
<span className={`ms-4 font-semibold`}>{companyName}</span>
</TextWithNAFallback>
);
},
header: 'Company Name',
}),
columnHelper.accessor('website', {
cell: info => {
const website = info.getValue();

return <TextWithNAFallback>{website}</TextWithNAFallback>;
},
header: 'Website',
}),
columnHelper.accessor('riskScore', {
cell: info => {
const riskScore = info.getValue();
const severity = getSeverityFromRiskScore(riskScore);

return (
<div className="flex items-center gap-2">
{!riskScore && riskScore !== 0 && <TextWithNAFallback className={'py-0.5'} />}
{(riskScore || riskScore === 0) && (
<Badge
className={ctw(
severityToClassName[
(severity?.toUpperCase() as keyof typeof severityToClassName) ?? 'DEFAULT'
],
'w-20 py-0.5 font-bold',
)}
>
{titleCase(severity ?? '')}
</Badge>
)}
</div>
);
},
header: 'Risk Level',
}),
columnHelper.accessor('monitoringStatus', {
cell: ({ getValue }) => {
return getValue() ? (
<WarningFilledSvg className={`ms-4 d-6`} />
<Tooltip delayDuration={300}>
<TooltipTrigger>
<CheckCircle
size={18}
className={`stroke-background`}
containerProps={{
className: 'me-3 bg-success mt-px',
}}
/>
</TooltipTrigger>
<TooltipContent>
This website is actively monitored for changes on a recurring basis
</TooltipContent>
</Tooltip>
) : (
<Minus className={`ms-4 text-[#D9D9D9] d-6`} />
<Tooltip delayDuration={300}>
<TooltipTrigger>
<IndicatorCircle
size={18}
className={`stroke-transparent`}
containerProps={{
className: 'bg-slate-500/20',
}}
/>
</TooltipTrigger>
<TooltipContent>This website is not currently monitored for changes</TooltipContent>
</Tooltip>
);
},
header: 'Alert',
header: () => (
<Tooltip delayDuration={300}>
<TooltipTrigger>
<span className={`max-w-[20ch] truncate`}>Monitored</span>
</TooltipTrigger>
<TooltipContent>
Indicates whether this website is being monitored for changes
</TooltipContent>
</Tooltip>
),
}),
columnHelper.accessor('reportType', {
cell: info => {
Expand All @@ -57,6 +146,16 @@ export const columns = [
},
header: 'Scan Type',
}),
columnHelper.accessor('isAlert', {
cell: ({ getValue }) => {
return getValue() ? (
<WarningFilledSvg className={`d-6`} />
) : (
<Minus className={`text-[#D9D9D9] d-6`} />
);
},
header: 'Alert',
}),
columnHelper.accessor('createdAt', {
cell: info => {
const createdAt = info.getValue();
Expand All @@ -73,32 +172,32 @@ export const columns = [

return (
<div className={`flex flex-col space-y-0.5`}>
<span className={`font-semibold`}>{date}</span>
<span>{date}</span>
<span className={`text-xs text-[#999999]`}>{time}</span>
</div>
);
},
header: 'Created At',
}),
columnHelper.accessor('merchantId', {
cell: info => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- ESLint doesn't like `cell` not being `Cell`.
const { ref, styles } = useEllipsesWithTitle<HTMLSpanElement>();

const id = info.getValue();

return (
<div className={`flex w-full max-w-[12ch] items-center space-x-2`}>
<TextWithNAFallback style={{ ...styles, width: '70%' }} ref={ref}>
{id}
</TextWithNAFallback>

<CopyToClipboardButton textToCopy={id ?? ''} />
</div>
);
},
header: 'Merchant ID',
}),
// columnHelper.accessor('merchantId', {
// cell: info => {
// // eslint-disable-next-line react-hooks/rules-of-hooks -- ESLint doesn't like `cell` not being `Cell`.
// const { ref, styles } = useEllipsesWithTitle<HTMLSpanElement>();
//
// const id = info.getValue();
//
// return (
// <div className={`flex w-full max-w-[12ch] items-center space-x-2`}>
// <TextWithNAFallback style={{ ...styles, width: '70%' }} ref={ref}>
// {id}
// </TextWithNAFallback>
//
// <CopyToClipboardButton textToCopy={id ?? ''} />
// </div>
// );
// },
// header: 'Merchant ID',
// }),
columnHelper.accessor('id', {
cell: info => {
// eslint-disable-next-line react-hooks/rules-of-hooks -- ESLint doesn't like `cell` not being `Cell`.
Expand All @@ -118,47 +217,6 @@ export const columns = [
},
header: 'Report ID',
}),
columnHelper.accessor('website', {
cell: info => {
const website = info.getValue();

return <TextWithNAFallback>{website}</TextWithNAFallback>;
},
header: 'Website',
}),
columnHelper.accessor('companyName', {
cell: info => {
const companyName = info.getValue();

return <TextWithNAFallback>{companyName}</TextWithNAFallback>;
},
header: 'Company Name',
}),
columnHelper.accessor('riskScore', {
cell: info => {
const riskScore = info.getValue();
const severity = getSeverityFromRiskScore(riskScore);

return (
<div className="flex items-center gap-2">
{!riskScore && riskScore !== 0 && <TextWithNAFallback className={'py-0.5'} />}
{(riskScore || riskScore === 0) && (
<Badge
className={ctw(
severityToClassName[
(severity?.toUpperCase() as keyof typeof severityToClassName) ?? 'DEFAULT'
],
'w-20 py-0.5 font-bold',
)}
>
{titleCase(severity ?? '')}
</Badge>
)}
</div>
);
},
header: 'Risk Level',
}),
columnHelper.accessor('status', {
cell: info => {
const status = info.getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { titleCase } from 'string-ts';
import { Link } from 'react-router-dom';
import { ChevronLeft } from 'lucide-react';
import React, { FunctionComponent } from 'react';
import { Badge, TextWithNAFallback } from '@ballerine/ui';
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
TextWithNAFallback,
} from '@ballerine/ui';

import { ctw } from '@/common/utils/ctw/ctw';
import { Notes } from '@/domains/notes/Notes';
Expand All @@ -28,6 +35,8 @@ export const MerchantMonitoringBusinessReport: FunctionComponent = () => {
activeTab,
notes,
isNotesOpen,
turnOngoingMonitoringOn,
turnOngoingMonitoringOff,
} = useMerchantMonitoringBusinessReportLogic();

return (
Expand All @@ -40,14 +49,41 @@ export const MerchantMonitoringBusinessReport: FunctionComponent = () => {
>
<SidebarInset>
<section className="flex h-full flex-col px-6 pb-6 pt-4">
<div>
<div className={`flex justify-between`}>
<Button
variant={'ghost'}
onClick={onNavigateBack}
className={'mb-6 flex items-center space-x-px pe-3 ps-1 font-semibold'}
>
<ChevronLeft size={18} /> <span>Back</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className={
'px-2 py-0 text-xs aria-disabled:pointer-events-none aria-disabled:opacity-50'
}
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className={`w-full px-8 py-1`} asChild>
<Button
onClick={() => {
businessReport?.monitoringStatus
? turnOngoingMonitoringOff(businessReport?.merchantId)
: turnOngoingMonitoringOn(businessReport?.merchantId);
}}
variant={'ghost'}
className="justify-start"
>
Turn Monitoring {businessReport?.monitoringStatus ? 'Off' : 'On'}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<TextWithNAFallback as={'h2'} className="pb-4 text-2xl font-bold">
{websiteWithNoProtocol}
Expand All @@ -73,11 +109,22 @@ export const MerchantMonitoringBusinessReport: FunctionComponent = () => {
?.text ?? titleCase(businessReport?.status ?? '')}
</Badge>
</div>
<div>
<span className={`me-2 text-sm leading-6 text-slate-400`}>Created at</span>
<div className={`text-sm`}>
<span className={`me-2 leading-6 text-slate-400`}>Created at</span>
{businessReport?.createdAt &&
dayjs(new Date(businessReport?.createdAt)).format('HH:mm MMM Do, YYYY')}
</div>
<div className={`flex items-center space-x-2 text-sm`}>
<span className={`text-slate-400`}>Monitoring Status</span>
<span
className={ctw('select-none rounded-full d-3', {
'bg-success': businessReport?.monitoringStatus,
'bg-slate-400': !businessReport?.monitoringStatus,
})}
>
&nbsp;
</span>
</div>
<NotesButton numberOfNotes={notes?.length} />
</div>
<Tabs defaultValue={activeTab} className="w-full" key={activeTab}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

import { Method } from '@/common/enums';
import { apiClient } from '@/common/api-client/api-client';
import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error';

export const turnOngoingMonitoring = async ({
merchantId,
state,
}: {
merchantId: string;
state: 'on' | 'off';
}) => {
const [data, error] = await apiClient({
endpoint: `../external/businesses/${merchantId}/monitoring/${state}`,
method: Method.PATCH,
schema: z.undefined(),
timeout: 300_000,
});

return handleZodError(error, data);
};
Loading

0 comments on commit 8b6eb3f

Please sign in to comment.