import { DecimalPipe, PercentPipe } from '@angular/common'; import { EventEmitter, Injectable } from '@angular/core'; import { CurrencyService } from '@core/services/currency.service'; import { PolicyService } from '@core/services/policy.service'; import { AdHocReportingAPI } from '@core/typings/api/ad-hoc-reporting.typing'; import { ColorPaletteType } from '@core/typings/branding.typing'; import { User } from '@core/typings/client-user.typing'; import { AdHocReportingUI } from '@core/typings/ui/ad-hoc-reporting.typing'; import { AudienceMember } from '@features/audience/audience.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { StandardProductConfigurationService } from '@features/platform-admin/standard-product-configuration/standard-product-configuration.service'; import { AdHocReportingDefinitions, RelatedObjectNames, RootObjectNames } from '@features/reporting/services/ad-hoc-reporting-definitions.service'; import { AdHocReportingMappingService } from '@features/reporting/services/ad-hoc-reporting-mapping.service'; import { AdHocReportingService } from '@features/reporting/services/ad-hoc-reporting.service'; import { UserService } from '@features/users/user.service'; import { AdvancedFilterGroup, APIResultData, APISortColumn, ArrayHelpersService, BLANK_PAGINATION_OPTIONS, ChartService, Column, ColumnFilterRow, DebounceFactory, FileService, FilterColumn, FilterHelpersService, FilterModalTypes, PaginatedResponse, PaginationOptions, SelectOption, SkeletonDisplayConfig, SwitchState, TableDataFactory, 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 { DisplayGrid, GridsterConfig, GridsterItem } from 'angular-gridster2'; import { Chart, ChartType, Color, TooltipItem } from 'chart.js'; import { cloneDeep, get, isUndefined } from 'lodash'; import { take } from 'rxjs'; import { DashboardsResources } from './dashboards.resources'; import { DashboardsState } from './dashboards.state'; import { Dashboards, DEFAULT_MAX_GROUPS, DEFAULT_ROWS_PER_PAGE, GCDashboards } from './dashboards.typing'; export const OTHER_OPTION = Symbol('OTHER_OPTION'); @AttachYCState(DashboardsState) @Injectable({ providedIn: 'root' }) export class DashboardsService extends BaseYCService { defaultGridsterConfig: GridsterConfig = { minCols: 12, minRows: 12, displayGrid: DisplayGrid.None, resizable: { enabled: false }, draggable: { enabled: false }, swap: true, swapWhileDragging: true, push: true }; canSeeBudgets = this.policyService.system.canManageBudgets() || this.policyService.insights.canViewBudgets(); canSeeSources = this.policyService.system.canManageFundingSources() || this.policyService.insights.canViewFundingSources(); private decimal = new DecimalPipe('en-US'); private percent = new PercentPipe('en-US'); constructor ( private logger: LogService, private arrayHelper: ArrayHelpersService, private dashboardsResources: DashboardsResources, private chartService: ChartService, private clientSettings: ClientSettingsService, private adHocService: AdHocReportingService, private adHocDefinitions: AdHocReportingDefinitions, private i18n: I18nService, private userService: UserService, private notifier: NotifierService, private adHocReportingMapper: AdHocReportingMappingService, private filterHelperService: FilterHelpersService, private currencyService: CurrencyService, private policyService: PolicyService, private fileService: FileService, private standardProductConfigService: StandardProductConfigurationService ) { super(); } get widgetDisplayMap () { return this.get('widgetDisplayMap'); } get isRootZone () { return this.clientSettings?.clientSettings?.isRootClient; } get allWidgets () { return this.get('allWidgets'); } get allDashboards () { return this.get('allDashboards'); } get visibleDashboards () { return this.get('visibleDashboards'); } get dashboardManagerRows () { return this.get('dashboardManagerRows'); } get dashboardDetails () { return this.get('dashboardDetails'); } get homeRoute () { return this.get('homeRoute') || '/management/home/my-workspace'; } get editing () { return this.get('editing'); } get editingDashId () { return this.get('editingDashId'); } get widgetEditMap () { return this.get('widgetEditMap'); } get lastRefreshMap () { return this.get('lastRefreshMap'); } get aggregationOptions () { return [{ value: AdHocReportingAPI.ChartAggregateType.Count, label: this.i18n.translate( 'GLOBAL:textCount', {}, 'Count' ) }, { value: AdHocReportingAPI.ChartAggregateType.Max, label: this.i18n.translate( 'GLOBAL:textMax', {}, 'Max' ) }, { value: AdHocReportingAPI.ChartAggregateType.Min, label: this.i18n.translate( 'GLOBAL:textMin', {}, 'Min' ) }, { value: AdHocReportingAPI.ChartAggregateType.Sum, label: this.i18n.translate( 'GLOBAL:textSum', {}, 'Sum' ) }]; } get typeSelectOptions (): TypeaheadSelectOption[] { return this.arrayHelper.sort([{ label: this.i18n.translate( 'common:textStat', {}, 'Stat' ), value: 'stat' }, { label: this.i18n.translate( 'FORMS:textTable', {}, 'Table' ), value: 'table' }, { label: this.i18n.translate( 'common:textPie', {}, 'Pie' ), value: 'pie' }, { label: this.i18n.translate( 'common:textBar', {}, 'Bar' ), value: 'bar' }, { label: this.i18n.translate( 'common:textLine', {}, 'Line' ), value: 'line' }], 'label'); } get legendLocationOptions () { return this.arrayHelper.sort([{ label: this.i18n.translate( 'common:textBottom', {}, 'Bottom' ), value: 'bottom' }, { label: this.i18n.translate( 'common:textTop', {}, 'Top' ), value: 'top' }, { label: this.i18n.translate( 'common:textLeft', {}, 'Left' ), value: 'left' }, { label: this.i18n.translate( 'common:textRight', {}, 'Right' ), value: 'right' }], 'label'); } get sortDirectionOptions () { return [{ label: this.i18n.translate( 'common:textAscendingLowercase', {}, 'ascending' ), value: true }, { label: this.i18n.translate( 'common:textDescendingLowercase', {}, 'descending' ), value: false }]; } get promptingForUnsavedChanges () { return this.get('promptingForUnsavedChanges'); } setWidgetEditMap (id: number, config: GCDashboards.WidgetConfigFromApi) { this.set('widgetEditMap', { ...this.get('widgetEditMap'), [id]: config }); } setAllWidgets (allWidgets: GCDashboards.SimpleWidgetConfig[]) { this.set('allWidgets', allWidgets); } setLastRefreshDate (id: number, date: string) { this.set('lastRefreshMap', { ...this.get('lastRefreshMap'), [id]: date }); } resetWidgetEditMap () { this.set('widgetEditMap', {}); } setEditing (editing: boolean, dashboardId: number) { this.set('editingDashId', dashboardId); this.set('editing', editing); } setPromptingForUnsavedChanges (value: boolean) { return this.set('promptingForUnsavedChanges', value); } cancelEditMode () { this.setEditing(false, null); this.resetWidgetEditMap(); } async resetAllWidgets () { this.setAllWidgets(undefined); await this.fetchWidgets(); } async fetchWidgets () { if (!this.allWidgets) { const widgets = await this.dashboardsResources.getAllWidgets(); const allWidgets = widgets.map((widget) => { return this.adaptToSimpleWidget(widget); }); this.setAllWidgets(allWidgets); } } adaptToSimpleWidget ( report: GCDashboards.SimpleWidgetConfigFromApi ): GCDashboards.SimpleWidgetConfig { const object = this.adHocService.getObjectByReportModelType(report.reportModelType); return { id: report.id, name: report.name, description: report.description, reportModelType: report.reportModelType, reportType: report.reportType, chartType: report.chartType, chartConfig: report.chartConfig, type: this.mapApiChartTypeToType(report.chartType), object }; } getDefaultWidgetFilters ( object: RootObjectNames, buckets: AdHocReportingUI.ColumnBucket[] ): AdvancedFilterGroup { const simpleColumn = this.getSimpleDefaultFilterColumn(object); let filters: ColumnFilterRow[] = []; const columns = this.adHocReportingMapper.mapSimpleColumnToColumnImplementation( [simpleColumn], buckets ); const column = columns[0]; if (column && column.filterColumn) { column.filterColumn.filters.push({ filterType: FilterModalTypes.Last365Days, filterValue: '' }); filters = this.adHocReportingMapper.mapColumnToTableFilter( column ); } return { useAnd: SwitchState.Toggled, filters }; } getSimpleDefaultFilterColumn (object: RootObjectNames) { let simpleColumn: GCDashboards.SimpleColumn; switch (object) { case 'application': case 'customForm': case 'applicationInKind': default: simpleColumn = { columnNameOverride: '', column: 'application.submittedDate' }; break; case 'award': case 'awardInKind': simpleColumn = { columnNameOverride: '', column: 'award.awardDate' }; break; case 'payment': case 'paymentInKind': simpleColumn = { columnNameOverride: '', column: 'payment.scheduledDate' }; break; } return simpleColumn; } extractGroupingColumns ( columns: AdHocReportingAPI.UserSavedReportColumn[], availableColumns: AdHocReportingUI.ColumnImplementation[] ): { groupColumn: AdHocReportingAPI.UserSavedReportColumn; subGroupColumn: AdHocReportingAPI.UserSavedReportColumn; } { let groupColumn: AdHocReportingAPI.UserSavedReportColumn; let subGroupColumn: AdHocReportingAPI.UserSavedReportColumn; columns.forEach((col) => { if (col.isChartGroupingColumn && !col.isChartSubGroupingColumn) { const exists = this.checkIfDefinitionExists(availableColumns, col.columnName); if (exists) { groupColumn = col; } } if (col.isChartSubGroupingColumn) { const exists = this.checkIfDefinitionExists(availableColumns, col.columnName); if (exists) { subGroupColumn = col; } } }); return { groupColumn, subGroupColumn }; } checkIfDefinitionExists ( availableColumns: AdHocReportingUI.ColumnImplementation[], columnNameToFind: string ): boolean { // this is to handle form components that are deleted const foundDef = availableColumns.find(({ definition }) => { const columnDefName = `${definition.parentBucket}.${definition.column}`; return columnNameToFind === columnDefName; }); return !!foundDef; } async adaptReportToChart ( report: GCDashboards.WidgetConfigFromApi ): Promise { const columns = this.arrayHelper.sort( report.userSavedReportColumns, 'sortPriority' ); const formIds = report.forms.map(form => form.id); const objectName = this.adHocService.getObjectByReportModelType(report.reportModelType); const object = this.adHocDefinitions[objectName]; const type = this.mapApiChartTypeToType(report.chartType); await this.adHocService.resolveFormInfo( formIds, object ); const columnImplementations = this.getColumnDefs( report.reportModelType, formIds, columns ); const columnDefs = columnImplementations.map(col => col.definition); const parsedConfig: GCDashboards.WidgetConfig = this.parseWidgetConfig(report); const sortColumn = report.userSavedReportColumns.find(reportColumn => { return reportColumn.sortType !== AdHocReportingAPI.SortTypes.NoSort; }); const { groupColumn, subGroupColumn } = this.extractGroupingColumns( report.userSavedReportColumns, columnImplementations ); const aggregateColumn = report.userSavedReportColumns.find(column => { return column.isChartAggregate; }); const drilldownColumns = this.mapUserSavedColToDrilldownCols(columns, columnImplementations); let summaryRecordId: number; // summary reports are intended for a single record // tables show multiple records if (object.supportsSummaryWidgets && type !== 'table') { const idUserSavedColumn = report.advancedFilterColumns .find(group => { return group.find(filter => { return filter.columnName === `${object.property}.id`; }); }); summaryRecordId = +idUserSavedColumn[0].filterValue; } const config = { ...parsedConfig, id: report.id, name: report.name, description: report.description, maxGroups: report.chartMaxRows, type, summaryRecordId, customForms: formIds.filter((formId) => formId !== report.primaryFormId), primaryFormId: report.primaryFormId, sortColumn: sortColumn ? sortColumn.columnName : null, sortAscending: sortColumn ? sortColumn.sortType === AdHocReportingAPI.SortTypes.Ascending : null, object: objectName, groupColumn: this.checkIfFormData( groupColumn, parsedConfig.groupColumn ), subGroupColumn: this.checkIfFormData( subGroupColumn, parsedConfig.subGroupColumn ), aggregateColumn: this.checkIfFormData( aggregateColumn, parsedConfig.aggregateColumn ), aggregationType: aggregateColumn ? aggregateColumn.chartAggregateType : null, filters: this.mapUserSavedColToColumnFilterRows(columns, columnImplementations), drilldownColumns, advancedFilters: this.adHocService.adaptAdvancedAPIFiltersToUIGroups( report.advancedFilterColumns || [], columnDefs ), useAnd: report.useLogicalOperatorAnd ? SwitchState.Toggled : SwitchState.Untoggled }; const additionalProps = Object.keys(object.additionalDashboardProperties || {}) .reduce((acc, key) => { const widgetProp = object.additionalDashboardProperties[key]; return { ...acc, [widgetProp]: report[key as keyof typeof report] }; }, {}); return { ...config, ...additionalProps }; } parseWidgetConfig (widgetFromAPI: GCDashboards.WidgetConfigFromApi): GCDashboards.WidgetConfig { return JSON.parse(widgetFromAPI.chartConfig) || {}; } mapUserSavedColToDrilldownCols ( columns: AdHocReportingAPI.UserSavedReportColumn[], columnDefs: AdHocReportingUI.ColumnImplementation[] ) { return columns.filter((col) => { // drilldown columns are represented by having a sortPriority // maybe we should be using "isVisible" instead? return !!col.sortPriority; }).map((col) => { return columnDefs.find((column) => { return col.columnName === `${column.definition.parentBucket}.${column.definition.column}`; }); }).filter((col) => !!col); } mapUserSavedColToColumnFilterRows ( columns: AdHocReportingAPI.UserSavedReportColumn[], columnDefs: AdHocReportingUI.ColumnImplementation[] ) { return columns.filter((col) => { return col.userSavedFilterColumns.length > 0; }).reduce[]>((acc, column) => { const found = columnDefs.find((col) => { return column.columnName === `${col.definition.parentBucket}.${col.definition.column}`; }); if (found) { found.filterColumn.filters = column.userSavedFilterColumns; return [ ...acc, ...this.adHocReportingMapper.mapColumnToTableFilter(found) ]; } return [ ...acc ]; }, []); } getColumnDefs ( reportModelType: AdHocReportingAPI.AdHocReportModelType, formIds: number[], userSavedReportColumns: AdHocReportingAPI.UserSavedReportColumn[] ) { const property: RootObjectNames = this.adHocService.getObjectByReportModelType( reportModelType ); const buckets = this.adHocService.getBuckets( property, formIds, [], AdHocReportingUI.Usage.DASHBOARDS ); const rootObject = this.adHocDefinitions[property]; const categoryBuckets = this.adHocReportingMapper.getCategoryBuckets( formIds, this.adHocService.formComponentMap, AdHocReportingUI.Usage.AD_HOC, rootObject ); const relatedObjects = rootObject.relatedObjects .map(objName => objName as RelatedObjectNames) .map((obj: RelatedObjectNames) => this.adHocDefinitions[obj]) .concat(categoryBuckets); return this.adHocReportingMapper.mapReportColumnToColumnImplementation( userSavedReportColumns, rootObject, relatedObjects, buckets ); } checkIfFormData ( column: AdHocReportingAPI.UserSavedReportColumn, parsedConfigColumn: string ) { return column ? (column.columnName.includes('formData') ? parsedConfigColumn : column.columnName) : null; } async getWidgetDetail (widgetId: number): Promise { const widget = await this.dashboardsResources.getWidgetDetail( widgetId ); this.set('widgetDetail', { ...this.get('widgetDetail') || {}, [widgetId]: widget }); return this.adaptReportToChart(widget); } mapTypeToApiChartType ( type: Dashboards.WidgetType ): AdHocReportingAPI.ChartType { switch (type) { case 'bar': default: return AdHocReportingAPI.ChartType.Bar; case 'pie': return AdHocReportingAPI.ChartType.Pie; case 'line': return AdHocReportingAPI.ChartType.Line; case 'table': return AdHocReportingAPI.ChartType.Table; case 'stat': return AdHocReportingAPI.ChartType.Stat; } } mapApiChartTypeToType ( type: AdHocReportingAPI.ChartType ): Dashboards.WidgetType { switch (type) { case AdHocReportingAPI.ChartType.Bar: default: return 'bar'; case AdHocReportingAPI.ChartType.Pie: return 'pie'; case AdHocReportingAPI.ChartType.Line: return 'line'; case AdHocReportingAPI.ChartType.Table: return 'table'; case AdHocReportingAPI.ChartType.Stat: return 'stat'; } } getShouldIncludeOther (type: AdHocReportingAPI.ChartType) { return [ AdHocReportingAPI.ChartType.Bar, AdHocReportingAPI.ChartType.Pie ].includes(type); } async saveWidgetGridUpdates ( widget: GCDashboards.WidgetConfigFromApi ) { await this.dashboardsResources.updateWidget({ ...widget, formIds: widget.forms.map((form) => form.id), reportType: AdHocReportingAPI.AdHocReportType.Chart, chartMaxRows: widget.chartMaxRows || DEFAULT_MAX_GROUPS, chartIncludeOtherAggregate: this.getShouldIncludeOther(widget.chartType) }); } async saveWidget ( widget: GCDashboards.WidgetConfig, dashboardId: number, isUpdate = false ) { try { const widgetConfig = this.adaptChartConfigForAPI(widget); const extraProps = this.getExtraPropsFromWidgetConfig(widget); const widgetForApi = { ...widgetConfig, ...extraProps }; const chartIncludeOtherAggregate = this.getShouldIncludeOther( widgetForApi.chartType ); if (isUpdate) { await this.dashboardsResources.updateWidget({ id: widget.id, chartIncludeOtherAggregate, chartMaxRows: widget.maxGroups, ...widgetForApi }); } else { await this.dashboardsResources.createWidget({ widget: widgetForApi, dashboardId }); } if (isUpdate && this.widgetEditMap[widget.id]) { // we already called update widget here, so no need to call it again in save dashboard edits this.setWidgetEditMap(widget.id, undefined); } await this.saveDashboardEdits(); await this.getDashboardDetail(dashboardId); await this.resetAllWidgets(); this.notifier.success(this.i18n.translate( 'common:notificationSuccessSavingWidget', {}, 'Successfully saved the widget' ) ); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:notificationErrorSavingWidget', {}, 'There was an error saving the widget' )); } } async removeWidget ( widgetId: number, dashboardId: number, keepAsTemplate: boolean ) { try { await this.saveDashboardEdits(); await this.dashboardsResources.removeWidget( widgetId, dashboardId, keepAsTemplate ); if (this.widgetEditMap[widgetId]) { this.setWidgetEditMap(widgetId, undefined); } this.notifier.success(this.i18n.translate( 'common:notificationSuccessRemovingWidget', {}, 'Successfully removed the widget' )); await this.resetAllWidgets(); await this.getDashboardDetail(dashboardId); } catch (e) { this.notifier.error(this.i18n.translate( 'common:notificationErrorRemovingWidget', {}, 'There was an error removing the widget' )); } } getSourceSelectOptions () { const hasNominations = (this.userService.get('user') as User)?.clientHasNominations; return this.arrayHelper.sort([{ label: this.i18n.translate( hasNominations ? 'common:lblApplicationNomination' : 'common:lblApplication', {}, hasNominations ? 'Application / Nomination' : 'Application' ), value: 'application' }, { label: this.i18n.translate( 'common:hdrInKindAmountRequested', {}, 'In kind amount requested' ), value: 'applicationInKind' }, { label: this.i18n.translate( 'common:lblAward', {}, 'Award' ), value: 'award' }, { label: this.i18n.translate( 'common:hdrInKindItemsAwarded', {}, 'In kind items awarded' ), value: 'awardInKind' }, { label: this.i18n.translate( 'common:lblCustomForms', {}, 'Custom forms' ), value: 'customForm' }, { label: this.i18n.translate( 'common:textFieldGroup', {}, 'Field group' ), value: 'fieldGroup' }, { label: this.i18n.translate( 'common:lblPayment', {}, 'Payment' ), value: 'payment' }, { label: this.i18n.translate( 'common:hdrInKindItemsPaid', {}, 'In kind items paid' ), value: 'paymentInKind' }, this.canSeeBudgets ? { label: this.i18n.translate( 'common:lblBudget', {}, 'Budget' ), value: 'budgets' } : undefined, this.canSeeSources ? { label: this.i18n.translate( 'BUDGET:lblFundingSource', {}, 'Funding Source' ), value: 'fundingSources' } : undefined].filter((item) => !!item), 'label'); } getSupportedBooleans ( type: Dashboards.WidgetType, object: RootObjectNames ): { aggregateSupported: boolean; groupingSupported: boolean; secondaryGroupingSupported: boolean; sortingSupported: boolean; drilldownSupported: boolean; legendSupported: boolean; axisLabelSupported: boolean; dataTabEnabled: boolean; } { const supportedBooleans = this.getSupportedBooleansByWidgetType(type); let { sortingSupported, groupingSupported, secondaryGroupingSupported, dataTabEnabled, aggregateSupported } = supportedBooleans; const { drilldownSupported, legendSupported, axisLabelSupported } = supportedBooleans; switch (object) { case 'fieldGroup': sortingSupported = false; groupingSupported = false; secondaryGroupingSupported = false; dataTabEnabled = false; aggregateSupported = false; break; } return { aggregateSupported, groupingSupported, sortingSupported, drilldownSupported, legendSupported, axisLabelSupported, secondaryGroupingSupported, dataTabEnabled }; } private getSupportedBooleansByWidgetType (type: string) { let aggregateSupported = true; let groupingSupported = true; let sortingSupported = true; let drilldownSupported = true; let legendSupported = true; let axisLabelSupported = true; let secondaryGroupingSupported = true; const dataTabEnabled = true; switch (type) { case 'bar': break; case 'line': break; case 'stat': sortingSupported = false; groupingSupported = false; secondaryGroupingSupported = false; drilldownSupported = true; legendSupported = false; axisLabelSupported = false; break; case 'table': default: aggregateSupported = false; groupingSupported = false; secondaryGroupingSupported = false; sortingSupported = false; drilldownSupported = false; legendSupported = false; axisLabelSupported = false; break; case 'pie': sortingSupported = false; axisLabelSupported = false; secondaryGroupingSupported = false; break; } return { sortingSupported, groupingSupported, secondaryGroupingSupported, dataTabEnabled, aggregateSupported, drilldownSupported, legendSupported, axisLabelSupported }; } determineColors (maxColors: number) { const { chartPrimary, chartSecondary, chartUtility, colorPalette } = this.clientSettings.clientBranding; const createShades = colorPalette === ColorPaletteType.SHADES; const mode = colorPalette === ColorPaletteType.STANDARD ? 'rgb' : 'hsl'; const colors = this.chartService.getFixedAmountOfColors( maxColors, [chartPrimary, chartSecondary, chartUtility], createShades, mode ); return colors; } async getChartData ( chartConfig: GCDashboards.WidgetConfig, refreshData: boolean, dashboardId?: number ): Promise { const def = this.adHocDefinitions[chartConfig.object]; if (def.supportsSummaryWidgets) { return this.getSummaryChartData(chartConfig); } const endpoint = this.adHocService.getEndpointByObjectName( chartConfig.object, 'chartEndpoint' ); const result = await this.dashboardsResources.getChartData( endpoint, chartConfig.id, refreshData ); this.setLastRefreshDate(dashboardId, result.lastRefreshDate); return this.mapResultToChartData(chartConfig, result); } mapResultToChartData ( chartConfig: GCDashboards.WidgetConfig, results: GCDashboards.DashboardDataResults ): GCDashboards.ChartDataReturn { const chartDataResult = results.results; const formIds = this.getFormIds(chartConfig); const buckets = this.adHocService.getBuckets( chartConfig.object, formIds, [], AdHocReportingUI.Usage.DASHBOARDS ); const allColumns = buckets.reduce((acc, bucket) => { return [ ...acc, ...bucket.allColumns ]; }, [] as AdHocReportingUI.ColumnImplementation[]); const groupColumn = this.getGroupColumnDef( chartConfig.groupColumn, allColumns ); const subGroupColumn = this.getGroupColumnDef( chartConfig.subGroupColumn, allColumns ); const isFormGrouping = chartConfig.groupColumn ? chartConfig.groupColumn.startsWith('formData') : false; const isFormSubGrouping = chartConfig.subGroupColumn ? chartConfig.subGroupColumn.startsWith('formData') : false; const isRefFieldGrouping = chartConfig.groupColumn ? chartConfig.groupColumn.startsWith('category.') : false; const isRefFieldSubGrouping = chartConfig.subGroupColumn ? chartConfig.subGroupColumn.startsWith('category.') : false; const groupKey = groupColumn ? chartConfig.groupColumn.split('.') : []; const subGroupKey = subGroupColumn ? chartConfig.subGroupColumn.split('.') : []; let mappedChartData = chartDataResult.map((result) => { const { extractedValue, displayValue } = this.getValues( isFormGrouping, result, groupKey, groupColumn, 'formGroupingAttribute1', isRefFieldGrouping, 'referenceFieldGroupingAttribute1' ); let subDisplayValue: string; let subExtractedValue: string; if (subGroupColumn) { const response = this.getValues( isFormSubGrouping, result, subGroupKey, subGroupColumn, 'formGroupingAttribute2', isRefFieldSubGrouping, 'referenceFieldGroupingAttribute2' ); subDisplayValue = response.displayValue; subExtractedValue = response.extractedValue; } return { label: displayValue, subLabel: subDisplayValue, subValue: subExtractedValue, xValue: extractedValue, yValue: result.aggregate, count: result.count }; }).filter((result) => { if (chartConfig.type === 'line') { return result.xValue !== null && !isUndefined(result.xValue); } return result; }); if (mappedChartData.length === 0) { mappedChartData = [{ subLabel: null, subValue: null, label: this.i18n.translate( 'common:textNone', {}, 'None' ), xValue: '', yValue: 0, count: 0 }]; } if ( !subGroupColumn && this.getShouldIncludeOther(this.mapTypeToApiChartType(chartConfig.type)) ) { mappedChartData = this.addOtherAggregate( mappedChartData, results.otherAggregate, results.otherCount ); } const colors = this.determineColors(mappedChartData.length); if (chartConfig.subGroupColumn) { return this.getSubGroupedChartData(mappedChartData, chartConfig); } else { const needToSort = chartConfig.sortColumn === chartConfig.groupColumn; let sortedData = mappedChartData; if (needToSort) { sortedData = this.arrayHelper.sort(mappedChartData, 'label', !chartConfig.sortAscending); const otherOption = sortedData.find(row => { return row.xValue === OTHER_OPTION; }); if (otherOption) { sortedData = [ ...sortedData.filter(row => row.xValue !== OTHER_OPTION), otherOption ]; } } return this.getGroupedChartData(chartConfig, sortedData, colors); } } getGroupColumnDef ( columnName: string, allColumns: AdHocReportingUI.ColumnImplementation[] = [] ) { if (columnName) { const foundGroup = allColumns.find(({ definition }) => { return columnName.startsWith(definition.parentBucket) && columnName.endsWith(definition.column); }); return foundGroup?.definition ?? undefined; } return undefined; } getValues ( isFormGrouping: boolean, result: GCDashboards.ChartResult, groupKey: string[], groupColumn: AdHocReportingUI.ColumnDefinition, formGroupProp: 'formGroupingAttribute1'|'formGroupingAttribute2', isRefFieldGrouping: boolean, refFieldGroupProp: 'referenceFieldGroupingAttribute1'|'referenceFieldGroupingAttribute2' ) { let extractedValue = get(result.data, [groupKey[0], groupKey[1]]) as string; if (isFormGrouping) { extractedValue = result.data[formGroupProp] as string; } else if (isRefFieldGrouping) { extractedValue = result.data[refFieldGroupProp] as string; } // extract the label of the grouping column value let displayValue = extractedValue; if (groupColumn) { const adaptedRow = this.adHocReportingMapper.addReferenceFieldMapToReportingRow( result.data ); displayValue = this.adHocReportingMapper.getFormattedDisplayValue( displayValue, groupColumn, adaptedRow, true ); } else { const noValue = isUndefined(displayValue) || displayValue === null; if (noValue) { displayValue = this.i18n.translate('common:textNone', {}, 'None'); } } return { extractedValue, displayValue }; } getGroupedChartData ( chartConfig: GCDashboards.ChartConfig, mappedChartData: GCDashboards.ChartDataResult[], colors: string[] ): GCDashboards.ChartDataReturn { const data = chartConfig.type === 'bar' ? mappedChartData.map((result, index) => { // bar charts require that the color and label be passed in with each data point return { backgroundColor: colors[index], hoverBackgroundColor: this.chartService.shadeColor(colors[index], .5), label: result.label, value: null, formats: [], data: [result.yValue], counts: [result.count] }; }) : [{ backgroundColor: colors, hoverBackgroundColor: colors.map((_color) => { return this.chartService.shadeColor(_color, .50); }), // set fill to false if it's a line chart, this prevents the space below the line from being a color fill: chartConfig.type !== 'line', // if we are using a line chart, then we want the legend to show the x axis label // otherwise use the labels of each data point in the legend label: chartConfig.type === 'line' ? chartConfig.xAxisLabel : undefined, value: null, formats: [], borderColor: colors[0], counts: mappedChartData.map((result) => { return result.count; }), data: mappedChartData.map((result) => { return result.yValue; }) }]; return { data, // we do this so when the user drills into a bar // we can access what the filter for the drilldown will actually be // program id vs program name labels: mappedChartData.map((val, index) => { return { color: colors[index], value: val.xValue, display: val.label }; }) }; } getSubGroupedChartData ( mappedChartData: GCDashboards.ChartDataResult[], chartConfig: GCDashboards.ChartConfig ): GCDashboards.ChartDataReturn { const orientedColumnList = this.getSortedColumnList( mappedChartData, chartConfig.aggregateColumn, chartConfig.sortColumn, chartConfig.sortAscending, chartConfig.type ); const orientedSubColumns = mappedChartData.reduce((acc, result) => { if (!!result.subLabel) { return { ...acc, [result.subLabel]: [ ...(acc[result.subLabel] || []), result ] }; } return acc; }, {} as Record); const subColumnValues = Object.keys(orientedSubColumns); const groupColors = this.determineColors(subColumnValues.length); const data = Object.keys(orientedSubColumns) .map((result, index) => { const foundResults = orientedColumnList.map((orientedColumn) => { return orientedColumn.records.find((record) => record.subLabel === result); }); const subData = foundResults.map((foundResult) => { return foundResult ? foundResult.yValue : chartConfig.type === 'line' ? 0 : null; }); const counts = foundResults.map((foundResult) => foundResult?.count ?? 0); const baseDataSet: GCDashboards.ChartDataSet = { label: result, formats: [], value: orientedSubColumns[result][0].subValue, data: subData, counts }; const color = groupColors[index]; const hoverColor = this.chartService.shadeColor(color, .50); switch (chartConfig.type) { case 'line': return { ...baseDataSet, pointBackgroundColor: color, pointBorderColor: '#FFF', pointHoverBorderColor: hoverColor, pointHoverBackgroundColor: hoverColor, borderColor: color, hoverBorderColor: hoverColor, fill: false }; // tslint:disable-next-line:no-switch-case-fall-through case 'bar': return { ...baseDataSet, backgroundColor: color, hoverBackgroundColor: hoverColor }; } return baseDataSet; }); const returnVal = { data, labels: orientedColumnList.map((result) => { const firstChunk = result.records[0]; return { display: firstChunk.label, value: firstChunk.xValue, color: null }; }) }; return returnVal; } getSortedColumnList ( mappedChartData: GCDashboards.ChartDataResult[], aggregateColumn: string, sortColumn: string, sortAscending: boolean, type: Dashboards.WidgetType ) { let orientedColumnList = mappedChartData.reduce((acc, result) => { // look for a record set for this label let previous = acc.find(potentialResult => { return potentialResult.label === result.label; }); // if no record set was found, create a new one and add to accumulator if (!previous) { previous = { records: [], label: result.label, recordSum: 0 }; acc = [ ...acc, previous ]; } // add this record to the record set previous.records.push(result); // add this result's value to the sum of the record set previous.recordSum += result.yValue; return acc; }, [] as { records: GCDashboards.ChartDataResult[]; recordSum: number; label: string }[]); const sortOnAggregate = sortColumn === aggregateColumn; const { sortingSupported } = this.getSupportedBooleansByWidgetType(type); if (sortingSupported) { orientedColumnList = this.arrayHelper.sort( orientedColumnList, sortOnAggregate ? 'recordSum' : 'label', !sortAscending ); } return orientedColumnList; } addOtherAggregate ( chartDataResult: GCDashboards.ChartDataResult[], otherAggregate: number, otherCount: number ) { if (otherAggregate && otherAggregate > 0) { const other: GCDashboards.ChartDataResult = { label: this.i18n.translate('common:textOther', {}, 'Other'), subLabel: null, subValue: null, xValue: OTHER_OPTION, yValue: otherAggregate, count: otherCount }; return [ ...chartDataResult, other ]; } return chartDataResult; } getHeaderFromClickEvent (event: Dashboards.ChartClickEvent) { let header = event.groupDisplay; if (!!event.chart.subGroupColumn) { header += ' - ' + event.subGroupDisplay; } return header; } mapGCChartToCommon ( chart: GCDashboards.WidgetConfig, chartData: GCDashboards.ChartDataReturn = { data: [], labels: [] }, buckets: AdHocReportingUI.ColumnBucket[] ): GCDashboards.Widget { const isTableOrStat = chart.type === 'table' || chart.type === 'stat'; const isCurrency = this.shouldFormatAsCurrency( chart.aggregateColumn, buckets ); const def = this.adHocDefinitions[chart.object]; const isPie = chart.type === 'pie'; const showCount = (chart.aggregationType !== AdHocReportingAPI.ChartAggregateType.Count && !isTableOrStat) || def.supportsSummaryWidgets; const emitter = new EventEmitter(); const colors: Color[] = !!chart.subGroupColumn ? [] : chart.type !== 'bar' ? chartData.labels[0] ? [chartData.labels[0].color as Color] : [] : chartData.labels.map(l => l.color as Color); const conf: GCDashboards.Widget = { id: chart.id, name: chart.name, description: chart.description, object: chart.object, chartData: chartData.data, chartType: chart.type, onElementClick: emitter, formatAsCurrency: isCurrency, rowsPerPage: chart.rowsPerPage || DEFAULT_ROWS_PER_PAGE, chartOptions: { maintainAspectRatio: false, onClick (event) { // if user clicked on a bar/slice (not other places on the chart) // AND the chart has at least one drilldown column // AND is not a summary chart // we can drill into it const foundChart = Chart.getChart(event.native.target as HTMLCanvasElement); const els = foundChart.getElementsAtEventForMode(event.native, 'nearest', { intersect: true }, true); const [el] = els; if (el && chart.drilldownColumns.length && !def.supportsSummaryWidgets) { const index = chart.type === 'bar' && !chart.subGroupColumn ? el.datasetIndex : el.index; const dataSet: GCDashboards.ChartDataSet = foundChart.data.datasets[ el.datasetIndex ] as GCDashboards.ChartDataSet; emitter.emit({ values: chartData.labels, chart, groupDisplay: chartData.labels[index].display, groupValue: chartData.labels[index].value, subGroupDisplay: dataSet.label, subGroupValue: dataSet.value }); } }, plugins: { title: { display: false, text: chart.name, font: { size: 14, // color: '#4a4d50', family: '"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif' } }, legend: { position: chart.legendLocation }, tooltip: { callbacks: { title ([tooltipItem]: TooltipItem[]) { if (chart.subGroupColumn) { return tooltipItem.label; } return tooltipItem.chart.data.datasets[tooltipItem.datasetIndex].label ?? tooltipItem.label; }, label (tooltipItem: TooltipItem) { if (chart.subGroupColumn) { return tooltipItem.chart.data.datasets[tooltipItem.datasetIndex].label; } const dataset = tooltipItem.dataset; return dataset.data[tooltipItem.dataIndex].toString(); }, afterLabel: (tooltipItem: TooltipItem) => { if (!chart.subGroupColumn) { return ''; } const data = tooltipItem.chart.data; const dataset = data.datasets[tooltipItem.datasetIndex] as GCDashboards.ChartDataSet; const count = dataset.counts[tooltipItem.dataIndex]; const format = dataset.formats[tooltipItem.dataIndex]; const totalCount = dataset.counts.reduce((acc, val) => { return acc + val; }, 0); const totalValue = (dataset.data as number[]).reduce((acc, val) => { return acc + val; }, 0); const r = this.formatTooltipValue({ isPie, isDollarAmount: format === 'currency', isPercent: format === 'percent', tooltipItem, data, count, showCount, totalCount, totalValue }); return r; } } } }, scales: { yAxes: { display: chart.type !== 'pie', stacked: chart.type === 'bar' && !!chart.subGroupColumn, title: { text: chart.yAxisLabel, display: chart.type !== 'pie' }, beginAtZero: true, ticks: { callback: (value: any) => { return this.formatYAxisValue(isPie, isCurrency, value); } } }, xAxes: { display: chart.type !== 'pie', stacked: chart.type === 'bar' && !!chart.subGroupColumn, title: { text: chart.xAxisLabel, display: chart.type !== 'pie' }, beginAtZero: true } } }, grid: this.convertChartToGridConfig(chart, chart.id), // bar charts have a label in each data point labels: chart.type !== 'bar' || !!chart.subGroupColumn ? chartData.labels.map(v => v.display) : [''], // bar charts need the colors passed in here, other charts have the color on each data point colors, filters: chart.filters, drilldownColumns: chart.drilldownColumns, tableDataFactory: isTableOrStat ? this.getTableDataFactory(chart) : null, aggregationType: chart.aggregationType }; return conf; } convertChartToGridConfig ( chart: GCDashboards.WidgetConfig, widgetId: number ): GridsterItem { return { x: chart.x, y: chart.y, rows: chart.height, cols: chart.width, minItemRows: chart.type === 'stat' ? 1 : 2, minItemCols: chart.type === 'stat' ? 1 : 2, id: widgetId }; } getSkeletonConfigByType ( widgetConfig: GCDashboards.WidgetConfig ): SkeletonDisplayConfig { switch (widgetConfig.type) { case 'table': return GCDashboards.DashboardTableSkeleton; case 'stat': return GCDashboards.StatSkeleton; case 'bar': case 'line': case 'pie': default: return GCDashboards.BarChartSkeleton; } } formatYAxisValue ( isPie: boolean, isDollarAmount: boolean, value: any ) { if (!isPie) { return isDollarAmount ? this.currencyService.formatMoney(value) : this.decimal.transform( value ); } return ''; } formatTooltipValue (payload: AdHocReportingUI.FormatTooltipValuePayload) { let percentString = ''; const value = payload.isPie ? +payload.data.datasets[0].data[payload.tooltipItem.dataIndex] as number : +payload.tooltipItem.raw; const decimalVal = payload.isDollarAmount ? this.currencyService.formatMoney(+value) : payload.isPercent ? this.percent.transform(value) : this.decimal.transform(value); if (payload.isPie) { if (!payload.showCount) { percentString = `${(payload.count / payload.totalCount * 100).toFixed(2)}%`; } else { percentString = `${(value / payload.totalValue * 100).toFixed(2)}%`; } } const hasCountAndShowCount = !!(payload.showCount && payload.count); const countString = decimalVal + ' (' + payload.count + ')'; const returnVal = (hasCountAndShowCount ? countString : decimalVal) + (percentString ? ` (${percentString})` : ''); return returnVal; } mapToUserSavedReportColumnsForAggregation ( chartConfig: GCDashboards.WidgetConfig ): AdHocReportingAPI.UserSavedReportColumn[] { // make sure that whatever the backend needs from chartConfig is adapted into the report column model const { groupColumn, aggregateColumn, subGroupColumn } = chartConfig; const advancedFilters = chartConfig.advancedFilters.reduce((acc, group) => ([ ...acc, ...group.filters ]), []); // backend is treating group, aggregate, and subgroup columns as report columns so this is de-duplicating (if you pick the same column for group/agg and filter they show up twice) const initialGroupedColumns = (chartConfig.filters || []) .reduce>((acc, filter) => { const columnName = filter.column.prop; return { ...acc, [columnName]: [ ...(acc[columnName] || []), { ...filter, applyToFilters: true } ] }; }, {}); const groupedColumns = advancedFilters .reduce>((acc, filter) => { const columnName = filter.column.prop ?? filter.column.columnName; return { ...acc, [columnName]: [ ...(acc[columnName] || []), { ...filter, // advanced filters are stored separately on the widget applyToFilters: false } ] }; }, initialGroupedColumns); // make sure all drilldown columns are stored with charts chartConfig.drilldownColumns.forEach(drilldownColumn => { const key = drilldownColumn.definition.parentBucket + '.' + drilldownColumn.definition.column; groupedColumns[key] = groupedColumns[key] || []; }); if ( chartConfig.groupColumn && !groupedColumns[chartConfig.groupColumn] ) { groupedColumns[chartConfig.groupColumn] = []; } if ( chartConfig.subGroupColumn && !groupedColumns[chartConfig.subGroupColumn] ) { groupedColumns[chartConfig.subGroupColumn] = []; } if ( chartConfig.aggregateColumn && !groupedColumns[chartConfig.aggregateColumn] ) { groupedColumns[chartConfig.aggregateColumn] = []; } // check to see if report exists, are we updating or creating? const existingReport = this.get('widgetDetail')[chartConfig.id]; const columnIdMap = existingReport ? existingReport.userSavedReportColumns .reduce>((acc, column) => ({ ...acc, [column.columnName]: column.id }), {}) : {}; const columns = Object.keys(groupedColumns); const essentialColumns = [ aggregateColumn, groupColumn, subGroupColumn ]; const arrangedColumns = [ ...essentialColumns.filter((column, index) => { return !!column && (essentialColumns.indexOf(column) === index); }), // put at the beginning of array, dedupe, and remove nulls ...columns.filter(col => { return ![groupColumn, subGroupColumn, aggregateColumn].includes(col); }) ]; if (!chartConfig.sortColumn) { chartConfig.sortColumn = chartConfig.aggregateColumn; chartConfig.sortAscending = false; } return arrangedColumns .map((columnName) => { // determine what type of columns we have const isAggregate = chartConfig.aggregateColumn === columnName; const isGroupColumn = chartConfig.groupColumn === columnName; const isSubGroupColumn = chartConfig.subGroupColumn === columnName; const isSortColumn = chartConfig.sortColumn === columnName; const drilldownIndex = chartConfig.drilldownColumns.findIndex(column => { return `${column.definition.parentBucket}.${column.definition.column}` === columnName; }); const isDrilldownColumn = drilldownIndex !== -1; const columnNameForApi = this.adHocReportingMapper.adaptFormColumnNameForApi( columnName ); const refField = this.adHocReportingMapper.getRefFieldByColumnName(columnName); const referenceFieldId = refField?.referenceFieldId; // columns are in the right order and filtered down return { referenceFieldId, chartAggregateType: isAggregate ? chartConfig.aggregationType : null, columnName: columnNameForApi, id: columnIdMap[columnName], isChartAggregate: isAggregate, isChartGroupingColumn: isGroupColumn || isSubGroupColumn, isChartSubGroupingColumn: isSubGroupColumn, displayName: isDrilldownColumn ? chartConfig.drilldownColumns[drilldownIndex].columnNameOverride : '', sortPriority: isDrilldownColumn ? drilldownIndex + 1 : null, sortType: isSortColumn ? chartConfig.sortAscending ? AdHocReportingAPI.SortTypes.Ascending : AdHocReportingAPI.SortTypes.Descending : AdHocReportingAPI.SortTypes.NoSort, // API won't return data for columns that aren't sent // have them return if column is a part of drilldown isVisible: isDrilldownColumn, userSavedFilterColumns: groupedColumns[columnName] .filter((column) => column.applyToFilters) .reduce((acc, column) => { if (column.filter.api === FilterModalTypes.between) { column = { ...column, value: column.value.join(this.adHocReportingMapper.filterValueSplit) }; } const value: any[] = (column.value instanceof Array) ? column.value : [column.value]; return [ ...acc, ...value.map(subValue => { return { filterType: column.filter.api, filterValue: subValue === null ? '' : subValue }; })]; }, []) }; }); } mapToUserSavedReportColumnsForTables ( chartConfig: GCDashboards.WidgetConfig ): AdHocReportingAPI.UserSavedReportColumn[] { const columns = chartConfig.drilldownColumns; let userSavedReportCols = this.adHocReportingMapper.mapColumnsToApi( columns ); if (chartConfig.sortColumn) { userSavedReportCols.forEach((column) => { if (column.columnName === chartConfig.sortColumn) { column.sortType = chartConfig.sortAscending ? AdHocReportingAPI.SortTypes.Ascending : AdHocReportingAPI.SortTypes.Descending; } else { column.sortType = AdHocReportingAPI.SortTypes.NoSort; } }); } const filters = this.mapFilterColumns(chartConfig.filters); userSavedReportCols = this.mapFilterColumnsToUserSavedReportCols( filters, userSavedReportCols, true ); const advancedFilters = this.mapAdvancedFilterGroups(chartConfig.advancedFilters); userSavedReportCols = this.mapFilterColumnsToUserSavedReportCols( advancedFilters, userSavedReportCols, false ); return userSavedReportCols; } mapFilterColumnsToUserSavedReportCols ( filters: FilterColumn[], userSavedReportCols: AdHocReportingAPI.UserSavedReportColumn[], applyFilters: boolean ) { filters.forEach((filter) => { if (filter.columnName.includes('category.')) { filter.columnName = this.adHocReportingMapper.adaptFormColumnNameForApi(filter.columnName); } const foundIndex = userSavedReportCols.findIndex((col) => { return col.columnName === filter.columnName; }); if (foundIndex !== -1) { userSavedReportCols = [ ...userSavedReportCols.slice(0, foundIndex), { ...userSavedReportCols[foundIndex], userSavedFilterColumns: applyFilters ? this.mapToUserSavedFilterCol(filter) : [] }, ...userSavedReportCols.slice(foundIndex + 1) ]; } else { const refField = this.adHocReportingMapper.getRefFieldByColumnName(filter.columnName); userSavedReportCols = [ ...userSavedReportCols, { columnName: filter.columnName, sortType: AdHocReportingAPI.SortTypes.NoSort, sortPriority: null, userSavedFilterColumns: applyFilters ? this.mapToUserSavedFilterCol(filter) : [], displayName: '', isVisible: true, isChartGroupingColumn: false, isChartSubGroupingColumn: false, isChartAggregate: false, referenceFieldId: refField?.referenceFieldId, chartAggregateType: null } ]; } }); return userSavedReportCols; } mapToUserSavedFilterCol (filter: FilterColumn) { return filter.filters.map((f) => { return { filterType: f.filterType, filterValue: f.filterValue }; }); } setAdvancedFilterForRecordSummary ( chartConfig: GCDashboards.WidgetConfig ): AdHocReportingAPI.AdvancedUserSavedFilterColumn[] { const object = this.adHocDefinitions[chartConfig.object]; const columnName = `${object.property}.id`; const idColumn: AdHocReportingAPI.AdvancedUserSavedFilterColumn = { columnName, filterType: 'eq', filterValue: chartConfig.summaryRecordId, filterValues: [], useLogicalOperatorAnd: false }; return [ idColumn ]; } mapToUserSavedReportColumnsForSummary ( chartConfig: GCDashboards.WidgetConfig, userSavedReportColumns: AdHocReportingAPI.UserSavedReportColumn[] ): AdHocReportingAPI.UserSavedReportColumn[] { const def = this.adHocDefinitions[chartConfig.object]; const columnName = `${def.property}.id`; const idColumn: AdHocReportingAPI.UserSavedReportColumn = userSavedReportColumns.find(col => { return col.columnName === columnName; }) ?? { columnName, userSavedFilterColumns: [], chartAggregateType: null, isChartAggregate: false, isChartGroupingColumn: false, isChartSubGroupingColumn: false, sortPriority: null, sortType: AdHocReportingAPI.SortTypes.NoSort, referenceFieldId: null, isVisible: false, displayName: '' }; return [ ...userSavedReportColumns, idColumn ]; } adaptChartConfigForAPI ( chartConfig: GCDashboards.WidgetConfig ): AdHocReportingAPI.CreateChartPayload { const object = this.adHocDefinitions[chartConfig.object]; const formIds = this.getFormIds(chartConfig); // map the report for the table endpoint // if the chart is a table // or is a summary chart (e.g. pie chart representing a single record versus multiple) const isTable = chartConfig.type === 'table' || object.supportsSummaryWidgets; let userSavedReportColumns: AdHocReportingAPI.UserSavedReportColumn[]; if (isTable) { userSavedReportColumns = this.mapToUserSavedReportColumnsForTables( chartConfig ); } else { userSavedReportColumns = this.mapToUserSavedReportColumnsForAggregation( chartConfig ); } let advancedFilterColumns = this.adHocService.adaptAdvancedUIFiltersToReportAPI(chartConfig.advancedFilters); // make sure the chart is only returning the one record // that the summary is for if (object.supportsSummaryWidgets && chartConfig.type !== 'table') { advancedFilterColumns = [this.setAdvancedFilterForRecordSummary(chartConfig)]; userSavedReportColumns = this.mapToUserSavedReportColumnsForSummary( chartConfig, userSavedReportColumns ); } const chartType = this.mapTypeToApiChartType(chartConfig.type); const payload: AdHocReportingAPI.CreateChartPayload = { reportType: AdHocReportingAPI.AdHocReportType.Chart, name: chartConfig.name, description: chartConfig.description, advancedFilterColumns, useLogicalOperatorAnd: chartConfig.useAnd === SwitchState.Toggled, reportModelType: object.type, chartType, chartConfig: JSON.stringify({ ...chartConfig, drilldownColumns: undefined, filters: undefined, advancedFilters: undefined, useAnd: undefined }), formIds, primaryFormId: chartConfig.primaryFormId, reportUsers: [], referenceFieldTableId: null, userSavedReportColumns: userSavedReportColumns.map((col) => { return { ...col, columnName: this.adHocReportingMapper.adaptFormColumnNameForApi( col.columnName ) }; }), chartMaxRows: chartConfig.maxGroups || DEFAULT_MAX_GROUPS, chartIncludeOtherAggregate: !chartConfig.subGroupColumn && this.getShouldIncludeOther(chartType) }; return payload; } adHocColumnToFilterColumn ( columns: AdHocReportingUI.ColumnDefinition[] ): Column[] { return this.arrayHelper.sort( columns.map(column => { const columnDisplay = column.display; return { label: `${columnDisplay} (${column.parentBucketName})`, htmlLabel: this.getHtmlColumnLabel(columnDisplay, column.parentBucketName), option: this.getHtmlOptionLabel(columnDisplay, column.parentBucketName), groupBy: column.parentBucketName, options: ('filterOptions' in column ? (column.filterOptions as (SelectOption|TypeaheadSelectOption)[]).map((opt: SelectOption|TypeaheadSelectOption) => { return { label: opt.label, display: opt.label, value: opt.value, hidden: 'hidden' in opt ? opt.hidden : undefined }; }) : []), prop: `${column.parentBucket}.${column.column}`, type: column.type, visible: true, filterOnly: false, labelOnly: true }; }), 'label' ); } getHtmlColumnLabel (columnDisplay: string, parentBucketName: string) { return `
${columnDisplay} ${parentBucketName}
`; } getHtmlOptionLabel (columnDisplay: string, parentBucketName: string) { return `
${columnDisplay} ${parentBucketName}
`; } async resetMyDashboards () { this.set('allDashboards', undefined); this.set('visibleDashboards', undefined); await this.setMyDashboards(); } async fetchWidgetDetailForDashboard ( widget: GCDashboards.WidgetConfigFromApi, dashboardId: number, isPreview: boolean, isRefresh: boolean ) { try { const widgetConfig = await this.adaptReportToChart(widget); const buckets = this.adHocService.getBuckets( widgetConfig.object, this.getFormIds(widgetConfig), [], AdHocReportingUI.Usage.DASHBOARDS ); const adapted = await this.mapToChartWidget( widgetConfig, buckets, isPreview, isRefresh, dashboardId ); this.setWidgetDisplay(adapted.id, adapted); return adapted; } catch (e) { this.logger.error(e); throw e; } } async getDashboardDetail (dashboardId: number) { const dashboard = await this.dashboardsResources.getDashboardDetail(dashboardId); const oldWidgets = cloneDeep(dashboard.widgets); this.set('dashboardDetails', { ...this.get('dashboardDetails'), [dashboardId]: { ...dashboard, oldWidgets, widgets: dashboard.widgets } }); } async setMyDashboards () { // here we need to also fetch the standard product dbs and add them to the list shown in db manager const fetchNeeded = !this.allDashboards || !this.visibleDashboards || !this.standardProductConfigService.standardProductDashboards; if (fetchNeeded) { await this.standardProductConfigService.setStandardProductDashboardTemplates(); const standardProductDashboards = this.standardProductConfigService.standardProductDashboards; const tabs = await this.dashboardsResources.getDashboardTabs(); tabs.forEach((tab) => { switch (tab.dashboardType) { case GCDashboards.DashboardTypes.CUSTOM: default: break; case GCDashboards.DashboardTypes.MY_WORKSPACE: tab.aliasRoute = 'my-workspace'; if (tab.name === 'My Workspace') { // Translate if they haven't renamed tab.name = this.i18n.translate( 'common:hdrMyWorkspace', {}, 'My Workspace' ); } break; } // we need to mark whether // a dashboard is a standard product dashboard and has // been published or not if (standardProductDashboards) { const standardProductDBIds = standardProductDashboards.map((spdb => spdb.dashboardId)); tab.isStandardDashboardPublished = standardProductDBIds.includes(tab.dashboardId); } }); this.set('allDashboards', tabs); this.set('visibleDashboards', tabs.filter(tab => { return !tab.isHidden; })); const firstDash = this.arrayHelper.sort(this.visibleDashboards, 'order')[0]; if (firstDash) { this.set('homeRoute', `/management/home/${ firstDash.aliasRoute || firstDash.dashboardId }`); } else { this.set('homeRoute', '/management/home/my-workspace'); } const standardProductDBs = standardProductDashboards ? standardProductDashboards : []; const dashboardManagerRows = this.isRootZone ? [ ...tabs ] : [ ...standardProductDBs, ...tabs ]; const sortedRows = this.arrayHelper.sort( dashboardManagerRows, 'name' ); this.set( 'dashboardManagerRows', sortedRows ); } } async handleCreateDashboard (name: string) { try { const id = await this.dashboardsResources.createDashboard( name, this.allDashboards.length + 1 ); await this.resetMyDashboards(); this.notifier.success(this.i18n.translate( 'DASHBOARD:txtSuccessCreateDashboard', {}, 'Successfully created the dashboard.' )); return id; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'DASHBOARD:textErrorCreatingDashboard', {}, 'There was an error creating the dashboard' )); return null; } } async handleRenameDashboard ( dashboardId: number, name: string, order: number ) { try { await this.dashboardsResources.updateDashboardTab( dashboardId, name, order ); await this.resetMyDashboards(); this.notifier.success(this.i18n.translate( 'DASHBOARD:txtSuccessRenameDashboard', {}, 'Successfully renamed dashboard.' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'DASHBOARD:textErrorRenameDashboard', {}, 'Error renaming dashboard' )); } } async handleUpdateTabOrder ( dashboardId: number, name: string, order: number, isDashboardOwner: boolean ) { try { if (isDashboardOwner) { await this.dashboardsResources.updateDashboardTab( dashboardId, name, order ); } else { await this.dashboardsResources.moveLockedDashboard({ dashboardId, order }); } await this.resetMyDashboards(); this.notifier.success(this.i18n.translate( 'DASHBOARD:txtSuccessUpdateTabOrder', {}, 'Successfully updated tab order.' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'DASHBOARD:textErrorUpdatingTabOrder', {}, 'Error updating tab order' )); } } async mapToChartWidget ( chartConfig: GCDashboards.WidgetConfig, buckets: AdHocReportingUI.ColumnBucket[], isPreview: boolean, refreshData: boolean, dashboardId?: number ) { let chartData: GCDashboards.ChartDataReturn; if (chartConfig.type !== 'table') { chartData = isPreview ? await this.returnWidgetPreview(chartConfig) : await this.getChartData(chartConfig, refreshData, dashboardId); } return this.mapGCChartToCommon(chartConfig, chartData, buckets); } shouldFormatAsCurrency ( aggregateColumn: string, buckets: AdHocReportingUI.ColumnBucket[] ): boolean { const allColumns = buckets.reduce((acc, bucket) => { return [ ...acc, ...bucket.allColumns ]; }, [] as AdHocReportingUI.ColumnImplementation[]); const foundCol = allColumns.find((col) => { return aggregateColumn === `${ col.definition.parentBucket}.${col.definition.column }`; }); if (foundCol) { return foundCol.definition.type === 'currency'; } return false; } // Makes sure that the pagination options has a filter for the drilled in data point alignPaginationOptionsForDrilldown ( chartConfig: GCDashboards.WidgetConfig, paginationOptions: PaginationOptions, drilldownValue: Dashboards.ChartClickEvent ) { const clickedValue = drilldownValue.groupValue; const foundColumn = this.findAndAddPaginationGroupColumn( paginationOptions, chartConfig.groupColumn ); if (clickedValue === OTHER_OPTION) { const filterColumnsToAdd = drilldownValue.values.filter((value) => { return value.value !== OTHER_OPTION; }).map((value) => { const isNull = value.value === null; return { columnName: chartConfig.groupColumn.includes('category.') ? this.adHocReportingMapper.adaptFormColumnNameForApi( chartConfig.groupColumn ) : chartConfig.groupColumn, filters: [{ filterType: isNull ? FilterModalTypes.notBlank : FilterModalTypes.notEqual, filterValue: isNull ? value.value : '' + value.value }] }; }); paginationOptions.filterColumns = paginationOptions.filterColumns.concat( filterColumnsToAdd ); } else { foundColumn.filters = foundColumn.filters = [{ filterType: clickedValue === null ? FilterModalTypes.isBlank : FilterModalTypes.equals, filterValue: clickedValue === null ? clickedValue as null : '' + clickedValue?.toString() }]; if (!!chartConfig.subGroupColumn) { const foundSubGroupColumn = this.findAndAddPaginationGroupColumn( paginationOptions, chartConfig.subGroupColumn ); const subGroupValue = drilldownValue.subGroupValue; foundSubGroupColumn.filters = foundSubGroupColumn.filters = [{ filterType: subGroupValue === null ? FilterModalTypes.isBlank : FilterModalTypes.equals, filterValue: subGroupValue === null ? subGroupValue : '' + subGroupValue }]; } } return paginationOptions; } private findAndAddPaginationGroupColumn ( paginationOptions: PaginationOptions, groupColumn: string ) { let foundColumn = paginationOptions.filterColumns.find(col => { return col.columnName === groupColumn; }); if (!foundColumn) { foundColumn = { columnName: groupColumn.includes('category.') ? this.adHocReportingMapper.adaptFormColumnNameForApi(groupColumn) : groupColumn, filters: [] }; paginationOptions.filterColumns.push(foundColumn); } return foundColumn; } // Makes sure that the report columns have the group column (filtering won't work without) alignReportColumnsForDrilldown ( chartConfig: GCDashboards.WidgetConfig, userSavedReportColumns: AdHocReportingAPI.UserSavedReportColumn[], drilldownValue: Dashboards.ChartClickEvent ): AdHocReportingAPI.UserSavedReportColumn[] { const clickedValue = drilldownValue.groupValue; const foundGroupReportColumn = this.findAndAddReportGroupColumn( userSavedReportColumns, chartConfig.groupColumn ); if (clickedValue === OTHER_OPTION) { foundGroupReportColumn.userSavedFilterColumns = foundGroupReportColumn.userSavedFilterColumns.concat(drilldownValue.values .filter(value => value.value !== OTHER_OPTION) .map(value => { return { filterType: FilterModalTypes.notEqual, filterValue: '' + value.value }; })); } else { foundGroupReportColumn.userSavedFilterColumns.push({ filterType: FilterModalTypes.equals, filterValue: '' + clickedValue?.toString() }); if (!!chartConfig.subGroupColumn) { const foundSubGroupReportColumn = this.findAndAddReportGroupColumn(userSavedReportColumns, chartConfig.subGroupColumn); foundSubGroupReportColumn.userSavedFilterColumns.push({ filterType: FilterModalTypes.equals, filterValue: drilldownValue.subGroupValue }); } } return userSavedReportColumns; } private findAndAddReportGroupColumn ( userSavedReportColumns: AdHocReportingAPI.UserSavedReportColumn[], groupColumn: string ) { let foundGroupReportColumn = userSavedReportColumns.find(reportColumn => { return reportColumn.columnName === groupColumn; }); if (!foundGroupReportColumn) { foundGroupReportColumn = { chartAggregateType: null, columnName: groupColumn, displayName: '', isChartAggregate: false, isChartGroupingColumn: false, isChartSubGroupingColumn: false, isVisible: true, sortPriority: null, sortType: null, referenceFieldId: null, userSavedFilterColumns: [] }; userSavedReportColumns.push(foundGroupReportColumn); } return foundGroupReportColumn; } getTableDataFactory ( chartConfig: GCDashboards.WidgetConfig, drilldownValue?: Dashboards.ChartClickEvent ): TableDataFactory { return DebounceFactory.createSimple( async (paginationOptions: PaginationOptions) => { const filterColumns = this.mapFilterColumns( chartConfig.filters ); const columns = chartConfig.drilldownColumns; paginationOptions = { ...paginationOptions, filterColumns }; paginationOptions = this.adHocReportingMapper.mapColumnsForPagination( columns, paginationOptions, chartConfig.rowsPerPage ); const { sortColumn, sortAscending } = this.getSortAttrsFromPagination( paginationOptions ); chartConfig.sortColumn = sortColumn; chartConfig.sortAscending = sortAscending; const endpoint = this.adHocService.getEndpointByObjectName( chartConfig.object, 'editEndpoint' ); let userSavedReportColumns = this.mapToUserSavedReportColumnsForTables( chartConfig ); // drilldown value is used to add one additional filters for the bar/slice/dot the user // drilled into, ensuring the data in the drill down is filtered down to just that data point if (drilldownValue) { paginationOptions = this.alignPaginationOptionsForDrilldown( chartConfig, paginationOptions, drilldownValue ); userSavedReportColumns = this.alignReportColumnsForDrilldown( chartConfig, userSavedReportColumns, drilldownValue ); } const formIds = this.getFormIds(chartConfig); const referenceFieldIds = this.adHocService.extractRefFieldIds( columns, filterColumns, userSavedReportColumns ); let baseFormId: number; if (endpoint === this.adHocDefinitions.customForm.editEndpoint) { baseFormId = chartConfig.primaryFormId; } const advancedPaginationOptions: AdHocReportingAPI.AdvancedPaginationOptionsModel = { ...paginationOptions, advancedFilterColumns: this.adHocService.adaptAdvancedUIFiltersToPaginationAPI(chartConfig.advancedFilters), useLogicalOperatorAnd: chartConfig.useAnd === SwitchState.Toggled }; const extraPostOptions = this.getExtraPropsFromWidgetConfig(chartConfig); const results = await this.dashboardsResources.getReportRowsForTable( advancedPaginationOptions, endpoint, formIds, [], userSavedReportColumns.map((col) => { return { ...col, columnName: this.adHocReportingMapper.adaptFormColumnNameForApi( col.columnName ) }; }), baseFormId, referenceFieldIds, extraPostOptions ); this.enforceMaxRows(chartConfig.maxRows, results); return { success: true, data: { records: results.records, recordCount: results.recordCount } }; } ); } enforceMaxRows ( maxRows: number, results: APIResultData ) { if (!!maxRows) { if (results.recordCount > maxRows) { results.recordCount = maxRows; if (results.records.length > maxRows) { results.records = results.records.slice(0, maxRows); } } } } getSortAttrsFromPagination (options: PaginationOptions) { const sortColumn = options.sortColumns[0]; return { sortColumn: sortColumn.columnName, sortAscending: sortColumn.sortAscending }; } mapAdvancedFilterGroups ( groups: AdvancedFilterGroup[] ): FilterColumn[] { return groups.reduce((acc, group) => ([ ...acc, ...this.mapFilterColumns(group.filters) ]), []); } mapFilterColumns ( columns: ColumnFilterRow[] ): FilterColumn[] { return this.filterHelperService.mapToFilterColumns(columns); } async handleDeleteDashboard (dashboardId: number) { try { await this.dashboardsResources.deleteDashboardTab(dashboardId); await this.resetMyDashboards(); this.notifier.success(this.i18n.translate( 'DASHBOARD:txtSuccessDeleteDashboard', {}, 'Dashboard successfully deleted.' )); } catch (e) { this.notifier.error(this.i18n.translate( 'DASHBOARD:txtErrorDeleteDashboard', {}, 'Error deleting dashboard' )); } } async returnWidgetPreview ( widgetConfig: GCDashboards.WidgetConfig ): Promise { // Step 1: Get payload from config const formIds = this.getFormIds(widgetConfig); const payload = this.returnPreviewPayloadFromConfig( widgetConfig, formIds ); // Step 2: Get preview data from API const def = this.adHocDefinitions[widgetConfig.object]; if (def.supportsSummaryWidgets) { return this.getSummaryChartData(widgetConfig); } // Step 3: Add additional props from config const extraProps = this.getExtraPropsFromWidgetConfig(widgetConfig); const response = await this.fetchPreviewData(payload, widgetConfig, extraProps); // Step 4: Call mapResultToChartData with data and config const chartData = this.mapResultToChartData(widgetConfig, response); return chartData; } private getExtraPropsFromWidgetConfig (widgetConfig: GCDashboards.WidgetConfig) { const def = this.adHocDefinitions[widgetConfig.object]; return Object.keys(def.additionalDashboardProperties ?? {}) .reduce((acc, key) => { const mapping = def.additionalDashboardProperties[key]; const value = widgetConfig[mapping]; return { ...acc, [key]: value }; }, {}); } async getSummaryChartData (widgetConfig: GCDashboards.WidgetConfig) { const formIds = this.getFormIds(widgetConfig); const def = this.adHocDefinitions[widgetConfig.object]; const columnName = `${def.property}.id`; const extraProps = this.getExtraPropsFromWidgetConfig(widgetConfig); // set up default pagination options // with a filter for the summary record // and user report columns for each column being summarized const results = await this.dashboardsResources.getReportRowsForTable({ ...BLANK_PAGINATION_OPTIONS, useLogicalOperatorAnd: true, sortColumns: [{ columnName, sortAscending: true }], advancedFilterColumns: [[{ filter: { filterType: 'eq', filterValues: [], filterValue: widgetConfig.summaryRecordId, useLogicalOperatorAnd: true }, columnName }]] }, def.editEndpoint, formIds, [], [{ columnName, chartAggregateType: null, isChartAggregate: false, isChartGroupingColumn: false, isChartSubGroupingColumn: false, displayName: '', referenceFieldId: null, sortPriority: 0, sortType: AdHocReportingAPI.SortTypes.Descending, userSavedFilterColumns: [] }].concat(widgetConfig.drilldownColumns.map(column => { return { columnName: `${def.property}.${column.definition.column}`, chartAggregateType: null, isChartAggregate: false, isChartGroupingColumn: false, isChartSubGroupingColumn: false, displayName: '', referenceFieldId: null, sortPriority: 0, sortType: AdHocReportingAPI.SortTypes.NoSort, userSavedFilterColumns: [] }; })), null, [], extraProps); const adapted = this.adaptPaginatedResponseToChartDataReturn(results, widgetConfig); return adapted; } adaptPaginatedResponseToChartDataReturn ( response: PaginatedResponse, widgetConfig: GCDashboards.WidgetConfig ): GCDashboards.ChartDataReturn { const objectDef = this.adHocDefinitions[widgetConfig.object]; const backgroundColor = this.determineColors(widgetConfig.drilldownColumns.length); return { data: [{ label: '', value: '', formats: widgetConfig.drilldownColumns.map(col => { const def = col.definition; return def.type === 'number' ? def.format : def.type === 'currency' ? 'currency' : 'number'; }), data: widgetConfig.drilldownColumns.map(column => { const value = response.records[0][objectDef.property][column.definition.column]; return +value; }), counts: [], backgroundColor }], labels: widgetConfig.drilldownColumns.map(column => { return { display: column.columnNameOverride || column.definition.display, value: null, color: null }; }) }; } returnPreviewPayloadFromConfig ( widgetConfig: GCDashboards.WidgetConfig, formIds: number[] ): GCDashboards.PreviewDataPayload { const userSavedReportColumnList = this.mapToUserSavedReportColumnsForAggregation( widgetConfig ); const referenceFieldIds = this.adHocService.extractRefFieldIds( [], [], userSavedReportColumnList ); const paginationOptions = this.getPaginationOptionsFromUserSavedCols( this.adHocDefinitions[widgetConfig.object].type, formIds, userSavedReportColumnList ); const previewPayload: GCDashboards.PreviewDataPayload = { userSavedReportColumnList: userSavedReportColumnList.map((col) => { return { ...col, columnName: this.adHocReportingMapper.adaptFormColumnNameForApi( col.columnName ) }; }), formIds, primaryFormId: widgetConfig.primaryFormId, paginationOptions: { ...paginationOptions, advancedFilterColumns: this.adHocService.adaptAdvancedUIFiltersToPaginationAPI( widgetConfig.advancedFilters ), useLogicalOperatorAnd: widgetConfig.useAnd === SwitchState.Toggled }, chartMaxRows: widgetConfig.maxGroups, chartIncludeOtherAggregate: this.getShouldIncludeOther( this.mapTypeToApiChartType(widgetConfig.type) ), referenceFieldIds }; return previewPayload; } getPaginationOptionsFromUserSavedCols ( reportModelType: AdHocReportingAPI.AdHocReportModelType, formIds: number[], columns: AdHocReportingAPI.UserSavedReportColumn[] ): PaginationOptions { const columnDefs = this.getColumnDefs( reportModelType, formIds, columns ); const columnFilterRows = this.mapUserSavedColToColumnFilterRows( columns, columnDefs ); const filterColumns = this.mapFilterColumns(columnFilterRows); let sortColumn: APISortColumn; columns.forEach((col) => { if (col.sortType && (col.sortType !== AdHocReportingAPI.SortTypes.NoSort)) { sortColumn = { columnName: this.adHocReportingMapper.adaptFormColumnNameForApi( col.columnName ), sortAscending: col.sortType === AdHocReportingAPI.SortTypes.Ascending }; } }); if (!sortColumn) { // stats don't have sort, but it is required. Mock it here sortColumn = { columnName: 'application.id', sortAscending: true }; } return { rowsPerPage: 15, pageNumber: 1, sortColumns: [ sortColumn ], filterColumns: filterColumns.map((col) => { return { ...col, columnName: this.adHocReportingMapper.adaptFormColumnNameForApi( col.columnName ) }; }), retrieveTotalRecordCount: false, returnAll: true }; } async fetchPreviewData ( previewPayload: GCDashboards.PreviewDataPayload, config: GCDashboards.WidgetConfig, extras: Record ) { const object = this.adHocDefinitions[config.object]; const api = object.previewChartEndpoint; return this.dashboardsResources.getPreviewDashboardData({ ...previewPayload, ...extras }, api); } getFormIds (widgetConfig: GCDashboards.WidgetConfig) { const formIds = this.adHocService.getFormIdsForAPI( widgetConfig.customForms, widgetConfig.primaryFormId ); return formIds.filter((id) => !!id); } shouldPromptForUnsavedChanges () { return this.editing && !this.promptingForUnsavedChanges && Object.keys(this.widgetEditMap || {}).length > 0; } async saveDashboardEdits () { try { await Promise.all(Object.keys(this.widgetEditMap).map((id) => { if (this.widgetEditMap[+id]) { return this.saveWidgetGridUpdates(this.widgetEditMap[+id]); } return null; })); this.resetWidgetEditMap(); this.notifier.success( this.i18n.translate( 'DASHBOARD:txtSuccessMovingWidgets', {}, 'Successfully updated dashboard' )); } catch (e) { this.notifier.error( this.i18n.translate( 'DASHBOARD:txtErrorMovingWidgets', {}, 'There was an error updating the dashboard' )); this.logger.error(e); } } handleWidgetPositionChange ( item: GridsterItem, oldWidgets: GCDashboards.WidgetConfigFromApi[] ) { if (this.editing) { const foundWidget = oldWidgets.find(widget => { return +widget.id === +item.id; }); const oldConfig: GCDashboards.WidgetConfig = JSON.parse(foundWidget.chartConfig); if ( oldConfig.x !== item.x || oldConfig.y !== item.y || oldConfig.width !== item.cols || oldConfig.height !== item.rows ) { foundWidget.chartConfig = JSON.stringify({ ...oldConfig, x: item.x, y: item.y, width: item.cols, height: item.rows }); } this.setWidgetEditMap(item.id, foundWidget); } } refreshDashboardData (dashboardId: number) { const dashboard = this.get('dashboardDetails')[dashboardId]; dashboard.widgets.forEach((widget) => { this.setWidgetDisplay(widget.id, null); }); } setWidgetDisplay (id: number, adapted: GCDashboards.Widget) { this.set('widgetDisplayMap', { ...this.widgetDisplayMap, [id]: adapted }); } async hideDashboard (dashboardId: number) { try { await this.updateVisibility( dashboardId, true ); this.notifier.success( this.i18n.translate( 'DASHBOARD.textSuccessHidingDashboard', {}, 'Successfully hid dashboard' ) ); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'DASHBOARD.textErrorHidingDashboard', {}, 'There was an error hiding your dashboard' ) ); } } async showDashboard (dashboardId: number) { try { await this.updateVisibility( dashboardId, false ); this.notifier.success( this.i18n.translate( 'DASHBOARD.textSuccessShowingDashboard', {}, 'Successfully showed dashboard' ) ); } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'DASHBOARD.textErrorShowingDashboard', {}, 'There was an error showing your dashboard' ) ); } } private async updateVisibility ( dashboardId: number, isHidden: boolean ) { await this.dashboardsResources.updateDashboardVisibility({ dashboardId, isHidden }); await this.resetMyDashboards(); } async fetchDashboardSharedUsers ( dashboardId: number ): Promise { const dashboardUsers = await this.dashboardsResources.getDashboardSharedUsers( dashboardId ); return dashboardUsers.map(dashboardUser => { return { canManage: dashboardUser.isDashboardOwner, email: dashboardUser.email, external: false, id: dashboardUser.userId, name: dashboardUser.firstName + ' ' + dashboardUser.lastName }; }); } async handleShareDashboardModalResult ( dashboardId: number, result: GCDashboards.SharedDashboardModalResult ) { try { const shareChanges: GCDashboards.ShareDashboardUserPayload = { dashboardId, usersToShareWith: result.changes.map((user) => { return { sharedToUserId: user.id, isDashboardOwner: user.canManage }; }) }; const revocations: GCDashboards.RevokeDashboardUserPayload = { dashboardId, userIdsToRevoke: result.removals.map(user => { return user.id; }) }; if (revocations.userIdsToRevoke.length) { await this.dashboardsResources.revokeDashboardForUsers(revocations); } if (shareChanges.usersToShareWith.length) { await this.dashboardsResources.setSharedDashboardForUsers(shareChanges); } await this.resetMyDashboards(); this.notifier.success(this.i18n.translate( 'DASHBOARD:textSuccessSharingDashboard', {}, 'Successfully updated dashboard sharing' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'DASHBOARD:textFailedSharingDashboard', {}, 'Failed to update dashboard sharing' )); } } determineShareChanges ( oldMembers: AudienceMember[], newMembers: AudienceMember[] ) { const removals = oldMembers.filter(originalMember => { return !newMembers.some(member => originalMember.id === member.id && originalMember.canManage === member.canManage); }); const changes = newMembers.filter(member => { // it's a change if either they weren't on the list before OR they have a different manage permission return !oldMembers.some(originalMember => { return member.id === originalMember.id && member.canManage === originalMember.canManage; }); }); return { removals, changes }; } getTypeSelectOptionsByObject (object: RootObjectNames) { const allTypeOptions = this.typeSelectOptions; switch (object) { case 'budgets': case 'fundingSources': return allTypeOptions.filter((type) => { return [ 'table', 'pie', 'stat' ].includes(type.value); }); case 'fieldGroup': return allTypeOptions.filter((type) => { return [ 'bar', 'pie' ].includes(type.value); }); default: return allTypeOptions; } } async handleDownload ( widget: GCDashboards.Widget, masked: boolean, element?: HTMLCanvasElement ) { if (widget.chartType === 'table') { const paginationOptions: PaginationOptions = { returnAll: true, rowsPerPage: 0, pageNumber: 0, sortColumns: [], filterColumns: [], retrieveTotalRecordCount: false }; const func = 'exec' in widget.tableDataFactory ? widget.tableDataFactory.exec : widget.tableDataFactory; const response = await func(paginationOptions).pipe(take(1)).toPromise(); const csv = this.adHocService.processCsv( response.data, widget.drilldownColumns, masked ); return this.fileService.downloadCSV(csv); } else if (element) { const blob = await this.fileService.getBlobFromCanvas(element); this.fileService.saveAs(blob, 'export.png'); } } }