Skip to content

Commit

Permalink
fix: manage sanction errors (#706)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChibiBlasphem authored Feb 19, 2025
1 parent 9a6401a commit beafd1f
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { decisionsI18n } from '@app-builder/components';
import { type SanctionCheck } from '@app-builder/models/sanction-check';
import {
isSanctionCheckError,
type SanctionCheck,
} from '@app-builder/models/sanction-check';
import { useTranslation } from 'react-i18next';
import * as R from 'remeda';
import { Collapsible } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { MatchCard } from '../Sanctions/MatchCard';
import { SanctionCheckErrors } from '../Sanctions/SanctionCheckErrors';
import { SanctionStatusTag } from '../Sanctions/SanctionStatusTag';

export function SanctionCheckDetail({
Expand All @@ -14,11 +18,7 @@ export function SanctionCheckDetail({
sanctionCheck: SanctionCheck;
}) {
const { t } = useTranslation(decisionsI18n);
const searchInputs = R.pipe(
R.values(sanctionCheck.request.queries),
R.flatMap((query) => R.values(query.properties)),
R.flat(),
);
const hasError = isSanctionCheckError(sanctionCheck);

return (
<Collapsible.Container className="bg-grey-100">
Expand All @@ -33,26 +33,55 @@ export function SanctionCheckDetail({
</div>
</Collapsible.Title>
<Collapsible.Content>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span>{t('sanctions:search_input')}</span>
{searchInputs.map((input, i) => (
<div
key={i}
className="border-grey-90 flex items-center gap-2 rounded border p-2"
>
<span className="bg-grey-95 size-6 rounded-sm p-1">
<Icon icon="string" className="size-4" />
</span>
{input}
</div>
<div className="flex flex-col gap-4">
{hasError ? (
<SanctionCheckErrors sanctionCheck={sanctionCheck} />
) : null}
{sanctionCheck.request ? (
<SearchInput request={sanctionCheck.request} />
) : null}
<div className="flex flex-col gap-2">
{sanctionCheck.matches.map((match) => (
<MatchCard
readonly
key={match.id}
unreviewable={hasError || sanctionCheck.partial}
match={match}
/>
))}
</div>
{sanctionCheck.matches.map((match) => (
<MatchCard readonly key={match.id} match={match} />
))}
</div>
</Collapsible.Content>
</Collapsible.Container>
);
}

const SearchInput = ({
request,
}: {
request: NonNullable<SanctionCheck['request']>;
}) => {
const { t } = useTranslation(decisionsI18n);
const searchInputList = R.pipe(
R.values(request.queries),
R.flatMap((query) => R.values(query.properties)),
R.flat(),
);

return (
<div className="flex items-center gap-2">
<span>{t('sanctions:search_input')}</span>
{searchInputList.map((input, i) => (
<div
key={i}
className="border-grey-90 flex items-center gap-2 rounded border p-2"
>
<span className="bg-grey-95 size-6 rounded-sm p-1">
<Icon icon="string" className="size-4" />
</span>
{input}
</div>
))}
</div>
);
};
8 changes: 4 additions & 4 deletions packages/app-builder/src/components/Sanctions/MatchCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import { StatusTag } from './StatusTag';
type MatchCardProps = {
match: SanctionCheckMatch;
readonly?: boolean;
disabled?: boolean;
unreviewable?: boolean;
defaultOpen?: boolean;
};

export const MatchCard = ({
match,
readonly,
disabled,
unreviewable,
defaultOpen,
}: MatchCardProps) => {
const { t } = useTranslation(sanctionsI18n);
Expand Down Expand Up @@ -59,9 +59,9 @@ export const MatchCard = ({
</div>
</CollapsibleV2.Title>
<div className="inline-flex h-8">
{disabled ? (
{unreviewable ? (
<Tag border="square" color="grey">
{t('sanctions:match.not_reviewed')}
{t('sanctions:match.not_reviewable')}
</Tag>
) : (
<StatusTag
Expand Down
55 changes: 36 additions & 19 deletions packages/app-builder/src/components/Sanctions/RefineSearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ export function RefineSearchModal({
onClose: _onClose,
}: RefineSearchModalProps) {
const { t } = useTranslation(sanctionsI18n);
const searchInputs = R.pipe(
R.values(sanctionCheck.request.queries),
R.flatMap((query) => R.values(query.properties)),
R.flat(),
);
const searchFetcher = useFetcher<typeof searchAction>();
const refineFetcher = useFetcher<typeof refineAction>();
const formDataRef = useRef<FormData | null>(null);
Expand Down Expand Up @@ -192,7 +187,10 @@ export function RefineSearchModal({
className="flex-1"
variant="primary"
onClick={handleRefine}
disabled={searchResults.length > sanctionCheck.request.limit}
disabled={
searchResults.length >
(sanctionCheck.request?.limit ?? Infinity)
}
>
{t('sanctions:refine_modal.apply_search')}
</Button>
Expand All @@ -202,19 +200,9 @@ export function RefineSearchModal({
) : (
<searchFetcher.Form onSubmit={handleSubmit(form)} className="contents">
<div className="flex h-full flex-col gap-6 overflow-y-scroll p-8">
<Field label="Search covers the following fields:">
{searchInputs.map((input, i) => (
<div
key={i}
className="border-grey-90 flex items-center gap-2 rounded border p-2"
>
<span className="bg-grey-95 size-6 rounded-sm p-1">
<Icon icon="string" className="size-4" />
</span>
{input}
</div>
))}
</Field>
{sanctionCheck.request ? (
<SearchInput request={sanctionCheck.request} />
) : null}
<form.Field
name="entityType"
listeners={{ onChange: onSearchEntityChange }}
Expand Down Expand Up @@ -359,3 +347,32 @@ function EntitySelect({ name, value, onChange }: EntitySelectProps) {
</Select.Root>
);
}

function SearchInput({
request,
}: {
request: NonNullable<SanctionCheck['request']>;
}) {
const { t } = useTranslation(['sanctions']);
const searchInputs = R.pipe(
R.values(request.queries),
R.flatMap((query) => R.values(query.properties)),
R.flat(),
);

return (
<Field label={t('sanctions:refine_modal.search_input_label')}>
{searchInputs.map((input, i) => (
<div
key={i}
className="border-grey-90 flex items-center gap-2 rounded border p-2"
>
<span className="bg-grey-95 size-6 rounded-sm p-1">
<Icon icon="string" className="size-4" />
</span>
{input}
</div>
))}
</Field>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type SanctionCheckError } from '@app-builder/models/sanction-check';
import { useTranslation } from 'react-i18next';
import { Icon } from 'ui-icons';

export function SanctionCheckErrors({
sanctionCheck,
}: {
sanctionCheck: SanctionCheckError;
}) {
const { t } = useTranslation(['sanctions']);

return (
<div className="text-s bg-red-95 text-red-47 flex items-center gap-4 rounded p-4">
<Icon icon="error" className="size-5 shrink-0" />
<div className="flex flex-col">
<span className="font-semibold">
{t('sanctions:error_label', {
count: sanctionCheck.errorCodes.length,
})}
</span>
{sanctionCheck.errorCodes.map((errorCode) => (
<div key={errorCode}>{t(`sanctions:error.${errorCode}`)}</div>
))}
</div>
</div>
);
}
37 changes: 24 additions & 13 deletions packages/app-builder/src/components/Sanctions/SanctionReview.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Callout } from '@app-builder/components/Callout';
import { type SanctionCheck } from '@app-builder/models/sanction-check';
import {
isSanctionCheckError,
type SanctionCheck,
} from '@app-builder/models/sanction-check';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { filter } from 'remeda';
import { match } from 'ts-pattern';
import { Button } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { MatchCard } from './MatchCard';
import { RefineSearchModal } from './RefineSearchModal';
import { SanctionCheckErrors } from './SanctionCheckErrors';
import { sanctionsI18n } from './sanctions-i18n';

export type SanctionReviewSectionProps = {
Expand All @@ -25,7 +30,7 @@ export function SanctionReviewSection({
sanctionCheck.matches,
(m) => m.status === 'pending',
).length;
const needsRefine = sanctionCheck.partial;
const hasError = isSanctionCheckError(sanctionCheck);

return (
<div className="flex h-fit flex-[2] flex-col gap-6">
Expand All @@ -47,23 +52,29 @@ export function SanctionReviewSection({
{t('sanctions:refine_search')}
</Button>
</div>
{!needsRefine ? (
<Callout bordered>{t('sanctions:callout.review')}</Callout>
) : (
<div className="text-s bg-red-95 text-red-47 flex items-center gap-2 rounded p-2">
<Icon icon="error" className="size-5 shrink-0" />
{t('sanctions:callout.needs_refine', {
matchCount: sanctionCheck.request.limit,
})}
</div>
)}
{match(sanctionCheck)
.when(isSanctionCheckError, (sc) => (
<SanctionCheckErrors sanctionCheck={sc} />
))
.otherwise((sc) => {
return !sc.partial ? (
<Callout bordered>{t('sanctions:callout.review')}</Callout>
) : (
<div className="text-s bg-red-95 text-red-47 flex items-center gap-2 rounded p-2">
<Icon icon="error" className="size-5 shrink-0" />
{t('sanctions:callout.needs_refine', {
matchCount: sc.request.limit,
})}
</div>
);
})}
</div>
<div className="flex flex-col gap-2">
{sanctionCheck.matches.map((sanctionMatch) => (
<MatchCard
key={sanctionMatch.id}
match={sanctionMatch}
disabled={needsRefine}
unreviewable={sanctionCheck.partial || hasError}
defaultOpen={sanctionCheck.matches.length === 1}
/>
))}
Expand Down
8 changes: 6 additions & 2 deletions packages/app-builder/src/locales/ar/sanctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"entity.schema.organization": "منظمة",
"entity.schema.person": "شخص",
"entity.schema.thing": "شيء",
"match.not_reviewed": "لم تتم مراجعتها",
"match.similarity": "التشابه {{٪}} ٪",
"match.status.confirmed_hit": "أكد المباراة",
"match.status.no_hit": "لا تطابق",
Expand Down Expand Up @@ -113,5 +112,10 @@
"match.unique_counterparty_identifier": "معرف فريد للطرف المقابل",
"sanction_check": "فحص العقوبة",
"see_details": "انظر التفاصيل",
"review_modal.confirmation": "تأكيد"
"review_modal.confirmation": "تأكيد",
"error.all_fields_null_or_empty": "جميع حقول البحث فارغة أو فارغة",
"error_label_one": "التحقق من العقوبة خطأ للسبب التالي",
"error_label_others": "فحص العقوبة خطأ للأسباب التالية",
"match.not_reviewable": "غير قابل للمراجعة",
"refine_modal.search_input_label": "يغطي البحث الحقول التالية:"
}
6 changes: 5 additions & 1 deletion packages/app-builder/src/locales/en/sanctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"callout.review": "There are matches between the beneficiary information and the sanctions check. Check if it is the same beneficiary or not.",
"callout.needs_review": "{{toReview}}/{{totalMatches}} to review",
"callout.needs_refine": "There are too many potential matches (>{{matchCount}}). To get more relevant results, please refine your search.",
"error_label_one": "Sanction check is in error for the following reason",
"error_label_others": "Sanction check is in error for the following reasons",
"error.all_fields_null_or_empty": "All the search fields are null or empty",
"refine_modal.apply_search": "Apply changes",
"refine_modal.back_search": "Back to search",
"refine_modal.no_match_callout": "By applying this search, the sanction check will be set as: <Status />",
Expand All @@ -15,6 +18,7 @@
"refine_modal.schema.organization": "Organization",
"refine_modal.schema.vehicle": "Vehicle",
"refine_modal.search_by": "Search by:",
"refine_modal.search_input_label": "Search covers the following fields:",
"refine_modal.test_search": "Test this search",
"refine_modal.title": "Refine search",
"refine_search": "Refine search",
Expand All @@ -32,7 +36,7 @@
"status.no_hit": "No match",
"status.error": "Error",
"match.similarity": "Similarity {{percent}}%",
"match.not_reviewed": "Not reviewed",
"match.not_reviewable": "Not reviewable",
"match.status.pending": "To review",
"match.status.no_hit": "No match",
"match.status.confirmed_hit": "Match confirmed",
Expand Down
8 changes: 6 additions & 2 deletions packages/app-builder/src/locales/fr/sanctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"entity.schema.organization": "Organisation",
"entity.schema.person": "Personne",
"entity.schema.thing": "Chose",
"match.not_reviewed": "Non examiné",
"match.similarity": "Similitude {{pour cent}}%",
"match.status.confirmed_hit": "Correspondance confirmée",
"match.status.no_hit": "Pas de correspondance",
Expand Down Expand Up @@ -113,5 +112,10 @@
"match.status.skipped": "Sauté",
"match.unique_counterparty_identifier": "Identifiant unique de contrepartie",
"sanction_check": "Filtrage des sanction",
"see_details": "Voir les détails"
"see_details": "Voir les détails",
"error.all_fields_null_or_empty": "Tous les champs de recherche sont nuls ou vides",
"error_label_one": "Le filtrage de sanction est erronée pour la raison suivante",
"error_label_others": "Le filtrage de sanction est erronée pour les raisons suivantes",
"match.not_reviewable": "Non révisable",
"refine_modal.search_input_label": "La recherche couvre les champs suivants:"
}
Loading

0 comments on commit beafd1f

Please sign in to comment.