import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SpinnerService } from '@core/services/spinner.service'; import { FooterState } from '@core/states/footer.state'; import { CyclesAPI } from '@core/typings/api/cycles.typing'; import { ConfigureProgram, GrantProgramSteps, NomProgramSteps, ProgramApplicantType } from '@core/typings/program.typing'; import { ProgramTranslationKeys } from '@core/typings/translation.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { ProgramCycleModalComponent } from '@features/cycles/program-cycle-modal/program-cycle-modal.component'; import { EmailService } from '@features/system-emails/email.service'; import { ArrayHelpersService, Panel, PanelSection, PanelTypes, SimpleStringMap } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { DomService } from '@yourcause/common/dom'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { isUndefined } from 'lodash'; import moment from 'moment'; import { Subscription } from 'rxjs'; import { ProgramService } from '../program.service'; @Component({ selector: 'gc-program-tabs', templateUrl: './program-tabs.component.html', styleUrls: ['./program-tabs.component.scss'] }) export class ProgramTabsComponent implements OnInit, OnDestroy { PanelTypes = PanelTypes; NomProgramSteps = NomProgramSteps; GrantProgramSteps = GrantProgramSteps; foundPanel: Panel; stepsReady = false; panelSections: PanelSection[] = []; workflowStep = { context: this.isNomination ? NomProgramSteps.Workflow : GrantProgramSteps.Workflow, name: this.i18n.translate( 'common:lblWorkflow' ), description: '', icon: '', iconClass: '', addRequiredAsterisk: true }; programStep = { context: NomProgramSteps.GrantProgram, name: this.i18n.translate( 'common:hdrGrantProgram' ), description: '', icon: '', iconClass: '' }; formStep = { context: this.isNomination ? NomProgramSteps.Forms : GrantProgramSteps.Forms, name: this.i18n.translate( this.isNomination ? 'GLOBAL:hdrNominationForm' : 'common:textForms', {}, this.isNomination ? 'Nomination Form' : 'Forms' ), description: '', icon: '', iconClass: '', addRequiredAsterisk: true }; commStep = { context: this.isNomination ? NomProgramSteps.Communications : GrantProgramSteps.Communications, name: this.i18n.translate( 'GLOBAL:textCommunications' ), description: '', icon: '', iconClass: '' }; sub = new Subscription(); originalProgram: ConfigureProgram; hasInternational = this.clientSettingsService.clientSettings.hasInternational; translationKeyMap: ProgramTranslationKeys[] = [{ key: 'name', translated: this.i18n.translate( 'PROGRAM:textProgramName', {}, 'Program name' ) }, { key: 'description', translated: this.i18n.translate( 'common:textDescription' ) }, { key: 'charityBucketDescriptions', translated: this.i18n.translate( 'PROGRAM:textCharityBucketGuidelines', {}, 'Charity bucket guidelines' ) }, { key: 'successMessage', translated: this.i18n.translate( 'PROGRAM:textEligibilitySuccessMessage', {}, 'Eligibility success message' ) }, { key: 'failMessage', translated: this.i18n.translate( 'PROGRAM:textEligibilityFailMessage', {}, 'Eligibility fail message' ) }]; currentTab = 0; constructor ( private programService: ProgramService, private i18n: I18nService, private spinnerService: SpinnerService, private router: Router, private footerState: FooterState, activatedRoute: ActivatedRoute, private clientSettingsService: ClientSettingsService, private modalFactory: ModalFactory, private arrayHelper: ArrayHelpersService, private domService: DomService, private emailService: EmailService, private analyticsService: AnalyticsService ) { this.sub.add(activatedRoute.params.subscribe(() => { this.panelSections = [{ name: '', panels: [{ context: GrantProgramSteps.Details, name: this.i18n.translate( 'GLOBAL:textDetails' ), description : '', icon: '', iconClass: '', addRequiredAsterisk: true }, { context: GrantProgramSteps.Settings, name: this.i18n.translate( 'GLOBAL:textSettings' ), description : '', icon: '', iconClass: '', addRequiredAsterisk: false }, { context: GrantProgramSteps.Cycles, name: this.i18n.translate( 'PROGRAM:textCycles' ), description: '', icon: '', iconClass: '', addRequiredAsterisk: true }] }]; this.ngOnInit(); this.init(); })); this.sub.add(this.programService.changesTo$('configureProgramMap').subscribe(() => { this.updateValidityAlerts(); })); this.setFooterInfo(); } get isNomination () { return location.pathname.includes('nomination'); } get programMap () { return this.programService.get('configureProgramMap'); } get activeProgramId () { return this.programService.get('activeProgramId'); } get program () { return this.programMap[this.activeProgramId]; } get panels () { return this.panelSections[0].panels; } ngOnInit () { this.originalProgram = { ...this.program }; } init () { if (this.isNomination) { this.panelSections[0].panels = this.panels.concat([ this.formStep, this.workflowStep, this.programStep ]); } else { this.panelSections[0].panels = this.panels.concat([ this.formStep, this.workflowStep ]); } this.panelSections[0].panels = this.panels.concat([ this.commStep ]); this.foundPanel = this.panels[this.currentTab || 0]; this.stepsReady = true; } setFooterInfo () { this.footerState.set('footerState', this.footerState.FOOTER_STATES.ACTION); this.footerState.setActionLabel( this.program.isPublished ? this.i18n.translate('common:btnSave') : this.i18n.translate( 'common:textSaveAndPublish', {}, 'Save and publish' ) ); this.footerState.setPrimaryAction(() => this.saveAndPublish()); if (!this.program.isPublished) { this.footerState.setSecondaryLabel(this.i18n.translate( 'common:textSaveAsDraft', {}, 'Save as draft' )); this.footerState.setSecondaryAction(() => this.saveAsDraft()); } this.footerState.setCancelLabel(this.i18n.translate('common:btnCancel')); this.footerState.setCancelAction(() => this.routeToProgramsPage()); } onPanelClick (step: Partial) { this.currentTab = step; this.analyticsService.emitEvent({ eventName: 'Change program step', eventType: EventType.Click, extras: null }); } routeToProgramsPage () { if (!this.isNomination) { this.router.navigate(['/management/program-setup/programs/all']); } else { this.router.navigate(['/management/program-setup/nomination-programs/all']); } } getIsCompleteMapGrant (): Record { return { [GrantProgramSteps.Details]: this.isDetailComplete(), [GrantProgramSteps.Settings]: this.isSettingsComplete(), [GrantProgramSteps.Cycles]: this.isCycleStepComplete(), [GrantProgramSteps.Forms]: this.isFormComplete(), [GrantProgramSteps.Workflow]: this.isWorkflowComplete(), [GrantProgramSteps.Communications]: true }; } getIsCompleteMapNom (): Record { return { [NomProgramSteps.Details]: this.isDetailComplete(), [NomProgramSteps.Settings]: this.isSettingsComplete(), [NomProgramSteps.Cycles]: this.isCycleStepComplete(), [NomProgramSteps.Forms]: this.isFormComplete(), [NomProgramSteps.Workflow]: this.isWorkflowComplete(), [NomProgramSteps.GrantProgram]: this.isGrantProgramComplete(), [NomProgramSteps.Communications]: true }; } isDetailComplete (): boolean { const langComplete = (!this.hasInternational || !!this.program.defaultLanguage); return ( langComplete && this.program.name && this.program.description && (this.program.image || this.program.imageName) && this.program.image && ( this.program.applicantType === ProgramApplicantType.ORGS_WITH_BUCKET ? !!this.program.charityBucketId : true ) ); } isSettingsComplete (): boolean { return !!this.program.senderDisplayName && (this.program.senderDisplayName.length <= 50); } isCycleStepComplete (): boolean { return this.program.cycles && this.program.cycles.length > 0; } isFormComplete (): boolean { return !!this.program.defaultForm; } isWorkflowComplete (): boolean { return !!(this.program.workflow && this.program.defaultLevel); } isGrantProgramComplete (): boolean { return !!(this.program.grantPrograms && this.program.grantPrograms.length); } areAllStepsComplete (): boolean { const baseStepsComplete = !!(this.isDetailComplete() && this.isSettingsComplete() && this.isCycleStepComplete() && this.isFormComplete() && this.isWorkflowComplete()); if (this.isNomination) { return !!(baseStepsComplete && this.isGrantProgramComplete()); } return baseStepsComplete; } checkIfNeedsConfirmModalForTranslations () { if (this.hasInternational) { const changed: ProgramTranslationKeys[] = []; this.translationKeyMap.forEach((item) => { if ( !!this.originalProgram[item.key] && (this.program[item.key] !== this.originalProgram[item.key]) ) { if (['successMessage', 'failMessage'].includes(item.key)) { if (!!this.program.eligibilityForm) { changed.push(item); } } else { changed.push(item); } } }); return changed; } return []; } async addCycle () { let cycle: CyclesAPI.ConfigureProgramCycle = null; // default cycle budget values to those of last cycle created if (this.program.cycles) { if (this.program.cycles.length === 1) { const item = this.program.cycles[0]; cycle = this.getCycleWithDefaultedBudgets(item); } else if (this.program.cycles.length > 1) { let lastCreatedIndex: number; const noCreatedDates = this.program.cycles.every(c => !c.createdDate); if (noCreatedDates) { lastCreatedIndex = 0; } else { const cycleMap: SimpleStringMap = {}; this.program.cycles.forEach((c, index) => { cycleMap[index] = c.createdDate; }); Object.keys(cycleMap).forEach((index) => { if (cycleMap[index] === null) { // created by system return; } else if (isUndefined(lastCreatedIndex)) { lastCreatedIndex = +index; } else { const last = moment(cycleMap[lastCreatedIndex]); const current = moment(cycleMap[index]); if (current.isAfter(last)) { lastCreatedIndex = +index; } } }); } if (lastCreatedIndex || lastCreatedIndex === 0) { cycle = this.getCycleWithDefaultedBudgets( this.program.cycles[lastCreatedIndex] ); } } } const response = await this.modalFactory.open( ProgramCycleModalComponent, { isNew: true, cycle: { ...cycle }, otherCycles: this.program.cycles } ); if (response) { const cycles = [ ...this.program.cycles, response ]; this.programService.setMapProperty( this.activeProgramId, 'cycles', this.arrayHelper.sort(cycles, 'name') ); } } getCycleWithDefaultedBudgets (item: CyclesAPI.ConfigureProgramCycle) { return { budgets: item.budgets, defaultCashBudgetId: item.defaultCashBudgetId, defaultCashFundingSourceId: item.defaultCashFundingSourceId, defaultInKindBudgetId: item.defaultInKindBudgetId, defaultInKindFundingSourceId: item.defaultInKindFundingSourceId, clientOrganizationsProcessingTypeId: item.clientOrganizationsProcessingTypeId } as CyclesAPI.ConfigureProgramCycle; } updateValidityAlerts () { if (this.program.draftValidityAlert) { this.markForDraftValidation(this.canSaveAsDraft()); } if (this.program.publishedValidityAlert) { this.markForPublishValidation(this.areAllStepsComplete()); } } canSaveAsDraft () { return this.program.name && this.program.name.length <= 50; } canSaveAndPublish () { return this.areAllStepsComplete(); } async saveAsDraft () { if (this.canSaveAsDraft()) { this.spinnerService.startSpinner(); await this.programService.handleSaveProgram( this.program, true, this.isNomination, true ); this.routeToProgramsPage(); this.spinnerService.stopSpinner(); } else { if (this.program.publishedValidityAlert) { this.programService.setMapProperty( this.activeProgramId, 'publishedValidityAlert', '' ); } this.markForDraftValidation(); } } markForDraftValidation (isValid = false) { const updateAlert = (isValid && !!this.program.draftValidityAlert) || (!isValid && !this.program.draftValidityAlert); if (updateAlert) { this.programService.setMapProperty( this.activeProgramId, 'draftValidityAlert', isValid ? '' : this.i18n.translate( 'PROGRAM:textToSaveProgramAsDraftEnterRequiredFields', {}, 'To save your program as a draft, please enter all required information on the Details tab' ) ); if (!isValid) { this.domService.scrollToTop(); } } const detailPanel = this.panels[0]; detailPanel.rightIcon = isValid ? '' : 'exclamation-circle'; detailPanel.rightIconClass = isValid ? '' : 'danger'; } markForPublishValidation (isValid = false) { const updateAlert = (isValid && !!this.program.publishedValidityAlert) || (!isValid && !this.program.publishedValidityAlert); if (updateAlert) { this.programService.setMapProperty( this.activeProgramId, 'publishedValidityAlert', isValid ? '' : this.i18n.translate( 'PROGRAM:textToSaveProgramAsPublishedEnterRequiredFields', {}, 'To save and publish your program please enter all required information' ) ); if (!isValid) { this.domService.scrollToTop(); } } const completeMap = this.isNomination ? this.getIsCompleteMapNom() : this.getIsCompleteMapGrant(); const keys = this.isNomination ? NomProgramSteps : GrantProgramSteps; for (const enumKey in keys) { if (keys.hasOwnProperty(enumKey)) { let panel: Panel; let stepValid = false; const actualKey = enumKey as keyof typeof keys; const stepIndex = keys[actualKey]; switch (stepIndex) { case GrantProgramSteps.Details: case GrantProgramSteps.Settings: case GrantProgramSteps.Cycles: case GrantProgramSteps.Forms: case GrantProgramSteps.Workflow: panel = this.panels[stepIndex]; stepValid = completeMap[stepIndex]; break; case NomProgramSteps.GrantProgram as any: if (this.isNomination) { panel = this.panels[stepIndex]; stepValid = (completeMap as any)[stepIndex]; } break; } if (panel) { panel.rightIcon = stepValid ? '' : 'exclamation-circle'; panel.rightIconClass = stepValid ? '' : 'danger'; } } } } async saveAndPublish () { if (this.canSaveAndPublish()) { this.startSave(); } else { if (this.program.draftValidityAlert) { this.programService.setMapProperty( this.activeProgramId, 'draftValidityAlert', '' ); } this.markForPublishValidation(); } } async startSave () { const changed = this.checkIfNeedsConfirmModalForTranslations(); if (changed.length) { const confirm1 = this.i18n.translate( 'PROGRAM:textSaveProgramChangesConfirm', {}, 'With international enabled, changes to the following may require updates to translations' ); const confirm2 = this.i18n.translate( this.isNomination ? 'PROGRAM:textSaveNominationProgramChangesConfirm2' : 'PROGRAM:textSaveProgramChangesConfirm2', {}, this.isNomination ? 'To edit translations proceed to Program Setup > Nomination Programs > Translation and filter to the correct program.' : 'To edit translations proceed to Program Setup > Grant Programs > Translation and filter to the correct program.' ); const changedList = changed.map((item) => { return `
${ item.translated }
`; }); const deps = { confirmText: `
${confirm1}:
${changedList.join('')}
${confirm2}
`, modalHeader: this.i18n.translate('PROGRAM:hdrSaveProgramChanges'), confirmButtonText: this.i18n.translate('common:btnSave') }; const proceed = await this.modalFactory.open( ConfirmationModalComponent, deps ); if (proceed) { await this.saveProgram(); } } else { await this.saveProgram(); } } async saveProgram () { this.spinnerService.startSpinner(); const successful = await this.programService.handleSaveProgram( this.program, true, this.isNomination, false ); if (successful) { this.routeToProgramsPage(); } this.spinnerService.stopSpinner(); } ngOnDestroy () { this.footerState.clearAll(); this.sub.unsubscribe(); this.programService.resetConfigureProgramMap(); this.emailService.emptyTemplateMap(); } }