From 57010a59ebad9f8d6be2f3e99b815e6c58da558b Mon Sep 17 00:00:00 2001 From: Sabrina Baggett Date: Wed, 23 Apr 2025 22:05:46 -0700 Subject: [PATCH 1/4] adding user settings to iam-support --- app/api/employees.js | 52 +++++++ .../src/elements/pages/bundles/index.js | 2 +- app/client/src/elements/pages/bundles/main.js | 1 + .../pages/ucdlib-iam-page-user-settings.js | 142 ++++++++++++++++++ .../ucdlib-iam-page-user-settings.tpl.js | 35 +++++ app/client/src/elements/ucdlib-iam-app.tpl.js | 2 + app/client/src/models/AppStateModel.js | 13 ++ app/client/src/stores/AppStateStore.js | 2 + app/lib/config.js | 2 +- .../ucdlib-iam-support-local-dev/compose.yaml | 2 +- .../db-entrypoint/001-create-empty-db.sql | 6 + lib/src/models/EmployeeModel.js | 35 +++++ lib/src/services/EmployeeService.js | 29 ++++ lib/src/stores/EmployeeStore.js | 59 +++++++- lib/src/utils/employees.js | 63 +++++++- utils/api/routes/employees.js | 39 +++++ 16 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 app/client/src/elements/pages/ucdlib-iam-page-user-settings.js create mode 100644 app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js diff --git a/app/api/employees.js b/app/api/employees.js index fb28f95..2986f2c 100644 --- a/app/api/employees.js +++ b/app/api/employees.js @@ -62,4 +62,56 @@ export default (api) => { return res.json(out); }); + + api.get(`/employees/metadata/:id`, async (req, res) => { + + // query for employee + if ( !req.params.id ) { + return res.status(400).json({ + error: 'Missing employee identifier' + }); + } + + const out = { + total: 0, + results: [], + } + + const r = await UcdlibEmployees.getById(req.params.id, "employeeId", {includeMetadata:true}) + + out.total = r.res.rowCount; + out.results = r.res.rows; + + return res.json(out); + + }); + + api.post(`/employees/metadata/:id`, async (req, res) => { + + // query for employee + if ( !req.params.id ) { + return res.status(400).json({ + error: 'Missing employee identifier' + }); + } + + const out = { + total: 0, + results: [], + } + + const id = req.body[0].id; + const employee_id = req.body[0].employeeId; + const metadataKey = req.body[0].metadataKey; + const metadataValue = req.body[0].metadataValue; + + + const results = await UcdlibEmployees.updateMetadata(id, metadataKey, metadataValue, employee_id) + + out.total = results.res.rowCount; + out.results = results.res.rows; + + return res.json(out); + + }); } diff --git a/app/client/src/elements/pages/bundles/index.js b/app/client/src/elements/pages/bundles/index.js index 334b92a..3fa9811 100644 --- a/app/client/src/elements/pages/bundles/index.js +++ b/app/client/src/elements/pages/bundles/index.js @@ -1,7 +1,7 @@ const defs = { main : [ 'home', 'onboarding', 'separation', 'separation-new', 'separation-single', 'onboarding-new', - 'onboarding-single', 'permissions-single', 'permissions' + 'onboarding-single', 'permissions-single', 'settings', 'permissions' ], tools: ['patron', 'orgchart', 'tools'] }; diff --git a/app/client/src/elements/pages/bundles/main.js b/app/client/src/elements/pages/bundles/main.js index 67b0cc3..d1862f4 100644 --- a/app/client/src/elements/pages/bundles/main.js +++ b/app/client/src/elements/pages/bundles/main.js @@ -7,3 +7,4 @@ import "../ucdlib-iam-page-permissions-single"; import "../ucdlib-iam-page-separation"; import "../ucdlib-iam-page-separation-new"; import "../ucdlib-iam-page-separation-single"; +import "../ucdlib-iam-page-user-settings"; diff --git a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js new file mode 100644 index 0000000..43b4ea9 --- /dev/null +++ b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js @@ -0,0 +1,142 @@ +import { LitElement } from 'lit'; +import {render} from "./ucdlib-iam-page-user-settings.tpl.js"; +import { LitCorkUtils, Mixin } from '@ucd-lib/cork-app-utils'; + + +/** + * @description Landing page for user settings + */ +export default class UcdlibIamPageUserSettings extends Mixin(LitElement) + .with(LitCorkUtils) { + + static get properties() { + return { + updateInProgress: {state: true}, + results: {type: Array} + }; + } + + constructor() { + super(); + this.render = render.bind(this); + this._injectModel('AppStateModel','AuthModel', 'EmployeeModel'); + this.token = this.AuthModel.getToken().token || null; + this.employeeId = this.AuthModel.getToken().token.employeeId || null; + this.firstName = this.AuthModel.getToken().token.given_name || null; + this.lastName = this.AuthModel.getToken().token.family_name || null; + this.metadata = []; + this.updateList = []; + this.noChange = true; + + } + + /** + * @description Disables the shadowdom + * @returns + */ + createRenderRoot() { + return this; + } + + + /* Add the check for the new user_setting enable flag */ + + /** + * @method handleCheckboxChange + * @description handle the checkbox changing + * + * @param {Object} e + */ + handleDirectReportChange(e) { + this.noChange = e.target.checked === this.enableDirectReportNotification; + this.requestUpdate(); + } + + + /** + * @method _onAppStateUpdate + * @description bound to AppStateModel app-state-update event + * + * @param {Object} e + */ + async _onAppStateUpdate(e) { + if ( e.page != this.id ) return; + this.AppStateModel.showLoaded(this.id); + + const r = await this.EmployeeModel.getMetadata(this.employeeId); + this.metadata = r.payload.results[0].metadata; + + this.currentDirectReportStatus(); + + /* More Metadata should be made into a new function */ + + } + + /** + * @method currentDirectReportStatus + * @description get the current Direct Report Status + */ + async currentDirectReportStatus(){ + let meta = this.metadata.find(m => m.metadataKey === "direct_reports_notification"); + this.directReportsId = meta.id; + meta = meta ? meta.metadataValue.toLowerCase() : false; + this.enableDirectReportNotification = meta === "true" ? true: false; + + this.requestUpdate(); + } + + /** + * @description Event handler for when the edit form is submitted + * @param {*} e - form submit event + * @returns + */ + async _onEditSubmit(e){ + e.preventDefault(); + + if ( this.updateInProgress ) return; + this.updateInProgress = true; + + const checkboxes = this.renderRoot.querySelectorAll('input[type="checkbox"]'); + + checkboxes.forEach(checkbox => { + + if(checkbox.name == "direct-reports") { + this.updateList.push({ + id: this.directReportsId, + employeeId: this.employeeId, + metadataKey: "direct_reports_notification", + metadataValue: String(checkbox.checked) + }); + } + + /* Add future if statements for metadata here */ + }); + + const r = await this.EmployeeModel.updateMetadata(this.updateList, this.employeeId); + + let successText = `${this.firstName} ${this.lastName}'s User Settings has been updated.`; + + if ( r.state === 'loaded' ) { + this.AppStateModel.refresh(); + setTimeout(() => { + this.AppStateModel.showAlertBanner({message: successText, brandColor: 'quad'}); + }, 1000); + this.updateInProgress = false; + + } else { + if ( r.error?.payload?.is400 ) { + this.requestUpdate(); + this.AppStateModel.showAlertBanner({message: 'Error when updating the user settings. Form data needs fixing.', brandColor: 'double-decker'}); + this.logger.error('Error in form updating user settings', r); + } else { + this.AppStateModel.showAlertBanner({message: 'An unknown error occurred when updating your User Settings', brandColor: 'double-decker'}); + this.logger.error('Error updating user settings', r); + } + } + } + + + +} + +customElements.define('ucdlib-iam-page-user-settings', UcdlibIamPageUserSettings); \ No newline at end of file diff --git a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js new file mode 100644 index 0000000..2dfd23a --- /dev/null +++ b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js @@ -0,0 +1,35 @@ +import { html } from 'lit'; + +export function render() { +return html` +
+
+
+

RT Functionality

+
+
    +
  • + + +
  • +
+
+ + + +
+
+
+ +`;} \ No newline at end of file diff --git a/app/client/src/elements/ucdlib-iam-app.tpl.js b/app/client/src/elements/ucdlib-iam-app.tpl.js index 7bc1107..778eb6b 100644 --- a/app/client/src/elements/ucdlib-iam-app.tpl.js +++ b/app/client/src/elements/ucdlib-iam-app.tpl.js @@ -9,6 +9,7 @@ export function render() { Logout + User Settings
    @@ -54,5 +55,6 @@ export function render() { + `;} diff --git a/app/client/src/models/AppStateModel.js b/app/client/src/models/AppStateModel.js index 0af9fc4..76c0272 100644 --- a/app/client/src/models/AppStateModel.js +++ b/app/client/src/models/AppStateModel.js @@ -94,6 +94,12 @@ class AppStateModelImpl extends AppStateModel { update.location.path.length > 1 ) { p = 'orgchart'; + } else if( + update.location.path[0] == 'settings' && + update.location.path.length > 1 + ) { + console.log("S:", update.location.path); + p = 'settings'; } else if( update.location.path[0] == 'patron' && update.location.path.length > 1 @@ -142,6 +148,9 @@ class AppStateModelImpl extends AppStateModel { } else if ( update.page === 'patron' ){ title.show = this.store.pageTitles.patronLookup ? true : false; title.text = this.store.pageTitles.patronLookup; + } else if ( update.page === 'settings' ){ + title.show = this.store.pageTitles.userSettings ? true : false; + title.text = this.store.pageTitles.userSettings; } else if ( update.page === 'tools' ){ title.show = this.store.pageTitles.tools ? true : false; title.text = this.store.pageTitles.tools; @@ -229,6 +238,10 @@ class AppStateModelImpl extends AppStateModel { breadcrumbs.show = true; breadcrumbs.breadcrumbs.push(this.store.breadcrumbs.patronLookup); } + else if ( update.page === 'settings' ){ + breadcrumbs.show = true; + breadcrumbs.breadcrumbs.push(this.store.breadcrumbs.userSettings); + } else if ( update.page === 'tools' ){ breadcrumbs.show = true; breadcrumbs.breadcrumbs.push(this.store.breadcrumbs.tools); diff --git a/app/client/src/stores/AppStateStore.js b/app/client/src/stores/AppStateStore.js index 72fd936..c73b184 100644 --- a/app/client/src/stores/AppStateStore.js +++ b/app/client/src/stores/AppStateStore.js @@ -28,6 +28,7 @@ class AppStateStoreImpl extends AppStateStore { permissionsEmployee: {text: 'Select a UC Davis Employee', link: '/permissions#employee'}, orgchart: {text: 'Create Organizational Chart Tool', link: '/orgchart'}, patronLookup: {text: 'Search by Patron Lookup Tool', link: '/patron'}, + userSettings: {text: 'User Settings', link: '/settings'}, tools: {text: 'Support Tools', link: '/tools'} }; @@ -41,6 +42,7 @@ class AppStateStoreImpl extends AppStateStore { permissions: 'Employee Permissions', orgchart: 'Organization Chart Tool', patronLookup: 'Patron Lookup', + userSettings: 'User Settings', tools: 'Support Tools' }; diff --git a/app/lib/config.js b/app/lib/config.js index a723cf2..d1ab9ee 100644 --- a/app/lib/config.js +++ b/app/lib/config.js @@ -8,7 +8,7 @@ class AppConfig extends BaseConfig { this.version = corkBuild.version; - this.routes = ['onboarding', 'separation', 'logout', 'permissions', 'orgchart', 'patron', 'tools']; + this.routes = ['onboarding', 'separation', 'logout', 'permissions', 'orgchart', 'patron', 'settings', 'tools']; this.title = 'UC Davis Library Identity and Access Management'; this.baseUrl = env.UCDLIB_BASE_URL || 'https://iam.staff.library.ucdavis.edu'; } diff --git a/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml b/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml index a884537..30b3b8e 100644 --- a/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml +++ b/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml @@ -142,7 +142,7 @@ services: - 5432:5432 volumes: - db-data:/var/lib/postgresql/data - #- ../../deploy/utils/db-entrypoint:/docker-entrypoint-initdb.d + - ../../deploy/utils/db-entrypoint:/docker-entrypoint-initdb.d adminer: image: adminer diff --git a/deploy/utils/db-entrypoint/001-create-empty-db.sql b/deploy/utils/db-entrypoint/001-create-empty-db.sql index 917ce22..4b6ec62 100644 --- a/deploy/utils/db-entrypoint/001-create-empty-db.sql +++ b/deploy/utils/db-entrypoint/001-create-empty-db.sql @@ -130,6 +130,12 @@ CREATE TABLE job_logs ( data jsonb NOT NULL DEFAULT '{}'::jsonb, created timestamp DEFAULT NOW() ); +CREATE TABLE metadata ( + id SERIAL PRIMARY KEY, + metadata_key varchar(50) NOT NULL, + metadata_value text NOT NULL, + employee_id varchar(20) +); -- Request Statuses --1 diff --git a/lib/src/models/EmployeeModel.js b/lib/src/models/EmployeeModel.js index 9edd188..8649c26 100644 --- a/lib/src/models/EmployeeModel.js +++ b/lib/src/models/EmployeeModel.js @@ -50,6 +50,41 @@ class EmployeeModel extends BaseModel { return this.store.data.byName[name]; } + /** + * @description Get the metadata for employees by employee id + * @param {String} id + * @returns {Object} {total, results} + */ + async getMetadata(id){ + let state = this.store.data.byMetadata[id]; + try { + if ( state && state.state === 'loading' ){ + await state.request + } else { + await this.service.getMetadata(id); + } + } catch(e) {} + return this.store.data.byMetadata[id]; + } + + /** + * @description Update the metadata for employees by employee id + * @param {Object} data + * @param {String} id + * @returns {Object} {total, results} + */ + async updateMetadata(data, id) { + let state = this.store.data.updateMetadata[id]; + try { + if ( state && state.state === 'loading' ){ + await state.request + } else { + await this.service.updateMetadata(data, id) ; + } + } catch(e) {} + return this.store.data.updateMetadata[id]; + } + } const model = new EmployeeModel(); diff --git a/lib/src/services/EmployeeService.js b/lib/src/services/EmployeeService.js index ae576d4..ab8d94a 100644 --- a/lib/src/services/EmployeeService.js +++ b/lib/src/services/EmployeeService.js @@ -1,6 +1,7 @@ import BaseService from './BaseService.js'; import EmployeeStore from '../stores/EmployeeStore.js'; + class EmployeeService extends BaseService { constructor() { @@ -30,6 +31,34 @@ class EmployeeService extends BaseService { }); } + getMetadata(id){ + + return this.request({ + url : `/api/employees/metadata/${id}`, + onLoading : request => this.store.byMetadataLoading(request, id), + checkCached : () => this.store.data.byMetadata[id], + onLoad : result => this.store.byMetadataLoaded(result.body, id), + onError : e => this.store.byMetadataError(e, id) + }); + } + + + updateMetadata(payload, id){ + return this.request({ + url : `/api/employees/metadata/${id}`, + fetchOptions : { + method : 'POST', + body : payload + }, + json: true, + onLoading : request => this.store.updateMetadataLoading(request, id, payload), + checkCached: () => this.store.data.updateMetadata[id], + onLoad : result => this.store.updateMetadataLoaded(result.body, id), + onError : e => this.store.updateMetadataError(e, id, payload) + }); + } + + } const service = new EmployeeService(); diff --git a/lib/src/stores/EmployeeStore.js b/lib/src/stores/EmployeeStore.js index 0a4892a..bd86eec 100644 --- a/lib/src/stores/EmployeeStore.js +++ b/lib/src/stores/EmployeeStore.js @@ -8,10 +8,14 @@ class EmployeeStore extends BaseStore { this.data = { directReports: {}, byName: {}, + byMetadata: {}, + updateMetadata: {} }; this.events = { DIRECT_REPORTS_FETCHED: 'direct-reports-fetched', - EMPLOYEES_BY_NAME_FETCHED: 'employees-by-name-fetched' + EMPLOYEES_BY_NAME_FETCHED: 'employees-by-name-fetched', + METADATA_FETCHED: 'metadata-fetched', + METADATA_UPDATED: 'metadata-updated' }; } getDirectReportsLoading(request) { @@ -66,6 +70,59 @@ class EmployeeStore extends BaseStore { this.emit(this.events.EMPLOYEES_BY_NAME_FETCHED, state); } + byMetadataLoading(request, id) { + this._setByMetadataState({ + state : this.STATE.LOADING, + request + }, id); + } + + byMetadataLoaded(payload, id) { + this._setByMetadataState({ + state : this.STATE.LOADED, + payload + }, id); + } + + byMetadataError(error, id) { + this._setByMetadataState({ + state : this.STATE.ERROR, + error + }, id); + } + + _setByMetadataState(state, id) { + this.data.byMetadata[id] = state; + this.emit(this.events.METADATA_FETCHED, state); + } + + updateMetadataLoading(request, id) { + this._setUpdatedMetadataState({ + state : this.STATE.LOADING, + request + }, id); + } + + updateMetadataLoaded(payload, id) { + this._setUpdatedMetadataState({ + state : this.STATE.LOADED, + payload + }, id); + } + + updateMetadataError(error, id) { + this._setUpdatedMetadataState({ + state : this.STATE.ERROR, + error + }, id); + } + + _setUpdatedMetadataState(state, id) { + this.data.updateMetadata[id] = state; + this.emit(this.events.METADATA_UPDATED, state); + } + + } const store = new EmployeeStore(); diff --git a/lib/src/utils/employees.js b/lib/src/utils/employees.js index 1a3129f..f708e42 100644 --- a/lib/src/utils/employees.js +++ b/lib/src/utils/employees.js @@ -118,6 +118,42 @@ class UcdlibEmployees { return out; } + /** + * @description Update employee's metadata + * @param {String} id - employee id + * @param {String} key - metadata key + * @param {Text} value - metadata value + * @param {String} idType - id, iamId, employeeId, userId, email + * @returns + */ + async updateMetadata(id, key, value, employeeId, idType="employeeId") { + const toUpdate = {}; + + if ( !id ) { + return pg.returnError('id is required when updating updating'); + } + + toUpdate['metadata_key'] = key; + toUpdate['metadata_value'] = value; + toUpdate['employee_id'] = employeeId; + + + if ( !Object.keys(toUpdate).length ){ + return pg.returnError('no valid fields to update'); + } + + const updateClause = pg.toUpdateClause(toUpdate); + + const text = ` + UPDATE metadata SET ${updateClause.sql} + WHERE id = $${updateClause.values.length + 1} + RETURNING id + `; + + return await pg.query(text, [...updateClause.values, id]); + + } + /** * @description Returns employee by id * @param {String} id - employee id @@ -125,16 +161,18 @@ class UcdlibEmployees { * @param {Object} options - options object with the following properties: * @param {Boolean} options.returnGroups - return employee's groups * @param {Boolean} options.returnSupervisor - return employee's supervisor + * @param {Boolean} options.includeMetadata - return employee's metadata * @returns */ async getById(id, idType='id', options={}){ if ( !Array.isArray(id) ) id = [id]; const params = id; - const { returnGroups, returnSupervisor } = options; + const { returnGroups, returnSupervisor, includeMetadata=false} = options; const text = ` SELECT e.* ${returnGroups ? `, json_agg(${this.groupJson()}) as groups` : ''} ${returnSupervisor ? `, ${this.supervisorJson()} as supervisor` : ''} + ${includeMetadata ? `, COALESCE(json_agg(${this.metadataJson()}), '[]') as metadata`: ''} FROM employees as e ${returnGroups ? ` LEFT JOIN group_membership as gm on e.id = gm.employee_key @@ -144,15 +182,20 @@ class UcdlibEmployees { ${returnSupervisor ? ` LEFT JOIN employees as supervisor on e.supervisor_id = supervisor.iam_id ` : ''} + ${includeMetadata ? ` + LEFT JOIN metadata as md on e.employee_id = md.employee_id + ` : ''} WHERE e.${TextUtils.underscore(idType)} IN ${pg.valuesArray(params)} ${returnGroups ? 'AND (NOT g.archived OR g.archived IS NULL)' : ''} - ${returnGroups || returnSupervisor ? 'GROUP BY e.id' : ''} + ${returnGroups || returnSupervisor || includeMetadata? 'GROUP BY e.id' : ''} ${returnSupervisor ? ', supervisor.id' : ''} `; return await pg.query(text, params); } + + /** * @description Returns employee by any unique id type * @param {Object} ids - object with any of the following properties: iamId, employeeId, userId, email @@ -385,6 +428,22 @@ class UcdlibEmployees { ` } + /** + * @description Return 'json_build_object' SQL function for a metadata + * @returns {String} + */ + metadataJson(){ + const metadataTable = 'md'; + return ` + json_build_object( + 'id', ${metadataTable}.id, + 'metadataKey', ${metadataTable}.metadata_key, + 'metadataValue', ${metadataTable}.metadata_value, + 'employeeId', ${metadataTable}.employee_id + ) + ` + } + /** * @description Convert an employee db record to a brief object diff --git a/utils/api/routes/employees.js b/utils/api/routes/employees.js index 64fbab1..37c6077 100644 --- a/utils/api/routes/employees.js +++ b/utils/api/routes/employees.js @@ -56,6 +56,45 @@ export default ( api ) => { }); + + api.get(`${route}/metadata/:id`, async (req, res) => { + + // query for employee + if ( !req.params.id ) { + return res.status(400).json({ + error: 'Missing employee identifier' + }); + } + + }); + + + api.post(`${route}/metadata/:id?`, async (req, res) => { + + // query for employee + if ( !req.params.id ) { + return res.status(400).json({ + error: 'Missing employee identifier' + }); + } + + console.log(req.query); + + // const queryOptions = getQueryOptions(req); + // const employeeName = queryOptions.name || ''; + + // const results = await UcdlibEmployees.searchByName(employeeName, queryOptions); + // if ( results.err ) { + // return res.status(400).json({ + // error: 'Error getting employees' + // }); + // } + + // res.json(results.res.rows); + + }); + + /** * @description Get an employee by identifier * url params: From b2050c339547d63deeefadb7baf30c7e9b3f6a57 Mon Sep 17 00:00:00 2001 From: Sabrina Baggett Date: Wed, 23 Apr 2025 22:11:02 -0700 Subject: [PATCH 2/4] add to api to route --- utils/api/routes/employees.js | 40 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/utils/api/routes/employees.js b/utils/api/routes/employees.js index 37c6077..53e7077 100644 --- a/utils/api/routes/employees.js +++ b/utils/api/routes/employees.js @@ -65,11 +65,23 @@ export default ( api ) => { error: 'Missing employee identifier' }); } + + const out = { + total: 0, + results: [], + } + + const r = await UcdlibEmployees.getById(req.params.id, "employeeId", {includeMetadata:true}) + + out.total = r.res.rowCount; + out.results = r.res.rows; + + return res.json(out); }); - api.post(`${route}/metadata/:id?`, async (req, res) => { + api.post(`${route}/metadata/:id`, async (req, res) => { // query for employee if ( !req.params.id ) { @@ -78,19 +90,23 @@ export default ( api ) => { }); } - console.log(req.query); - - // const queryOptions = getQueryOptions(req); - // const employeeName = queryOptions.name || ''; + const out = { + total: 0, + results: [], + } + + const id = req.body[0].id; + const employee_id = req.body[0].employeeId; + const metadataKey = req.body[0].metadataKey; + const metadataValue = req.body[0].metadataValue; - // const results = await UcdlibEmployees.searchByName(employeeName, queryOptions); - // if ( results.err ) { - // return res.status(400).json({ - // error: 'Error getting employees' - // }); - // } - // res.json(results.res.rows); + const results = await UcdlibEmployees.updateMetadata(id, metadataKey, metadataValue, employee_id) + + out.total = results.res.rowCount; + out.results = results.res.rows; + + return res.json(out); }); From 8f0940652fe83b34229d5a0f6f6d5e798bf44d4c Mon Sep 17 00:00:00 2001 From: Sabrina Baggett Date: Fri, 2 May 2025 19:02:47 -0700 Subject: [PATCH 3/4] update for pull request --- app/api/employees.js | 27 +++++-- .../pages/ucdlib-iam-page-user-settings.js | 76 ++++++++++++------- .../ucdlib-iam-page-user-settings.tpl.js | 10 ++- app/client/src/models/AppStateModel.js | 1 - .../db-entrypoint/001-create-empty-db.sql | 6 -- deploy/utils/db-entrypoint/005-metadata.sql | 6 ++ lib/src/models/EmployeeModel.js | 4 +- lib/src/services/EmployeeService.js | 14 ++-- lib/src/stores/EmployeeStore.js | 12 +-- lib/src/utils/employees.js | 15 ++-- utils/api/routes/employees.js | 55 -------------- 11 files changed, 103 insertions(+), 123 deletions(-) create mode 100644 deploy/utils/db-entrypoint/005-metadata.sql diff --git a/app/api/employees.js b/app/api/employees.js index 2986f2c..a497855 100644 --- a/app/api/employees.js +++ b/app/api/employees.js @@ -63,7 +63,11 @@ export default (api) => { return res.json(out); }); - api.get(`/employees/metadata/:id`, async (req, res) => { +/** + * @description get the metadata for the employee + * - id: search by employee id +*/ + api.get(`/employees/:id/metadata`, async (req, res) => { // query for employee if ( !req.params.id ) { @@ -72,12 +76,17 @@ export default (api) => { }); } + const id_type = req.query.idType; + + let person = await UcdlibEmployees.getById(req.params.id, id_type); + let employeeId = person.res.rows[0].id; + const out = { total: 0, results: [], } - const r = await UcdlibEmployees.getById(req.params.id, "employeeId", {includeMetadata:true}) + const r = await UcdlibEmployees.getById(employeeId, "id", {includeMetadata:true}) out.total = r.res.rowCount; out.results = r.res.rows; @@ -86,7 +95,11 @@ export default (api) => { }); - api.post(`/employees/metadata/:id`, async (req, res) => { + /** + * @description post the updated metadata for the employee + * - id: search by employee id + */ + api.post(`/employees/:id/metadata/`, async (req, res) => { // query for employee if ( !req.params.id ) { @@ -100,13 +113,11 @@ export default (api) => { results: [], } - const id = req.body[0].id; - const employee_id = req.body[0].employeeId; - const metadataKey = req.body[0].metadataKey; - const metadataValue = req.body[0].metadataValue; + const id = req.body.id; + const metadataValue = req.body.metadataValue; - const results = await UcdlibEmployees.updateMetadata(id, metadataKey, metadataValue, employee_id) + const results = await UcdlibEmployees.updateMetadata(id, metadataValue) out.total = results.res.rowCount; out.results = results.res.rows; diff --git a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js index 43b4ea9..9d75692 100644 --- a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js +++ b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.js @@ -20,14 +20,14 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) super(); this.render = render.bind(this); this._injectModel('AppStateModel','AuthModel', 'EmployeeModel'); - this.token = this.AuthModel.getToken().token || null; - this.employeeId = this.AuthModel.getToken().token.employeeId || null; - this.firstName = this.AuthModel.getToken().token.given_name || null; - this.lastName = this.AuthModel.getToken().token.family_name || null; + this.token = null; + this.firstName = null; + this.lastName = null; this.metadata = []; this.updateList = []; this.noChange = true; - + this.enableDirectReportNotification = null; + this.initialDirectReportNotification = null; } /** @@ -42,13 +42,16 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) /* Add the check for the new user_setting enable flag */ /** - * @method handleCheckboxChange - * @description handle the checkbox changing + * @method handleDirectReportChange + * @description handle the checkbox changing direct report * * @param {Object} e */ handleDirectReportChange(e) { - this.noChange = e.target.checked === this.enableDirectReportNotification; + + this.enableDirectReportNotification = e.target.checked; + this.noChange = this.initialDirectReportNotification === this.enableDirectReportNotification; + this.requestUpdate(); } @@ -62,8 +65,15 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) async _onAppStateUpdate(e) { if ( e.page != this.id ) return; this.AppStateModel.showLoaded(this.id); + + this.token = this.AuthModel.getToken().token || null; + this.firstName = this.token.given_name || null; + this.lastName = this.token.family_name || null; + this.iamId = this.token.iamId || null; + this.ccReportsToolTip = "By default, only DIRECT supervisors are CCed on their employee's RT tickets. Checking this box will ensure you are CCed on RT tickets submitted by anyone below you in your reporting line."; + + const r = await this.EmployeeModel.getMetadata(this.iamId, "iamId"); - const r = await this.EmployeeModel.getMetadata(this.employeeId); this.metadata = r.payload.results[0].metadata; this.currentDirectReportStatus(); @@ -77,10 +87,17 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) * @description get the current Direct Report Status */ async currentDirectReportStatus(){ - let meta = this.metadata.find(m => m.metadataKey === "direct_reports_notification"); - this.directReportsId = meta.id; - meta = meta ? meta.metadataValue.toLowerCase() : false; - this.enableDirectReportNotification = meta === "true" ? true: false; + let meta = this.metadata.find(m => m.metadataKey === "cc_notification"); + this.directReportsId = meta.metadataId; + if(typeof meta.metadataValue === 'string') { + meta = meta ? meta.metadataValue.toLowerCase() : false; + this.initialDirectReportNotification = meta === "true" ? true: false; + + } else { + meta = meta.metadataValue; + this.initialDirectReportNotification = meta; + } + this.enableDirectReportNotification = this.initialDirectReportNotification; this.requestUpdate(); } @@ -96,27 +113,28 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) if ( this.updateInProgress ) return; this.updateInProgress = true; - const checkboxes = this.renderRoot.querySelectorAll('input[type="checkbox"]'); - - checkboxes.forEach(checkbox => { - if(checkbox.name == "direct-reports") { - this.updateList.push({ - id: this.directReportsId, - employeeId: this.employeeId, - metadataKey: "direct_reports_notification", - metadataValue: String(checkbox.checked) - }); - } - - /* Add future if statements for metadata here */ + this.updateList.push({ + id: this.directReportsId, + metadataValue: this.enableDirectReportNotification }); + + /* Add future object statements for metadata here */ + + let r = []; + + for(let element of this.updateList){ + let res = await this.EmployeeModel.updateMetadata(element, this.iamId); + r.push(res); + } + + const allLoaded = r.every(item => item.state === 'loaded'); + const hasIs400 = r.some(item => item.error?.payload?.is400 === true); - const r = await this.EmployeeModel.updateMetadata(this.updateList, this.employeeId); let successText = `${this.firstName} ${this.lastName}'s User Settings has been updated.`; - if ( r.state === 'loaded' ) { + if (allLoaded) { this.AppStateModel.refresh(); setTimeout(() => { this.AppStateModel.showAlertBanner({message: successText, brandColor: 'quad'}); @@ -124,7 +142,7 @@ export default class UcdlibIamPageUserSettings extends Mixin(LitElement) this.updateInProgress = false; } else { - if ( r.error?.payload?.is400 ) { + if ( hasIs400) { this.requestUpdate(); this.AppStateModel.showAlertBanner({message: 'Error when updating the user settings. Form data needs fixing.', brandColor: 'double-decker'}); this.logger.error('Error in form updating user settings', r); diff --git a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js index 2dfd23a..4979800 100644 --- a/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js +++ b/app/client/src/elements/pages/ucdlib-iam-page-user-settings.tpl.js @@ -1,7 +1,11 @@ import { html } from 'lit'; +/** + * @description Main render function for this element + * @returns {TemplateResult} + */ export function render() { -return html` + return html`
    @@ -15,7 +19,9 @@ return html` ?disabled=${this.updateInProgress} ?checked=${this.enableDirectReportNotification} @change=${this.handleDirectReportChange}> - +
diff --git a/app/client/src/models/AppStateModel.js b/app/client/src/models/AppStateModel.js index 76c0272..20cdc87 100644 --- a/app/client/src/models/AppStateModel.js +++ b/app/client/src/models/AppStateModel.js @@ -98,7 +98,6 @@ class AppStateModelImpl extends AppStateModel { update.location.path[0] == 'settings' && update.location.path.length > 1 ) { - console.log("S:", update.location.path); p = 'settings'; } else if( update.location.path[0] == 'patron' && diff --git a/deploy/utils/db-entrypoint/001-create-empty-db.sql b/deploy/utils/db-entrypoint/001-create-empty-db.sql index 4b6ec62..917ce22 100644 --- a/deploy/utils/db-entrypoint/001-create-empty-db.sql +++ b/deploy/utils/db-entrypoint/001-create-empty-db.sql @@ -130,12 +130,6 @@ CREATE TABLE job_logs ( data jsonb NOT NULL DEFAULT '{}'::jsonb, created timestamp DEFAULT NOW() ); -CREATE TABLE metadata ( - id SERIAL PRIMARY KEY, - metadata_key varchar(50) NOT NULL, - metadata_value text NOT NULL, - employee_id varchar(20) -); -- Request Statuses --1 diff --git a/deploy/utils/db-entrypoint/005-metadata.sql b/deploy/utils/db-entrypoint/005-metadata.sql new file mode 100644 index 0000000..b77aaaf --- /dev/null +++ b/deploy/utils/db-entrypoint/005-metadata.sql @@ -0,0 +1,6 @@ +CREATE TABLE metadata ( + metadata_id SERIAL PRIMARY KEY, + metadata_key varchar(50) NOT NULL, + metadata_value jsonb, + employee_id integer REFERENCES employees (id) +); \ No newline at end of file diff --git a/lib/src/models/EmployeeModel.js b/lib/src/models/EmployeeModel.js index 8649c26..9198347 100644 --- a/lib/src/models/EmployeeModel.js +++ b/lib/src/models/EmployeeModel.js @@ -55,13 +55,13 @@ class EmployeeModel extends BaseModel { * @param {String} id * @returns {Object} {total, results} */ - async getMetadata(id){ + async getMetadata(id, idType){ let state = this.store.data.byMetadata[id]; try { if ( state && state.state === 'loading' ){ await state.request } else { - await this.service.getMetadata(id); + await this.service.getMetadata(id, idType); } } catch(e) {} return this.store.data.byMetadata[id]; diff --git a/lib/src/services/EmployeeService.js b/lib/src/services/EmployeeService.js index ab8d94a..cfc2781 100644 --- a/lib/src/services/EmployeeService.js +++ b/lib/src/services/EmployeeService.js @@ -31,21 +31,23 @@ class EmployeeService extends BaseService { }); } - getMetadata(id){ + getMetadata(id, idType){ + const params = new URLSearchParams(); + params.set('idType', idType); return this.request({ - url : `/api/employees/metadata/${id}`, - onLoading : request => this.store.byMetadataLoading(request, id), + url : `/api/employees/${id}/metadata?${params.toString()}`, + onLoading : request => this.store.byMetadataLoading(request, id, idType), checkCached : () => this.store.data.byMetadata[id], - onLoad : result => this.store.byMetadataLoaded(result.body, id), - onError : e => this.store.byMetadataError(e, id) + onLoad : result => this.store.byMetadataLoaded(result.body, id, idType), + onError : e => this.store.byMetadataError(e, id, idType) }); } updateMetadata(payload, id){ return this.request({ - url : `/api/employees/metadata/${id}`, + url : `/api/employees/${id}/metadata`, fetchOptions : { method : 'POST', body : payload diff --git a/lib/src/stores/EmployeeStore.js b/lib/src/stores/EmployeeStore.js index bd86eec..7d1dcdc 100644 --- a/lib/src/stores/EmployeeStore.js +++ b/lib/src/stores/EmployeeStore.js @@ -70,25 +70,25 @@ class EmployeeStore extends BaseStore { this.emit(this.events.EMPLOYEES_BY_NAME_FETCHED, state); } - byMetadataLoading(request, id) { + byMetadataLoading(request, id, idType) { this._setByMetadataState({ state : this.STATE.LOADING, request - }, id); + }, id, idType); } - byMetadataLoaded(payload, id) { + byMetadataLoaded(payload, id, idType) { this._setByMetadataState({ state : this.STATE.LOADED, payload - }, id); + }, id, idType); } - byMetadataError(error, id) { + byMetadataError(error, id, idType) { this._setByMetadataState({ state : this.STATE.ERROR, error - }, id); + }, id, idType); } _setByMetadataState(state, id) { diff --git a/lib/src/utils/employees.js b/lib/src/utils/employees.js index f708e42..07cb0e0 100644 --- a/lib/src/utils/employees.js +++ b/lib/src/utils/employees.js @@ -126,17 +126,14 @@ class UcdlibEmployees { * @param {String} idType - id, iamId, employeeId, userId, email * @returns */ - async updateMetadata(id, key, value, employeeId, idType="employeeId") { + async updateMetadata(id, value) { const toUpdate = {}; if ( !id ) { return pg.returnError('id is required when updating updating'); } - toUpdate['metadata_key'] = key; toUpdate['metadata_value'] = value; - toUpdate['employee_id'] = employeeId; - if ( !Object.keys(toUpdate).length ){ return pg.returnError('no valid fields to update'); @@ -146,8 +143,8 @@ class UcdlibEmployees { const text = ` UPDATE metadata SET ${updateClause.sql} - WHERE id = $${updateClause.values.length + 1} - RETURNING id + WHERE metadata_id = $${updateClause.values.length + 1} + RETURNING metadata_id `; return await pg.query(text, [...updateClause.values, id]); @@ -165,6 +162,7 @@ class UcdlibEmployees { * @returns */ async getById(id, idType='id', options={}){ + if ( !Array.isArray(id) ) id = [id]; const params = id; const { returnGroups, returnSupervisor, includeMetadata=false} = options; @@ -183,7 +181,7 @@ class UcdlibEmployees { LEFT JOIN employees as supervisor on e.supervisor_id = supervisor.iam_id ` : ''} ${includeMetadata ? ` - LEFT JOIN metadata as md on e.employee_id = md.employee_id + LEFT JOIN metadata as md on e.id = md.employee_id ` : ''} WHERE e.${TextUtils.underscore(idType)} IN ${pg.valuesArray(params)} @@ -191,6 +189,7 @@ class UcdlibEmployees { ${returnGroups || returnSupervisor || includeMetadata? 'GROUP BY e.id' : ''} ${returnSupervisor ? ', supervisor.id' : ''} `; + return await pg.query(text, params); } @@ -436,7 +435,7 @@ class UcdlibEmployees { const metadataTable = 'md'; return ` json_build_object( - 'id', ${metadataTable}.id, + 'metadataId', ${metadataTable}.metadata_id, 'metadataKey', ${metadataTable}.metadata_key, 'metadataValue', ${metadataTable}.metadata_value, 'employeeId', ${metadataTable}.employee_id diff --git a/utils/api/routes/employees.js b/utils/api/routes/employees.js index 53e7077..64fbab1 100644 --- a/utils/api/routes/employees.js +++ b/utils/api/routes/employees.js @@ -56,61 +56,6 @@ export default ( api ) => { }); - - api.get(`${route}/metadata/:id`, async (req, res) => { - - // query for employee - if ( !req.params.id ) { - return res.status(400).json({ - error: 'Missing employee identifier' - }); - } - - const out = { - total: 0, - results: [], - } - - const r = await UcdlibEmployees.getById(req.params.id, "employeeId", {includeMetadata:true}) - - out.total = r.res.rowCount; - out.results = r.res.rows; - - return res.json(out); - - }); - - - api.post(`${route}/metadata/:id`, async (req, res) => { - - // query for employee - if ( !req.params.id ) { - return res.status(400).json({ - error: 'Missing employee identifier' - }); - } - - const out = { - total: 0, - results: [], - } - - const id = req.body[0].id; - const employee_id = req.body[0].employeeId; - const metadataKey = req.body[0].metadataKey; - const metadataValue = req.body[0].metadataValue; - - - const results = await UcdlibEmployees.updateMetadata(id, metadataKey, metadataValue, employee_id) - - out.total = results.res.rowCount; - out.results = results.res.rows; - - return res.json(out); - - }); - - /** * @description Get an employee by identifier * url params: From d5f13b91a90f1752862db7048d06a44bb2481a37 Mon Sep 17 00:00:00 2001 From: Sabrina Baggett Date: Fri, 2 May 2025 20:29:00 -0700 Subject: [PATCH 4/4] small change to org chart title --- app/client/src/elements/pages/ucdlib-iam-page-orgchart.tpl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/elements/pages/ucdlib-iam-page-orgchart.tpl.js b/app/client/src/elements/pages/ucdlib-iam-page-orgchart.tpl.js index 3f5cc38..c5be410 100644 --- a/app/client/src/elements/pages/ucdlib-iam-page-orgchart.tpl.js +++ b/app/client/src/elements/pages/ucdlib-iam-page-orgchart.tpl.js @@ -16,7 +16,7 @@ export function render() {

Upload the most recent csv for the organizational chart here. Make sure to use the format which includes headers and the required - columns:
(Lived Name, EE ID, Email, Notes, Department Name, Working Title, Appointment Type Code, Supervisor ID). + columns:
(Lived Name, External ID, Email, Notes, Department Name, Working Title, Appointment Type Code, External ID Reports To).