diff --git a/interfaces/Portalicious/eslint.config.js b/interfaces/Portalicious/eslint.config.js index ffa95949a5..411801fffa 100644 --- a/interfaces/Portalicious/eslint.config.js +++ b/interfaces/Portalicious/eslint.config.js @@ -128,6 +128,8 @@ module.exports = tseslint.config( 'p-dropdown[appendTo]', 'p-iconField[iconPosition]', 'p-sidebar[position]', + 'p-splitButton[icon]', + 'p-splitButton[menuStyleClass]', 'p-table[stateKey]', 'p-table[stateStorage]', 'styleClass', diff --git a/interfaces/Portalicious/src/app/components/confirmation-dialog/confirmation-dialog.component.ts b/interfaces/Portalicious/src/app/components/confirmation-dialog/confirmation-dialog.component.ts index 72ad742a61..0b1bd791c7 100644 --- a/interfaces/Portalicious/src/app/components/confirmation-dialog/confirmation-dialog.component.ts +++ b/interfaces/Portalicious/src/app/components/confirmation-dialog/confirmation-dialog.component.ts @@ -25,8 +25,8 @@ import { FormErrorComponent } from '~/components/form-error/form-error.component export class ConfirmationDialogComponent { private confirmationService = inject(ConfirmationService); - mutation = - input.required>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mutation = input.required>(); mutationData = input.required(); header = input($localize`:@@confirmation-dialog-header:Are you sure?`); headerIcon = input('pi pi-question'); diff --git a/interfaces/Portalicious/src/app/domains/event/event.api.service.ts b/interfaces/Portalicious/src/app/domains/event/event.api.service.ts new file mode 100644 index 0000000000..d58705fc09 --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/event/event.api.service.ts @@ -0,0 +1,36 @@ +import { HttpParams } from '@angular/common/http'; +import { Injectable, Signal } from '@angular/core'; + +import { DomainApiService } from '~/domains/domain-api.service'; + +const BASE_ENDPOINT = (projectId: Signal) => [ + 'programs', + projectId, + 'events', +]; + +@Injectable({ + providedIn: 'root', +}) +export class EventApiService extends DomainApiService { + getEvents({ + projectId, + params, + }: { + projectId: Signal; + params: HttpParams; + }) { + return this.httpWrapperService.perform121ServiceRequest({ + method: 'GET', + endpoint: this.pathToQueryKey([...BASE_ENDPOINT(projectId)]).join('/'), + params, + responseAsBlob: true, + }); + } + + public invalidateCache(projectId: Signal): Promise { + return this.queryClient.invalidateQueries({ + queryKey: this.pathToQueryKey(BASE_ENDPOINT(projectId)), + }); + } +} diff --git a/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts b/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts new file mode 100644 index 0000000000..579a68da56 --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/metric/metric.api.service.ts @@ -0,0 +1,54 @@ +import { HttpParams } from '@angular/common/http'; +import { inject, Injectable, Signal } from '@angular/core'; + +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; + +import { DomainApiService } from '~/domains/domain-api.service'; +import { ProjectMetrics } from '~/domains/metric/metric.model'; +import { PaginateQueryService } from '~/services/paginate-query.service'; + +const BASE_ENDPOINT = (projectId: Signal) => [ + 'programs', + projectId, + 'metrics', +]; + +@Injectable({ + providedIn: 'root', +}) +export class MetricApiService extends DomainApiService { + paginateQueryService = inject(PaginateQueryService); + + getProjectSummaryMetrics(projectId: Signal) { + return this.generateQueryOptions({ + path: [...BASE_ENDPOINT(projectId), 'program-stats-summary'], + }); + } + + exportMetrics({ + projectId, + type, + params, + }: { + projectId: Signal; + type: ExportType; + params: HttpParams; + }) { + return this.httpWrapperService.perform121ServiceRequest({ + method: 'GET', + endpoint: this.pathToQueryKey([ + ...BASE_ENDPOINT(projectId), + 'export-list', + type, + ]).join('/'), + params, + responseAsBlob: true, + }); + } + + public invalidateCache(projectId: Signal): Promise { + return this.queryClient.invalidateQueries({ + queryKey: this.pathToQueryKey(BASE_ENDPOINT(projectId)), + }); + } +} diff --git a/interfaces/Portalicious/src/app/domains/metric/metric.model.ts b/interfaces/Portalicious/src/app/domains/metric/metric.model.ts new file mode 100644 index 0000000000..e728388894 --- /dev/null +++ b/interfaces/Portalicious/src/app/domains/metric/metric.model.ts @@ -0,0 +1,5 @@ +import { ProgramStats } from '@121-service/src/metrics/dto/program-stats.dto'; + +import { Dto } from '~/utils/dto-type'; +// TODO: AB#30152 This type should be refactored to use Dto121Service +export type ProjectMetrics = Dto; diff --git a/interfaces/Portalicious/src/app/domains/project/project.api.service.ts b/interfaces/Portalicious/src/app/domains/project/project.api.service.ts index d6dcc03aee..93e93a23cb 100644 --- a/interfaces/Portalicious/src/app/domains/project/project.api.service.ts +++ b/interfaces/Portalicious/src/app/domains/project/project.api.service.ts @@ -3,19 +3,22 @@ import { inject, Injectable, Signal } from '@angular/core'; import { uniqBy } from 'lodash'; +import { ActionReturnDto } from '@121-service/src/actions/dto/action-return.dto'; +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; + import { DomainApiService } from '~/domains/domain-api.service'; import { ATTRIBUTE_LABELS } from '~/domains/project/project.helper'; import { Attribute, AttributeWithTranslatedLabel, Project, - ProjectMetrics, ProjectUser, ProjectUserAssignment, ProjectUserWithRolesLabel, } from '~/domains/project/project.model'; import { Role } from '~/domains/role/role.model'; import { TranslatableStringService } from '~/services/translatable-string.service'; +import { Dto } from '~/utils/dto-type'; const BASE_ENDPOINT = 'programs'; @@ -54,12 +57,6 @@ export class ProjectApiService extends DomainApiService { }); } - getProjectSummaryMetrics(projectId: Signal) { - return this.generateQueryOptions({ - path: [BASE_ENDPOINT, projectId, 'metrics/program-stats-summary'], - }); - } - getProjectUsers(projectId: Signal) { return this.generateQueryOptions< ProjectUser[], @@ -260,6 +257,26 @@ export class ProjectApiService extends DomainApiService { }); } + getLatestAction({ + projectId, + actionType, + }: { + projectId: Signal; + actionType: ExportType; + }) { + const params = new HttpParams({ + fromObject: { + actionType, + }, + }); + return this.generateQueryOptions>({ + path: [BASE_ENDPOINT, projectId, 'actions'], + requestOptions: { + params, + }, + }); + } + public invalidateCache(projectId?: Signal): Promise { const path: (Signal | string)[] = [BASE_ENDPOINT]; diff --git a/interfaces/Portalicious/src/app/domains/project/project.model.ts b/interfaces/Portalicious/src/app/domains/project/project.model.ts index f4bdbd21ab..b6990cbe49 100644 --- a/interfaces/Portalicious/src/app/domains/project/project.model.ts +++ b/interfaces/Portalicious/src/app/domains/project/project.model.ts @@ -1,4 +1,3 @@ -import { ProgramStats } from '@121-service/src/metrics/dto/program-stats.dto'; import { FoundProgramDto } from '@121-service/src/programs/dto/found-program.dto'; import { ProgramController } from '@121-service/src/programs/programs.controller'; import { UserController } from '@121-service/src/user/user.controller'; @@ -9,9 +8,6 @@ import { ArrayElement } from '~/utils/type-helpers'; // TODO: AB#30152 This type should be refactored to use Dto121Service export type Project = Dto; -// TODO: AB#30152 This type should be refactored to use Dto121Service -export type ProjectMetrics = Dto; - export type ProjectUser = ArrayElement< Dto121Service >; diff --git a/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts b/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts index 743e2b14f2..d0d8264323 100644 --- a/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts +++ b/interfaces/Portalicious/src/app/pages/project-monitoring/project-monitoring.page.ts @@ -19,6 +19,7 @@ import { import { InfoTooltipComponent } from '~/components/info-tooltip/info-tooltip.component'; import { PageLayoutComponent } from '~/components/page-layout/page-layout.component'; import { SkeletonInlineComponent } from '~/components/skeleton-inline/skeleton-inline.component'; +import { MetricApiService } from '~/domains/metric/metric.api.service'; import { PaymentApiService } from '~/domains/payment/payment.api.service'; import { ProjectApiService } from '~/domains/project/project.api.service'; import { MetricTileComponent } from '~/pages/project-monitoring/components/metric-tile/metric-tile.component'; @@ -52,13 +53,14 @@ export class ProjectMonitoringPageComponent { projectId = input.required(); readonly locale = inject(LOCALE_ID); + readonly metricApiService = inject(MetricApiService); readonly projectApiService = inject(ProjectApiService); readonly paymentApiService = inject(PaymentApiService); readonly translatableStringService = inject(TranslatableStringService); project = injectQuery(this.projectApiService.getProject(this.projectId)); metrics = injectQuery(() => ({ - ...this.projectApiService.getProjectSummaryMetrics(this.projectId)(), + ...this.metricApiService.getProjectSummaryMetrics(this.projectId)(), enabled: !!this.project.data()?.id, })); payments = injectQuery(() => ({ diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.html b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.html new file mode 100644 index 0000000000..e6c900bc4d --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.html @@ -0,0 +1,46 @@ + + +

+ You're about to download an Excel file for + {{ exportSelectedActionData()?.count }} selected registrations. +

+ +
+ +

You're about to download an Excel file for all registrations.

+ +
diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts new file mode 100644 index 0000000000..9b10315811 --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-registrations/components/export-registrations/export-registrations.component.ts @@ -0,0 +1,142 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input, + signal, + ViewChild, +} from '@angular/core'; + +import { injectMutation } from '@tanstack/angular-query-experimental'; +import { MenuItem } from 'primeng/api'; +import { SplitButtonModule } from 'primeng/splitbutton'; + +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; + +import { ConfirmationDialogComponent } from '~/components/confirmation-dialog/confirmation-dialog.component'; +import { Registration } from '~/domains/registration/registration.model'; +import { LatestExportDateComponent } from '~/pages/project-registrations/components/latest-export-date/latest-export-date.component'; +import { ExportService } from '~/services/export.service'; +import { + ActionDataWithPaginateQuery, + PaginateQuery, +} from '~/services/paginate-query.service'; +import { ToastService } from '~/services/toast.service'; + +const EXPORT_IN_PROGRESS_TOAST = 'export-in-progress'; + +@Component({ + selector: 'app-export-registrations', + standalone: true, + imports: [ + SplitButtonModule, + ConfirmationDialogComponent, + LatestExportDateComponent, + ], + templateUrl: './export-registrations.component.html', + styles: ``, + providers: [ToastService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExportRegistrationsComponent { + projectId = input.required(); + getActionData = + input.required< + () => ActionDataWithPaginateQuery | undefined + >(); + + private exportService = inject(ExportService); + private toastService = inject(ToastService); + + @ViewChild('exportSelectedDialog') + private exportSelectedDialog: ConfirmationDialogComponent; + @ViewChild('exportAllDialog') + private exportAllDialog: ConfirmationDialogComponent; + + exportSelectedActionData = signal< + ActionDataWithPaginateQuery | undefined + >(undefined); + + ExportType = ExportType; + + exportRegistrationsMutation = injectMutation(() => ({ + mutationFn: async (exportOptions: { + type: 'pa-data-changes' | ExportType; + paginateQuery?: PaginateQuery; + fromDate?: string; + toDate?: string; + minPayment?: number; + maxPayment?: number; + }) => { + this.toastService.showToast({ + summary: $localize`Preparing export`, + detail: $localize`This might take a few minutes.\n\nThe file will be automatically downloaded when ready. Closing this notification will not cancel the export.`, + severity: 'info', + showSpinner: true, + key: EXPORT_IN_PROGRESS_TOAST, + }); + + return this.exportService.getExportList({ + ...exportOptions, + projectId: this.projectId, + }); + }, + onSuccess: ({ exportResult, filename }) => { + const downloadURL = window.URL.createObjectURL(exportResult); + const link = document.createElement('a'); + link.href = downloadURL; + link.download = filename; + link.click(); + this.toastService.showToast({ + detail: $localize`Export downloaded.`, + severity: 'success', + }); + }, + })); + + exportSelectedRegistrations() { + const actionData = this.getActionData()(); + if (!actionData) { + return; + } + + this.exportSelectedActionData.set(actionData); + this.exportSelectedDialog.askForConfirmation(); + } + + exportOptions: MenuItem[] = [ + { + label: $localize`:@@export-all:Export all registrations`, + command: () => { + this.exportAllDialog.askForConfirmation(); + }, + }, + { + label: $localize`:@@export-duplicate:Export duplicate registrations`, + command: () => { + this.toastService.showToast({ + detail: 'That has not been implemented yet...', + severity: 'warn', + }); + }, + }, + { + label: $localize`:@@export-changes:Export status & data changes`, + command: () => { + this.toastService.showToast({ + detail: 'That has not been implemented yet...', + severity: 'warn', + }); + }, + }, + { + label: $localize`:@@export-verification:Export Account number verification`, + command: () => { + this.toastService.showToast({ + detail: 'That has not been implemented yet...', + severity: 'warn', + }); + }, + }, + ]; +} diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.html b/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.html new file mode 100644 index 0000000000..f5568be004 --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.html @@ -0,0 +1,14 @@ +@if (canSeeLastExportTime()) { +

+ @if (latestExport.isPending()) { + + } @else if (latestExport.data()?.created) { + Latest export has been done on: + {{ latestExport.data()?.created | date: 'medium' }} + } @else { + There has been no export yet. + } +

+} diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.ts b/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.ts new file mode 100644 index 0000000000..78272fa9cf --- /dev/null +++ b/interfaces/Portalicious/src/app/pages/project-registrations/components/latest-export-date/latest-export-date.component.ts @@ -0,0 +1,48 @@ +import { DatePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, +} from '@angular/core'; + +import { injectQuery } from '@tanstack/angular-query-experimental'; + +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; +import { PermissionEnum } from '@121-service/src/user/enum/permission.enum'; + +import { SkeletonInlineComponent } from '~/components/skeleton-inline/skeleton-inline.component'; +import { ProjectApiService } from '~/domains/project/project.api.service'; +import { AuthService } from '~/services/auth.service'; + +@Component({ + selector: 'app-latest-export-date', + standalone: true, + imports: [DatePipe, SkeletonInlineComponent], + templateUrl: './latest-export-date.component.html', + styles: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LatestExportDateComponent { + projectId = input.required(); + exportType = input.required(); + + private authService = inject(AuthService); + private projectApiService = inject(ProjectApiService); + + latestExport = injectQuery(() => ({ + ...this.projectApiService.getLatestAction({ + projectId: this.projectId, + actionType: this.exportType(), + })(), + enabled: this.canSeeLastExportTime(), + })); + + canSeeLastExportTime = computed(() => + this.authService.hasPermission({ + projectId: this.projectId(), + requiredPermission: PermissionEnum.ActionREAD, + }), + ); +} diff --git a/interfaces/Portalicious/src/app/pages/project-registrations/project-registrations.page.html b/interfaces/Portalicious/src/app/pages/project-registrations/project-registrations.page.html index 715b400ca8..fe2e44acfa 100644 --- a/interfaces/Portalicious/src/app/pages/project-registrations/project-registrations.page.html +++ b/interfaces/Portalicious/src/app/pages/project-registrations/project-registrations.page.html @@ -3,6 +3,14 @@ i18n-pageTitle="@@page-title-project-registrations" [projectId]="projectId()" > +
+ @if (canExport()) { + + } +
+ this.authService.hasPermission({ + projectId: this.projectId(), + requiredPermission: PermissionEnum.RegistrationPersonalEXPORT, + }), + ); + contextMenuItems = computed(() => { return [ { diff --git a/interfaces/Portalicious/src/app/pages/projects-overview/components/project-summary-card/project-summary-card.component.ts b/interfaces/Portalicious/src/app/pages/projects-overview/components/project-summary-card/project-summary-card.component.ts index 79ee65db38..6c0d384e7d 100644 --- a/interfaces/Portalicious/src/app/pages/projects-overview/components/project-summary-card/project-summary-card.component.ts +++ b/interfaces/Portalicious/src/app/pages/projects-overview/components/project-summary-card/project-summary-card.component.ts @@ -14,6 +14,7 @@ import { SkeletonModule } from 'primeng/skeleton'; import { AppRoutes } from '~/app.routes'; import { SkeletonInlineComponent } from '~/components/skeleton-inline/skeleton-inline.component'; +import { MetricApiService } from '~/domains/metric/metric.api.service'; import { PaymentApiService } from '~/domains/payment/payment.api.service'; import { ProjectApiService } from '~/domains/project/project.api.service'; import { ProjectMetricContainerComponent } from '~/pages/projects-overview/components/project-metric-container/project-metric-container.component'; @@ -37,6 +38,7 @@ import { TranslatableStringPipe } from '~/pipes/translatable-string.pipe'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectSummaryCardComponent { + private metricApiService = inject(MetricApiService); private projectApiService = inject(ProjectApiService); private paymentApiService = inject(PaymentApiService); @@ -44,7 +46,7 @@ export class ProjectSummaryCardComponent { public project = injectQuery(this.projectApiService.getProject(this.id)); public metrics = injectQuery(() => ({ - ...this.projectApiService.getProjectSummaryMetrics(this.id)(), + ...this.metricApiService.getProjectSummaryMetrics(this.id)(), enabled: !!this.project.data()?.id, })); public payments = injectQuery(() => ({ diff --git a/interfaces/Portalicious/src/app/services/export.service.ts b/interfaces/Portalicious/src/app/services/export.service.ts new file mode 100644 index 0000000000..575eed6c0f --- /dev/null +++ b/interfaces/Portalicious/src/app/services/export.service.ts @@ -0,0 +1,112 @@ +import { inject, Injectable, Signal } from '@angular/core'; + +import { ExportType } from '@121-service/src/metrics/enum/export-type.enum'; + +import { EventApiService } from '~/domains/event/event.api.service'; +import { MetricApiService } from '~/domains/metric/metric.api.service'; +import { + PaginateQuery, + PaginateQueryService, +} from '~/services/paginate-query.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ExportService { + private paginateQueryService = inject(PaginateQueryService); + private eventApiService = inject(EventApiService); + private metricApiService = inject(MetricApiService); + + private generateExportParams({ + type, + paginateQuery, + fromDate, + toDate, + minPayment, + maxPayment, + }: { + type: 'pa-data-changes' | ExportType; + paginateQuery?: PaginateQuery; + fromDate?: string; + toDate?: string; + minPayment?: number; + maxPayment?: number; + }) { + if (type !== ExportType.allPeopleAffected && paginateQuery) { + throw new Error('Paginate query is only supported for allPeopleAffected'); + } + + let params = + this.paginateQueryService.paginateQueryToHttpParams(paginateQuery); + + params = params.append('format', 'xlsx'); + + if (fromDate) { + params = params.append('fromDate', fromDate); + } + if (toDate) { + params = params.append('toDate', toDate); + } + if (minPayment) { + params = params.append('minPayment', minPayment); + } + if (maxPayment) { + params = params.append('maxPayment', maxPayment); + } + + return params; + } + + private toExportFileName(excelFileName: string): string { + const date = new Date(); + return `${excelFileName}-${date.getFullYear().toString()}-${( + date.getMonth() + 1 + ).toString()}-${date.getDate().toString()}.xlsx`; + } + + async getExportList({ + projectId, + type, + paginateQuery, + fromDate, + toDate, + minPayment, + maxPayment, + }: { + projectId: Signal; + type: 'pa-data-changes' | ExportType; + paginateQuery?: PaginateQuery; + fromDate?: string; + toDate?: string; + minPayment?: number; + maxPayment?: number; + }) { + const params = this.generateExportParams({ + type, + paginateQuery, + fromDate, + toDate, + minPayment, + maxPayment, + }); + + let exportResult: Blob; + + if (type === 'pa-data-changes') { + exportResult = await this.eventApiService.getEvents({ + projectId, + params, + }); + } else { + exportResult = await this.metricApiService.exportMetrics({ + projectId, + type, + params, + }); + } + + const filename = this.toExportFileName(type); + + return { exportResult, filename }; + } +} diff --git a/interfaces/Portalicious/src/app/services/toast.service.ts b/interfaces/Portalicious/src/app/services/toast.service.ts index 78e7ef460a..10a253ab88 100644 --- a/interfaces/Portalicious/src/app/services/toast.service.ts +++ b/interfaces/Portalicious/src/app/services/toast.service.ts @@ -12,7 +12,7 @@ export class ToastService { private messageService = inject(MessageService); - showToast(message: Message) { + showToast(message: { showSpinner?: boolean } & Message) { this.messageService.add({ ...message, life: message.life ?? 5000, @@ -24,6 +24,10 @@ export class ToastService { : $localize`:@@generic-success:Success`, detail: message.detail, key: ToastService.TOAST_KEY, + icon: message.showSpinner ? 'pi pi-spinner' : message.icon, + styleClass: message.showSpinner + ? '[&_.p-toast-message-icon]:animate-spin' + : message.styleClass, }); } diff --git a/interfaces/Portalicious/src/locale/messages.nl.xlf b/interfaces/Portalicious/src/locale/messages.nl.xlf index f21d6f21cc..dad4b2cb6d 100644 --- a/interfaces/Portalicious/src/locale/messages.nl.xlf +++ b/interfaces/Portalicious/src/locale/messages.nl.xlf @@ -1083,6 +1083,58 @@ Select a message from the template messages or write a custom message. Select a message from the template messages or write a custom message. + + Export + Export + + + Export duplicate registrations + Export duplicate registrations + + + Export status & data changes + Export status & data changes + + + Export Account number verification + Export Account number verification + + + Export selected registrations + Export selected registrations + + + You're about to download an Excel file for selected registrations. + You're about to download an Excel file for selected registrations. + + + Export all registrations + Export all registrations + + + Preparing export + Preparing export + + + This might take a few minutes. The file will be automatically downloaded when ready. Closing this notification will not cancel the export. + This might take a few minutes. The file will be automatically downloaded when ready. Closing this notification will not cancel the export. + + + You're about to download an Excel file for all registrations. + You're about to download an Excel file for all registrations. + + + Export downloaded. + Export downloaded. + + + Latest export has been done on: + Latest export has been done on: + + + There has been no export yet. + There has been no export yet. + \ No newline at end of file diff --git a/interfaces/Portalicious/src/locale/messages.xlf b/interfaces/Portalicious/src/locale/messages.xlf index dfa34b2939..4dfe92b7ca 100644 --- a/interfaces/Portalicious/src/locale/messages.xlf +++ b/interfaces/Portalicious/src/locale/messages.xlf @@ -815,6 +815,45 @@ You're about to send a message to registration(s). + + Export Account number verification + + + Export status & data changes + + + Export duplicate registrations + + + Export + + + Preparing export + + + This might take a few minutes. The file will be automatically downloaded when ready. Closing this notification will not cancel the export. + + + You're about to download an Excel file for selected registrations. + + + Export all registrations + + + Export selected registrations + + + You're about to download an Excel file for all registrations. + + + Export downloaded. + + + There has been no export yet. + + + Latest export has been done on: + \ No newline at end of file