import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { CurrencyService } from '@core/services/currency.service'; import { SpinnerService } from '@core/services/spinner.service'; import { FundingSourceTypes } from '@core/typings/budget.typing'; import { BatchItem, BatchItemFromApi, ProcessingTypes, SimplePaymentStats } from '@core/typings/payment.typing'; import { Program } from '@core/typings/program.typing'; import { BatchStatuses } from '@core/typings/status.typing'; import { APConfigService } from '@features/ap-config/ap-config.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { ProgramService } from '@features/programs/program.service'; import { WorkflowService } from '@features/workflow/workflow.service'; import { ArrayHelpersService, Tab, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { uniq } from 'lodash'; import moment from 'moment'; import { Subscription } from 'rxjs'; import { CreateOpenBatchModalComponent } from '../create-open-batch-modal/create-open-batch-modal.component'; import { EditBatchModalComponent } from '../edit-batch-modal/edit-batch-modal.component'; import { ImportAPPaymentsModalComponent } from '../import-ap-payments-modal/import-ap-payments-modal.component'; import { PaymentProcessingResources } from '../payment-processing.resources'; import { PaymentProcessingService } from '../payment-processing.service'; import { SendToProcessingModalComponent } from '../send-to-processing-modal/send-to-processing-modal.component'; import { UpdateBatchStatusModalComponent } from '../update-batch-status-modal/update-batch-status-modal.component'; interface ManagePaymentsFormGroup { programIds: number[]; } @Component({ selector: 'gc-manage-payments', templateUrl: './manage-payments.component.html', styleUrls: ['./manage-payments.component.scss'] }) export class ManagePaymentsComponent implements OnInit, OnDestroy { id: number|string; batch: BatchItem; availablePayments = this.paymentProcessingService.get('availablePayments'); tabs: Tab[] = []; BatchStatuses = BatchStatuses; createAndSendDisabled = false; ProcessingTypes = ProcessingTypes; loaded = false; sub = new Subscription(); apEnabled = this.clientSettingsService.clientSettings.apEnabled; programOptions: TypeaheadSelectOption[]; downloadDisabled = false; apConfig = this.apConfigService.apConfig; unableToDownloadToolTip = this.i18n.translate( 'MANAGE:textMustHaveAPConfigurationToDownload', {}, 'You must configure columns in Settings/AP Configuration' ); formGroup: TypeSafeFormGroup; programIds: number[]; programs: Program[]; workflowLevelOptions: TypeaheadSelectOption[]; constructor ( private activatedRoute: ActivatedRoute, private router: Router, private modalFactory: ModalFactory, private spinnerService: SpinnerService, private paymentProcessingService: PaymentProcessingService, private paymentProcessingResources: PaymentProcessingResources, private i18n: I18nService, private clientSettingsService: ClientSettingsService, private apConfigService: APConfigService, private formBuilder: TypeSafeFormBuilder, private programService: ProgramService, private arrayHelper: ArrayHelpersService, private currencyService: CurrencyService, private workflowService: WorkflowService ) { this.sub.add( this.paymentProcessingService.changesTo$('currentAvailableStats') .subscribe(async () => { const stats = this.paymentProcessingService.get('currentAvailableStats'); if (stats && this.isAvailable) { this.spinnerService.startSpinner(); await this.setAvailableBatch( this.getAvailable(), stats ); this.spinnerService.stopSpinner(); this.createAndSendDisabled = stats.includedPaymentsNumber === 0; } }) ); this.sub.add( this.paymentProcessingService.changesTo$('availablePayments') .subscribe(async () => { if (this.isAvailable) { this.spinnerService.startSpinner(); await this.setAvailableBatch( this.getAvailable(), this.paymentProcessingService.get('currentAvailableStats') ); this.spinnerService.stopSpinner(); } }) ); } get batchStatusAllowsDownload () { const validBatchStatuses = [ BatchStatuses.Processing, BatchStatuses.Funded, BatchStatuses.Disbursed, BatchStatuses.Complete ]; if (this.batch) { return validBatchStatuses.includes(this.batch.statusId); } else { return false; } } get isAvailable () { return location.pathname.includes('available'); } // shared page, if will be yourcause when viewing payments from the Available tab, otherwise we need the processor off the batch details get isYourCause () { return (this.id === 'yourcause') || (this.batch ? this.batch.processorType === ProcessingTypes.YourCause : false); } get isIncluded () { return location.pathname.includes('included'); } get hasValidAPConfig () { return this.apConfig ? this.apConfig.accountsPayableSettingsColumns.length > 0 : false; } get downloadAPFileToolTip () { if (this.hasValidAPConfig) { return null; } else { return this.unableToDownloadToolTip; } } async ngOnInit () { if (this.isAvailable) { this.id = this.activatedRoute.snapshot.params.processor; } else { this.id = this.activatedRoute.snapshot.params.id; } this.spinnerService.startSpinner(); await this.initializeData(); this.setTabs(); await this.setProgramOptions(); let programIds: number[] = []; if (!this.batch.statusId) { programIds = [this.programOptions[0].value]; await this.programChanged(programIds); } this.formGroup = this.formBuilder.group({ programIds: [programIds] }); this.spinnerService.stopSpinner(); this.loaded = true; } async initializeData () { if (this.isAvailable) { await this.setAvailableBatch( this.getAvailable(), this.paymentProcessingService.get('currentAvailableStats') ); } else { await this.getBatch(); } } getAvailable () { return this.paymentProcessingService.get('availablePayments') .find((item) => { return this.isYourCause ? item.processorType === ProcessingTypes.YourCause : item.processorType !== ProcessingTypes.YourCause; }); } async setAvailableBatch (available: BatchItem, stats: SimplePaymentStats) { if (stats && available && this.programIds?.length > 0) { const statsByProgram = await this.paymentProcessingService.getPaymentStatsByPrograms( this.isYourCause ? ProcessingTypes.YourCause : ProcessingTypes.Client, this.programIds ); this.batch = { ...available, availableDetails: { excludedPayments: { number: statsByProgram.excludedPaymentsNumber, total: statsByProgram.excludedPaymentsAmount }, includedPayments: { number: stats.includedPaymentsNumber || 0, total: stats.includedPaymentsAmount || 0 } }, grantProgramIds: available.grantProgramIds, paymentDates: stats.startDate && stats.endDate ? moment(stats.startDate).format('ll') + ' - ' + moment(stats.endDate).format('ll') : '', footerMessage: statsByProgram.excludedPaymentsNumber ? this.i18n.translate( 'MANAGE:textNumberOfPaymentsExcludedDueToHoldOrPayee3', { number: statsByProgram.excludedPaymentsNumber }, '__number__ payments will be excluded from processing from having a status of Hold, missing payee information, or having an alternate address request that has not been approved.' ) : '' }; } else { this.batch = available; } } async getBatch () { const batch = await this.paymentProcessingResources.getBatch(this.id as number); this.createAndSendDisabled = batch.totalPaymentsInBatch === 0; this.batch = { id: +this.id, name: `${ batch.externalBatchId ? batch.externalBatchId + ' - ' : '' }${batch.name}`, processorName: batch.processingTypeId === ProcessingTypes.YourCause ? 'YourCause' : batch.clientName, processorType: batch.processingTypeId, paymentDates: '', batchDetails: { statusDetailText: this.getStatusText(batch), totalPaymentsNumber: batch.totalPaymentsInBatch, totalPaymentsAmount: batch.totalPaymentsAmount }, statusId: batch.batchStatusId, notes: batch.notes, externalBatchId: batch.externalBatchId, grantProgramIds: batch.grantProgramIds || [] }; } getStatusText (batch: BatchItemFromApi) { return this.paymentProcessingService.getStatusText(batch); } setTabs () { this.tabs = [{ link: this.getLink(), labelKey: 'MANAGE:textIncludedPayments', label: 'Included Payments', activeRoutes: ['included'] }, { link: this.getLink(false), labelKey: this.isAvailable ? 'MANAGE:hdrExcludedPayments' : 'GLOBAL:hdrAvailablePayments', label: this.isAvailable ? 'Excluded Payments' : 'Available Payments', activeRoutes: ['excluded'] }]; } async programChanged (programIds: number[]) { this.programIds = programIds; const programs = this.programs.filter((prog) => { return programIds.includes(+prog.grantProgramId); }); if (programs.length > 0) { const workflowIds = uniq(programs.map((prog) => { return prog.workflowId; })); const workflows = uniq(this.workflowService.workflows.filter((flow) => { return workflowIds.includes(flow.id); })); if (workflows.length > 0) { const workflowLevelOptions: TypeaheadSelectOption[] = []; workflows.forEach((workflow) => { workflow.levels.forEach((level) => { workflowLevelOptions.push({ label: level.name, value: level.id }); level.subLevels.forEach((sub) => { workflowLevelOptions.push({ label: sub.name, value: sub.id }); }); }); }); this.workflowLevelOptions = this.arrayHelper.sort( workflowLevelOptions, 'label' ); } } if (!this.isIncluded) { await this.router.navigate(['./included'], { relativeTo: this.activatedRoute }); await this.initializeData(); } } getLink (isIncluded = true) { return `/management/payment-processing/${ this.isAvailable ? 'manage-available' : 'manage-batch' }/${this.id}/${isIncluded ? 'included' : 'excluded'}`; } goBack () { this.router.navigate([ `management/payment-processing/${ this.isAvailable ? 'available' : 'batches' }` ]); } async deleteBatch (item: BatchItem) { const deps = { confirmButtonText: this.i18n.translate( 'common:btnDelete', {}, 'Delete' ), confirmText: this.i18n.translate( 'MANAGE:textAreYouSureDeleteTheBatch', {}, 'Are you sure you want to delete the batch? This will move all associated batch payments to a status of Pending.' ), modalHeader: this.i18n.translate( 'MANAGE:hdrDeleteBatch', { name: item.name }, 'Delete Batch - __name__' ) }; const proceed = await this.modalFactory.open(ConfirmationModalComponent, deps); if (proceed) { this.spinnerService.startSpinner(); await this.paymentProcessingService.handleDeleteBatch(item.id); this.router.navigate([ '/management/payment-processing/batches' ]); this.spinnerService.stopSpinner(); } } async createOpenBatch (item: BatchItem) { const response = await this.modalFactory.open( CreateOpenBatchModalComponent, { isExternal: !this.isYourCause } ); if (response) { this.spinnerService.startSpinner(); const options = this.paymentProcessingService.get('currentIncludedOptions'); const batchId = await this.paymentProcessingService.handleAddBatchModal({ name: response.name, notes: response.notes, processingTypeId: item.processorType, externalBatchId: response.externalBatchId }, options); await this.paymentProcessingService.resetAvailableRepos(this.isYourCause, true); this.spinnerService.stopSpinner(); if (batchId) { this.router.navigate([ `/management/payment-processing/manage-batch/${batchId}/included` ]); } } } async updateClientBatchStatus (data: { item: BatchItem; newStatus: BatchStatuses; }) { const response = await this.modalFactory.open( UpdateBatchStatusModalComponent, { newStatus: data.newStatus, item: data.item } ); if (!!response) { this.spinnerService.startSpinner(); await this.paymentProcessingService.updateBatchStatusModal( data.item, response.notes, data.newStatus ); await this.getBatch(); this.spinnerService.stopSpinner(); } } async editBatch (batch: BatchItem) { const response = await this.modalFactory.open( EditBatchModalComponent, { batch } ); if (response) { this.spinnerService.startSpinner(); await this.paymentProcessingService.handleBatchUpdateModal( response, +this.id ); await this.getBatch(); this.paymentProcessingService.resetBatchRepos(batch.id); this.spinnerService.stopSpinner(); } } async sendToProcessing (item: BatchItem) { const totalPaymentsNumber = item.availableDetails ? item.availableDetails.includedPayments.number : item.batchDetails.totalPaymentsNumber; const totalPaymentsAmount = item.availableDetails ? item.availableDetails.includedPayments.total : item.batchDetails.totalPaymentsAmount; const adapted = { ...item, batchDetails: { totalPaymentsNumber, totalPaymentsAmount } }; const response = await this.modalFactory.open( SendToProcessingModalComponent, { item: adapted, isAvailable: this.isAvailable } ); if (response) { this.spinnerService.startSpinner(); if (this.isAvailable) { const id = await this.paymentProcessingService.handleSendAvailableToProcessing( item, response ); await this.paymentProcessingService.resetAvailableRepos( this.isYourCause, true ); if (id) { this.router.navigate([ `/management/payment-processing/manage-batch/${id}/included` ]); } } else { await this.paymentProcessingService.handleSendBatchToProcessing( item.id, response.notes, response.paymentStatus, response.issueDate ); await this.getBatch(); } this.spinnerService.stopSpinner(); } } async downloadAPFile () { this.spinnerService.startSpinner(); await this.paymentProcessingService.downloadPaymentBatchReport( this.batch.id, this.batch.name ); this.spinnerService.stopSpinner(); } async onImportClick () { const result = await this.modalFactory.open( ImportAPPaymentsModalComponent, {} ); if (result) { this.spinnerService.startSpinner(); this.paymentProcessingService.handlePaymentImport(result); await this.getBatch(); this.paymentProcessingService.resetBatchRepos(this.batch.id); this.spinnerService.stopSpinner(); } } returnProgramOption (program: Program) { const numberPayments = program.numberOfPayments ? program.numberOfPayments.toLocaleString() : 0; const paymentsText = numberPayments > 1 ? this.i18n.translate( 'common:textNumberOfPayments', { number: numberPayments }, '__number__ payments' ) : this.i18n.translate( 'common:textOnePayment', {}, '1 payment' ); const amount = program.paymentsTotal ? this.currencyService.formatMoney(program.paymentsTotal) : null; return `
${program.grantProgramName}
${paymentsText} | ${amount} `; } async setProgramOptions () { this.programs = await this.programService.getPaymentProcessingPrograms( this.isYourCause ? ProcessingTypes.YourCause : ProcessingTypes.Client, FundingSourceTypes.DOLLARS ); this.programOptions = this.arrayHelper.sort( this.programs.filter((program: Program) => { return !program.isArchived && program.numberOfPayments && program.numberOfPayments > 0; }).map((program) => { return { label: this.returnProgramOption(program), value: +program.grantProgramId }; }), 'label'); } ngOnDestroy () { this.sub.unsubscribe(); } }