import { Injectable } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { ApplicationFileService } from '@core/services/application-file.service'; import { CurrencyService } from '@core/services/currency.service'; import { PortalDeterminationService } from '@core/services/portal-determination.service'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { ApplicantFormForUI, BaseApplication } from '@core/typings/application.typing'; import { FileUploadForPDF, FormInfoForPDF, FormioComponentForPdf, TableCsvForPdf } from '@core/typings/pdf.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { DownloadFormVisibility } from '@features/configure-forms/download-form-modal/download-form-modal.component'; import { BaseApplicationForLogic, ComponentWithAnswer, Form, FormAudience, FormComponentsByTab, FormDecisionTypes, FormDefinitionComponent, FormDefinitionForUi, FormioAnswerValues, FormioChanges, FormioComponentWithRefFieldData, FormTab, StandardGmRequiredFields } from '@features/configure-forms/form.typing'; import { CustomDataTablesService } from '@features/custom-data-tables/custom-data-table.service'; import { KeyValue } from '@features/custom-data-tables/custom-data-tables.typing'; import { EmployeeSSOFieldsData } from '@features/employee-sso-fields/employee-sso-fields.typing'; import { InKindService } from '@features/in-kind/in-kind.service'; import { LogicBuilderService } from '@features/logic-builder/logic-builder.service'; import { LogicColumn, LogicState, LogicValueFormatType, NestedPropColumn } from '@features/logic-builder/logic-builder.typing'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { UserService } from '@features/users/user.service'; import { SelectOption, TableDataDownloadFormat, TypeaheadSelectOption, YcFile } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { CurrencyRadioOptions, CurrencyValue } from '@yourcause/common/masking'; import { uniq } from 'lodash'; import { ComponentHelperService, EMPLOYEE_SSO_TYPE_PREFIX } from '../component-helper/component-helper.service'; import { ReportFieldService } from '../report-field/report-field.service'; @Injectable({ providedIn: 'root' }) export class FormHelperService { readonly componentBucketIdPrefix = 'form-component-bucket'; constructor ( private customDataTableService: CustomDataTablesService, private inKindService: InKindService, private currencyService: CurrencyService, private i18n: I18nService, private applicationFileService: ApplicationFileService, private referenceFieldService: ReferenceFieldsService, private clientSettingsService: ClientSettingsService, private userService: UserService, private reportFieldService: ReportFieldService, private logicBuilderService: LogicBuilderService, private componentHelper: ComponentHelperService, private portal: PortalDeterminationService ) { } /** * Pulls out the files and tables for the forms passed in * * @param forms: forms to extract * @returns file uploads and table csvs */ extractFilesAndTablesForPdf (forms: FormInfoForPDF[]) { const fileUploads: FileUploadForPDF[] = []; const tableCsvs: TableCsvForPdf[] = []; const fileNameMap: Record = {}; forms.forEach((form) => { const components = this.getVisibleFileUploadsSubsetsAndTableComps( form ); components.forEach((component) => { const field = this.referenceFieldService.getReferenceFieldFromCompType( component.type ); const isTableType = [ ReferenceFieldsUI.ReferenceFieldTypes.Table, ReferenceFieldsUI.ReferenceFieldTypes.Subset ].includes(field.type); if (component.answer) { this.mapFileUploadsAndTableCsvs( component.answer, fileUploads, tableCsvs, field.name, field.referenceFieldId, fileNameMap, isTableType ); } }); }); const formWithSpecialHandling = forms.find((form) => { return !!form.specialHandling?.address1; }); const specialHandlingFileUrl = formWithSpecialHandling?.specialHandling?.fileUrl; if (specialHandlingFileUrl) { const details = this.applicationFileService.breakDownloadUrlDownToObject( specialHandlingFileUrl ); this.mapFileUploadsAndTableCsvs( [ new YcFile( details.fileName, null, specialHandlingFileUrl, +details.fileId ) ], fileUploads, [], '', null, fileNameMap, false ); } return { fileUploads, tableCsvs }; } /** * Maps the file uploads and table csvs * * @param answer: form answer * @param fileUploads: file uploads * @param tableCsvs: table csvs * @param label: label * @param referenceFieldId: reference field id * @param fileNameMap: file name map * @param isTable: is for table? */ mapFileUploadsAndTableCsvs ( answer: FormioAnswerValues, fileUploads: FileUploadForPDF[], tableCsvs: TableCsvForPdf[], label: string, referenceFieldId: number, fileNameMap: Record, isTable: boolean ) { let alreadyHave = false; if (!isTable) { (answer as YcFile[] || []).forEach((ycFile) => { const file = this.applicationFileService.breakDownloadUrlDownToObject(ycFile.fileUrl); alreadyHave = this.checkIfAlreadyHaveFile( file, fileUploads ); const passed = this.addToFileNameMap(alreadyHave, file.fileName, fileNameMap); if (passed) { fileUploads.push(file); } }); } else { const rows = this.referenceFieldService.mapRowsForTable( answer as ReferenceFieldsUI.TableResponseRowForUi[], referenceFieldId, true ); if (rows?.length > 0) { const tableCsv: TableCsvForPdf = { fileName: `${label}.csv`, csv: this.referenceFieldService.returnTableRowImportData( referenceFieldId, rows, false, TableDataDownloadFormat.CSV ), rows, referenceFieldId }; alreadyHave = this.checkIfAlreadyHaveTable( tableCsv, tableCsvs ); const passed = this.addToFileNameMap(alreadyHave, label, fileNameMap); if (passed) { tableCsvs.push(tableCsv); } } } } /** * Adds the file name to the map if we don't already have it * * @param alreadyHave: already have file name * @param fileName: file name * @param fileNameMap: file name map * @returns if passed */ addToFileNameMap ( alreadyHave: boolean, fileName: string, fileNameMap: Record ): boolean { if (!alreadyHave) { if (fileNameMap[fileName]) { ++fileNameMap[fileName]; fileName = `${fileName.split('.').slice(0, -1).join('.')}_${fileNameMap[fileName]}.${fileName.split('.').pop()}`; } fileNameMap[fileName] = fileNameMap[fileName] || 1; return true; } return false; } /** * Checks if we already have a file * * @param thisFile: this file upload * @param fileUploads: all file uploads * @returns if we already have the file */ checkIfAlreadyHaveFile ( thisFile: FileUploadForPDF, fileUploads: FileUploadForPDF[] ) { let alreadyHave = false; fileUploads.forEach((upload) => { if (thisFile.fileId === upload.fileId) { alreadyHave = true; } }); return alreadyHave; } /** * Checks if we already have a table * * @param thisTable: this table * @param allTables: all tables * @returns if we already have the table */ checkIfAlreadyHaveTable ( thisTable: TableCsvForPdf, allTables: TableCsvForPdf[] ) { let alreadyHave = false; allTables.forEach((table) => { if (thisTable.referenceFieldId === table.referenceFieldId) { alreadyHave = true; } }); return alreadyHave; } /** * Gets the visible file upload, table, and subset components * * @param form form to investigate * @returns visible file uploads, subsets, and tables */ getVisibleFileUploadsSubsetsAndTableComps (form: FormInfoForPDF) { const components: ComponentWithAnswer[] = []; const tabs = form.formDefinition; tabs.forEach((tab) => { const compsToCheck = (tab as FormDefinitionForUi).components; this.componentHelper.eachComponent(compsToCheck, (component) => { let answer: FormioAnswerValues; const noNestedComponents = !component.components || !component.components.length; const isRefField = this.componentHelper.isReferenceFieldComp(component.type); let isValidType = false; if (isRefField) { const field = this.referenceFieldService.getReferenceFieldFromCompType(component.type); answer = form.referenceFields[field.key]; const isFileUpload = field.type === ReferenceFieldsUI.ReferenceFieldTypes.FileUpload; const isTable = field.type === ReferenceFieldsUI.ReferenceFieldTypes.Table; const isSubset = field.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset; isValidType = isFileUpload || isTable || isSubset; if (isSubset || isTable) { const numberOfCols = this.referenceFieldService.getNumberOfColumns(field); const { showTableOnPdf } = this.componentHelper.getShowTableOnPdfAndRecordCount( form.referenceFields, field, numberOfCols ); isValidType = !showTableOnPdf; // Find all table file uploads if (isTable) { this.referenceFieldService.tableColumnsMap[ field.referenceFieldId ].filter((column) => { return column.referenceField.type === ReferenceFieldsUI.ReferenceFieldTypes.FileUpload; }).forEach((fileField) => { const comp = this.componentHelper.getComponentFromTableColumn( fileField ); let tableFileAnswer: FormioAnswerValues; const tableResponseRows = form.referenceFields[field.key] as ReferenceFieldsUI.TableResponseRowForUi[]; tableResponseRows?.forEach((row) => { row.columns.forEach((column) => { if (column.referenceFieldId === fileField.referenceFieldId) { tableFileAnswer = column.value; } }); }); const compToAdd = { ...comp, answer: tableFileAnswer }; components.push(compToAdd); }); } } } if ( isValidType && noNestedComponents && this.componentHelper.isCompVisible(component) ) { const compToAdd = { ...component, answer }; components.push(compToAdd); } }, true); }); return components; } /** * Gets PDF Components for only visible comps * * @param conditionalVisibilityState: the conditional visibility state * @param formDefinition: form definition * @returns visible pdf components aray */ getPdfComponentsForOnlyVisible ( conditionalVisibilityState: LogicState, formDefinition: FormDefinitionForUi[] ): FormComponentsByTab[] { const visibleTabs = conditionalVisibilityState ? this.filterHiddenTabs( formDefinition, conditionalVisibilityState ) : formDefinition; const formComponentsByTab: FormComponentsByTab[] = []; visibleTabs.forEach((tab) => { const components: FormDefinitionComponent[] = []; this.componentHelper.eachComponent((tab as FormDefinitionForUi).components, (component) => { this.componentHelper.updateApplicableComponentsArray( component, false, components ); }, true); formComponentsByTab.push({ tabName: (tab as FormDefinitionForUi).tabName, components: this.mapComponentsForPdf(components) }); }); return formComponentsByTab; } /** * Gets PDF components for the entire form * * @param formDefinition: the form definition * @param skipVisibility: skip visibility? * @returns components array based on arguments */ getPdfComponentsForAll ( formDefinition: FormDefinitionForUi[], skipVisibility: boolean ): FormComponentsByTab[] { const formComponentsByTab: FormComponentsByTab[] = []; formDefinition.map((tab) => { const applicableComponents: FormDefinitionComponent[] = []; this.componentHelper.eachComponent(tab.components, (formioComponent) => { this.componentHelper.updateApplicableComponentsArray( formioComponent, skipVisibility, applicableComponents ); }, true); formComponentsByTab.push({ tabName: tab.tabName, components: this.mapComponentsForPdf(applicableComponents) }); }); return formComponentsByTab; } /** * Prepares InKind on a form * * @param formDefinition: the form definition */ async prepareInKindForForm (formDefinition: FormDefinitionForUi[]) { const components = this.componentHelper.getAllComponents(formDefinition); const inKindComp = components.find((comp) => { return comp.type === 'inKindItems'; }); if (inKindComp) { const availableItems = inKindComp.items; if (availableItems?.length > 0) { const detailedItems = await this.inKindService.getItemsById( uniq(availableItems), undefined, this.userService.getCurrentUserCulture() ); inKindComp.inKindItemsForPdf = detailedItems.map((item) => { return { label: item.name, value: item.identification }; }); } else { inKindComp.inKindItemsForPdf = []; } } } /** * Prepares all components for render * * @param formDefinitions: the form definitions * @param formIds: the form ids */ async prepareComponentsForRenderForm ( formDefinitions: FormDefinitionForUi[][], formIds: number[] ) { let components: FormDefinitionComponent[] = []; formDefinitions.forEach((formDefinition) => { const formComps = this.componentHelper.getAllComponents(formDefinition); components = [ ...components, ...formComps ]; }); const additionalGuids = await this.prepareTablesAndSubsetsFromComponents(components); const clientId = this.clientSettingsService.clientSettings.clientId || null; await this.prepareCdtsAndSubsetsFromComponents( components, additionalGuids, formIds, clientId ); } /** * Prepares CDT and Subset components * * @param components: the components * @param additionalGuids: additional cdt guids * @param formIds: form ids * @param clientId: client id */ async prepareCdtsAndSubsetsFromComponents ( components: FormDefinitionComponent[], additionalGuids: string[], formIds: number[], clientId?: number ) { const guids: string[] = additionalGuids; const subsetIds: number[] = []; components.forEach((component) => { const foundField = this.referenceFieldService.getReferenceFieldFromCompType( component.type ); if (foundField?.customDataTableGuid) { guids.push(foundField.customDataTableGuid); } if (foundField?.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset) { subsetIds.push(foundField.referenceFieldId); } }); await this.customDataTableService.getAllCdtOptionsPerForm( guids, true, this.userService.getCurrentUserCulture(), formIds, clientId ); if (subsetIds.length > 0) { await Promise.all(uniq(subsetIds).map(async (id) => { await this.referenceFieldService.setDataPointsForSubset(id); })); } } /** * Prepares Tables and Subset Components * * @param components: the components * @returns cdt guids to fetch */ async prepareTablesAndSubsetsFromComponents ( components: FormDefinitionComponent[] ): Promise { const tableRefIds = components.map((component) => { const refFieldKey = this.componentHelper.getRefFieldKeyFromCompType( component.type ); const foundField = this.referenceFieldService.getReferenceFieldByKey( refFieldKey ); const isTableOrSubset = [ ReferenceFieldsUI.ReferenceFieldTypes.Table, ReferenceFieldsUI.ReferenceFieldTypes.Subset ].includes(foundField?.type); return isTableOrSubset ? foundField?.referenceFieldId : undefined; }).filter((id) => !!id); const tableFields = await this.referenceFieldService.setAllTableAndSubsetColumnsOnForm( tableRefIds ); const cdtsToFetchFromTable: string[] = []; tableFields.forEach((fields) => { fields.forEach((field) => { const found = this.referenceFieldService.referenceFieldMapById[ field.referenceFieldId ]; if (found?.customDataTableGuid) { cdtsToFetchFromTable.push(found.customDataTableGuid); } }); }); return cdtsToFetchFromTable; } /** * Maps the components for PDF * * @param components: the components * @returns mapped components for pdf */ mapComponentsForPdf ( components: FormDefinitionComponent[] ): FormioComponentWithRefFieldData[] { return components.map((component) => { const refFieldKey = this.componentHelper.getRefFieldKeyFromCompType( component.type ); const foundField = this.referenceFieldService.getReferenceFieldByKey( refFieldKey ); let referenceField: ReferenceFieldAPI.ReferenceFieldPdfData; let visibleColumns: ReferenceFieldsUI.TableFieldForUi[] = []; if (foundField) { let options: KeyValue[] = []; if (foundField.customDataTableGuid) { options = this.customDataTableService.customDataTableOptionsMap[foundField.customDataTableGuid]; options = (options || []).filter((opt) => opt.inUse); } const isTableOrSubset = [ ReferenceFieldsUI.ReferenceFieldTypes.Table, ReferenceFieldsUI.ReferenceFieldTypes.Subset ].includes(foundField.type); if (isTableOrSubset) { visibleColumns = this.referenceFieldService.getVisibleTableColumns( foundField.referenceFieldId ); } referenceField = { ...foundField, options: options.map((opt) => { return { label: opt.value, value: opt.key }; }) }; } return { ...component, referenceField, inKindItems: component.inKindItemsForPdf, visibleColumns, hideLabel: ['columns', 'well', 'table'].includes(component.type) }; }); } /** * Get the applicable components to show on PDF * * @param formDefinition: the form definition * @param referenceFields: reference field responses * @returns components applicable for pdf */ getApplicableComponentsForPdf ( formDefinition: FormDefinitionForUi[], referenceFields: ReferenceFieldsUI.RefResponseMap ): FormioComponentForPdf[] { const applicableComponents: FormDefinitionComponent[] = []; formDefinition.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (formioComponent) => { this.componentHelper.updateApplicableComponentsArray( formioComponent, false, applicableComponents ); }, true); }); return applicableComponents.map((comp) => { return this.mapComponentToComponentForPDF(comp, referenceFields); }); } /** * * @param comp: form component * @param referenceFields: reference field answers * @returns the form component for PDF */ mapComponentToComponentForPDF ( comp: FormDefinitionComponent, referenceFields: ReferenceFieldsUI.RefResponseMap ): FormioComponentForPdf { const referenceField = this.referenceFieldService.getReferenceFieldFromCompType( comp.type ); const visibleColumns = this.referenceFieldService.getVisibleTableColumns( referenceField?.referenceFieldId ); const numberOfCols = this.referenceFieldService.getNumberOfColumns(referenceField); const tableDetails = this.componentHelper.getShowTableOnPdfAndRecordCount( referenceFields, referenceField, numberOfCols ); return { ...comp, referenceField, isLayoutComponent: this.componentHelper.isLayoutComponent(comp.type), isTotaled: this.componentHelper.getIsTableTotaled(visibleColumns), showTableOnPdf: tableDetails.showTableOnPdf, totalTableRows: tableDetails.totalTableRows, visibleColumns }; } /** * Returns whether or not the field should be allowed to save data. * * @param field Reference field to be checked * @param isManagerForm Boolean indicating the audience of the form the field exists on */ getSaveIsDisabled ( field: ReferenceFieldAPI.ReferenceFieldDisplayModel, isManagerForm: boolean ) { const applicantFieldOnManagerForm = field?.formAudience === FormAudience.APPLICANT && isManagerForm; const managerFieldOnApplicantForm = field?.formAudience === FormAudience.MANAGER && !isManagerForm; const saveDisabled = applicantFieldOnManagerForm || managerFieldOnApplicantForm; return saveDisabled; } /** * Gets the formio changes array when copying an application * * @param referenceFields: reference field responses * @returns formio changes array for the new copied app */ getFormioChangesForCopyApp ( referenceFields: ReferenceFieldsUI.RefResponseMap ): FormioChanges[] { return Object.keys(referenceFields).map((key) => { return { key, type: `referenceFields-${key}`, isReferenceField: true, value: referenceFields[key] }; }); } /** * Gets the required reference field keys on submission * * @param conditionalVisibilityState: the conditional visibility state * @param formDefinition: the form definition * @returns the required reference field keys */ getRequiredReferenceFieldKeys ( conditionalVisibilityState: LogicState, formDefinition: FormDefinitionForUi[] ) { const visibleTabs = this.getVisibleTabs(conditionalVisibilityState, formDefinition); const requiredComps = this.componentHelper.getRequiredComponents(visibleTabs); return requiredComps.map((comp) => { return this.componentHelper.getRefFieldKeyFromCompType(comp.type); }).filter((key) => { return !!key; }); } /** * Gets the visible tabs * * @param conditionalVisibilityState: the conditional visibility state * @param formDefinition: the form definition * @returns the visible tabs */ getVisibleTabs ( conditionalVisibilityState: LogicState, formDefinition: FormDefinitionForUi[] ) { return conditionalVisibilityState ? this.filterHiddenTabs( formDefinition, conditionalVisibilityState ) : formDefinition; } /** * Gets the required standard fields on submission * * @param conditionalVisibilityState: the conditional visibility state * @param formDefinition: the form definition * @returns the required standard fields */ getRequiredStandardFields ( formDefinition: FormDefinitionForUi[] ): StandardGmRequiredFields { const requiredComps = this.componentHelper.getRequiredComponents(formDefinition); let reviewerRecommendedFundingAmountRequired = false; let decisionRequired = false; let amountRequestedRequired = false; let careOfRequired = false; let paymentDesignationRequired = false; requiredComps.forEach((comp) => { switch (comp.type) { case 'amountRequested': amountRequestedRequired = true; break; case 'decision': decisionRequired = true; break; case 'designation': paymentDesignationRequired = true; break; case 'careOf': careOfRequired = true; break; case 'reviewerRecommendedFundingAmount': reviewerRecommendedFundingAmountRequired = true; break; } }); return { reviewerRecommendedFundingAmountRequired, decisionRequired, amountRequestedRequired, careOfRequired, paymentDesignationRequired }; } /** * Gets the table and subset ids from the form definition * * @param formDefinitions: form definition * @returns table and subset ids */ getTableAndSubsetIdsFromFormDefinition ( formDefinitions: FormDefinitionForUi[][] ) { const tableIds: number[] = []; formDefinitions.forEach((formDefinition) => { formDefinition.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (component: FormDefinitionComponent) => { const field = this.referenceFieldService.getReferenceFieldFromCompType( component.type ); if ( field?.type === ReferenceFieldsUI.ReferenceFieldTypes.Table || field?.type === ReferenceFieldsUI.ReferenceFieldTypes.Subset ) { tableIds.push(field.referenceFieldId); } }); }); }); return uniq(tableIds); } /** * Gets the decision options * * @param allowRecused: allow recused? * @param recuseValue: recuse value * @param truthyValue: truthy value * @param falsyValue: falsy value * @returns the decision options */ getDecisionOptions ( allowRecused: boolean, recuseValue: string, truthyValue: string, falsyValue: string ) { const options = [{ label: truthyValue || this.i18n.translate('common:textYes'), value: FormDecisionTypes.Approve }, { label: falsyValue || this.i18n.translate('common:textNo'), value: FormDecisionTypes.Decline }]; if (allowRecused) { return [ ...options, { label: recuseValue || this.i18n.translate( 'common:textRecused', {}, 'Recused' ), value: FormDecisionTypes.Recused } ]; } return options; } /** * Gets the logic value format type from the component type * * @param compType: component type * @returns the format type */ getLogicValueFormatType ( compType: string ): LogicValueFormatType { const refField = this.referenceFieldService.getReferenceFieldFromCompType(compType); if (refField) { switch (refField.type) { default: case ReferenceFieldsUI.ReferenceFieldTypes.TextArea: case ReferenceFieldsUI.ReferenceFieldTypes.TextField: return 'text'; case ReferenceFieldsUI.ReferenceFieldTypes.SelectBoxes: case ReferenceFieldsUI.ReferenceFieldTypes.CustomDataTable: case ReferenceFieldsUI.ReferenceFieldTypes.Radio: return 'select'; case ReferenceFieldsUI.ReferenceFieldTypes.Checkbox: return 'checkbox'; case ReferenceFieldsUI.ReferenceFieldTypes.Date: return 'date'; case ReferenceFieldsUI.ReferenceFieldTypes.Number: return 'number'; case ReferenceFieldsUI.ReferenceFieldTypes.Currency: return 'currency'; } } else { switch (compType) { default: case 'careOf': case 'designation': return 'text'; case 'amountRequested': case 'reviewerRecommendedFundingAmount': return 'currency'; case 'decision': return 'select'; } } } /** * Gets teh options and format valeu type given the component * * @param component: the componet * @returns typeahead options and logic value format type */ async getOptionsAndFormatValueType ( component: FormDefinitionComponent ) { let options: (TypeaheadSelectOption|SelectOption)[] = []; const key = this.componentHelper.getRefFieldKeyFromCompType( component.type ); const referenceField = this.referenceFieldService.allReferenceFields .find((field) => { return field.key === key; }); const logicValueFormatType = this.getLogicValueFormatType(component.type); if (referenceField?.customDataTableGuid) { await this.customDataTableService.setCustomDataTableOptionsFromGuid( referenceField?.customDataTableGuid, true, this.userService.getCurrentUserCulture() ); const parentMapVal = this.referenceFieldService.parentPicklistValueMap[ referenceField.parentReferenceFieldId ]; options = this.customDataTableService.getTypeaheadOptionsForCdt( referenceField?.customDataTableGuid, null, referenceField.supportsMultiple, parentMapVal ); } else if (component.type === 'decision') { options = this.getDecisionOptions( component.allowRecused, component.recuseValue, component.truthyValue, component.falsyValue ); } return { options, logicValueFormatType }; } /** * Filters out hidden tabs * * @param tabs: tabs * @param validationState: validation state * @returns the filtered tabs */ filterHiddenTabs ( tabs: (FormTab|FormDefinitionForUi)[], validationState: LogicState ) { return tabs.filter((_, index) => { const column: NestedPropColumn = ['tabs', index]; return this.logicBuilderService.getCurrentLogicValueOfColumn( column as LogicColumn, validationState ) ?? true; }); } /** * Gets the value from the component * * @param component; the component * @param parentFields: parent fields * @param forceDefaultCurrency: are we forcing the user to enter in default currency? * @param returnCurrencyAmount: if we want to return the current currency amount instead of object * @returns the value / answer */ getValueFromComponent ( component: FormDefinitionComponent, parentFields: Partial, forceDefaultCurrency = false, returnCurrencyAmount = false ): FormioAnswerValues { let value: FormioAnswerValues = null; if ( component.type !== 'button' && !this.componentHelper.isLayoutComponent(component.type) ) { const field = this.referenceFieldService.getReferenceFieldFromCompType( component.type ); if (field) { value = parentFields.referenceFields[field.key] ?? this.referenceFieldService.getBlankValueForFormField( field, component, true ); if ( field.type === ReferenceFieldsUI.ReferenceFieldTypes.Currency && returnCurrencyAmount ) { value = (value as CurrencyValue).amountForControl; } } else if (component.type.startsWith(`${EMPLOYEE_SSO_TYPE_PREFIX}-`)) { const attr = component.type.split('-')[1] as keyof EmployeeSSOFieldsData; value = parentFields.employeeInfo ? parentFields.employeeInfo[attr] : ''; } else if (component.type === 'reportField') { if (parentFields.reportFieldResponse) { value = this.reportFieldService.getReportFieldValue( component.reportFieldDataOptions, parentFields.reportFieldResponse ); } } else if (component.type === 'amountRequested') { value = this.getValueForCurrency( parentFields.amountRequested, parentFields.currencyRequestedAmountEquivalent, parentFields.currencyRequested, forceDefaultCurrency, component.useCustomCurrency, component.customCurrency, component.defaultVal ); if (returnCurrencyAmount) { return parentFields.amountRequestedForEdit; } } else if (component.type === 'reviewerRecommendedFundingAmount') { // Recommended funding is always stored in client default currency value = this.getValueForCurrency( parentFields.reviewerRecommendedFundingAmount, parentFields.reviewerRecommendedFundingAmount, this.clientSettingsService.defaultCurrency, true, component.useCustomCurrency, component.customCurrency, component.defaultVal ); if (returnCurrencyAmount) { return value.amountForControl; } } else { value = (parentFields as any)[component.type] as FormioAnswerValues; } } else { value = undefined; } return value; } /** * Gets the value for currency * * @param amountInDefaultCurrency: amount in default currency of client * @param amountEquivalent: the equivalent amount in currency requested * @param currencyRequested: currency requested * @param forceDefaultCurrency: force the user to enter amount in default currency * @param useCustomCurrency: setting on the component * @param customCurrency: currency required for component * @param defaultVal: default value of the component * @returns the value for component */ getValueForCurrency ( amountInDefaultCurrency: number, amountEquivalent: number, currencyRequested: string, forceDefaultCurrency: boolean, useCustomCurrency: CurrencyRadioOptions, customCurrency: string, defaultVal: string ) { const currencyOptions = this.currencyService.getCurrencyOptionsForComponent( currencyRequested, useCustomCurrency, customCurrency ); const currency = this.componentHelper.getCurrencyForFormFieldControl( currencyRequested, useCustomCurrency, customCurrency, currencyOptions, this.clientSettingsService.defaultCurrency, this.userService.get('lastSelectedCurrency') ); let amountForControl = forceDefaultCurrency ? amountInDefaultCurrency : amountEquivalent; if ( defaultVal && typeof defaultVal === 'string' && !amountForControl && amountForControl !== 0 ) { amountForControl = +defaultVal; } return { amountInDefaultCurrency, amountEquivalent, amountForControl, currency }; } /** * Checks the form group validity * * @param formGroup: the form group * @param definition: the form definition * @returns is the form group valid? */ checkFormGroupValidity ( formGroup: FormGroup, definition: FormDefinitionForUi ): boolean { if (formGroup.valid) { return true; } else { let isValid = true; this.componentHelper.eachComponent(definition.components, (comp) => { const compKey = comp.key; const control = formGroup.get(compKey); if ( control && !control.valid && !comp.isHidden && !comp.hiddenFromParent ) { isValid = false; } }); return isValid; } } /** * Generates data for javascript execution * * @param app: application * @param formDefinition: form definition * @returns form data */ generateDataForFormDefinition ( app: Partial, formDefinitions: FormDefinitionForUi[] ): Record { const data: Record = {}; formDefinitions.forEach(formDefinition => { this.componentHelper.eachComponent(formDefinition.components, (comp) => { const value = this.getValueFromComponent(comp, app, false, true); data[comp.key] = value; if (comp.type === 'amountRequested') { data[comp.currencyDataKey] = app.currencyRequested; } }); }); return data; } /** * Gets the reference field from the form key * * @param formKey: form key * @param formDef: form definition * @returns reference field that goes with that key */ getRefFieldFromFormKey ( formKey: string, formDef: FormDefinitionForUi[] ) { let foundType: string; formDef.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (comp) => { if (comp.key === formKey) { foundType = comp.type; } }); }); if (foundType) { return this.referenceFieldService.getReferenceFieldFromCompType(foundType); } return null; } /** * Gets the Form components by tab * * @param visibility: Download form visibility enum * @param form: the form * @param conditionalVisibilityState: the conditional visibility state * @returns the form components by tab array */ getFormComponentsByTab ( visibility: DownloadFormVisibility, form: Form, conditionalVisibilityState: LogicState ) { let formComponentsByTab: FormComponentsByTab[]; if (visibility === DownloadFormVisibility.SHOW_ALL) { formComponentsByTab = this.getPdfComponentsForAll( form.formDefinition, true ); } else { formComponentsByTab = this.getPdfComponentsForOnlyVisible( conditionalVisibilityState, form.formDefinition ); } return formComponentsByTab; } /** * Returns whether we should force default currency in amount requested * * @param isManagerForm: is manager form? * @param isManagerEditingApplicantForm: is manager editing an applicant form? * @returns if we should force default currency on the form */ shouldForceDefaultCurrencyInAmountRequested ( isManagerForm: boolean, isManagerEditingApplicantForm: boolean ) { return isManagerForm || isManagerEditingApplicantForm; } /** * Can we toggle currency? * * @param currency: currency on form * @param isReadOnly: is the form disabled and read only * @param isForSetValue: is this for set value logic? * @returns if they can toggle currency */ canToggleCurrency ( currency: string, readOnly: boolean, isForSetValue: boolean ): boolean { if (!isForSetValue) { const defaultCurrency = this.clientSettingsService.defaultCurrency; if ( this.portal.isManager && readOnly === true && // this means it's read only and can toggle currency !== defaultCurrency ) { return true; } } return false; } /** * Gets the file IDs from the form * * @param formDef: form definition * @param specialHandlingFileUrl: special handling file url * @param refResponseMap: reference field responses map * @returns file IDs for the response */ getFileIDsFromForm ( formDef: FormDefinitionForUi[], specialHandlingFileUrl: string, refResponseMap: ReferenceFieldsUI.RefResponseMap ): number[] { const fileIds: number[] = []; formDef.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (component) => { let answer: FormioAnswerValues; const isSpecialHandling = component.type === 'specialHandling'; if (isSpecialHandling && specialHandlingFileUrl) { const extracted = this.applicationFileService.extractQueryParamsFromFileUrl( specialHandlingFileUrl ); if (extracted?.fileId) { fileIds.push(+extracted.fileId); } } const isRefField = this.componentHelper.isReferenceFieldComp(component.type); let refFieldKey = ''; let isRefFileUpload = false; if (isRefField) { refFieldKey = this.componentHelper.getRefFieldKeyFromCompType( component.type ); const field = this.referenceFieldService.getReferenceFieldByKey( refFieldKey ); if (field) { answer = refResponseMap[refFieldKey]; const isTable = field.type === ReferenceFieldsUI.ReferenceFieldTypes.Table; if (isTable) { // Check for file uploads within a table const tableAnswers = refResponseMap[field.key] as ReferenceFieldsUI.TableResponseRowForUi[]; if (tableAnswers) { tableAnswers.forEach((tableAnswer) => { tableAnswer.columns.forEach((column) => { const columnIsUploadField = this.referenceFieldService.referenceFieldMap[ column.referenceFieldKey ]?.type === ReferenceFieldsUI.ReferenceFieldTypes.FileUpload; if (columnIsUploadField) { const columnAnswer = column.value as YcFile[] || []; this.pushFileIdsToArrayFromAnswer( columnAnswer, fileIds ); } }); }); } } isRefFileUpload = field.type === ReferenceFieldsUI.ReferenceFieldTypes.FileUpload; } } if (isRefFileUpload && answer) { this.pushFileIdsToArrayFromAnswer( answer as YcFile[] || [], fileIds ); } }); }); return fileIds.filter((id) => !!id); } /** * Pushed file ids to array from answer * * @param answer: file response * @param fileIds: file ids */ pushFileIdsToArrayFromAnswer ( answer: YcFile[], fileIds: number[] ) { answer.forEach((file: YcFile) => { if (file?.fileUploadId) { fileIds.push(+file.fileUploadId); } }); } /** * Gets the default form given a list of forms */ getDefaultForm (forms: ApplicantFormForUI[]) { const found = forms.find((form) => { return form.isDefault; }); return found ? found : forms[0]; } /** * Returns a boolean for whether or not we should continue initializing the form in the form renderer * * @param initializing true if we are loading the entire form, false if we are reacting to changes to the form def * @param tabInnittedLoadedCount number of tabs that have been initialized/loaded * @param tabInnittedTotalCount total number of tabs * @returns whether we should keep initializing the form. we only need to continue if we're initializing and the loaded count doesn't yet match the total count */ shouldInitializeNextTabOnFormChange ( initializing: boolean, tabInnittedLoadedCount: number, tabInnittedTotalCount: number ) { if ( initializing && tabInnittedLoadedCount === tabInnittedTotalCount ) { return true; } else { return false; } } }