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

ongoing monitoring turning on and off #2941

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
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
Loading