From 33c146a0f251ae97c234e2ff3fd161ecd848c420 Mon Sep 17 00:00:00 2001 From: Domenico Gemoli Date: Tue, 18 Feb 2025 12:26:09 +0100 Subject: [PATCH] feat: duplicates in portalicious AB#33536 AB#33535 --- interfaces/Portalicious/package-lock.json | 14 ++++ interfaces/Portalicious/package.json | 1 + interfaces/Portalicious/src/app/app.config.ts | 2 + .../colored-chip/colored-chip.helper.ts | 10 +++ ...istration-duplicates-banner.component.html | 66 +++++++++++++++++++ ...egistration-duplicates-banner.component.ts | 35 ++++++++++ .../registration-page-layout.component.html | 49 +++++++++----- .../registration-page-layout.component.ts | 32 ++++++++- .../registration/registration.api.service.ts | 13 ++++ .../registration/registration.helper.ts | 6 ++ .../registration/registration.model.ts | 2 + .../registrations-table-column.service.ts | 20 +++++- .../Portalicious/src/assets/duplicates.svg | 15 +++++ .../Portalicious/src/locale/messages.xlf | 27 ++++++++ interfaces/Portalicious/src/styles.scss | 4 ++ 15 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.html create mode 100644 interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.ts create mode 100644 interfaces/Portalicious/src/assets/duplicates.svg diff --git a/interfaces/Portalicious/package-lock.json b/interfaces/Portalicious/package-lock.json index b6b11d1876..1886edf6e5 100644 --- a/interfaces/Portalicious/package-lock.json +++ b/interfaces/Portalicious/package-lock.json @@ -21,6 +21,7 @@ "@primeng/themes": "^19.0.2", "@tanstack/angular-query-experimental": "^5.62.16", "angular-mentions": "^1.5.0", + "angular-svg-icon": "^19.1.1", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", "filesize": "^9.0.11", @@ -7554,6 +7555,19 @@ "@angular/core": ">=7.2.0" } }, + "node_modules/angular-svg-icon": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/angular-svg-icon/-/angular-svg-icon-19.1.1.tgz", + "integrity": "sha512-4uKVdkc68ii2nadJAqJDbRfMFaD3JZ4AK2S28PjGDvXfvtE5T28lm/CFZA3MKqUesUZlneJAaOX4cpC3pvoCZQ==", + "dependencies": { + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "rxjs": ">=6.6.3" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", diff --git a/interfaces/Portalicious/package.json b/interfaces/Portalicious/package.json index 55a86e2076..4b92571703 100644 --- a/interfaces/Portalicious/package.json +++ b/interfaces/Portalicious/package.json @@ -44,6 +44,7 @@ "@primeng/themes": "^19.0.2", "@tanstack/angular-query-experimental": "^5.62.16", "angular-mentions": "^1.5.0", + "angular-svg-icon": "^19.1.1", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", "filesize": "^9.0.11", diff --git a/interfaces/Portalicious/src/app/app.config.ts b/interfaces/Portalicious/src/app/app.config.ts index 4afbae595d..8bf36625c2 100644 --- a/interfaces/Portalicious/src/app/app.config.ts +++ b/interfaces/Portalicious/src/app/app.config.ts @@ -19,6 +19,7 @@ import { provideTanStackQuery, QueryClient, } from '@tanstack/angular-query-experimental'; +import { provideAngularSvgIcon } from 'angular-svg-icon'; import { providePrimeNG } from 'primeng/config'; import { routes } from '~/app.routes'; @@ -33,6 +34,7 @@ export const getAppConfig = (locale: Locale): ApplicationConfig => ({ provideExperimentalZonelessChangeDetection(), provideAnimationsAsync(), provideHttpClient(withInterceptorsFromDi()), + provideAngularSvgIcon(), providePrimeNG({ theme: { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- AppTheme is typed as any in primeng diff --git a/interfaces/Portalicious/src/app/components/colored-chip/colored-chip.helper.ts b/interfaces/Portalicious/src/app/components/colored-chip/colored-chip.helper.ts index fc22614832..390f05e134 100644 --- a/interfaces/Portalicious/src/app/components/colored-chip/colored-chip.helper.ts +++ b/interfaces/Portalicious/src/app/components/colored-chip/colored-chip.helper.ts @@ -1,5 +1,6 @@ import { VisaCard121Status } from '@121-service/src/payments/fsp-integration/intersolve-visa/enums/wallet-status-121.enum'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +import { DuplicateStatus } from '@121-service/src/registration/enum/duplicate-status.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { ChipVariant } from '~/components/colored-chip/colored-chip.component'; @@ -9,6 +10,7 @@ import { MessageStatus, } from '~/domains/message/message.helper'; import { + DUPLICATE_STATUS_LABELS, REGISTRATION_STATUS_LABELS, VISA_CARD_STATUS_LABELS, } from '~/domains/registration/registration.helper'; @@ -85,3 +87,11 @@ export const getChipDataByVisaCardStatus = ( [VisaCard121Status.CardDataMissing]: 'orange', [VisaCard121Status.Paused]: 'orange', }); + +export const getChipDataByDuplicateStatus = ( + status?: DuplicateStatus | null, +): ChipData => + mapValueToChipData(status, DUPLICATE_STATUS_LABELS, { + [DuplicateStatus.unique]: 'green', + [DuplicateStatus.duplicate]: 'red', + }); diff --git a/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.html b/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.html new file mode 100644 index 0000000000..b97b6d60c1 --- /dev/null +++ b/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.html @@ -0,0 +1,66 @@ +@if (loading() || duplicates().length > 0) { +
+ + @if (loading()) { + Loading duplicate information... + + } @else { + Duplicated with: + + + } +
+} diff --git a/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.ts b/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.ts new file mode 100644 index 0000000000..de0b6bb564 --- /dev/null +++ b/interfaces/Portalicious/src/app/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component.ts @@ -0,0 +1,35 @@ +import { NgClass } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + inject, + input, +} from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; + +import { SvgIconComponent } from 'angular-svg-icon'; +import { TooltipModule } from 'primeng/tooltip'; + +import { registrationLink } from '~/domains/registration/registration.helper'; +import { DuplicatesResult } from '~/domains/registration/registration.model'; +import { TranslatableStringService } from '~/services/translatable-string.service'; + +@Component({ + selector: 'app-registration-duplicates-banner', + imports: [SvgIconComponent, RouterLink, TooltipModule, NgClass], + templateUrl: './registration-duplicates-banner.component.html', + styles: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistrationDuplicatesBannerComponent { + readonly translatableStringService = inject(TranslatableStringService); + readonly router = inject(Router); + + readonly projectId = input.required(); + readonly duplicates = input.required(); + readonly loading = input.required(); + + registrationLink = (registrationId: number | string) => + // XXX: does this work with target blank in production? + registrationLink({ projectId: this.projectId(), registrationId }); +} diff --git a/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.html b/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.html index 5d65c70f98..22afc481cc 100644 --- a/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.html +++ b/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.html @@ -5,31 +5,46 @@ [pageTitle]="registrationTitle()" [isPending]="registration.isPending()" > + -
-

+
+

{{ registrationTitle() }}

- @if (canUpdatePersonalData() && !registration.isError()) { - - @if (registration.isSuccess()) { - + @if (canUpdatePersonalData() && !registration.isError()) { + + @if (registration.isSuccess()) { + + } } - } +

diff --git a/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.ts b/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.ts index a3d58fcf0c..6721953b42 100644 --- a/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.ts +++ b/interfaces/Portalicious/src/app/components/registration-page-layout/registration-page-layout.component.ts @@ -12,16 +12,22 @@ import { injectQuery } from '@tanstack/angular-query-experimental'; import { ButtonModule } from 'primeng/button'; import { CardModule } from 'primeng/card'; +import { DuplicateStatus } from '@121-service/src/registration/enum/duplicate-status.enum'; import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; import { AppRoutes } from '~/app.routes'; -import { getChipDataByRegistrationStatus } from '~/components/colored-chip/colored-chip.helper'; +import { ColoredChipComponent } from '~/components/colored-chip/colored-chip.component'; +import { + getChipDataByDuplicateStatus, + getChipDataByRegistrationStatus, +} from '~/components/colored-chip/colored-chip.helper'; import { DataListComponent, DataListItem, } from '~/components/data-list/data-list.component'; import { PageLayoutComponent } from '~/components/page-layout/page-layout.component'; import { AddNoteFormComponent } from '~/components/registration-page-layout/components/add-note-form/add-note-form.component'; +import { RegistrationDuplicatesBannerComponent } from '~/components/registration-page-layout/components/registration-duplicates-banner/registration-duplicates-banner.component'; import { RegistrationMenuComponent } from '~/components/registration-page-layout/components/registration-menu/registration-menu.component'; import { SkeletonInlineComponent } from '~/components/skeleton-inline/skeleton-inline.component'; import { ProjectApiService } from '~/domains/project/project.api.service'; @@ -41,6 +47,8 @@ import { TranslatableStringService } from '~/services/translatable-string.servic SkeletonInlineComponent, AddNoteFormComponent, RegistrationMenuComponent, + RegistrationDuplicatesBannerComponent, + ColoredChipComponent, ], templateUrl: './registration-page-layout.component.html', styles: ``, @@ -63,6 +71,16 @@ export class RegistrationPageLayoutComponent { this.registrationId, ), ); + duplicates = injectQuery(() => ({ + ...this.registrationApiService.getDuplicates({ + projectId: this.projectId, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed by enabled + referenceId: this.registration.data()!.referenceId, + })(), + enabled: !!this.registration.data(), + })); + + readonly addNoteFormVisible = signal(false); readonly registrationData = computed(() => { const registrationRawData = this.registration.data(); @@ -129,14 +147,22 @@ export class RegistrationPageLayoutComponent { return `${localized}${this.registration.data()?.registrationProgramId.toString() ?? ''} - ${this.registration.data()?.name ?? ''}`; }); - readonly addNoteFormVisible = signal(false); - readonly canUpdatePersonalData = computed(() => this.authService.hasPermission({ projectId: this.projectId(), requiredPermission: PermissionEnum.RegistrationPersonalUPDATE, }), ); + + readonly duplicateArray = computed(() => this.duplicates.data() ?? []); + readonly duplicateChipData = computed(() => + getChipDataByDuplicateStatus( + this.duplicateArray().length > 0 + ? DuplicateStatus.duplicate + : DuplicateStatus.unique, + ), + ); + private getPaymentCountString( paymentCount?: null | number, maxPayments?: null | number, diff --git a/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts b/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts index 16253b032a..5e5cbd2d5f 100644 --- a/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/registration/registration.api.service.ts @@ -10,6 +10,7 @@ import { DomainApiService } from '~/domains/domain-api.service'; import { ActitivitiesResponse, ChangeStatusResult, + DuplicatesResult, FindAllRegistrationsResult, Registration, SendMessageData, @@ -52,6 +53,18 @@ export class RegistrationApiService extends DomainApiService { }); } + getDuplicates({ + projectId, + referenceId, + }: { + projectId: Signal; + referenceId: string; + }) { + return this.generateQueryOptions({ + path: [...BASE_ENDPOINT(projectId), referenceId, 'duplicates'], + }); + } + patchRegistration({ projectId, referenceId, diff --git a/interfaces/Portalicious/src/app/domains/registration/registration.helper.ts b/interfaces/Portalicious/src/app/domains/registration/registration.helper.ts index eac49e08ce..ca90669a6d 100644 --- a/interfaces/Portalicious/src/app/domains/registration/registration.helper.ts +++ b/interfaces/Portalicious/src/app/domains/registration/registration.helper.ts @@ -1,5 +1,6 @@ import { ActivityTypeEnum } from '@121-service/src/activities/enum/activity-type.enum'; import { VisaCard121Status } from '@121-service/src/payments/fsp-integration/intersolve-visa/enums/wallet-status-121.enum'; +import { DuplicateStatus } from '@121-service/src/registration/enum/duplicate-status.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { LanguageEnum } from '@121-service/src/shared/enum/language.enums'; @@ -53,6 +54,11 @@ export const REGISTRATION_STATUS_VERB_PROGRESSIVE: Record< [RegistrationStatusEnum.deleted]: $localize`Deleting`, }; +export const DUPLICATE_STATUS_LABELS: Record = { + [DuplicateStatus.duplicate]: $localize`:@@duplicate-status-duplicate:Duplicate`, + [DuplicateStatus.unique]: $localize`:@@duplicate-status-unique:Unique`, +}; + export const LANGUAGE_ENUM_LABEL: Record = { ar: $localize`Arabic`, en: $localize`English`, diff --git a/interfaces/Portalicious/src/app/domains/registration/registration.model.ts b/interfaces/Portalicious/src/app/domains/registration/registration.model.ts index 06e9245167..9cff34a2ad 100644 --- a/interfaces/Portalicious/src/app/domains/registration/registration.model.ts +++ b/interfaces/Portalicious/src/app/domains/registration/registration.model.ts @@ -7,6 +7,7 @@ import { StatusChangeActivity } from '@121-service/src/activities/interfaces/sta import { TransactionActivity } from '@121-service/src/activities/interfaces/transaction-activity.interface'; import { IntersolveVisaWalletDto } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/internal/intersolve-visa-wallet.dto'; import { BulkActionResultDto } from '@121-service/src/registration/dto/bulk-action-result.dto'; +import { DuplicateReponseDto } from '@121-service/src/registration/dto/duplicate-response.dto'; import { FindAllRegistrationsResultDto } from '@121-service/src/registration/dto/find-all-registrations-result.dto'; import { MappedPaginatedRegistrationDto } from '@121-service/src/registration/dto/mapped-paginated-registration.dto'; @@ -20,6 +21,7 @@ export type FindAllRegistrationsResult = { } & Omit, 'data'>; export type ChangeStatusResult = Dto; +export type DuplicatesResult = Dto; // The discriminated union type doesn't play well with our Dto utility type, so we need to define the Activity type manually export type Activity = diff --git a/interfaces/Portalicious/src/app/services/registrations-table-column.service.ts b/interfaces/Portalicious/src/app/services/registrations-table-column.service.ts index e7d82be2ed..9d980bdaa9 100644 --- a/interfaces/Portalicious/src/app/services/registrations-table-column.service.ts +++ b/interfaces/Portalicious/src/app/services/registrations-table-column.service.ts @@ -5,16 +5,21 @@ import { queryOptions, } from '@tanstack/angular-query-experimental'; +import { DuplicateStatus } from '@121-service/src/registration/enum/duplicate-status.enum'; import { RegistrationAttributeTypes } from '@121-service/src/registration/enum/registration-attribute.enum'; import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; -import { getChipDataByRegistrationStatus } from '~/components/colored-chip/colored-chip.helper'; +import { + getChipDataByDuplicateStatus, + getChipDataByRegistrationStatus, +} from '~/components/colored-chip/colored-chip.helper'; import { QueryTableColumn, QueryTableColumnType, } from '~/components/query-table/query-table.component'; import { ProjectApiService } from '~/domains/project/project.api.service'; import { + DUPLICATE_STATUS_LABELS, REGISTRATION_STATUS_LABELS, registrationLink, } from '~/domains/registration/registration.helper'; @@ -30,12 +35,14 @@ export const FILTERABLE_ATTRIBUTES_LABELS: Record = { paymentCountRemaining: $localize`:@@payment-count-remaining:Remaining payments`, maxPayments: $localize`:@@max-payments:Max payments`, lastMessageStatus: $localize`:@@last-message-status:Last message status`, + duplicateStatus: $localize`:@@duplicate-status:Duplicates`, }; export const DEFAULT_VISIBLE_FIELDS_SORTED: string[] = [ 'registrationProgramId', 'name', 'status', + 'duplicateStatus', 'phoneNumber', 'paymentCount', 'maxPayments', @@ -113,6 +120,17 @@ export class RegistrationsTableColumnService { getCellChipData: (registration) => getChipDataByRegistrationStatus(registration.status), }, + { + field: 'duplicateStatus', + header: $localize`:@@registration-duplicates:Duplicates`, + type: QueryTableColumnType.MULTISELECT, + options: Object.values(DuplicateStatus).map((status) => ({ + label: DUPLICATE_STATUS_LABELS[status], + value: status, + })), + getCellChipData: (registration) => + getChipDataByDuplicateStatus(registration.duplicateStatus), + }, { field: 'programFinancialServiceProviderConfigurationName', header: $localize`FSP`, diff --git a/interfaces/Portalicious/src/assets/duplicates.svg b/interfaces/Portalicious/src/assets/duplicates.svg new file mode 100644 index 0000000000..31c5c28db8 --- /dev/null +++ b/interfaces/Portalicious/src/assets/duplicates.svg @@ -0,0 +1,15 @@ + + + + diff --git a/interfaces/Portalicious/src/locale/messages.xlf b/interfaces/Portalicious/src/locale/messages.xlf index ba4191536a..91dfcad39d 100644 --- a/interfaces/Portalicious/src/locale/messages.xlf +++ b/interfaces/Portalicious/src/locale/messages.xlf @@ -1534,6 +1534,33 @@ Retry transfer + + Loading duplicate information... + + + Duplicated with: + + + (matching fields: ) + + + (Scope - ) + + + To handle duplications you can edit the personal information or decline the registration. + + + Duplicates + + + Duplicate + + + Unique + + + Duplicates + \ No newline at end of file diff --git a/interfaces/Portalicious/src/styles.scss b/interfaces/Portalicious/src/styles.scss index df73a7882b..69cd4273fe 100644 --- a/interfaces/Portalicious/src/styles.scss +++ b/interfaces/Portalicious/src/styles.scss @@ -20,6 +20,10 @@ body { --font-family: theme(fontFamily.body); } +svg-icon svg { + fill: currentColor; +} + @layer tailwind-base { h1 { @apply font-bold txt-h-1;