Skip to content

Commit

Permalink
feat: duplicates in portalicious
Browse files Browse the repository at this point in the history
AB#33536 AB#33535
  • Loading branch information
aberonni committed Feb 18, 2025
1 parent d4388ae commit 160241e
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 21 deletions.
14 changes: 14 additions & 0 deletions interfaces/Portalicious/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions interfaces/Portalicious/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions interfaces/Portalicious/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@if (loading() || duplicates().length > 0) {
<div
class="mb-6 flex w-full items-start rounded-2xl border p-4"
[ngClass]="{
'border-blue-500 bg-blue-100': loading(),
'border-red-500 bg-red-100': !loading(),
}"
>
<svg-icon
src="assets/duplicates.svg"
class="h-7 w-7 text-red-700"
[ngClass]="{
'text-blue-700': loading(),
'text-red-700': !loading(),
}"
></svg-icon>
@if (loading()) {
<strong
i18n
class="mx-2 me-auto text-blue-700"
>Loading duplicate information...</strong
>
<i class="pi pi-spinner animate-spin self-center text-xl"></i>
} @else {
<strong
i18n
class="mx-2 text-red-700"
>Duplicated with:</strong
>
<ul class="me-auto">
@for (duplicate of duplicates(); track $index) {
<li class="ms-6 list-disc">
<a
[routerLink]="registrationLink(duplicate.registrationId)"
class="underline hover:no-underline focus:no-underline"
target="_blank"
>
<ng-container i18n>Reg. #</ng-container
>{{ duplicate.registrationProgramId }} -
@if (duplicate.isDuplicateAccessibleWithinScope) {
{{ duplicate.name }}
} @else {
<ng-container i18n
>(Scope - {{ duplicate.scope }})</ng-container
>
}
<span class="pi pi-external-link ms-1"></span>
</a>
<span
i18n
class="ms-1"
>
(matching fields:
{{
translatableStringService.commaSeparatedList(
duplicate.attributeNames
)
}})</span
>
</li>
}
</ul>
<i
class="pi pi-info-circle cursor-help self-center text-xl"
pTooltip="To handle duplications you can edit the personal information or decline the registration."
i18n-pTooltip
></i>
}
</div>
}
Original file line number Diff line number Diff line change
@@ -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<string>();
readonly duplicates = input.required<DuplicatesResult[]>();
readonly loading = input.required<boolean>();

registrationLink = (registrationId: number | string) =>
// XXX: does this work with target blank in production?
registrationLink({ projectId: this.projectId(), registrationId });
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,46 @@
[pageTitle]="registrationTitle()"
[isPending]="registration.isPending()"
>
<app-registration-duplicates-banner
[projectId]="projectId()"
[duplicates]="duplicateArray()"
[loading]="duplicates.isPending()"
/>
<p-card
styleClass="[&_.p-card-content]:flex-end group relative h-full [&_.p-card-title]:flex-grow"
>
<div class="flex items-center space-x-4 border-b border-grey-300 pb-4">
<h1 class="me-auto">
<div
class="flex w-full items-center space-x-4 border-b border-grey-300 pb-4"
>
<h1>
{{ registrationTitle() }}
</h1>
@if (canUpdatePersonalData() && !registration.isError()) {
<p-button
label="Add note"
i18n-label="@@add-note"
rounded
outlined
[loading]="registration.isPending()"
icon="pi pi-pen-to-square"
(click)="addNoteFormVisible.set(true)"
@if (duplicates.isSuccess()) {
<app-colored-chip
[variant]="duplicateChipData().chipVariant"
[label]="duplicateChipData().chipLabel"
/>
@if (registration.isSuccess()) {
<app-add-note-form
[(formVisible)]="addNoteFormVisible"
[projectId]="projectId()"
[registrationId]="registrationId()"
}
<div class="!ms-auto">
@if (canUpdatePersonalData() && !registration.isError()) {
<p-button
label="Add note"
i18n-label="@@add-note"
rounded
outlined
[loading]="registration.isPending()"
icon="pi pi-pen-to-square"
(click)="addNoteFormVisible.set(true)"
/>
@if (registration.isSuccess()) {
<app-add-note-form
[(formVisible)]="addNoteFormVisible"
[projectId]="projectId()"
[registrationId]="registrationId()"
/>
}
}
}
</div>
</div>
<app-data-list [data]="registrationData()" />
<div class="mt-4 text-end">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -41,6 +47,8 @@ import { TranslatableStringService } from '~/services/translatable-string.servic
SkeletonInlineComponent,
AddNoteFormComponent,
RegistrationMenuComponent,
RegistrationDuplicatesBannerComponent,
ColoredChipComponent,
],
templateUrl: './registration-page-layout.component.html',
styles: ``,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DomainApiService } from '~/domains/domain-api.service';
import {
ActitivitiesResponse,
ChangeStatusResult,
DuplicatesResult,
FindAllRegistrationsResult,
Registration,
SendMessageData,
Expand Down Expand Up @@ -52,6 +53,18 @@ export class RegistrationApiService extends DomainApiService {
});
}

getDuplicates({
projectId,
referenceId,
}: {
projectId: Signal<number | string>;
referenceId: string;
}) {
return this.generateQueryOptions<DuplicatesResult[]>({
path: [...BASE_ENDPOINT(projectId), referenceId, 'duplicates'],
});
}

patchRegistration({
projectId,
referenceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -53,6 +54,11 @@ export const REGISTRATION_STATUS_VERB_PROGRESSIVE: Record<
[RegistrationStatusEnum.deleted]: $localize`Deleting`,
};

export const DUPLICATE_STATUS_LABELS: Record<DuplicateStatus, string> = {
[DuplicateStatus.duplicate]: $localize`:@@duplicate-status-duplicate:Duplicate`,
[DuplicateStatus.unique]: $localize`:@@duplicate-status-unique:Unique`,
};

export const LANGUAGE_ENUM_LABEL: Record<LanguageEnum, string> = {
ar: $localize`Arabic`,
en: $localize`English`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { DuplicateDto } from '@121-service/src/registration/dto/duplicate.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';

Expand All @@ -20,6 +21,7 @@ export type FindAllRegistrationsResult = {
} & Omit<Dto<FindAllRegistrationsResultDto>, 'data'>;

export type ChangeStatusResult = Dto<BulkActionResultDto>;
export type DuplicatesResult = Dto<DuplicateDto>;

// 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 =
Expand Down
Loading

0 comments on commit 160241e

Please sign in to comment.