diff --git a/interfaces/Portalicious/src/app/components/query-table/query-table.component.ts b/interfaces/Portalicious/src/app/components/query-table/query-table.component.ts index de520e21ba..85efda3283 100644 --- a/interfaces/Portalicious/src/app/components/query-table/query-table.component.ts +++ b/interfaces/Portalicious/src/app/components/query-table/query-table.component.ts @@ -50,6 +50,7 @@ import { PaginateQuery, PaginateQueryService, } from '~/services/paginate-query.service'; +import { ToastService } from '~/services/toast.service'; import { Locale } from '~/utils/locale'; export enum QueryTableColumnType { @@ -111,6 +112,7 @@ export type QueryTableSelectionEvent = { selectAll: true } | TData[]; QueryTableGlobalSearchComponent, QueryTableColumnManagementComponent, ], + providers: [ToastService], templateUrl: './query-table.component.html', styles: ``, changeDetection: ChangeDetectionStrategy.OnPush, @@ -118,6 +120,7 @@ export type QueryTableSelectionEvent = { selectAll: true } | TData[]; export class QueryTableComponent { locale = inject(LOCALE_ID); paginateQueryService = inject(PaginateQueryService); + toastService = inject(ToastService); items = input.required(); isPending = input.required(); @@ -135,7 +138,6 @@ export class QueryTableComponent { enableColumnManagement = input(false); readonly onUpdateContextMenuItem = output(); readonly onUpdatePaginateQuery = output(); - readonly onUpdateSelection = output>(); @ViewChild('table') table: Table; @ViewChild('contextMenu') contextMenu: Menu; @@ -349,10 +351,11 @@ export class QueryTableComponent { selectedItems = model([]); selectAll = model(false); + tableSelection = signal>([]); onSelectionChange(items: TData[]) { this.selectedItems.set(items); - this.onUpdateSelection.emit(items); + this.tableSelection.set(items); } onSelectAllChange(event: TableSelectAllChangeEvent) { @@ -362,9 +365,9 @@ export class QueryTableComponent { this.selectAll.set(checked); if (checked) { - this.onUpdateSelection.emit({ selectAll: true }); + this.tableSelection.set({ selectAll: true }); } else { - this.onUpdateSelection.emit([]); + this.tableSelection.set([]); } } @@ -377,7 +380,58 @@ export class QueryTableComponent { resetSelection() { this.selectedItems.set([]); this.selectAll.set(false); - this.onUpdateSelection.emit([]); + this.tableSelection.set([]); + } + + public getActionData({ + fieldForFilter, + currentPaginateQuery, + noSelectionToastMessage, + triggeredFromContextMenu = false, + contextMenuItem, + }: { + fieldForFilter: keyof TData & string; + noSelectionToastMessage: string; + currentPaginateQuery?: PaginateQuery; + triggeredFromContextMenu?: boolean; + contextMenuItem?: TData; + }) { + let selection = this.tableSelection(); + + if ('selectAll' in selection && !this.serverSideFiltering()) { + const filteredValue = this.table.filteredValue; + + if (!filteredValue) { + this.toastService.showGenericError(); + return; + } + + selection = [...(filteredValue as TData[])]; + } + + if (Array.isArray(selection) && selection.length === 0) { + if (triggeredFromContextMenu) { + if (!contextMenuItem) { + this.toastService.showGenericError(); + return; + } + selection = [contextMenuItem]; + } else { + this.toastService.showToast({ + severity: 'error', + detail: noSelectionToastMessage, + }); + return; + } + } + + return this.paginateQueryService.selectionEventToActionData({ + selection, + fieldForFilter, + totalCount: this.totalRecords(), + currentPaginateQuery, + previewItemForSelectAll: this.items()[0], + }); } /** diff --git a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.html b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.html index 05eeccfa31..9e126b300d 100644 --- a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.html +++ b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.html @@ -8,7 +8,6 @@ [serverSideTotalRecords]="totalRegistrations()" (onUpdatePaginateQuery)="paginateQuery.set($event)" [enableSelection]="true" - (onUpdateSelection)="tableSelection.set($event)" [contextMenuItems]="contextMenuItems()" (onUpdateContextMenuItem)="contextMenuRegistration.set($event)" [enableColumnManagement]="true" diff --git a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts index 06a328f4db..79a4b5ad3b 100644 --- a/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts +++ b/interfaces/Portalicious/src/app/components/registrations-table/registrations-table.component.ts @@ -19,7 +19,6 @@ import { QueryTableColumn, QueryTableColumnType, QueryTableComponent, - QueryTableSelectionEvent, } from '~/components/query-table/query-table.component'; import { ProjectApiService } from '~/domains/project/project.api.service'; import { RegistrationApiService } from '~/domains/registration/registration.api.service'; @@ -28,11 +27,7 @@ import { registrationLink, } from '~/domains/registration/registration.helper'; import { Registration } from '~/domains/registration/registration.model'; -import { - PaginateQuery, - PaginateQueryService, -} from '~/services/paginate-query.service'; -import { ToastService } from '~/services/toast.service'; +import { PaginateQuery } from '~/services/paginate-query.service'; import { TranslatableStringService } from '~/services/translatable-string.service'; @Component({ @@ -41,7 +36,6 @@ import { TranslatableStringService } from '~/services/translatable-string.servic imports: [QueryTableComponent], templateUrl: './registrations-table.component.html', styles: ``, - providers: [ToastService], changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistrationsTableComponent { @@ -51,10 +45,8 @@ export class RegistrationsTableComponent { overrideFilters = input>({}); showSelectionInHeader = input(false); - private paginateQueryService = inject(PaginateQueryService); private projectApiService = inject(ProjectApiService); private registrationApiService = inject(RegistrationApiService); - private toastService = inject(ToastService); private translatableStringService = inject(TranslatableStringService); PermissionEnum = PermissionEnum; @@ -64,7 +56,6 @@ export class RegistrationsTableComponent { protected RegistrationStatusEnum = RegistrationStatusEnum; protected paginateQuery = signal(undefined); - protected tableSelection = signal>([]); public contextMenuRegistration = signal(undefined); private registrationsPaginateQuery = computed(() => { @@ -168,31 +159,12 @@ export class RegistrationsTableComponent { }: { triggeredFromContextMenu?: boolean; } = {}) { - let selection = this.tableSelection(); - - if (Array.isArray(selection) && selection.length === 0) { - if (triggeredFromContextMenu) { - const contextMenuRegistration = this.contextMenuRegistration(); - if (!contextMenuRegistration) { - this.toastService.showGenericError(); - return; - } - selection = [contextMenuRegistration]; - } else { - this.toastService.showToast({ - severity: 'error', - detail: $localize`:@@no-registrations-selected:Select one or more registrations and try again.`, - }); - return; - } - } - - return this.paginateQueryService.selectionEventToActionData({ - selection, + return this.table.getActionData({ + triggeredFromContextMenu, + contextMenuItem: this.contextMenuRegistration(), fieldForFilter: 'referenceId', - totalCount: this.totalRegistrations(), currentPaginateQuery: this.registrationsPaginateQuery(), - previewItemForSelectAll: this.registrations()[0], + noSelectionToastMessage: $localize`:@@no-registrations-selected:Select one or more registrations and try again.`, }); } diff --git a/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts b/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts index 85820483ea..09681c7d61 100644 --- a/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts @@ -2,6 +2,7 @@ import { HttpParamsOptions } from '@angular/common/http'; import { Injectable, Signal } from '@angular/core'; import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; +import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { DomainApiService } from '~/domains/domain-api.service'; import { @@ -49,12 +50,22 @@ export class MetricApiService extends DomainApiService { projectId: Signal; payment: Signal; }) { - return this.generateQueryOptions<{ data: PaymentMetricDetails[] }>({ + return this.generateQueryOptions< + { data: PaymentMetricDetails[] }, + PaymentMetricDetails[] + >({ path: [...BASE_ENDPOINT(projectId), 'export-list', ExportType.payment], params: { minPayment: payment, maxPayment: payment, }, + processResponse: (response) => { + // TODO: AB#32158 - Should we filter out deleted transactions here? + return response.data.filter( + (transaction) => + transaction.registrationStatus !== RegistrationStatusEnum.deleted, + ); + }, }); } diff --git a/interfaces/Portalicious/src/app/domains/metric/metric.model.ts b/interfaces/Portalicious/src/app/domains/metric/metric.model.ts index 3f4cad34d2..f5836a7efd 100644 --- a/interfaces/Portalicious/src/app/domains/metric/metric.model.ts +++ b/interfaces/Portalicious/src/app/domains/metric/metric.model.ts @@ -1,6 +1,7 @@ import { FinancialServiceProviders } from '@121-service/src/financial-service-providers/enum/financial-service-provider-name.enum'; import { ProgramStats } from '@121-service/src/metrics/dto/program-stats.dto'; import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum'; +import { RegistrationStatusEnum } from '@121-service/src/registration/enum/registration-status.enum'; import { Dto } from '~/utils/dto-type'; @@ -23,4 +24,5 @@ export interface PaymentMetricDetails { amount: number; financialserviceprovider: FinancialServiceProviders; fullName: string; + registrationStatus: RegistrationStatusEnum; } diff --git a/interfaces/Portalicious/src/app/domains/payment/payment.api.service.ts b/interfaces/Portalicious/src/app/domains/payment/payment.api.service.ts index af664941b5..e312cbd2c7 100644 --- a/interfaces/Portalicious/src/app/domains/payment/payment.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/payment/payment.api.service.ts @@ -2,6 +2,7 @@ import { Injectable, Signal } from '@angular/core'; import { CreatePaymentDto } from '@121-service/src/payments/dto/create-payment.dto'; import { FspInstructions } from '@121-service/src/payments/dto/fsp-instructions.dto'; +import { RetryPaymentDto } from '@121-service/src/payments/dto/retry-payment.dto'; import { BulkActionResultPaymentDto } from '@121-service/src/registration/dto/bulk-action-result.dto'; import { DomainApiService } from '~/domains/domain-api.service'; @@ -72,6 +73,31 @@ export class PaymentApiService extends DomainApiService { }); } + retryFailedTransfers({ + projectId, + paymentId, + referenceIds, + }: { + projectId: Signal; + paymentId: number; + referenceIds: string[]; + }) { + const body: Dto = { + payment: paymentId, + referenceIds: { + referenceIds, + }, + }; + + return this.httpWrapperService.perform121ServiceRequest< + Dto + >({ + method: 'PATCH', + endpoint: this.pathToQueryKey([...BASE_ENDPOINT(projectId)]).join('/'), + body, + }); + } + exportFspInstructions({ projectId, paymentId, diff --git a/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.html b/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.html new file mode 100644 index 0000000000..e1814ae976 --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.html @@ -0,0 +1,16 @@ + +

+ You are about to retry + {{ referenceIdsForRetryTransfers().length }} payment(s). The transfer status + will change to Pending until received by the registration. +

+
diff --git a/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.ts b/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.ts new file mode 100644 index 0000000000..590dc28065 --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component.ts @@ -0,0 +1,90 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input, + signal, + ViewChild, +} from '@angular/core'; + +import { injectMutation } from '@tanstack/angular-query-experimental'; + +import { ConfirmationDialogComponent } from '~/components/confirmation-dialog/confirmation-dialog.component'; +import { QueryTableComponent } from '~/components/query-table/query-table.component'; +import { PaymentMetricDetails } from '~/domains/metric/metric.model'; +import { PaymentApiService } from '~/domains/payment/payment.api.service'; +import { ToastService } from '~/services/toast.service'; + +@Component({ + selector: 'app-retry-transfers-dialog', + standalone: true, + imports: [ConfirmationDialogComponent], + templateUrl: './retry-transfers-dialog.component.html', + styles: ``, + providers: [ToastService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RetryTransfersDialogComponent { + readonly projectId = input.required(); + readonly paymentId = input.required(); + + private paymentApiService = inject(PaymentApiService); + private toastService = inject(ToastService); + + referenceIdsForRetryTransfers = signal([]); + + @ViewChild('retryTransfersConfirmationDialog') + private retryTransfersConfirmationDialog: ConfirmationDialogComponent; + + retryFailedTransfersMutation = injectMutation(() => ({ + mutationFn: (referenceIds: string[]) => + this.paymentApiService.retryFailedTransfers({ + projectId: this.projectId, + paymentId: this.paymentId(), + referenceIds, + }), + onSuccess: () => + this.paymentApiService.invalidateCache(this.projectId, this.paymentId), + onError: (error) => { + this.toastService.showToast({ + severity: 'error', + detail: error.message, + }); + }, + })); + + public retryFailedTransfers({ + table, + triggeredFromContextMenu, + contextMenuItem, + }: { + table: QueryTableComponent; + triggeredFromContextMenu: boolean; + contextMenuItem?: PaymentMetricDetails; + }) { + const actionData = table.getActionData({ + triggeredFromContextMenu, + contextMenuItem, + fieldForFilter: 'referenceId', + noSelectionToastMessage: $localize`:@@no-registrations-selected:Select one or more registrations and try again.`, + }); + + if (!actionData) { + return; + } + + const selection = actionData.selection; + + if (!Array.isArray(selection) || selection.length === 0) { + this.toastService.showGenericError(); // Should never happen + return; + } + + const referenceIds = selection.map( + (transaction) => transaction.referenceId, + ); + + this.referenceIdsForRetryTransfers.set(referenceIds); + this.retryTransfersConfirmationDialog.askForConfirmation(); + } +} diff --git a/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.html b/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.html index 8b048efb01..a5bde835fb 100644 --- a/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.html +++ b/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.html @@ -23,7 +23,7 @@ @@ -49,6 +49,8 @@ [chipLabel]="successfulPaymentsAmount()" chipIcon="pi pi-check-circle" chipVariant="green" + metricTooltip="The total payment amount is calculated by summing up the transfer values of each included registration added to the payment." + i18n-metricTooltip /> @let paymentDetails = this.payment.data(); @@ -74,13 +76,15 @@ + + diff --git a/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.ts b/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.ts index 4a3d875bca..efbecf7e25 100644 --- a/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.ts +++ b/interfaces/Portalicious/src/app/pages/project-payment/project-payment.page.ts @@ -8,6 +8,7 @@ import { LOCALE_ID, Signal, signal, + ViewChild, } from '@angular/core'; import { Router } from '@angular/router'; @@ -39,6 +40,7 @@ import { import { MetricTileComponent } from '~/pages/project-monitoring/components/metric-tile/metric-tile.component'; import { ExportPaymentInstructionsComponent } from '~/pages/project-payment/components/export-payment-instructions/export-payment-instructions.component'; import { ProjectPaymentChartComponent } from '~/pages/project-payment/components/project-payment-chart/project-payment-chart.component'; +import { RetryTransfersDialogComponent } from '~/pages/project-payment/components/retry-transfers-dialog/retry-transfers-dialog.component'; import { AuthService } from '~/services/auth.service'; import { ToastService } from '~/services/toast.service'; import { TranslatableStringService } from '~/services/translatable-string.service'; @@ -59,7 +61,7 @@ export interface TransactionsTableCellContext { ProjectPaymentChartComponent, SkeletonModule, ExportPaymentInstructionsComponent, - CurrencyPipe, + RetryTransfersDialogComponent, ], templateUrl: './project-payment.page.html', styles: ``, @@ -80,6 +82,11 @@ export class ProjectPaymentPageComponent { private toastService = inject(ToastService); private translatableStringService = inject(TranslatableStringService); + @ViewChild('table') + private table: QueryTableComponent; + @ViewChild('retryTransfersDialog') + private retryTransfersDialog: RetryTransfersDialogComponent; + contextMenuSelection = signal(undefined); project = injectQuery(this.projectApiService.getProject(this.projectId)); @@ -253,11 +260,7 @@ export class ProjectPaymentPageComponent { label: $localize`Retry failed transfers`, icon: 'pi pi-refresh', command: () => { - // TODO AB#31728: Implement this - this.toastService.showToast({ - detail: - "Haven't done this yet. Here is a lollipop while you wait: 🍭", - }); + this.retryFailedTransfers({ triggeredFromContextMenu: true }); }, visible: this.canRetryTransfers() && @@ -291,13 +294,26 @@ export class ProjectPaymentPageComponent { return this.transactions .data() - .data.some((payment) => payment.status === TransactionStatusEnum.error); + .some((payment) => payment.status === TransactionStatusEnum.error); }); - retryFailedTransfers() { - // TODO AB#31728: Implement this - this.toastService.showToast({ - detail: "Haven't done this yet. Here is a lollipop while you wait: 🍭", + retryFailedTransfers({ + triggeredFromContextMenu = false, + }: { + triggeredFromContextMenu?: boolean; + } = {}) { + if (this.paymentStatus.data()?.inProgress) { + this.toastService.showToast({ + severity: 'warn', + detail: $localize`A payment is currently in progress. Please wait until it has finished.`, + }); + return; + } + + this.retryTransfersDialog.retryFailedTransfers({ + table: this.table, + triggeredFromContextMenu, + contextMenuItem: this.contextMenuSelection(), }); } } diff --git a/interfaces/Portalicious/src/app/services/paginate-query.service.ts b/interfaces/Portalicious/src/app/services/paginate-query.service.ts index 0454b690fd..058eff31eb 100644 --- a/interfaces/Portalicious/src/app/services/paginate-query.service.ts +++ b/interfaces/Portalicious/src/app/services/paginate-query.service.ts @@ -24,6 +24,7 @@ export interface PaginateQuery { export interface ActionDataWithPaginateQuery { query: PaginateQuery; count: number; + selection: QueryTableSelectionEvent; selectAll: boolean; previewItem: T; } @@ -229,6 +230,7 @@ export class PaginateQueryService { limit: undefined, }, count: totalCount, + selection, selectAll: true, previewItem: previewItemForSelectAll, }; @@ -241,6 +243,7 @@ export class PaginateQueryService { }, }, count: selection.length, + selection, selectAll: false, previewItem: selection[0], }; diff --git a/interfaces/Portalicious/src/locale/messages.nl.xlf b/interfaces/Portalicious/src/locale/messages.nl.xlf index 97536161e9..8c40dae883 100644 --- a/interfaces/Portalicious/src/locale/messages.nl.xlf +++ b/interfaces/Portalicious/src/locale/messages.nl.xlf @@ -1584,6 +1584,18 @@ Registrations included Registrations included + + Retry failed transfers + Retry failed transfers + + + Retry transfers + Retry transfers + + + You are about to retry payment(s). The transfer status will change to Pending until received by the registration. + You are about to retry payment(s). The transfer status will change to Pending until received by the registration. + - + \ No newline at end of file diff --git a/interfaces/Portalicious/src/locale/messages.xlf b/interfaces/Portalicious/src/locale/messages.xlf index d65dec18ad..924b9db217 100644 --- a/interfaces/Portalicious/src/locale/messages.xlf +++ b/interfaces/Portalicious/src/locale/messages.xlf @@ -1190,6 +1190,15 @@ Registrations included + + Retry transfers + + + You are about to retry payment(s). The transfer status will change to Pending until received by the registration. + + + Retry failed transfers + \ No newline at end of file