Skip to content

Commit

Permalink
feat(sanctionCheck): display enrich button and new fields for matches (
Browse files Browse the repository at this point in the history
…#733)

* feat(sanctionCheck): display enrich button and new fields for matches

* add success/error management
  • Loading branch information
ChibiBlasphem authored Mar 7, 2025
1 parent 3410108 commit 6f93625
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 107 deletions.
100 changes: 100 additions & 0 deletions packages/app-builder/src/components/Sanctions/EntityProperties.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
createPropertyTransformer,
getSanctionEntityProperties,
type PropertyForSchema,
type SanctionCheckEntityProperty,
} from '@app-builder/constants/sanction-check-entity';
import { type OpenSanctionEntity } from '@app-builder/models/sanction-check';
import { useFormatLanguage } from '@app-builder/utils/format';
import { Fragment, type ReactNode, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

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

export function EntityProperties<T extends OpenSanctionEntity>({
entity,
forcedProperties,
showUnavailable = false,
before,
after,
}: {
entity: T;
forcedProperties?: PropertyForSchema<T['schema']>[];
showUnavailable?: boolean;
before?: ReactNode;
after?: ReactNode;
}) {
const [displayAll, setDisplayAll] = useState<
Partial<Record<SanctionCheckEntityProperty, boolean>>
>({});

const displayProperties = forcedProperties ?? getSanctionEntityProperties(entity.schema);
const { t, i18n } = useTranslation(sanctionsI18n);
const language = useFormatLanguage();
const entityPropertyList = displayProperties
.map((property) => {
const items = entity.properties[property] ?? [];
const itemsToDisplay = displayAll[property] ? items : items.slice(0, 5);
return {
property,
values: itemsToDisplay,
restItemsCount: Math.max(0, items.length - itemsToDisplay.length),
};
})
.filter((prop) => (showUnavailable ? true : prop.values.length > 0));

const TransformProperty = useMemo(
() =>
createPropertyTransformer({
language: i18n.language,
formatLanguage: language,
}),
[i18n.language, language],
);

const handleShowMore = (prop: string) => {
setDisplayAll((prev) => ({ ...prev, [prop]: true }));
};

return (
<div className="grid grid-cols-[168px,_1fr] gap-2">
{before}
{entityPropertyList.map(({ property, values, restItemsCount }) => {
return (
<Fragment key={property}>
<span className="font-bold">{t(`sanctions:entity.property.${property}`)}</span>
<span className="break-all">
{values.length > 0 ? (
<>
{values.map((v, i) => (
<Fragment key={i}>
<TransformProperty property={property} value={v} />
{i === values.length - 1 ? null : <span className="mx-1">·</span>}
</Fragment>
))}
{restItemsCount > 0 ? (
<>
<span className="mx-1">·</span>
<button
onClick={(e) => {
e.preventDefault();
handleShowMore(property);
}}
className="text-purple-65 font-semibold"
>
+ {restItemsCount} more
</button>
</>
) : null}
</>
) : (
<span className="text-grey-50">not available</span>
)}
</span>
</Fragment>
);
})}
{after}
</div>
);
}
8 changes: 7 additions & 1 deletion packages/app-builder/src/components/Sanctions/MatchCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type SanctionCheckMatch } from '@app-builder/models/sanction-check';
import { SanctionCheckReviewModal } from '@app-builder/routes/ressources+/cases+/review-sanction-match';
import { EnrichMatchButton } from '@app-builder/routes/ressources+/sanction-check+/enrich-match.$matchId';
import { useOrganizationUsers } from '@app-builder/services/organization/organization-users';
import { getFullName } from '@app-builder/services/user';
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
Expand Down Expand Up @@ -34,7 +35,7 @@ export const MatchCard = ({ match, readonly, unreviewable, defaultOpen }: MatchC
<div className="grid grid-cols-[max-content_1fr_max-content] gap-x-6 gap-y-2">
<CollapsibleV2.Provider defaultOpen={defaultOpen}>
<div className="bg-grey-98 col-span-full grid grid-cols-subgrid rounded-md">
<div className="col-span-full flex items-center justify-between px-4 py-3">
<div className="col-span-full flex items-center justify-between gap-2 px-4 py-3">
<CollapsibleV2.Title className="focus-visible:text-purple-65 group flex grow items-center gap-2 rounded outline-none transition-colors">
<Icon
icon="smallarrow-up"
Expand All @@ -51,6 +52,11 @@ export const MatchCard = ({ match, readonly, unreviewable, defaultOpen }: MatchC
</Tag>
</div>
</CollapsibleV2.Title>
{!match.enriched ? (
<div>
<EnrichMatchButton matchId={match.id} />
</div>
) : null}
<div className="inline-flex h-8">
{unreviewable ? (
<Tag border="square" color="grey">
Expand Down
132 changes: 66 additions & 66 deletions packages/app-builder/src/components/Sanctions/MatchDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,83 @@
import { type PropertyForSchema } from '@app-builder/constants/sanction-check-entity';
import {
createPropertyTransformer,
getSanctionEntityProperties,
type SanctionCheckEntityProperty,
} from '@app-builder/constants/sanction-check-entity';
import { type SanctionCheckMatch } from '@app-builder/models/sanction-check';
import { useFormatLanguage } from '@app-builder/utils/format';
import { Fragment, useMemo, useState } from 'react';
type SanctionCheckMatch,
type SanctionCheckSanctionEntity,
} from '@app-builder/models/sanction-check';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ModalV2 } from 'ui-design-system';
import { Icon } from 'ui-icons';

import { EntityProperties } from './EntityProperties';
import { sanctionsI18n } from './sanctions-i18n';

export type MatchDetailsProps = {
entity: SanctionCheckMatch['payload'];
};

export function MatchDetails({ entity }: MatchDetailsProps) {
const { t, i18n } = useTranslation(sanctionsI18n);
const language = useFormatLanguage();
const [displayAll, setDisplayAll] = useState<
Partial<Record<SanctionCheckEntityProperty, boolean>>
>({});
const sanctionProps = [
'country',
'authority',
'authorityId',
'startDate',
'endDate',
'listingDate',
'program',
'programId',
'programUrl',
'summary',
'reason',
'sourceUrl',
] satisfies PropertyForSchema<'Sanction'>[];

const TransformProperty = useMemo(
() =>
createPropertyTransformer({
language: i18n.language,
formatLanguage: language,
}),
[i18n.language, language],
export function MatchDetails({ entity }: MatchDetailsProps) {
const { t } = useTranslation(sanctionsI18n);
const [selectedSanction, setSelectedSanction] = useState<SanctionCheckSanctionEntity | null>(
null,
);

const displayProperties = getSanctionEntityProperties(entity.schema);
const entityPropertyList = displayProperties
.map((property) => {
const items = entity.properties[property] ?? [];
const itemsToDisplay = displayAll[property] ? items : items.slice(0, 5);
return {
property,
values: itemsToDisplay,
restItemsCount: Math.max(0, items.length - itemsToDisplay.length),
};
})
.filter((prop) => prop.values.length > 0);

const handleShowMore = (prop: string) => {
setDisplayAll((prev) => ({ ...prev, [prop]: true }));
};

return (
<div className="grid grid-cols-[168px,_1fr] gap-2">
{entityPropertyList.map(({ property, values, restItemsCount }) => {
return (
<Fragment key={property}>
<span className="font-bold">{t(`sanctions:entity.property.${property}`)}</span>
<span className="flex flex-wrap gap-1 break-all">
{values.map((v, i) => (
<Fragment key={i}>
<TransformProperty property={property} value={v} />
{i === values.length - 1 ? null : <span>·</span>}
</Fragment>
))}
{restItemsCount > 0 ? (
<>
<span>·</span>
<button
onClick={(e) => {
e.preventDefault();
handleShowMore(property);
}}
className="text-purple-65 font-semibold"
<div className="flex flex-col gap-4">
<EntityProperties
entity={entity}
after={
entity.properties.sanctions ? (
<>
<span className="font-bold">{t('sanctions:entity.property.sanctions')}</span>
<div className="flex flex-col gap-2">
{entity.properties.sanctions.map((sanction) => (
<div
key={sanction.id}
className="group/sanction bg-grey-100 grid grid-cols-[1fr_20px] gap-2 rounded p-2"
>
+ {restItemsCount} more
</button>
</>
) : null}
</span>
</Fragment>
);
})}
<span className="truncate">{sanction.properties['authority']}</span>
<button type="button" onClick={() => setSelectedSanction(sanction)}>
<Icon
icon="visibility-on"
className="text-grey-90 hover:text-purple-65 size-5 cursor-pointer"
/>
</button>
</div>
))}
</div>

<ModalV2.Content
open={!!selectedSanction}
onClose={() => setSelectedSanction(null)}
size="large"
className="max-h-[80vh]"
>
<ModalV2.Title>{t('sanctions:sanction_detail.title')}</ModalV2.Title>
<div className="overflow-y-auto p-6">
{selectedSanction ? (
<EntityProperties entity={selectedSanction} forcedProperties={sanctionProps} />
) : null}
</div>
</ModalV2.Content>
</>
) : null
}
/>
</div>
);
}
37 changes: 30 additions & 7 deletions packages/app-builder/src/constants/sanction-check-entity.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ExternalLink } from '@app-builder/components/ExternalLink';
import { type SanctionCheckEntitySchema } from '@app-builder/models/sanction-check';
import { type OpenSanctionEntitySchema } from '@app-builder/models/sanction-check';

export type PropertyDataType = 'string' | 'country' | 'url' | 'date' | 'wikidataId';
export type PropertyForSchema<
Schema extends SanctionCheckEntitySchema,
Schema extends OpenSanctionEntitySchema,
_R = never,
> = (typeof schemaInheritence)[Schema] extends null
? _R | (typeof schemaProperties)[Schema][number]
: (typeof schemaInheritence)[Schema] extends infer P extends SanctionCheckEntitySchema
: (typeof schemaInheritence)[Schema] extends infer P extends OpenSanctionEntitySchema
? PropertyForSchema<P, _R | (typeof schemaProperties)[Schema][number]>
: never;

Expand Down Expand Up @@ -88,7 +88,21 @@ export const schemaProperties = {
Vehicle: ['registrationNumber'] as const,
Airplane: [] as const,
Vessel: [] as const,
} satisfies Record<SanctionCheckEntitySchema, string[]>;
Sanction: [
'country',
'authority',
'authorityId',
'program',
'startDate',
'endDate',
'listingDate',
'sourceUrl',
'reason',
'summary',
'programId',
'programUrl',
] as const,
} satisfies Record<OpenSanctionEntitySchema, string[]>;

export type SanctionCheckEntityProperty =
(typeof schemaProperties)[keyof typeof schemaProperties][number];
Expand All @@ -102,7 +116,8 @@ const schemaInheritence = {
Vehicle: 'Thing',
Vessel: 'Vehicle',
Airplane: 'Vehicle',
} satisfies Record<SanctionCheckEntitySchema, SanctionCheckEntitySchema | null>;
Sanction: null,
} satisfies Record<OpenSanctionEntitySchema, OpenSanctionEntitySchema | null>;

const propertyMetadata = {
address: { type: 'string' },
Expand Down Expand Up @@ -170,10 +185,18 @@ const propertyMetadata = {
website: { type: 'url' },
weight: { type: 'string' },
wikidataId: { type: 'wikidataId' },
authority: { type: 'string' },
authorityId: { type: 'string' },
startDate: { type: 'date' },
endDate: { type: 'date' },
programId: { type: 'string' },
programUrl: { type: 'url' },
reason: { type: 'string' },
listingDate: { type: 'date' },
} satisfies Record<SanctionCheckEntityProperty, { type: PropertyDataType }>;

export function getSanctionEntityProperties(schema: SanctionCheckEntitySchema) {
let currentSchema: SanctionCheckEntitySchema | null = schema;
export function getSanctionEntityProperties(schema: OpenSanctionEntitySchema) {
let currentSchema: OpenSanctionEntitySchema | null = schema;
const properties: SanctionCheckEntityProperty[] = [];

do {
Expand Down
15 changes: 14 additions & 1 deletion packages/app-builder/src/locales/ar/sanctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,18 @@
"error_label_one": "التحقق من العقوبة خطأ للسبب التالي",
"error_label_others": "فحص العقوبة خطأ للأسباب التالية",
"match.not_reviewable": "غير قابل للمراجعة",
"refine_modal.search_input_label": "يغطي البحث الحقول التالية:"
"refine_modal.search_input_label": "يغطي البحث الحقول التالية:",
"entity.property.authority": "سلطة",
"entity.property.authorityId": "المعرف الصادر عن السلطة",
"entity.property.endDate": "تاريخ الانتهاء",
"entity.property.listingDate": "تاريخ الإدراج",
"entity.property.programId": "معرف البرنامج",
"entity.property.programUrl": "برنامج URL",
"entity.property.reason": "سبب",
"entity.property.startDate": "تاريخ البدء",
"enrich_button": "إثراء",
"entity.property.sanctions": "العقوبات",
"sanction_detail.title": "تفاصيل العقوبات",
"error.match_already_enriched": "تطابق المخصب بالفعل",
"success.match_enriched": "تطابق المباراة بنجاح"
}
15 changes: 14 additions & 1 deletion packages/app-builder/src/locales/en/sanctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@
"entity.property.hairColor": "Hair color",
"entity.property.appearance": "Appearance",
"entity.property.political": "Political association",
"entity.property.sanctions": "Sanctions",
"entity.property.programId": "Program ID",
"entity.property.programUrl": "Program URL",
"entity.property.authority": "Authority",
"entity.property.authorityId": "Authority-issued Identifier",
"entity.property.startDate": "Start date",
"entity.property.endDate": "End date",
"entity.property.listingDate": "Listing date",
"entity.property.reason": "Reason",
"entity.schema.airplane": "Airplane",
"entity.schema.company": "Company",
"entity.schema.person": "Person",
Expand All @@ -117,5 +126,9 @@
"entity.schema.legalentity": "Legal Entity",
"entity.schema.vehicle": "Vehicle",
"entity.schema.vessel": "Vessel",
"refine_modal.schema.legalentity": "Legal Entity"
"refine_modal.schema.legalentity": "Legal Entity",
"sanction_detail.title": "Sanction details",
"enrich_button": "Enrich",
"success.match_enriched": "Match successfully enriched",
"error.match_already_enriched": "Match already enriched"
}
Loading

0 comments on commit 6f93625

Please sign in to comment.