import { Injectable } from '@angular/core'; import { AddEditWorkflow, SimpleWorkflowLevelUser, Sublevel, ToggleWorkflowLevelResponse, UsersAndRoutesLevel, Workflow, WorkflowDetail, WorkflowLevel, WorkflowLevelPermissions, WorkflowLevelUser, WorkflowManager, WorkflowManagerActions } from '@core/typings/workflow.typing'; import { UserService } from '@features/users/user.service'; import { WorkflowResources } from '@features/workflow/workflow.resources'; import { ArrayHelpersService, DragItem, SimpleStringMap, TypeaheadSelectOption } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { difference } from 'lodash'; import { WorkflowState } from './workflow.state'; @AttachYCState(WorkflowState) @Injectable({ providedIn: 'root' }) export class WorkflowService extends BaseYCService { constructor ( private logger: LogService, private workflowResources: WorkflowResources, private i18n: I18nService, private notifier: NotifierService, private userService: UserService, private arrayHelper: ArrayHelpersService ) { super(); } get workflows () { return this.get('workflows'); } get workflowMap () { return this.get('workflowMap'); } get myWorkflowOptions () { return this.get('myWorkflowOptions'); } get myWorkflowLevelOptions () { return this.get('myWorkflowLevelOptions'); } get myWorkflowManagerRolesMap () { return this.get('myWorkflowManagerRolesMap'); } get allWfManagerActions () { return Object.keys(WorkflowManagerActions).filter((key) => { return isNaN(+key); }).map((key) => { return key as keyof typeof WorkflowManagerActions; }).map((key) => { return WorkflowManagerActions[key]; }); } get standardWfManagerActions () { return this.allWfManagerActions.filter((val) => { return !this.draftWfManagerActions.includes(val); }); } get draftWfManagerActions () { return [ WorkflowManagerActions.ViewCommunications ]; } getWorkflow (workflowId: number) { return this.workflowResources.getWorkflow(workflowId, true); } async getAndSetWorkflow (workflowId: number) { const map = this.get('workflowMap'); let workflow: WorkflowDetail = map[workflowId]; if (!workflow) { workflow = await this.getWorkflow(workflowId); this.setWorkflowMap({ ...this.get('workflowMap'), [workflowId]: workflow }); } return workflow; } setWorkflowMap (map: { [w: string]: WorkflowDetail; }) { this.set('workflowMap', map); } setMapForNewWorkflow ( name: string, description: string ) { this.setWorkflowMap({ ...this.get('workflowMap'), new: { id: null, name, description, levels: [], hasApplications: false, active: true, workflowLevelManagers: [] } }); } async resetWorkflows () { this.set('workflows', undefined); await this.getWorkflows(); } async getWorkflows (): Promise { if (!this.workflows) { const workflows = await this.workflowResources.getWorkflows(); workflows.forEach((workflow) => { workflow.workflowLevelManagers.forEach((manager) => { manager.fullName = manager.firstName + ' ' + manager.lastName; }); }); this.set('workflows', workflows); await this.setMyWorkflowOptions(); return workflows; } return this.workflows; } async getWorkflowManagers ( workflowId: number ): Promise { await this.getAndSetWorkflowMap(workflowId); return this.workflowMap[workflowId].workflowLevelManagers; } async getWorkflowLevelUsers ( workflowId: number, workflowLevelId: number ): Promise { await this.getAndSetWorkflowMap(workflowId); const level = this.findWorkflowLevel( this.workflowMap[workflowId], workflowLevelId ); if (level) { level.workflowLevelUsers.forEach((user) => { user.fullName = `${user.firstName} ${user.lastName}`; }); } return level?.workflowLevelUsers || []; } findWorkflowLevel ( workflow: WorkflowDetail, workflowLevelId: number ) { let foundLevel: WorkflowLevel|Sublevel; workflow.levels.forEach((level) => { if (level.id === workflowLevelId) { foundLevel = level; } level.subLevels.forEach((subLevel) => { if (subLevel.id === workflowLevelId) { foundLevel = subLevel; } }); }); return foundLevel; } async setMyWorkflowOptions () { const [ myWorkflows, myWorkflowLevels ] = await Promise.all([ this.workflowResources.getWorkflows(true), this.workflowResources.getMyWorkflowLevels() ]); const myWorkflowOptions = myWorkflows.map((workflow) => { return { label: workflow.name, value: workflow.id }; }); const myWorkflowLevelOptions: TypeaheadSelectOption[] = []; this.workflows.forEach((workflow) => { workflow.levels.forEach((level) => { if (myWorkflowLevels.includes(level.id)) { myWorkflowLevelOptions.push({ label: level.name, value: level.id }); } level.subLevels.forEach((sub) => { if (myWorkflowLevels.includes(sub.id)) { myWorkflowLevelOptions.push({ label: sub.name, value: sub.id }); } }); }); }); this.set( 'myWorkflowOptions', this.arrayHelper.sort(myWorkflowOptions, 'label') ); this.set( 'myWorkflowLevelOptions', this.arrayHelper.sort(myWorkflowLevelOptions, 'label') ); } async getAndSetWorkflowMap (id: number, force = false) { const workflowMap = this.workflowMap; let detail = workflowMap[id]; if (!detail || force) { detail = await this.workflowResources.getWorkflow(id, true); this.arrayHelper.sort(detail.levels, 'sortOrder'); detail.levels.forEach((level) => { let subIsDefaultOnProgram = false; level.subLevels.forEach((sub) => { if (sub.isDefaultLevelOnProgram) { subIsDefaultOnProgram = true; } }); level.isDefaultLevelOnProgram = level.isDefaultLevelOnProgram || subIsDefaultOnProgram; this.arrayHelper.sort(level.subLevels, 'sortOrder'); }); this.set('workflowMap', { ...workflowMap, [id]: { ...detail, workflowLevelManagers: detail.workflowLevelManagers.map((manager) => { manager.fullName = manager.firstName + ' ' + manager.lastName; return manager; }) } }); } return detail; } async resetWorkflowDetail (id: number) { this.set('workflowMap', { ...this.workflowMap, [id]: undefined }); await this.getAndSetWorkflowMap(id); } async setMyWorkflowManagerRolesMap () { if (!this.myWorkflowManagerRolesMap) { const response = await this.workflowResources.getMyWorkflowManagerRoles(); const map: SimpleStringMap = {}; response.forEach((item) => { map[item.workflowId] = item.workflowActions; }); this.set('myWorkflowManagerRolesMap', map); } } async resetMyWorkflowManagerRolesMap () { this.set('myWorkflowManagerRolesMap', undefined); await this.setMyWorkflowManagerRolesMap(); } async handleSaveWorkflowLevels (payload: AddEditWorkflow) { try { const id = await this.workflowResources.saveWorkflow(payload); this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessSavingWorkflowLevel', {}, 'Successfully saved the workflow level' )); await this.resetWorkflows(); await this.resetWorkflowDetail(id); return id; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorSavingWorkflowLevel', {}, 'There was an error saving the workflow level' )); return null; } } async handleDeleteWorkflow (id: number) { try { await this.workflowResources.deleteWorkflow(id); this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessfullyDeletedWorkflow', {}, 'Successfully deleted workflow' )); await this.resetWorkflows(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorDeletingWorkflow', {}, 'There was an error deleting the workflow' )); } } async handleDisableEnableWorkflowLevel (payload: ToggleWorkflowLevelResponse) { try { if (payload.disable) { await this.workflowResources.disableWorkflowLevel(payload.workflowLevelId); } else { await this.workflowResources.enableWorkflowLevel(payload.workflowLevelId); } this.notifier.success(this.i18n.translate( payload.disable ? 'WORKFLOW:textSuccessfullyDisableWorkflowLevel' : 'WORKFLOW:textSuccessfullyEnableWorkflowLevel', {}, payload.disable ? 'Successfully disabled workflow level' : 'Successfully enabled workflow level' )); } catch (error) { this.logger.error(error); this.notifier.error(this.i18n.translate( payload.disable ? 'WORKFLOW:textErrorDisableWorkflowLevel' : 'WORKFLOW:textErrorEnableWorkflowLevel', {}, payload.disable ? 'There was an error disabling the workflow level' : 'There was an error enabling the workflow level' )); } } async handleUpdateWorkflowDetails ( id: number, name: string, description: string ) { try { await this.workflowResources.updateWorkflowDetails( id, name, description ); this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessfullyUpdatedWorkflowDetails', {}, 'Successfully updated workflow details' )); await this.resetWorkflows(); await this.resetWorkflowDetail(id); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorUpdatingWorkflowDetails', {}, 'There was an error updating workflow details' )); } } async updateWorkflowLevelUsers ( workflowId: number, workflowLevelId: number, workflowLevelUsers: SimpleWorkflowLevelUser[] ) { try { await this.workflowResources.updateWorkflowLevelUsers( workflowId, workflowLevelId, workflowLevelUsers ); this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessfullyUpdatedWorkflowLevelUsers', {}, 'Successfully updated workflow level users' )); await this.resetWorkflows(); await this.resetWorkflowDetail(workflowId); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorUpdatingWorkflowLevelUsers', {}, 'There was an error updating workflow level users' )); } } async handleRouteChanges ( workflowId: number, level: UsersAndRoutesLevel, routes: DragItem[] ) { const existingRouteIds = level.routes.map((route) => route.canRouteToWorkflowLevelId); const newRoutes = routes.map((route) => route.context.id); const routesToAdd = difference(newRoutes, existingRouteIds); const routesToRemove = difference(existingRouteIds, newRoutes); try { await Promise.all(routesToAdd.map(async (route) => { await this.workflowResources.addLevelToWorkflowLevelRoutes( workflowId, level.id, route ); })); await Promise.all(routesToRemove.map(async (route) => { await this.workflowResources.deleteLevelFromWorkflowLevelRoutes( workflowId, level.id, route ); })); await this.resetWorkflowDetail(workflowId); this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessfullyUpdatedWorkflowLevelRoutes', {}, 'Successfully updated workflow level routes' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorUpdatingWorkflowLevelRoutes', {}, 'There was an error updating workflow level routes' )); } } async handleAddWorkflowManager ( workflowId: number, manager: WorkflowManager, isNew = false ) { try { await this.workflowResources.addOrUpdateWorkflowManager({ workflowId, clientUserId: manager.clientUserId, workflowLevelActions: manager.workflowActions }); await this.resetWorkflows(); await this.resetWorkflowDetail(workflowId); if (manager.clientUserId === this.userService.currentUser.id) { await this.resetMyWorkflowManagerRolesMap(); } this.notifier.success(this.i18n.translate( isNew ? 'WORKFLOW:textSuccessfullyAddedWorkflowManager' : 'WORKFLOW:textSuccessfullyUpdatedWorkflowManager', {}, isNew ? 'Successfully added the workflow manager' : 'Successfully updated the workflow manager' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isNew ? 'WORKFLOW:textErrorAddingWorkflowManager' : 'WORKFLOW:textErrorUpdatingWorkflowManager', {}, isNew ? 'There was an error adding the workflow manager' : 'There was an error updating the workflow manager' )); } } async handleRemoveWorkflowManager ( workflowId: number, managerId: number ) { try { await this.workflowResources.removeWorkflowManager( workflowId, managerId ); if (managerId === this.userService.currentUser.id) { await this.resetMyWorkflowManagerRolesMap(); } this.notifier.success(this.i18n.translate( 'WORKFLOW:textSuccessfullyRemovedWorkflowManager', {}, 'Successfully removed the workflow manager' )); await this.resetWorkflows(); await this.resetWorkflowDetail(workflowId); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'WORKFLOW:textErrorRemovingWorkflowManager', {}, 'There was an error removing the workflow manager' )); } } getWorkflowForEdit (id: number) { // this filters out the potential disabled routes so they are not visible in the routing modal until enabled again. const workflow = this.workflowMap[id]; if (workflow) { return { ...workflow, levels: workflow.levels.map((level) => { level.routes = level.routes.filter((route) => !route.disabled); level.subLevels = level.subLevels.map((sub) => { sub.routes = sub.routes.filter((route) => !route.disabled); return sub; }); return level; }) }; } else { return undefined; } } getLevelOptions (workflowId: number) { const workflow = this.workflows.find((flow) => { return flow.id === workflowId; }); const workflowLevelOptions: TypeaheadSelectOption[] = []; if (workflow) { workflow.levels.forEach((level) => { if (!level.disabled) { workflowLevelOptions.push({ label: level.name, value: level.id }); } level.subLevels.forEach((subLevel) => { if (!subLevel.disabled) { workflowLevelOptions.push({ label: subLevel.name, value: subLevel.id }); } }); }); } return this.arrayHelper.sort(workflowLevelOptions, 'label'); } getWorkflowUserPermissionDisplay ( clientUserId: number, workflowLevelAccessType: WorkflowLevelPermissions, workflowManagers: WorkflowManager[] ) { const isManager = workflowManagers.some((user) => { return user.clientUserId === clientUserId; }); if (isManager) { return this.i18n.translate( 'WORKFLOW:textWorkflowManager', {}, 'Workflow manager' ); } if (workflowLevelAccessType === WorkflowLevelPermissions.LEVEL_ONLY) { return this.i18n.translate( 'WORKFLOW:textWorkflowLevelOnly', {}, 'Workflow level only ' ); } else if (workflowLevelAccessType === WorkflowLevelPermissions.PASSTHROUGH) { return this.i18n.translate( 'WORKFLOW:textWorkflowPassthrough', {}, 'Workflow passthrough' ); } return ''; } getWorkflowForms (programId: number) { return this.workflowResources.getWorkflowForms(programId); } }