Skip to content

Commit 6f93625

Browse files
feat(sanctionCheck): display enrich button and new fields for matches (#733)
* feat(sanctionCheck): display enrich button and new fields for matches * add success/error management
1 parent 3410108 commit 6f93625

File tree

17 files changed

+434
-107
lines changed

17 files changed

+434
-107
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
createPropertyTransformer,
3+
getSanctionEntityProperties,
4+
type PropertyForSchema,
5+
type SanctionCheckEntityProperty,
6+
} from '@app-builder/constants/sanction-check-entity';
7+
import { type OpenSanctionEntity } from '@app-builder/models/sanction-check';
8+
import { useFormatLanguage } from '@app-builder/utils/format';
9+
import { Fragment, type ReactNode, useMemo, useState } from 'react';
10+
import { useTranslation } from 'react-i18next';
11+
12+
import { sanctionsI18n } from './sanctions-i18n';
13+
14+
export function EntityProperties<T extends OpenSanctionEntity>({
15+
entity,
16+
forcedProperties,
17+
showUnavailable = false,
18+
before,
19+
after,
20+
}: {
21+
entity: T;
22+
forcedProperties?: PropertyForSchema<T['schema']>[];
23+
showUnavailable?: boolean;
24+
before?: ReactNode;
25+
after?: ReactNode;
26+
}) {
27+
const [displayAll, setDisplayAll] = useState<
28+
Partial<Record<SanctionCheckEntityProperty, boolean>>
29+
>({});
30+
31+
const displayProperties = forcedProperties ?? getSanctionEntityProperties(entity.schema);
32+
const { t, i18n } = useTranslation(sanctionsI18n);
33+
const language = useFormatLanguage();
34+
const entityPropertyList = displayProperties
35+
.map((property) => {
36+
const items = entity.properties[property] ?? [];
37+
const itemsToDisplay = displayAll[property] ? items : items.slice(0, 5);
38+
return {
39+
property,
40+
values: itemsToDisplay,
41+
restItemsCount: Math.max(0, items.length - itemsToDisplay.length),
42+
};
43+
})
44+
.filter((prop) => (showUnavailable ? true : prop.values.length > 0));
45+
46+
const TransformProperty = useMemo(
47+
() =>
48+
createPropertyTransformer({
49+
language: i18n.language,
50+
formatLanguage: language,
51+
}),
52+
[i18n.language, language],
53+
);
54+
55+
const handleShowMore = (prop: string) => {
56+
setDisplayAll((prev) => ({ ...prev, [prop]: true }));
57+
};
58+
59+
return (
60+
<div className="grid grid-cols-[168px,_1fr] gap-2">
61+
{before}
62+
{entityPropertyList.map(({ property, values, restItemsCount }) => {
63+
return (
64+
<Fragment key={property}>
65+
<span className="font-bold">{t(`sanctions:entity.property.${property}`)}</span>
66+
<span className="break-all">
67+
{values.length > 0 ? (
68+
<>
69+
{values.map((v, i) => (
70+
<Fragment key={i}>
71+
<TransformProperty property={property} value={v} />
72+
{i === values.length - 1 ? null : <span className="mx-1">·</span>}
73+
</Fragment>
74+
))}
75+
{restItemsCount > 0 ? (
76+
<>
77+
<span className="mx-1">·</span>
78+
<button
79+
onClick={(e) => {
80+
e.preventDefault();
81+
handleShowMore(property);
82+
}}
83+
className="text-purple-65 font-semibold"
84+
>
85+
+ {restItemsCount} more
86+
</button>
87+
</>
88+
) : null}
89+
</>
90+
) : (
91+
<span className="text-grey-50">not available</span>
92+
)}
93+
</span>
94+
</Fragment>
95+
);
96+
})}
97+
{after}
98+
</div>
99+
);
100+
}

packages/app-builder/src/components/Sanctions/MatchCard.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type SanctionCheckMatch } from '@app-builder/models/sanction-check';
22
import { SanctionCheckReviewModal } from '@app-builder/routes/ressources+/cases+/review-sanction-match';
3+
import { EnrichMatchButton } from '@app-builder/routes/ressources+/sanction-check+/enrich-match.$matchId';
34
import { useOrganizationUsers } from '@app-builder/services/organization/organization-users';
45
import { getFullName } from '@app-builder/services/user';
56
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
@@ -34,7 +35,7 @@ export const MatchCard = ({ match, readonly, unreviewable, defaultOpen }: MatchC
3435
<div className="grid grid-cols-[max-content_1fr_max-content] gap-x-6 gap-y-2">
3536
<CollapsibleV2.Provider defaultOpen={defaultOpen}>
3637
<div className="bg-grey-98 col-span-full grid grid-cols-subgrid rounded-md">
37-
<div className="col-span-full flex items-center justify-between px-4 py-3">
38+
<div className="col-span-full flex items-center justify-between gap-2 px-4 py-3">
3839
<CollapsibleV2.Title className="focus-visible:text-purple-65 group flex grow items-center gap-2 rounded outline-none transition-colors">
3940
<Icon
4041
icon="smallarrow-up"
@@ -51,6 +52,11 @@ export const MatchCard = ({ match, readonly, unreviewable, defaultOpen }: MatchC
5152
</Tag>
5253
</div>
5354
</CollapsibleV2.Title>
55+
{!match.enriched ? (
56+
<div>
57+
<EnrichMatchButton matchId={match.id} />
58+
</div>
59+
) : null}
5460
<div className="inline-flex h-8">
5561
{unreviewable ? (
5662
<Tag border="square" color="grey">
Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,83 @@
1+
import { type PropertyForSchema } from '@app-builder/constants/sanction-check-entity';
12
import {
2-
createPropertyTransformer,
3-
getSanctionEntityProperties,
4-
type SanctionCheckEntityProperty,
5-
} from '@app-builder/constants/sanction-check-entity';
6-
import { type SanctionCheckMatch } from '@app-builder/models/sanction-check';
7-
import { useFormatLanguage } from '@app-builder/utils/format';
8-
import { Fragment, useMemo, useState } from 'react';
3+
type SanctionCheckMatch,
4+
type SanctionCheckSanctionEntity,
5+
} from '@app-builder/models/sanction-check';
6+
import { useState } from 'react';
97
import { useTranslation } from 'react-i18next';
8+
import { ModalV2 } from 'ui-design-system';
9+
import { Icon } from 'ui-icons';
1010

11+
import { EntityProperties } from './EntityProperties';
1112
import { sanctionsI18n } from './sanctions-i18n';
1213

1314
export type MatchDetailsProps = {
1415
entity: SanctionCheckMatch['payload'];
1516
};
1617

17-
export function MatchDetails({ entity }: MatchDetailsProps) {
18-
const { t, i18n } = useTranslation(sanctionsI18n);
19-
const language = useFormatLanguage();
20-
const [displayAll, setDisplayAll] = useState<
21-
Partial<Record<SanctionCheckEntityProperty, boolean>>
22-
>({});
18+
const sanctionProps = [
19+
'country',
20+
'authority',
21+
'authorityId',
22+
'startDate',
23+
'endDate',
24+
'listingDate',
25+
'program',
26+
'programId',
27+
'programUrl',
28+
'summary',
29+
'reason',
30+
'sourceUrl',
31+
] satisfies PropertyForSchema<'Sanction'>[];
2332

24-
const TransformProperty = useMemo(
25-
() =>
26-
createPropertyTransformer({
27-
language: i18n.language,
28-
formatLanguage: language,
29-
}),
30-
[i18n.language, language],
33+
export function MatchDetails({ entity }: MatchDetailsProps) {
34+
const { t } = useTranslation(sanctionsI18n);
35+
const [selectedSanction, setSelectedSanction] = useState<SanctionCheckSanctionEntity | null>(
36+
null,
3137
);
3238

33-
const displayProperties = getSanctionEntityProperties(entity.schema);
34-
const entityPropertyList = displayProperties
35-
.map((property) => {
36-
const items = entity.properties[property] ?? [];
37-
const itemsToDisplay = displayAll[property] ? items : items.slice(0, 5);
38-
return {
39-
property,
40-
values: itemsToDisplay,
41-
restItemsCount: Math.max(0, items.length - itemsToDisplay.length),
42-
};
43-
})
44-
.filter((prop) => prop.values.length > 0);
45-
46-
const handleShowMore = (prop: string) => {
47-
setDisplayAll((prev) => ({ ...prev, [prop]: true }));
48-
};
49-
5039
return (
51-
<div className="grid grid-cols-[168px,_1fr] gap-2">
52-
{entityPropertyList.map(({ property, values, restItemsCount }) => {
53-
return (
54-
<Fragment key={property}>
55-
<span className="font-bold">{t(`sanctions:entity.property.${property}`)}</span>
56-
<span className="flex flex-wrap gap-1 break-all">
57-
{values.map((v, i) => (
58-
<Fragment key={i}>
59-
<TransformProperty property={property} value={v} />
60-
{i === values.length - 1 ? null : <span>·</span>}
61-
</Fragment>
62-
))}
63-
{restItemsCount > 0 ? (
64-
<>
65-
<span>·</span>
66-
<button
67-
onClick={(e) => {
68-
e.preventDefault();
69-
handleShowMore(property);
70-
}}
71-
className="text-purple-65 font-semibold"
40+
<div className="flex flex-col gap-4">
41+
<EntityProperties
42+
entity={entity}
43+
after={
44+
entity.properties.sanctions ? (
45+
<>
46+
<span className="font-bold">{t('sanctions:entity.property.sanctions')}</span>
47+
<div className="flex flex-col gap-2">
48+
{entity.properties.sanctions.map((sanction) => (
49+
<div
50+
key={sanction.id}
51+
className="group/sanction bg-grey-100 grid grid-cols-[1fr_20px] gap-2 rounded p-2"
7252
>
73-
+ {restItemsCount} more
74-
</button>
75-
</>
76-
) : null}
77-
</span>
78-
</Fragment>
79-
);
80-
})}
53+
<span className="truncate">{sanction.properties['authority']}</span>
54+
<button type="button" onClick={() => setSelectedSanction(sanction)}>
55+
<Icon
56+
icon="visibility-on"
57+
className="text-grey-90 hover:text-purple-65 size-5 cursor-pointer"
58+
/>
59+
</button>
60+
</div>
61+
))}
62+
</div>
63+
64+
<ModalV2.Content
65+
open={!!selectedSanction}
66+
onClose={() => setSelectedSanction(null)}
67+
size="large"
68+
className="max-h-[80vh]"
69+
>
70+
<ModalV2.Title>{t('sanctions:sanction_detail.title')}</ModalV2.Title>
71+
<div className="overflow-y-auto p-6">
72+
{selectedSanction ? (
73+
<EntityProperties entity={selectedSanction} forcedProperties={sanctionProps} />
74+
) : null}
75+
</div>
76+
</ModalV2.Content>
77+
</>
78+
) : null
79+
}
80+
/>
8181
</div>
8282
);
8383
}

packages/app-builder/src/constants/sanction-check-entity.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ExternalLink } from '@app-builder/components/ExternalLink';
2-
import { type SanctionCheckEntitySchema } from '@app-builder/models/sanction-check';
2+
import { type OpenSanctionEntitySchema } from '@app-builder/models/sanction-check';
33

44
export type PropertyDataType = 'string' | 'country' | 'url' | 'date' | 'wikidataId';
55
export type PropertyForSchema<
6-
Schema extends SanctionCheckEntitySchema,
6+
Schema extends OpenSanctionEntitySchema,
77
_R = never,
88
> = (typeof schemaInheritence)[Schema] extends null
99
? _R | (typeof schemaProperties)[Schema][number]
10-
: (typeof schemaInheritence)[Schema] extends infer P extends SanctionCheckEntitySchema
10+
: (typeof schemaInheritence)[Schema] extends infer P extends OpenSanctionEntitySchema
1111
? PropertyForSchema<P, _R | (typeof schemaProperties)[Schema][number]>
1212
: never;
1313

@@ -88,7 +88,21 @@ export const schemaProperties = {
8888
Vehicle: ['registrationNumber'] as const,
8989
Airplane: [] as const,
9090
Vessel: [] as const,
91-
} satisfies Record<SanctionCheckEntitySchema, string[]>;
91+
Sanction: [
92+
'country',
93+
'authority',
94+
'authorityId',
95+
'program',
96+
'startDate',
97+
'endDate',
98+
'listingDate',
99+
'sourceUrl',
100+
'reason',
101+
'summary',
102+
'programId',
103+
'programUrl',
104+
] as const,
105+
} satisfies Record<OpenSanctionEntitySchema, string[]>;
92106

93107
export type SanctionCheckEntityProperty =
94108
(typeof schemaProperties)[keyof typeof schemaProperties][number];
@@ -102,7 +116,8 @@ const schemaInheritence = {
102116
Vehicle: 'Thing',
103117
Vessel: 'Vehicle',
104118
Airplane: 'Vehicle',
105-
} satisfies Record<SanctionCheckEntitySchema, SanctionCheckEntitySchema | null>;
119+
Sanction: null,
120+
} satisfies Record<OpenSanctionEntitySchema, OpenSanctionEntitySchema | null>;
106121

107122
const propertyMetadata = {
108123
address: { type: 'string' },
@@ -170,10 +185,18 @@ const propertyMetadata = {
170185
website: { type: 'url' },
171186
weight: { type: 'string' },
172187
wikidataId: { type: 'wikidataId' },
188+
authority: { type: 'string' },
189+
authorityId: { type: 'string' },
190+
startDate: { type: 'date' },
191+
endDate: { type: 'date' },
192+
programId: { type: 'string' },
193+
programUrl: { type: 'url' },
194+
reason: { type: 'string' },
195+
listingDate: { type: 'date' },
173196
} satisfies Record<SanctionCheckEntityProperty, { type: PropertyDataType }>;
174197

175-
export function getSanctionEntityProperties(schema: SanctionCheckEntitySchema) {
176-
let currentSchema: SanctionCheckEntitySchema | null = schema;
198+
export function getSanctionEntityProperties(schema: OpenSanctionEntitySchema) {
199+
let currentSchema: OpenSanctionEntitySchema | null = schema;
177200
const properties: SanctionCheckEntityProperty[] = [];
178201

179202
do {

packages/app-builder/src/locales/ar/sanctions.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,18 @@
117117
"error_label_one": "التحقق من العقوبة خطأ للسبب التالي",
118118
"error_label_others": "فحص العقوبة خطأ للأسباب التالية",
119119
"match.not_reviewable": "غير قابل للمراجعة",
120-
"refine_modal.search_input_label": "يغطي البحث الحقول التالية:"
120+
"refine_modal.search_input_label": "يغطي البحث الحقول التالية:",
121+
"entity.property.authority": "سلطة",
122+
"entity.property.authorityId": "المعرف الصادر عن السلطة",
123+
"entity.property.endDate": "تاريخ الانتهاء",
124+
"entity.property.listingDate": "تاريخ الإدراج",
125+
"entity.property.programId": "معرف البرنامج",
126+
"entity.property.programUrl": "برنامج URL",
127+
"entity.property.reason": "سبب",
128+
"entity.property.startDate": "تاريخ البدء",
129+
"enrich_button": "إثراء",
130+
"entity.property.sanctions": "العقوبات",
131+
"sanction_detail.title": "تفاصيل العقوبات",
132+
"error.match_already_enriched": "تطابق المخصب بالفعل",
133+
"success.match_enriched": "تطابق المباراة بنجاح"
121134
}

packages/app-builder/src/locales/en/sanctions.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@
109109
"entity.property.hairColor": "Hair color",
110110
"entity.property.appearance": "Appearance",
111111
"entity.property.political": "Political association",
112+
"entity.property.sanctions": "Sanctions",
113+
"entity.property.programId": "Program ID",
114+
"entity.property.programUrl": "Program URL",
115+
"entity.property.authority": "Authority",
116+
"entity.property.authorityId": "Authority-issued Identifier",
117+
"entity.property.startDate": "Start date",
118+
"entity.property.endDate": "End date",
119+
"entity.property.listingDate": "Listing date",
120+
"entity.property.reason": "Reason",
112121
"entity.schema.airplane": "Airplane",
113122
"entity.schema.company": "Company",
114123
"entity.schema.person": "Person",
@@ -117,5 +126,9 @@
117126
"entity.schema.legalentity": "Legal Entity",
118127
"entity.schema.vehicle": "Vehicle",
119128
"entity.schema.vessel": "Vessel",
120-
"refine_modal.schema.legalentity": "Legal Entity"
129+
"refine_modal.schema.legalentity": "Legal Entity",
130+
"sanction_detail.title": "Sanction details",
131+
"enrich_button": "Enrich",
132+
"success.match_enriched": "Match successfully enriched",
133+
"error.match_already_enriched": "Match already enriched"
121134
}

0 commit comments

Comments
 (0)