import { Component, OnInit } from '@angular/core'; import { LocationService } from '@core/services/location.service'; import { PolicyService } from '@core/services/policy.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TranslationService } from '@core/services/translation.service'; import { PortalFormAvailabilityInfo } from '@core/typings/application.typing'; import { ProgramTypes } from '@core/typings/program.typing'; import { WorkflowManagerActions } from '@core/typings/workflow.typing'; import { ApplicantManagerService } from '@features/applicant/applicant-manager.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ExtendFormDueDateModalComponent } from '@features/application-manager/extend-form-due-date-modal/extend-form-due-date-modal.component'; import { FormReminderModalComponent } from '@features/application-manager/form-reminder-modal/form-reminder-modal.component'; import { FormManagerService } from '@features/application-manager/services/form-manager/form-manager.service'; import { ViewFormModalComponent } from '@features/application-manager/view-form-modal/view-form-modal.component'; import { CancelRequestRevisionModalComponent } from '@features/application-view/cancel-request-revision-modal/cancel-request-revision-modal.component'; import { RequestRevisionModalComponent } from '@features/application-view/request-revision-modal/request-revision-modal.component'; import { CompletionRequirementType, FormAudience, FormDecisionTypes, FormManagerRecordForUi, FormManagerRecordFromApi, FormStatuses } from '@features/configure-forms/form.typing'; import { FormStatusService } from '@features/configure-forms/services/form-status/form-status.service'; import { CyclesService } from '@features/cycles/cycles.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramService } from '@features/programs/program.service'; import { EmailService } from '@features/system-emails/email.service'; import { WorkflowService } from '@features/workflow/workflow.service'; import { AddressFormatterService, ALL_SKIP_FILTER, AutoTableRepositoryFactory, BulkAction, DebounceFactory, FilterHelpersService, FilterModalTypes, PaginationOptions, TableDataFactory, TopLevelCustomLink, TopLevelFilter, TopLevelFilterOption, TopLevelFilterOptionsConfig, TypeaheadSelectOption } from '@yourcause/common'; import { CachedAttr, CACHE_TYPES } from '@yourcause/common/cache'; import { I18nService } from '@yourcause/common/i18n'; import { ModalFactory } from '@yourcause/common/modals'; import { SendFormDueEmailModalComponent } from '../send-form-due-email-modal/send-form-due-email-modal.component'; @Component({ selector: 'gc-form-manager-table', templateUrl: './form-manager-table.component.html', styleUrls: ['./form-manager-table.component.scss'] }) export class FormManagerTableComponent implements OnInit { tableDataFactory: TableDataFactory; topLevelFilters: TopLevelFilter[] = []; hiddenFilters: TopLevelFilter[] = []; topLevelCustomLinks: TopLevelCustomLink[] = []; FormStatuses = FormStatuses; FormAudience = FormAudience; DecisionTypes = FormDecisionTypes; bulkActions: BulkAction[]; hasMaskedAppsOnPage = false; programOptions: TypeaheadSelectOption[] = this.programService.allManagerPrograms .map((prog) => { return { label: prog.grantProgramName, value: prog.grantProgramId }; }); cycleOptions: TypeaheadSelectOption[] = this.cycleService.allMyCycleSelectOptions .map((cycle) => { return { label: cycle.label, value: cycle.value }; }); workflowOptions = this.workflowService.myWorkflowOptions; workflowLevelOptions = this.workflowService.myWorkflowLevelOptions; showMaskedApplicantsIcon = 'user-shield'; hideMaskedApplicantsIcon = 'user-times'; hideMaskedApplicantsTooltip = this.i18n.translate( 'MANAGE:textShowApplicantInformation', {}, 'Show applicant information' ); showMaskedApplicantsTooltip = this.i18n.translate( 'MANAGE:textHideApplicantInformation', {}, 'Hide applicant information' ); fromRoute = this.applicantManagerService.getFromRouteForProfile(); statusMapApp = this.statusService.applicationStatusMap; tableKey: string; formAudienceOptions = { selectOptions: [{ display: this.i18n.translate( 'FORMS:textGrantManagerForms', {}, 'Grant manager forms' ), value: FormAudience.MANAGER }, { display: this.i18n.translate( this.isNomination ? 'FORMS:textNominatorForms' : 'FORMS:textApplicantForms', {}, this.isNomination ? 'Nominator forms' : 'Applicant forms' ), value: FormAudience.APPLICANT }] }; formStatusArrayManager: TopLevelFilterOption[] = this.formStatusService.getFormStatusOptions(true); formStatusArrayApplicant: TopLevelFilterOption[] = this.formStatusService.getFormStatusOptions(false); formStatusOptions: TopLevelFilterOptionsConfig; canTakeAllActions = this.policyService.grantApplication.canManageAllApplications() || this.policyService.grantApplication.canTakeActionsOnAllApps(); myWorkflowActions = this.workflowService.myWorkflowManagerRolesMap; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, false, undefined, undefined, (self: FormManagerTableComponent) => { return `ycShowMasked_${self.getTableKey()}`; } ) showMaskedApplicants: boolean; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, FormAudience.APPLICANT, undefined, undefined, (self: FormManagerTableComponent) => { return `ycActiveFormAudience_${self.getTableKey()}`; } ) activeFormAudience: FormAudience; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, ALL_SKIP_FILTER, undefined, undefined, (self: FormManagerTableComponent) => { return `ycActiveFormStatus_${self.getTableKey()}`; } ) activeFormStatus: FormStatuses; constructor ( private applicantManagerService: ApplicantManagerService, private nonprofitService: NonprofitService, private statusService: StatusService, private i18n: I18nService, private translationService: TranslationService, private policyService: PolicyService, private workflowService: WorkflowService, private addressFormatter: AddressFormatterService, private modalFactory: ModalFactory, private spinnerService: SpinnerService, private programService: ProgramService, private cycleService: CyclesService, private emailService: EmailService, private autoTableFactory: AutoTableRepositoryFactory, private locationService: LocationService, private filterHelpersService: FilterHelpersService, private applicationFormService: ApplicationFormService, private formManagerService: FormManagerService, private formStatusService: FormStatusService ) { } get isNomination () { return location.pathname.includes('nomination'); } get applicantRouterLink () { return this.applicantManagerService.get('applicantProfileRouterLink'); } get nonprofitRouterLink () { return this.nonprofitService.get('nonprofitProfileRouterLink'); } ngOnInit () { this.tableKey = this.getTableKey(); this.setHiddenFilters(); this.setTopLevelFilters(); this.setTableDataFactory(); this.setBulkActions(); } getTableKey () { return this.isNomination ? 'NOM_FORM_MANAGER' : 'APP_FORM_MANAGER'; } setBulkActions () { const applicantFormBulkActions: BulkAction[] = [{ label: this.i18n.translate( 'FORMS:textRequestRevision', {}, 'Request revision' ), disabled: (forms: FormManagerRecordForUi[]) => { return !this.formManagerService.formsPassRevisionRequestCriteria(forms); }, exec: (arg: FormManagerRecordForUi[]) => { return this.requestRevisionModal(arg); } }, { label: this.i18n.translate( 'FORMS:textSendRevisionReminder', {}, 'Send revision reminder' ), disabled: (forms: FormManagerRecordForUi[]) => { return !this.formManagerService.formsPassRevisionRequestCriteria(forms, true); }, exec: (arg: FormManagerRecordForUi[]) => { return this.revisionReminderModal(arg); }}]; this.bulkActions = this.activeFormAudience === FormAudience.MANAGER ? null : applicantFormBulkActions; } setHiddenFilters () { const grantProgramType = this.isNomination ? ProgramTypes.NOMINATION : ProgramTypes.GRANT; const programFilter = new TopLevelFilter( 'equals', 'grantProgramType', grantProgramType, undefined ); let hideViewOnlyFilter: TopLevelFilter = null; if (this.activeFormAudience === FormAudience.MANAGER) { hideViewOnlyFilter = new TopLevelFilter( 'notEqual', 'completionRequirementType', CompletionRequirementType.VIEW_ONLY, undefined ); } this.hiddenFilters = [ programFilter, hideViewOnlyFilter ].filter((filter) => !!filter); } setTopLevelFilters () { this.formStatusOptions = { selectOptions: this.activeFormAudience === FormAudience.MANAGER ? this.formStatusArrayManager : this.formStatusArrayApplicant }; const placeholder = this.i18n.translate( this.isNomination ? 'APPLY:textSearchByApplicationIdApplicantOrgOrForm' : 'APPLY:textSearchByApplicationIdNominatorOrgOrForm', {}, this.isNomination ? 'Search by nomination id, nominator, organization, or form name' : 'Search by application id, applicant, organization, or form name' ); this.topLevelFilters = [ new TopLevelFilter( 'text', 'applicantFullName', '', placeholder, undefined, placeholder, [{ column: 'applicantFullName', filterType: 'cn' }, { column: 'organizationName', filterType: 'cn' }, { column: 'organizationIdentification', filterType: 'cn' }, { column: 'applicantEmail', filterType: 'cn' }, { column: 'applicationId', filterType: 'eq' }, { column: 'formName', filterType: 'cn' }] ), new TopLevelFilter( 'typeaheadSingleEquals', 'formAudience', this.activeFormAudience, '', this.formAudienceOptions, this.i18n.translate( 'GLOBAL:textFormAudience', {}, 'Form audience' ) ), new TopLevelFilter( 'typeaheadSingleEquals', 'applicationFormStatusId', this.activeFormStatus, '', this.formStatusOptions, this.i18n.translate( 'GLOBAL:textFormStatus', {}, 'Form status' ) ), new TopLevelFilter( 'relativeDate', 'relativeDate', FilterModalTypes.Last6Months, '', this.filterHelpersService.getRelativeDateFilterConfig(), this.i18n.translate( 'common:textFilterByDate', {}, 'Filter by date' ) ) ]; } setTableDataFactory () { this.tableDataFactory = DebounceFactory.createSimple( async (options: PaginationOptions) => { const response = await this.formManagerService.getRecordsForFormManager( options ); return { success: true, data: { records: this.setupFormManagerRecords(response.records), recordCount: response.recordCount } }; }); const existingRepo = this.autoTableFactory.getRepository(this.tableKey); if (existingRepo) { this.setupFormManagerRecords(existingRepo.currentSet); } } setupFormManagerRecords (records: FormManagerRecordFromApi[]) { const viewTranslations = this.translationService.viewTranslations; const programTranslations = viewTranslations.Grant_Program; const cycleTranslations = viewTranslations.Grant_Program_Cycle; const formTranslations = viewTranslations.FormTranslation; let hasMaskedApps = false; const adaptedRecords = records.map((record) => { const formTranslation = formTranslations[record.formId]; const progTranslation = programTranslations[record.grantProgramId]; const cycleTranslation = cycleTranslations[record.grantProgramCycleId]; const formName = formTranslation?.Name ?? record.formName; const grantProgramName = progTranslation?.Name ?? record.grantProgramName; const grantProgramCycleName = cycleTranslation?.Name ?? record.grantProgramCycleName; const applicantAddressString = this.addressFormatter.formatSimpleGrantsAddressToSingleLine( record.applicantAddress ); const orgAddressString = this.addressFormatter.formatSimpleGrantsAddressToSingleLine( record.organizationAddress ); const dueDate = record.notSubmitted ? record.dueDate : ''; const notStarted = !record.applicationFormId; const canExtendDueDate = !!dueDate; const canSendFormDueEmail = dueDate && !record.isOverdue; const canSendFormOverdueEmail = dueDate && record.isOverdue; const hasSendReminderPermission = this.canSendReminder(record); const canSendReminder = this.activeFormAudience !== FormAudience.MANAGER && hasSendReminderPermission && !dueDate && record.notSubmitted; const canRequestRevision = this.applicationFormService.checkRevisionRequestCriteria( this.activeFormAudience, record.applicationStatus, record.applicationFormStatusId ); const canSendRevisionReminder = this.applicationFormService.checkRevisionRequestCriteria( this.activeFormAudience, record.applicationStatus, record.applicationFormStatusId, true ); const notStartedText = notStarted && this.i18n.translate( 'GLOBAL:textNotStarted', {}, 'Not started' ); const statusString = notStartedText || this.statusService.getDynamicStatusString( record.applicationFormStatusId, record.updatedDate, record.submittedDate, record.revisionLastSentDate, record.portalAvailabilityDetails || {} as PortalFormAvailabilityInfo ); this.applicantManagerService.setApplicantRouterLinkMap( record.applicantId ); this.nonprofitService.setNonprofitRouterLinkMap( (record.nonprofitGuid as any) || record.organizationId ); if (record.isMasked && record.canViewMaskedApplicantInfo) { hasMaskedApps = true; } return { ...record, dueDate, statusString, formName, grantProgramName, grantProgramCycleName, orgAddressString, applicantAddressString, canExtendDueDate, canSendFormDueEmail, canSendFormOverdueEmail, canSendReminder, canSendRevisionReminder, canRequestRevision }; }); this.hasMaskedAppsOnPage = hasMaskedApps; if (this.hasMaskedAppsOnPage) { this.setMaskedIconAndTooltip(); } return adaptedRecords; } canSendReminder (record: FormManagerRecordFromApi): boolean { const actions = this.myWorkflowActions[record.workflowId] || []; return actions.includes(WorkflowManagerActions.SendReminder); } onTopLevelFilterChange (filter: TopLevelFilter) { if (filter.column === 'formAudience') { this.activeFormAudience = filter.value; // reset top level filters because options are different between manager and applicant this.setTopLevelFilters(); this.setHiddenFilters(); } if (filter.column === 'applicationFormStatusId') { this.activeFormStatus = filter.value; } this.setBulkActions(); } toggleShowMaskedApplicants () { this.showMaskedApplicants = !this.showMaskedApplicants; this.setMaskedIconAndTooltip(); } setMaskedIconAndTooltip () { const tooltip = this.showMaskedApplicants ? this.showMaskedApplicantsTooltip : this.hideMaskedApplicantsTooltip; const item = { name: tooltip, icon: this.showMaskedApplicants ? this.hideMaskedApplicantsIcon : this.showMaskedApplicantsIcon, tooltip, isIcon: true, onClick: () => { this.toggleShowMaskedApplicants(); } }; if (this.topLevelCustomLinks.length === 0) { this.topLevelCustomLinks = [item]; } else { this.topLevelCustomLinks[0] = item; } } async extendDueDateModal (row: FormManagerRecordForUi) { const response = await this.modalFactory.open( ExtendFormDueDateModalComponent, { dueDate: row.dueDate, formName: row.formName } ); if (response) { await this.applicationFormService.handleExtendFormDueDate( row.applicationId, row.applicationFormId, row.formId, row.workflowLevelId, response.dueDate, true ); this.tableDataFactory.reset.emit(); } } async formDueEmailModal (row: FormManagerRecordForUi, isOverdue = false) { const isManager = this.activeFormAudience === FormAudience.MANAGER; const response = await this.modalFactory.open( SendFormDueEmailModalComponent, { isOverdue, isNomination: this.isNomination, programId: row.grantProgramId, formName: row.formName, isManager, applicationFormId: row.applicationFormId, formId: row.formId, workflowLevelId: row.workflowLevelId, userId: row.userId, applicationId: row.applicationId } ); if (response) { this.spinnerService.startSpinner(); await this.applicationFormService.handleSendFormDue( response, isOverdue ); this.tableDataFactory.reset.emit(); this.spinnerService.stopSpinner(); } } async formReminderModal (row: FormManagerRecordForUi) { const response = await this.modalFactory.open( FormReminderModalComponent, { isNomination: this.isNomination, formName: row.formName, programId: row.grantProgramId } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsModel.attachments, row.applicationId ); const emailOptionsModel = { ...response.emailOptionsModel, attachments }; this.spinnerService.stopSpinner(); await this.applicationFormService.sendFormReminder({ ...response, applicationId: row.applicationId, formId: row.formId, emailOptionsModel }); } } async requestRevisionModal (rows: FormManagerRecordForUi[]) { const response = await this.modalFactory.open( RequestRevisionModalComponent, { programId: rows[0].grantProgramId, isNomination: this.isNomination, numberOfApplications: rows.length, formName: rows[0].formName } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsRequest.attachments, rows[0].applicationId ); this.spinnerService.stopSpinner(); const emailOptionsRequest = { ...response.emailOptionsRequest, attachments }; const payload = { ...response, emailOptionsRequest }; if (rows.length > 1) { await this.applicationFormService.handleBulkRequestRevision( rows.map((thing) => thing.applicationId), rows[0].formId, payload, rows[0].grantProgramId ); } else { await this.applicationFormService.handleRequestRevision( rows[0].applicationId, rows[0].applicationFormId, payload ); } this.tableDataFactory.reset.emit(); } } async cancelRevisionModal (row: FormManagerRecordForUi) { const deps = { programId: row.grantProgramId, isNomination: this.isNomination }; const payload = await this.modalFactory.open( CancelRequestRevisionModalComponent, deps ); if (payload) { this.spinnerService.startSpinner(); await this.applicationFormService.handleCancelRevision( payload, row.applicationId, row.applicationFormId ); this.tableDataFactory.reset.emit(); this.spinnerService.stopSpinner(); } } async revisionReminderModal (rows: FormManagerRecordForUi[]) { const response = await this.modalFactory.open( RequestRevisionModalComponent, { programId: rows[0].grantProgramId, isNomination: this.isNomination, isReminder: true, numberOfApplications: rows.length, formName: rows[0].formName } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsRequest.attachments, rows[0].applicationId ); const emailOptionsModel = { ...response.emailOptionsRequest, attachments }; if (rows.length > 1) { await this.applicationFormService.handleBulkRevisionReminder( rows.map((row) => row.applicationId), rows[0].formId, response.notes, response.clientEmailTemplateId, rows[0].grantProgramId ); } else { await this.applicationFormService.handleRevisionReminder( rows[0].applicationId, rows[0].formId, response.notes, response.clientEmailTemplateId, emailOptionsModel ); } this.tableDataFactory.reset.emit(); this.spinnerService.stopSpinner(); } } async viewFormModal (row: FormManagerRecordForUi) { this.spinnerService.startSpinner(); const form = await this.formManagerService.getFormFromFormManagerRecord( row, this.activeFormAudience ); this.spinnerService.stopSpinner(); if (form) { const deps = { form, applicationId: row.applicationId, forOrg: !!row.organizationId, isNomination: this.isNomination, isManagerForm: this.activeFormAudience === FormAudience.MANAGER }; this.modalFactory.open( ViewFormModalComponent, deps ); } } navigateToApp (row: FormManagerRecordForUi) { const base = this.isNomination ? '/management/nomination-view' : '/management/application-view'; const url = `${base}/${row.applicationId}/form/no-form`; const path = this.locationService.getRelativeUrl(url); window.open(path); } }