diff --git a/app/api/employees.js b/app/api/employees.js
index fb28f95..a497855 100644
--- a/app/api/employees.js
+++ b/app/api/employees.js
@@ -62,4 +62,67 @@ export default (api) => {
return res.json(out);
});
+
+/**
+ * @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 ) {
+ return res.status(400).json({
+ error: 'Missing employee identifier'
+ });
+ }
+
+ 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(employeeId, "id", {includeMetadata:true})
+
+ out.total = r.res.rowCount;
+ out.results = r.res.rows;
+
+ return res.json(out);
+
+ });
+
+ /**
+ * @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 ) {
+ return res.status(400).json({
+ error: 'Missing employee identifier'
+ });
+ }
+
+ const out = {
+ total: 0,
+ results: [],
+ }
+
+ const id = req.body.id;
+ const metadataValue = req.body.metadataValue;
+
+
+ const results = await UcdlibEmployees.updateMetadata(id, metadataValue)
+
+ 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-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).
+
+`;}
\ 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..20cdc87 100644
--- a/app/client/src/models/AppStateModel.js
+++ b/app/client/src/models/AppStateModel.js
@@ -94,6 +94,11 @@ class AppStateModelImpl extends AppStateModel {
update.location.path.length > 1
) {
p = 'orgchart';
+ } else if(
+ update.location.path[0] == 'settings' &&
+ update.location.path.length > 1
+ ) {
+ p = 'settings';
} else if(
update.location.path[0] == 'patron' &&
update.location.path.length > 1
@@ -142,6 +147,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 +237,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 9e805ae..ceb0f1f 100644
--- a/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml
+++ b/deploy/compose/ucdlib-iam-support-local-dev/compose.yaml
@@ -143,7 +143,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/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 9edd188..9198347 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, idType){
+ let state = this.store.data.byMetadata[id];
+ try {
+ if ( state && state.state === 'loading' ){
+ await state.request
+ } else {
+ await this.service.getMetadata(id, idType);
+ }
+ } 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..cfc2781 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,36 @@ class EmployeeService extends BaseService {
});
}
+ getMetadata(id, idType){
+ const params = new URLSearchParams();
+ params.set('idType', idType);
+
+ return this.request({
+ 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, idType),
+ onError : e => this.store.byMetadataError(e, id, idType)
+ });
+ }
+
+
+ updateMetadata(payload, id){
+ return this.request({
+ url : `/api/employees/${id}/metadata`,
+ 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..7d1dcdc 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, idType) {
+ this._setByMetadataState({
+ state : this.STATE.LOADING,
+ request
+ }, id, idType);
+ }
+
+ byMetadataLoaded(payload, id, idType) {
+ this._setByMetadataState({
+ state : this.STATE.LOADED,
+ payload
+ }, id, idType);
+ }
+
+ byMetadataError(error, id, idType) {
+ this._setByMetadataState({
+ state : this.STATE.ERROR,
+ error
+ }, id, idType);
+ }
+
+ _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..07cb0e0 100644
--- a/lib/src/utils/employees.js
+++ b/lib/src/utils/employees.js
@@ -118,6 +118,39 @@ 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, value) {
+ const toUpdate = {};
+
+ if ( !id ) {
+ return pg.returnError('id is required when updating updating');
+ }
+
+ toUpdate['metadata_value'] = value;
+
+ 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 metadata_id = $${updateClause.values.length + 1}
+ RETURNING metadata_id
+ `;
+
+ return await pg.query(text, [...updateClause.values, id]);
+
+ }
+
/**
* @description Returns employee by id
* @param {String} id - employee id
@@ -125,16 +158,19 @@ 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 +180,21 @@ class UcdlibEmployees {
${returnSupervisor ? `
LEFT JOIN employees as supervisor on e.supervisor_id = supervisor.iam_id
` : ''}
+ ${includeMetadata ? `
+ LEFT JOIN metadata as md on e.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 +427,22 @@ class UcdlibEmployees {
`
}
+ /**
+ * @description Return 'json_build_object' SQL function for a metadata
+ * @returns {String}
+ */
+ metadataJson(){
+ const metadataTable = 'md';
+ return `
+ json_build_object(
+ 'metadataId', ${metadataTable}.metadata_id,
+ 'metadataKey', ${metadataTable}.metadata_key,
+ 'metadataValue', ${metadataTable}.metadata_value,
+ 'employeeId', ${metadataTable}.employee_id
+ )
+ `
+ }
+
/**
* @description Convert an employee db record to a brief object