import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PolicyService } from '@core/services/policy.service'; import { StaticResourceService } from '@core/services/static-resource.service'; import { StatusService } from '@core/services/status.service'; import { TimeZoneService } from '@core/services/time-zone.service'; import { TranslationService } from '@core/services/translation.service'; import { CyclesAPI } from '@core/typings/api/cycles.typing'; import { TimeZone } from '@core/typings/api/time-zone.typing'; import { Budget, FundingSourceTypes } from '@core/typings/budget.typing'; import { User } from '@core/typings/client-user.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ArchiveProgramPayload, ConfigureProgram, ConfigureProgramMap, CreateEditProgramApi, CreateProgramModalResponse, EligibilityProgramMessage, GrantProgramCycleBudgetFundingSource, MasterResponse, OfflineProgramDetail, PaymentStats, PaymentStatusStat, Program, ProgramApplicantType, ProgramDashboardMap, ProgramDetail, ProgramDetailFromApi, ProgramDropdownOptionsForInsights, ProgramExport, ProgramForDashboard, ProgramImport, ProgramStatusStat, ProgramStatusStatApi, ProgramTypes, WorkflowFormMap } from '@core/typings/program.typing'; import { ApplicationStatuses, PaymentStatus } from '@core/typings/status.typing'; import { CyclesUI } from '@core/typings/ui/cycles.typing'; import { environment } from '@environment'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { AvailabilityOptions, CompletionRequirementType, FormTypes, ResponseVisibilityOptions, WorkflowLevelFormApi } from '@features/configure-forms/form.typing'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { CyclesService } from '@features/cycles/cycles.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { InKindService } from '@features/in-kind/in-kind.service'; import { InvitationApplicantHelpers } from '@features/invitations/invitation.typing'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { EmailService } from '@features/system-emails/email.service'; import { UserService } from '@features/users/user.service'; import { WorkflowService } from '@features/workflow/workflow.service'; import { ArrayHelpersService, AutoTableRepositoryFactory, Base64, FileService, PaginationOptions, SelectOption, SimpleStringMap, TypeaheadSelectOption } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import moment from 'moment'; import { Subscription } from 'rxjs'; import { ProgramResources } from './program.resources'; import { ProgramState } from './program.state'; @AttachYCState(ProgramState) @Injectable({ providedIn: 'root' }) export class ProgramService extends BaseYCService { sub = new Subscription(); programTypes = [{ label: this.i18n.translate( 'FORMS:textNomination', {}, 'Nomination' ), value: ProgramTypes.NOMINATION }, { label: this.i18n.translate( 'GLOBAL:textGrant', {}, 'Grant' ), value: ProgramTypes.GRANT }]; constructor ( private logger: LogService, private i18n: I18nService, private userService: UserService, private clientSettingsService: ClientSettingsService, private workflowService: WorkflowService, private programResources: ProgramResources, private formService: FormsService, private fileService: FileService, private staticResourceService: StaticResourceService, private statusService: StatusService, private arrayHelper: ArrayHelpersService, private translationService: TranslationService, private notifier: NotifierService, private autoTableRepository: AutoTableRepositoryFactory, private inKindService: InKindService, private referenceFieldsService: ReferenceFieldsService, private cyclesService: CyclesService, private timeZoneService: TimeZoneService, private emailService: EmailService, private policyService: PolicyService, private formLogicService: FormLogicService ) { super(); this.sub.add(this.translationService.changesTo$('viewTranslations').subscribe(() => { this.resetProgramTranslations(); })); } get programMap () { return this.get('configureProgramMap'); } get activeProgramId () { return this.get('activeProgramId'); } get program () { return this.programMap[this.activeProgramId]; } get allActiveManagerPrograms () { return this.get('allActiveManagerPrograms'); } get allActiveManagerProgramOptions () { return this.get('allActiveManagerProgramOptions'); } get allManagerPrograms () { return this.get('allManagerPrograms'); } get allPublishedPrograms () { return this.get('allPublishedPrograms'); } get allPublishedActivePrograms () { return this.get('allPublishedActivePrograms'); } get allPrograms () { return this.get('allPrograms'); } get dashboardPrograms () { return this.get('dashboardPrograms'); } get nonMaskedDashboardPrograms () { return this.get('nonMaskedDashboardPrograms'); } get ycProcessingPrograms () { return this.get('ycProcessingPrograms'); } get clientProcessingPrograms () { return this.get('clientProcessingPrograms'); } get passMessage () { return this.i18n.translate( 'PROGRAM:textDefaultEligibilitySuccess', {}, 'Congratulations! You have passed eligibility and can now proceed with your application.' ); } get failMessage () { return this.i18n.translate( 'APPLY:textEligibilityNonSuccessDefaultMessage', {}, 'Unfortunately you have not met the eligibility qualifications. If you have any questions, feel free to reach out to your Grant Manager.' ); } get programTranslationMap () { return this.translationService.viewTranslations.Grant_Program; } get currentProgramDashCycleIds () { return this.get('currentProgramDashCycleIds') || []; } private isLocalhost () { return environment.isLocalhost; } get user (): User { return 'isRootUser' in this.userService.user ? this.userService.user : null; } async getProgramCycleBudgets ( programId: number ): Promise { const budgetMap = this.get('programCycleBudgetFundingSource'); if (budgetMap[programId]) { return budgetMap[programId]; } const result = await this.programResources.getProgramCycleBudgetsAndFundingSources( programId ); this.set('programCycleBudgetFundingSource', { ...this.get('programCycleBudgetFundingSource'), [programId]: result }); return result; } resetProgramCycleBudgetMap () { this.set('programCycleBudgetFundingSource', {}); } setYCProcessingPrograms (programs: Program[]) { this.set('ycProcessingPrograms', programs); } setClientProcessingPrograms (programs: Program[]) { this.set('clientProcessingPrograms', programs); } setProgramDashCycleIdsMap (cycleIds: number[]) { this.set('currentProgramDashCycleIds', cycleIds); } setConfigureProgramMap (map: ConfigureProgramMap) { this.set('configureProgramMap', map); } setProgramDashboardMap (map: ProgramDashboardMap) { this.set('programDashboardMap', map); } setWorkflowMap (id: number) { return this.workflowService.getAndSetWorkflowMap(id); } setAllActiveManagerPrograms (active: Program[]) { const programs = this.arrayHelper.sort(active, 'grantProgramName'); this.set('allActiveManagerPrograms', programs); const options = programs.map((prog) => { return { label: prog.grantProgramName, value: prog.grantProgramId }; }); this.set('allActiveManagerProgramOptions', options); } setAllManagerPrograms (progs: Program[]) { this.set( 'allManagerPrograms', this.arrayHelper.sort(progs, 'grantProgramName') ); } setActiveProgramId (id: string) { this.set('activeProgramId', id); } setOfflineProgramMap (map: { [p: string]: OfflineProgramDetail; }) { this.set('offlineProgramMap', map); } setMapProperty ( id: keyof ConfigureProgramMap, prop: K, value: ConfigureProgram[K] ) { const current = this.programMap[id]; this.set('configureProgramMap', { ...this.programMap, [id]: { ...current, [prop]: value } }); } async resetProgramAttrs () { await Promise.all([ this.resetAllPrograms(), this.resetAllActiveManagerPrograms() ]); } resetProgramTranslations () { const resetAttrs = [ 'dashboardPrograms', 'nonMaskedDashboardPrograms', 'allActiveManagerPrograms', 'allPublishedActivePrograms', 'allPrograms', 'allPublishedPrograms', 'clientProcessingPrograms', 'ycProcessingPrograms' ]; resetAttrs.forEach((attr: any) => { const programs: Program[] = this.get(attr); if (programs) { this.updateProgramNamesWithTranslations(programs); this.set(attr, programs); } }); } resetConfigureProgramMap () { this.setConfigureProgramMap({}); } programExportAllowed (): boolean { return this.user.isRootUser && ( this.isLocalhost() || environment.locationBase.includes('uat') || environment.locationBase.includes('qa') ); } programImportAllowed (): boolean { return this.user.isRootUser && ( this.isLocalhost() || environment.locationBase.includes('qa') || !environment.locationBase.includes('uat') ); } async getProgram ( grantProgramId: string ): Promise { const program = await this.programResources.getProgramFromApi( grantProgramId ); this.set('programMap', { ...this.get('programMap'), [grantProgramId]: program }); return program; } async getOfflineProgramDetail ( programId: number ): Promise { const programFromState = this.get('offlineProgramMap')[programId]; if (!programFromState) { const program = await this.programResources.getProgramWithForm( programId ); const detail = await this.getProgram('' + programId); const adaptedDetail: OfflineProgramDetail = { ...detail, clientId: program.clientId }; this.setOfflineProgramMap({ ...this.get('offlineProgramMap'), programId: adaptedDetail }); return adaptedDetail; } return programFromState; } async getCycleFromProgram (programId: number, cycleId: number) { const program = await this.getProgram('' + programId); return program.cycles.find((c) => { return c.id === cycleId; }); } async resetAllActiveManagerPrograms () { this.set('allActiveManagerPrograms', undefined); await this.getAllActiveManagerPrograms(); } async getAllActiveManagerPrograms () { if (!this.allActiveManagerPrograms) { let managerPrograms = await this.programResources.getProgramsForManager(true); managerPrograms = managerPrograms.filter(prog => !prog.isDraft); this.updateProgramNamesWithTranslations(managerPrograms); const active = managerPrograms.filter((item) => !item.isArchived); this.setAllActiveManagerPrograms(active); this.setAllManagerPrograms(managerPrograms); this.setMyCycleAttrs(active); } return this.allActiveManagerPrograms; } setMyCycleAttrs (programs: Program[]) { const myCycles: CyclesAPI.BaseProgramCycle[] = []; programs.forEach((program) => { program.grantProgramCycles.forEach((cycle) => { myCycles.push(cycle); }); }); this.cyclesService.setMyCycleAttrs(myCycles); } async getProgramForDashboard (grantProgramId: number, programCycleIds: number[]) { const [ detail, programStats, paymentStats, stats ] = await Promise.all([ this.getProgram('' + grantProgramId), this.programResources.getProgramStats(grantProgramId, programCycleIds), this.programResources.getPaymentStats(grantProgramId, programCycleIds), this.programResources.getProgramInsightStats([grantProgramId]) ]); const translationMap = this.programTranslationMap[grantProgramId]; detail.name = translationMap ? translationMap.Name : detail.name; Object.keys(paymentStats).map(key => key as keyof PaymentStats).forEach((key: keyof PaymentStats) => { paymentStats[key] = paymentStats[key] || 0; }); this.setProgramDashboardMap({ ...this.get('programDashboardMap'), [grantProgramId]: { detail, programStats, paymentStats, stats } }); } async resetProgramStats (grantProgramId: number, programCycleIds: number[]) { const [ programStats, paymentStats ] = await Promise.all([ this.programResources.getProgramStats(grantProgramId, programCycleIds), this.programResources.getPaymentStats(grantProgramId, programCycleIds) ]); Object.keys(paymentStats).map(key => key as keyof PaymentStats).forEach((key: keyof PaymentStats) => { paymentStats[key] = paymentStats[key] || 0; }); this.setProgramDashboardMap({ ...this.get('programDashboardMap'), [grantProgramId]: { ...this.get('programDashboardMap')[grantProgramId].detail, programStats, paymentStats } }); } async getProgramOptionsForApproveNom (programId: number): Promise[]> { const [ prog ] = await Promise.all([ this.getProgram('' + programId), this.setAllPrograms() ]); return prog.grantProgramsForNominationProgram.filter((id) => { const activeIds = this.allPublishedPrograms .filter((program) => { return (program.grantProgramCycles || []).some((cycle) => { return moment().isBetween(cycle.startDate, cycle.endDate); }); }) .map((p) => +p.grantProgramId); return activeIds.includes(+id); }).map((id) => { const found = this.allPublishedPrograms.find((program) => { return +program.grantProgramId === +id; }); const map = this.programTranslationMap[id]; return { label: map && map.Name ? map.Name : found.grantProgramName, value: id }; }); } async getProgramForEdit ( id: number, processor: ProcessingTypes, budgets: Budget[] ): Promise { const foundPublished = this.allPublishedPrograms.find((prog) => { return +prog.grantProgramId === +id; }); const [ program, masterResponse, workflowForms, emailSettings ] = await Promise.all([ this.getProgram('' + id), this.programResources.getMasterFormResponse(id), this.workflowService.getWorkflowForms(id), this.programResources.getProgramEmailSettings(id) ]); await this.setWorkflowMap(program.workflowId); const programTimeZone = this.timeZoneService.returnTimeZoneFromID( program.timezoneId ); const adaptedCycles = this.adaptCyclesForEdit( program.cycles || [], processor, budgets, programTimeZone ); return { id, name: program.name, description: program.description || '', charityBucketId: program.charityBucketId, charityBucketDescriptions: program.charityBucketDescription || '', image: program.imageUrl, logo: program.logoUrl, imageUrlOrBase64: program.imageUrl, logoUrlOrBase64: program.logoUrl, logoName: program.logoName, imageName: program.imageName, timezoneId: program.timezoneId || this.clientSettingsService.clientSettings.defaultTimezone, defaultForm: program.defaultFormId, workflow: program.workflowId, defaultLevel: program.defaultWorkflowLevelId, applicantType: program.programApplicantType || ProgramApplicantType.ORGS, allowCollaboration: program.allowCollaboration, deadlines: program.deadlines || [], successMessage: program.eligibilityPassMessage || this.passMessage, failMessage: program.eligibilityFailMessage || this.failMessage, masterResponse: await this.getMasterResponse( program.additionalForms[0], masterResponse ), eligibilityForm: program.additionalForms[0], workflowFormMap: this.constructFormsForEdit( program.workflowId, workflowForms, program.defaultFormId ), hasApplications: program.hasApplications, grantPrograms: program.grantProgramsForNominationProgram, notifyNominators: program.notifyNominators, allowAddOrg: program.allowAddOrg, canChangeDefaultForm: program.canChangeDefaultForm, maskApplicantInfo: program.maskApplicantInfo, clientTemplates: [], useProgramLogo: program.useProgramLogo, senderDisplayName: emailSettings.senderDisplayName || this.clientSettingsService.clientBranding.name, defaultLanguage: program.defaultLanguageId || this.clientSettingsService.defaultLanguage, emailNotificationTypesPreferences: (emailSettings.disabledEmails || []) .map((email) => { return { id: email.id, isExcluded: true }; }), daysBeforeEndDateReminders: program.daysBeforeEndDateReminders || 7, hideCycleDatesInApplicantPortal: !program.showCycles, cycles: adaptedCycles, originalCycles: [ ...adaptedCycles.map((cycle) => { return { ...cycle }; }) ], cyclesToDelete: [], isArchived: program.isArchived, inviteOnly: program.inviteOnly, isPublished: !!foundPublished, formDueReminderFrequencyForApplicant: program.formDueReminderFrequencyForApplicant, formDueReminderFrequencyForReviewer: program.formDueReminderFrequencyForReviewer, formOverDueReminderFrequencyForApplicant: program.formOverDueReminderFrequencyForApplicant, formOverDueReminderFrequencyForReviewer: program.formOverDueReminderFrequencyForReviewer, reserveFunds: program.reserveFunds, autoDeclineBudgetAssignment: program.autoDeclineBudgetAssignment, autoDeclineBudgetAssignmentSendEmail: program.autoDeclineBudgetAssignmentSendEmail, autoDeclineBudgetAssignmentDeclinationComment: program.autoDeclineBudgetAssignmentDeclinationComment, hidePaymentStatus: program.hidePaymentStatus, alternatePaymentStatusText: program.alternatePaymentStatusText }; } adaptCyclesForEdit ( cycles: CyclesUI.ProgramCycle[] = [], processor: ProcessingTypes, budgets: Budget[], timeZone: TimeZone ): CyclesAPI.ConfigureProgramCycle[] { return cycles.map((cycle) => { // here we need to apply the timezone offset return { id: cycle.id, name: cycle.name, startDate: this.timeZoneService.adaptDateForEditIfDST(cycle.startDate, timeZone.offset), endDate: this.timeZoneService.adaptDateForEditIfDST(cycle.endDate, timeZone.offset), budgets: cycle.budgetIds, defaultCashBudgetId: cycle.defaultCashBudgetId, defaultCashFundingSourceId: cycle.defaultCashFundingSourceId, defaultInKindBudgetId: cycle.defaultInKindBudgetId, defaultInKindFundingSourceId: cycle.defaultInKindFundingSourceId, clientOrganizationsProcessingTypeId: this.getProcessorTypeForProgram( cycle.clientOrganizationsProcessingTypeId, cycle.budgetIds, processor, budgets ).processor, createdBy: cycle.createdBy, createdDate: cycle.createdDate, updatedBy: cycle.updatedBy, updatedDate: cycle.updatedDate, hasApplications: cycle.hasApplications, isArchived: cycle.isArchived, createImpersonatedBy: cycle.createImpersonatedBy, impersonatedBy: cycle.impersonatedBy, status: this.cyclesService.getCycleStatus(cycle.startDate, cycle.endDate), isClientProcessing: cycle.isClientProcessing }; }); } getProcessorTypeForProgram ( programProcessingType: ProcessingTypes, programBudgets: number[], processor: ProcessingTypes, allBudgets: Budget[] ) { let hasClient = false; let hasYourCause = false; allBudgets.forEach((budget) => { if (programBudgets.includes(budget.id)) { if (budget.hasClientProcessingType) { hasClient = true; } if (budget.hasYcProcessingType) { hasYourCause = true; } } }); if (programProcessingType) { return { processor: programProcessingType, hasOptions: (processor === ProcessingTypes.Both) && hasClient && hasYourCause }; } if (processor !== ProcessingTypes.Both) { return { processor, hasOptions: false }; } if (hasClient && hasYourCause) { return { processor: ProcessingTypes.Client, hasOptions: true }; } else if (hasClient) { return { processor: ProcessingTypes.Client, hasOptions: false }; } else { return { processor: ProcessingTypes.YourCause, hasOptions: false }; } } constructFormsForEdit ( workflowId: number, workflowLevelForms: WorkflowLevelFormApi[], programDefaultForm: number ) { const map: { [x: string]: WorkflowLevelFormApi[]; } = {}; (workflowLevelForms || []).forEach((form) => { const adapted: WorkflowLevelFormApi = { ...form, formType: this.formService.getFormTypeFromFormId(form.formId), isDefaultForm: form.formId === programDefaultForm }; const left = '' + form.workflowLevelId; if (map[left]) { map[left].push(adapted); } else { map[left] = [adapted]; } }); const workflowDetail = this.workflowService.get('workflowMap')[workflowId]; workflowDetail.levels.forEach((level) => { if (!map[level.id]) { map[level.id] = []; } else { map[level.id] = this.arrayHelper.sort(map[level.id], 'sortOrder'); } level.subLevels.forEach((sub) => { if (!map[sub.id]) { map[sub.id] = []; } else { map[sub.id] = this.arrayHelper.sort(map[sub.id], 'sortOrder'); } }); }); return map; } getPassFailMessage ( translationArray: EligibilityProgramMessage[], type: 'pass'|'fail' ): string { if (translationArray && translationArray.length) { const found = translationArray.find((msg) => { return msg.language === 'en-US'; }); if (found) { return found.translation; } else { return type === 'pass' ? this.passMessage : this.failMessage; } } else { return type === 'pass' ? this.passMessage : this.failMessage; } } setAllPublishedPrograms (programs: Program[]) { programs = this.arrayHelper.sort(programs, 'grantProgramName'); this.set('allPublishedPrograms', programs); const publishedActivePrograms = programs.filter((prog) => !prog.isArchived); this.set('allPublishedActivePrograms', publishedActivePrograms); } async resetAllPrograms () { this.set('allPrograms', undefined); this.set('allPublishedPrograms', undefined); this.set('allPublishedActivePrograms', undefined); await this.setAllPrograms(); } async setAllPrograms () { if (!this.get('allPrograms')) { const programs = await this.programResources.getAllProgramsDraftAndPublished(); const publishedPrograms = programs.filter((program) => { return !program.isDraft; }); this.setAllPublishedPrograms(publishedPrograms); this.updateProgramNamesWithTranslations(programs); this.set('allPrograms', programs); } } updateProgramNamesWithTranslations (programs: (Program|ProgramForDashboard)[]) { programs.forEach((program) => { const map = this.programTranslationMap[program.grantProgramId]; program.grantProgramName = map && map.Name ? map.Name : program.grantProgramName; ((program as Program).grantProgramCycles || []).forEach((cycle) => { const cycleMap = this.translationService.viewTranslations.Grant_Program_Cycle[ cycle.id ]; cycle.name = cycleMap && cycleMap.Name ? cycleMap.Name : cycle.name; }); }); } async setCycleBudgetsMap () { try { const map = await this.programResources.getCycleBudgetsMap(); this.set('cycleBudgetsMap', map); } catch (e) { this.logger.error(e); this.set('cycleBudgetsMap', {}); } } clearPaymentProcessingPrograms () { this.set('ycProcessingPrograms', undefined); this.set('clientProcessingPrograms', undefined); } async getPaymentProcessingPrograms ( procType: ProcessingTypes, fSType: FundingSourceTypes ) { let programs; if (procType === ProcessingTypes.YourCause) { programs = await this.getYCProcessingPrograms(fSType); } else { programs = await this.getClientProcessingPrograms(fSType); } return programs; } async getYCProcessingPrograms (fSType: FundingSourceTypes) { if (this.ycProcessingPrograms) { return this.ycProcessingPrograms; } else { const programs = await this.programResources.getPaymentProcessingPrograms(ProcessingTypes.YourCause, fSType); this.updateProgramNamesWithTranslations(programs); this.setYCProcessingPrograms(programs); return this.ycProcessingPrograms; } } async getClientProcessingPrograms (fSType: FundingSourceTypes) { if (this.clientProcessingPrograms) { return this.clientProcessingPrograms; } else { const programs = await this.programResources.getPaymentProcessingPrograms(ProcessingTypes.Client, fSType); this.updateProgramNamesWithTranslations(programs); this.setClientProcessingPrograms(programs); return this.clientProcessingPrograms; } } async getMasterResponse (formId: number, masterResponse: MasterResponse[]) { if (!this.formService.published) { await this.formService.getAndSetForms(); } const found = this.formService.published.find((form) => { return +form.formId === +formId; }); if (found) { const revisionId = found.revisionId; const foundResponse = masterResponse.find((res) => { return +res.formRevisionId === +revisionId; }); return foundResponse ? foundResponse : null; } return null; } getProgramCycleOptionsForInsights ( filterValue: ProgramDropdownOptionsForInsights, isOrgInsights = false, cycleStatus = CyclesAPI.CycleStatuses.Open, isApplicantInsights = false ): SelectOption[] { const programCycleMap = this.cyclesService.programCycleMap; const filtered = this.filterByProgramStatus(filterValue, isOrgInsights, isApplicantInsights); return this.arrayHelper.sort( filtered.map((program) => { return { label: program.grantProgramName, value: program.grantProgramId, dependentOptions: (programCycleMap[program.grantProgramId] || []) .filter((cycle) => { if (cycleStatus === CyclesAPI.CycleStatuses.All) { return [ CyclesAPI.CycleStatuses.Open, CyclesAPI.CycleStatuses.Past ].includes(cycle.status); } else { return cycle.status === cycleStatus; } }).map((cycle) => { let label = cycle.name; const archivedText = this.i18n.translate( 'GLOBAL:textArchivedLowercase', {}, 'archived' ); if (cycleStatus === CyclesAPI.CycleStatuses.All) { const status = this.cyclesService.getTranslatedCycleStatus( cycle.status ).toLowerCase(); label = label + ` (${status}${ cycle.isArchived ? `/${archivedText})` : ')' }`; } else if (cycle.isArchived) { label = label + ` (${archivedText})`; } return { label, value: cycle.id }; }) }; }).filter((program) => program.dependentOptions.length > 0), 'label' ); } filterByProgramStatus ( filterValue: ProgramDropdownOptionsForInsights, isOrgInsights = false, isApplicantInsights = false ) { let records = this.dashboardPrograms; if (isOrgInsights || isApplicantInsights) { if (!this.policyService.grantApplication.canSeeMaskedApplicants) { records = this.nonMaskedDashboardPrograms; } } const programs = records.filter((prog) => { const orgFilter = isOrgInsights ? prog.programApplicantType !== ProgramApplicantType.INDIVIDUAL : true; return !prog.isDraft && orgFilter; }); return programs.filter((program) => { switch (filterValue) { case ProgramDropdownOptionsForInsights.ALL: default: return program; case ProgramDropdownOptionsForInsights.ACTIVE: return !program.isArchived; case ProgramDropdownOptionsForInsights.ARCHIVED: return program.isArchived; } }); } async setProgramsForDashboard (nonMaskedOnly = false) { const programs = await this.programResources.getProgramsForDashboard(nonMaskedOnly); const sanitizedPrograms = programs.filter((program) => program.numberOfGrantProgramCycles > 0); // we have to sanitize due to bad data in QA, // some programs don't have cycles and this will cause issues downstream this.updateProgramNamesWithTranslations(sanitizedPrograms); const attr = nonMaskedOnly ? 'nonMaskedDashboardPrograms' : 'dashboardPrograms'; this.set(attr, sanitizedPrograms); } downloadProgramExport (programExport: ProgramExport) { const b64Input = Base64.encode(JSON.stringify(programExport.exportModel)); const fileName = `program_export_${moment().format('YYYYMMDDHHmmss')}.bin`; if (this.fileService.testCanDownload(b64Input)) { this.fileService.downloadRaw( b64Input, fileName ); } else { this.fileService.downloadUrlAs( programExport.fileUrl, fileName ); } } async handlePublishProgram (programId: number) { try { await this.programResources.activateProgram(programId); this.notifier.success( this.i18n.translate( 'PROGRAM:textSuccessPublishProgram', {}, 'Successfully published the program' ) ); await this.resetProgramAttrs(); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'PROGRAM:textErrorPublishingProgram', {}, 'There was an error publishing the program' ) ); } } async handleCopyProgram ( program: Program, isNomination = false, processor: ProcessingTypes, budgets: Budget[] ) { const [ detail ] = await Promise.all([ this.getProgramForEdit( +program.grantProgramId, processor, budgets ) ]); const adapted = await this.adaptForApi( { ...detail, id: null }, isNomination, true, true ); try { await this.programResources.saveProgram(adapted); this.notifier.success( this.i18n.translate( 'PROGRAM:textSuccessCopyProgram', {}, 'Successfully copied the program' ) ); await this.resetAllPrograms(); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'PROGRAM:textErrorCopyingProgram', {}, 'There was an error copying the program' ) ); } } constructWorkflowLevelForms (workflowFormMap: WorkflowFormMap) { const forms: WorkflowLevelFormApi[] = []; const map = workflowFormMap || {}; Object.keys(map).forEach((levelId) => { map[levelId].forEach((form) => { forms.push(form); }); }); return forms; } async getArchivePaymentStatsForPrograms (programIds: number[]) { const response = await this.programResources.getArchivePaymentStatsForPrograms( programIds ); return response; } async handleCreateProgram ( response: CreateProgramModalResponse, isNomination = false ) { await this.workflowService.getAndSetWorkflowMap(response.workflowId); const workflow = this.workflowService.workflowMap[response.workflowId]; const workflowFormMap: WorkflowFormMap = {}; const defaultForm: WorkflowLevelFormApi = { formId: response.defaultFormId, workflowLevelId: null, formType: !isNomination ? FormTypes.REQUEST : FormTypes.NOMINATION, managerActionType: ResponseVisibilityOptions.VIEW_AT_ALL_WORKFLOW, completionRequirementType: CompletionRequirementType.NONE, isDefaultForm: true, specificNumberForCompletion: null, portalAvailability: AvailabilityOptions.AUTO, portalAvailabilityDetails: null, dueDateDetails: null, clientEmailTemplateId: null, sortOrder: 1 }; workflow.levels.forEach((level) => { workflowFormMap[level.id] = [{ ...defaultForm, workflowLevelId: level.id }]; level.subLevels.forEach((sub) => { workflowFormMap[sub.id] = [{ ...defaultForm, workflowLevelId: sub.id }]; }); }); const adapted = { name: response.name, description: '', imageName: '', logoName: '', image: null, logo: null, imageUrlOrBase64: null, logoUrlOrBase64: null, timezoneId: this.clientSettingsService.clientSettings.defaultTimezone, charityBucketId: null, charityBucketDescriptions: null, eligibilityForm: null, masterResponse: null, defaultForm: response.defaultFormId, workflow: response.workflowId, defaultLevel: null, workflowFormMap, applicantType: ProgramApplicantType.ORGS, allowCollaboration: false, deadlines: [], successMessage: null, failMessage: null, hasApplications: false, canChangeDefaultForm: true, maskApplicantInfo: false, emailNotificationTypesPreferences: [], useProgramLogo: false, senderDisplayName: null, defaultLanguage: response.defaultLang, daysBeforeEndDateReminders: 7, isArchived: false, hideCycleDatesInApplicantPortal: false, originalCycles: [], cycles: [], cyclesToDelete: [], inviteOnly: false, isPublished: false, grantPrograms: [], notifyNominators: false, allowAddOrg: false } as ConfigureProgram; return this.handleSaveProgram( adapted, false, isNomination, true ); } saveProgramSuccess (isNew = false) { this.notifier.success(this.i18n.translate( isNew ? 'PROGRAM:textSuccessfullyCreatedProgram' : 'PROGRAM:textSuccessfullyUpdatedProgram', {}, `Successfully ${isNew ? 'created' : 'updated'} the program` )); } saveProgramError (isNew = false) { this.notifier.error(this.i18n.translate( isNew ? 'PROGRAM:textErrorCreatingProgram' : 'PROGRAM:textErrorUpdatingProgram', {}, `There was an error ${isNew ? 'creating' : 'updating'} the program` )); } saveProgramOverlappingCyclesError () { this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorOverlappingCycles', {}, 'Cycles contain overlapping dates.' )); } async handleSaveProgram ( program: ConfigureProgram, refetchTranslations: boolean, isNomination = false, saveAsDraft = true ) { let id: number; try { id = await this.createOrEditProgram( program, isNomination, refetchTranslations, saveAsDraft ); this.resetAllActiveManagerPrograms(); await this.getAllActiveManagerPrograms(); this.cyclesService.resetCycleMap(); this.setOfflineProgramMap({}); this.saveProgramSuccess(); return id; } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if (e?.error?.message === 'Cycles contain overlapping dates.') { this.saveProgramOverlappingCyclesError(); } else{ this.saveProgramError(); } return null; } } async createOrEditProgram ( program: ConfigureProgram, isNomination = false, refetchTranslations = false, saveAsDraft = false ) { await Promise.all([ this.cyclesService.handleDeletedCycles(program.cyclesToDelete), this.cyclesService.handleArchivedCycles( program.cycles, program.originalCycles ) ]); const adapted = await this.adaptForApi( program, isNomination, false, saveAsDraft ); const id = await this.programResources.saveProgram(adapted); await Promise.all([ this.formService.refreshForms(), this.resetProgramAttrs() ]); if (refetchTranslations) { await this.translationService.resetViewTranslations(); } if (program.id) { this.emailService.emptyTemplateMap(); } return id; } async adaptForApi ( program: ConfigureProgram, isNomination = false, isCopy = false, saveAsDraft = false ): Promise { const imageChanged = program.image instanceof Blob; const logoChanged = program.logo instanceof Blob; const programApplicantType = ( program.applicantType === ProgramApplicantType.ORGS_WITH_BUCKET ? ProgramApplicantType.ORGS : program.applicantType ); let name = program.name; if (isCopy) { name = program.name + ' Copy'; if (name.length > 50) { name = program.name; } } const programTimeZone = this.timeZoneService.returnTimeZoneFromID( program.timezoneId ); const adapted: CreateEditProgramApi = { saveAsDraft, id: program.id, name, charityBucketId: program.charityBucketId, charityBucketDescription: program.charityBucketDescriptions || '', description: program.description, imageName: imageChanged ? '' : program.imageName, logoName: logoChanged ? '' : program.logoName, defaultForm: program.defaultForm, workflow: program.workflow, defaultWorkflowLevel: program.defaultLevel, timezone: program.timezoneId, useProgramLogo: program.useProgramLogo || false, deadlines: (program.deadlines || []).map((deadline) => { return { id: deadline.id, title: deadline.name, date: deadline.date, notes: deadline.description }; }), programApplicantType, allowCollaboration: program.allowCollaboration || false, workflowLevelForms: this.constructWorkflowLevelForms(program.workflowFormMap), programType: isNomination ? ProgramTypes.NOMINATION : ProgramTypes.GRANT, allowAddOrg: program.allowAddOrg || false, maskApplicantInfo: program.applicantType === ProgramApplicantType.INDIVIDUAL ? program.maskApplicantInfo || false : false, clientEmailTemplates: program.clientTemplates, emailNotificationTypesPreferences: program.emailNotificationTypesPreferences, defaultLanguageId: program.defaultLanguage || this.clientSettingsService.defaultLanguage, daysBeforeEndDateReminders: program.daysBeforeEndDateReminders || 7, senderDisplayName: ( !program.senderDisplayName || (program.senderDisplayName === this.clientSettingsService.clientBranding.name) ) ? null : program.senderDisplayName, hideCycleDatesInApplicantPortal: program.hideCycleDatesInApplicantPortal, grantProgramCycles: this.adaptCyclesForApi( program.cycles, isNomination, isCopy, programTimeZone.offset, programApplicantType ), inviteOnly: program.inviteOnly, formDueReminderFrequencyForApplicant: program.formDueReminderFrequencyForApplicant, formDueReminderFrequencyForReviewer: program.formDueReminderFrequencyForReviewer, formOverDueReminderFrequencyForApplicant: program.formOverDueReminderFrequencyForApplicant, formOverDueReminderFrequencyForReviewer: program.formOverDueReminderFrequencyForReviewer, reserveFunds: program.reserveFunds, autoDeclineBudgetAssignment: program.autoDeclineBudgetAssignment, autoDeclineBudgetAssignmentSendEmail: program.autoDeclineBudgetAssignmentSendEmail, autoDeclineBudgetAssignmentDeclinationComment: program.autoDeclineBudgetAssignmentDeclinationComment, hidePaymentStatus: program.hidePaymentStatus, alternatePaymentStatusText: program.alternatePaymentStatusText }; if (imageChanged && program.image) { adapted.imageName = await this.handleProgramImage(program.image, program.name); } if (logoChanged && program.logo) { adapted.logoName = await this.handleProgramImage(program.logo, program.name); } if (isNomination) { adapted.grantProgramsForNominationProgram = program.grantPrograms; adapted.nofityNominators = program.notifyNominators || false; } else { adapted.additionalForms = program.eligibilityForm ? [program.eligibilityForm] : []; adapted.masterFormResponses = program.eligibilityForm && program.masterResponse ? [{ formRevisionId: program.masterResponse.formRevisionId, masterResponse: program.masterResponse.masterResponse }] : []; adapted.eligibilityPassMessage = program.successMessage !== this.passMessage ? program.successMessage : ''; adapted.eligibilityFailMessage = program.failMessage !== this.failMessage ? program.failMessage : ''; } return adapted; } adaptCyclesForApi ( cycles: CyclesAPI.ConfigureProgramCycle[] = [], isNomination = false, isCopy = false, timeZoneOffset: number, programApplicantType: ProgramApplicantType ) { return cycles.map((cycle) => { cycle.endDate = this.timeZoneService.adaptDateForSaveIfDST( cycle.endDate, timeZoneOffset ); cycle.startDate = this.timeZoneService.adaptDateForSaveIfDST( cycle.startDate, timeZoneOffset ); // Individuals can never be processed through YC, so assume processor is client const clientOrganizationsProcessingTypeId = programApplicantType === ProgramApplicantType.INDIVIDUAL ? ProcessingTypes.Client : cycle.clientOrganizationsProcessingTypeId; const adapted = { id: isCopy ? null : cycle.id, name: cycle.name, startDate: cycle.startDate, endDate: cycle.endDate, budgets: cycle.budgets, defaultCashBudgetId: cycle.defaultCashBudgetId, defaultCashFundingSourceId: cycle.defaultCashFundingSourceId, defaultInKindBudgetId: cycle.defaultInKindBudgetId, defaultInKindFundingSourceId: cycle.defaultInKindFundingSourceId, isClientProcessing: cycle.isClientProcessing, clientOrganizationsProcessingTypeId }; if (isNomination) { delete adapted.budgets; delete adapted.defaultCashBudgetId; delete adapted.defaultCashFundingSourceId; delete adapted.defaultInKindBudgetId; delete adapted.defaultInKindFundingSourceId; } return adapted; }); } async handleProgramImage (file: Blob, programName: string): Promise { const response = await this.staticResourceService.uploadManagerImage( file as File, '' ); return response.fileName; } async getProgramStatusStats (grantProgramId: number, programCycleIds: number[]): Promise { const response = await this.programResources.getProgramStatusStats(grantProgramId, programCycleIds); const appStatusMap = this.statusService.applicationStatusMap; const mapped = response.filter((record) => record.status !== ApplicationStatuses.Canceled) .map((item: ProgramStatusStatApi) => { return { statusName: appStatusMap[item.status].translated, requestAmount: item.statusInfo.requestAmount, numberOfApplications: item.statusInfo.numberOfApplications }; }); return this.arrayHelper.sortByAttributes( mapped, 'requestAmount', 'numberOfApplications', true ); } async getProgramsPaginated (options: PaginationOptions) { const response = await this.programResources.getProgramsPaginated(options); return response; } async getPaymentStatusStatsByCycle ( cycleIds: number[] ): Promise { const response = await this.programResources.getPaymentStatusStatsByCycle( cycleIds ); const paymentStatusMap = this.statusService.paymentStatusMap; const mapped = response.filter((item) => { return item.status !== PaymentStatus.Voided; }).map((item) => { return { statusId: item.status, statusName: paymentStatusMap[item.status].translated, paymentsTotal: item.statusInfo.paymentsTotal, numberOfPayments: item.statusInfo.numberOfPayments }; }); return this.arrayHelper.sortByAttributes( mapped, 'paymentsTotal', 'numberOfPayments', true ); } getMostCommonDefaultLangFromArray ( ids: number[] ) { const programs = this.allPrograms || []; return this.translationService.getMostCommonDefaultLangFromArray( programs.map((prog) => { return { defaultLanguageId: prog.defaultLanguageId, id: +prog.grantProgramId }; }), ids ); } deleteProgram (programId: number) { return this.programResources.deleteProgram(programId); } async handleArchiveProgram (payload: ArchiveProgramPayload) { try { await this.programResources.archiveProgram(payload); this.notifier.success(this.i18n.translate( payload.archive ? 'PROGRAM:textSuccessArchiveProgram' : 'PROGRAM:textSuccessUnarchiveProgram', {}, payload.archive ? 'Successfully archived the program' : 'Successfully unarchived the program' )); await this.resetProgramAttrs(); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( payload.archive ? 'PROGRAM:textErrorArchivingProgram' : 'PROGRAM:textErrorUnarchivingProgram', {}, payload.archive ? 'There was an error archiving the program' : 'There was an error unarchiving the program' )); return false; } } async handleProgramExport (programs: Program[]) { try { await this.referenceFieldsService.resetFieldsAndCategories(); // We need to ensure all associated forms have the correct custom data table guids associated in the DB so they are exported properly if (this.referenceFieldsService.allReferenceFields.length > 0) { await this.formService.getAndSetForms(); const formIdsToExport: number[] = []; await Promise.all(programs.map(async (prog) => { const detail = await this.getProgram(prog.grantProgramId); if (!formIdsToExport.includes(detail.defaultFormId)) { formIdsToExport.push(detail.defaultFormId); } const eligibilityFormId = detail.additionalForms[0]; if ( eligibilityFormId && !formIdsToExport.includes(eligibilityFormId) ) { formIdsToExport.push(eligibilityFormId); } })); const formsToExport = formIdsToExport.map((formId) => { const found = this.formService.published.find((form) => { return form.formId === formId; }); return { formId, revisionId: found.revisionId }; }); await this.formService.getAndSetForms(); await this.formLogicService.ensureReferenceFieldPicklistsAreSaved(formsToExport); } return this.programResources.exportPrograms( programs.map(p => +p.grantProgramId) ); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorExportingPrograms', {}, 'There was an error exporting the program(s)' )); return null; } } async checkExistingDataForImport (programImport: ProgramImport) { try { const data = await this.programResources.checkExistingDataForImport(programImport); return data; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorCheckingExistingDataForImport', {}, 'There was an error checking existing data before import' )); } } async handleProgramImport ( programImport: ProgramImport, isNomination = false ): Promise { try { await this.programResources.importPrograms(programImport); await Promise.all([ this.formService.refreshForms(), this.clientSettingsService.setBranding(), this.resetProgramAttrs(), this.referenceFieldsService.resetFieldsAndCategories() ]); this.inKindService.resetInKindState(); const key = isNomination ? 'NOMINATION_PROGRAMS' : 'GRANT_PROGRAMS'; const repo = this.autoTableRepository.getRepository(key); if (repo) { repo.reset(); } this.notifier.success(this.i18n.translate( 'PROGRAM:notificationSuccessProgramImport', {}, 'Successfully imported the program' )); return true; } catch (err) { this.notifier.error(this.i18n.translate( 'PROGRAM:notificationErrorWithImport', {}, 'There was an error importing your programs' )); this.logger.error(err); return false; } } async setCycleMap () { if (Object.keys(this.cyclesService.programCycleMap).length === 0) { const cycles = await this.cyclesService.getAllCycles(); const map: SimpleStringMap = {}; const translateMap = this.translationService.viewTranslations.Grant_Program_Cycle; cycles.forEach((cycle) => { cycle.status = this.cyclesService.getCycleStatus( cycle.startDate, cycle.endDate ); cycle.name = translateMap[cycle.id].Name; const existing = map[cycle.grantProgramId]; if (existing) { map[cycle.grantProgramId] = [ ...existing, cycle ]; } else { map[cycle.grantProgramId] = [cycle]; } }); this.cyclesService.setCycleMap(map); } } async canApplicantApplyToProgram ( programGuid: string ): Promise { return this.programResources.canApplicantApplyToProgram( this.userService.currentUser.id, programGuid ); } async getAndAdaptProgramForApplicant ( programGuid: string|number ): Promise { const program = await this.getProgramForApplicant(programGuid); const adapted: ProgramDetail = { ...program, form: this.formLogicService.adaptFormForTabs( program.form, program.form.formId, program.form.formRevisionId ) }; return adapted; } async getProgramForApplicant ( programGuid: string|number ) { try { const program = await this.programResources.getProgramForApplicant(programGuid); return program; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'PROGRAM:textErrorLoadingProgram', {}, 'There was an error loading the program' ) ); return null; } } getCycleOptions (program: Program) { return program.grantProgramCycles.map((cycle) => { return { label: cycle.name, value: cycle.id }; }); } async getProgramLogo (programId: number) { const detail = await this.getProgram('' + programId); return detail.logoUrl; } async getDefaultWflId (programId: number) { await this.getAllActiveManagerPrograms(); return this.allActiveManagerPrograms.find((prog) => { return +prog.grantProgramId === +programId; }).defaultWorkflowLevelId; } /** * Gets published active program options filtered by applicant type * * @param programApplicantType: the type of applicant - org or individual * @returns Typeahead select options for published and active programs */ getPublishedActiveProgramOptions ( programApplicantType?: ProgramApplicantType ): TypeaheadSelectOption[] { const translations = this.translationService.viewTranslations.Grant_Program; return this.arrayHelper.sort( this.allPublishedActivePrograms.filter((prog) => { if (programApplicantType) { return prog.programApplicantType === programApplicantType; } return prog; }).map((prog) => { return { label: translations[prog.grantProgramId]?.Name ?? prog.grantProgramName, value: prog.grantProgramId }; }), 'label' ); } /** * * @param existingList the current list of forms * @param itemIndex the index of the item being moved * @param isUp whether the move is higher in the list * @returns updated list of forms */ updateFormsListSortOrder ( existingList: WorkflowLevelFormApi[], itemIndex: number, isUp: boolean ) { // when an item is being moved down, we are essentially moving the next item up if (!isUp) { ++itemIndex; isUp = true; } const updatedIndex = itemIndex - 1; const sliceIndexEnd = itemIndex + 1; const temp1 = existingList[itemIndex]; const temp2 = existingList[updatedIndex]; const updatedFormsArray = [ ...existingList.slice(0, updatedIndex), temp1, temp2, ...existingList.slice(sliceIndexEnd) ]; const adapted = updatedFormsArray.map((item, index) => { return { ...item, sortOrder: index + 1 }; }); return adapted; } }