From 4d99dc91e8568bc9ce9e6fc5c3bef6a75fb53bfa Mon Sep 17 00:00:00 2001 From: Luke Ogburn <21106956+logburn@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:41:56 -0500 Subject: [PATCH 1/2] saving --- src/components/access-control/GroupsMenu.vue | 46 ------- .../{GroupFlags.vue => RoleFlags.vue} | 16 +-- src/components/access-control/RolesMenu.vue | 40 ++++++ src/components/contracts/ContractsTable.vue | 52 ++++--- .../reports/ReportsCertifications.vue | 2 +- src/shared/api.js | 30 +++- src/utils/storeUtils.js | 22 +-- src/views/AccessControl.vue | 128 +++++++++--------- store/index.js | 14 +- 9 files changed, 186 insertions(+), 164 deletions(-) delete mode 100644 src/components/access-control/GroupsMenu.vue rename src/components/access-control/{GroupFlags.vue => RoleFlags.vue} (50%) create mode 100644 src/components/access-control/RolesMenu.vue diff --git a/src/components/access-control/GroupsMenu.vue b/src/components/access-control/GroupsMenu.vue deleted file mode 100644 index f2e253005..000000000 --- a/src/components/access-control/GroupsMenu.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/src/components/access-control/GroupFlags.vue b/src/components/access-control/RoleFlags.vue similarity index 50% rename from src/components/access-control/GroupFlags.vue rename to src/components/access-control/RoleFlags.vue index aeebfd4c1..d1f9fc2a9 100644 --- a/src/components/access-control/GroupFlags.vue +++ b/src/components/access-control/RoleFlags.vue @@ -4,32 +4,24 @@ class="d-inline-block pr-2" v-model="flags.showOnMemberProfile" label="Show on employee profile" - v-tooltip="{text: `Show employees who their ${groupName.toLowerCase()} is`, location: 'top', offset: -10 }" + v-tooltip="{text: `Show employees who their ${roleName.toLowerCase()} is`, location: 'top', offset: -10 }" /> diff --git a/src/components/access-control/RolesMenu.vue b/src/components/access-control/RolesMenu.vue new file mode 100644 index 000000000..8d40055ae --- /dev/null +++ b/src/components/access-control/RolesMenu.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/contracts/ContractsTable.vue b/src/components/contracts/ContractsTable.vue index 51a7c607c..788e4acac 100644 --- a/src/components/contracts/ContractsTable.vue +++ b/src/components/contracts/ContractsTable.vue @@ -255,7 +255,7 @@ import _forEach from 'lodash/forEach'; import _some from 'lodash/some'; import _map from 'lodash/map'; import api from '@/shared/api'; -import { updateStoreEmployees } from '@/utils/storeUtils'; +import { updateStoreEmployees, updateStoreContracts, updateStoreAccessRoles } from '@/utils/storeUtils'; import { asyncForEach, isMobile } from '@/utils/utils'; import { getProject, getProjectCurrentEmployees } from '@/shared/contractUtils'; import { contractFilter } from '@/shared/filterUtils'; @@ -275,7 +275,6 @@ import { ref, inject, onBeforeMount, onBeforeUnmount, computed, watch } from 'vu import { useDisplay } from 'vuetify'; import { useStore } from 'vuex'; import { useDisplayError, useDisplaySuccess } from '@/components/shared/StatusSnackbar.vue'; -import { updateStoreContracts } from '../../utils/storeUtils'; // |--------------------------------------------------| // | | @@ -405,6 +404,13 @@ const contractHeaders = ref([ class: 'smaller-text description', type: 'textarea' }, + { + title: 'Access Control', + key: 'accessControlLink', + align: 'start', + customWidth: 'large', + disableEdit: true + }, { title: 'Active Employees', key: 'activeEmployees', @@ -423,6 +429,8 @@ const contractHeaders = ref([ const contractHeadcounts = ref({}); const form = ref(null); +let accessControl = {}; + // |--------------------------------------------------| // | | // | LIFECYCLE HOOKS | @@ -490,6 +498,21 @@ onBeforeMount(async () => { emitter.on('toggle-project-checkBox', ({ contract, project }) => { toggleProjectCheckBox(contract, project); }); + + // get any missing data from API + let [contractAC, projectAC] = await Promise.all([ + api.getContractAccessControl(), + api.getProjectAccessControl(), + store.getters.employees ? '' : updateStoreEmployees(), + store.getters.contracts ? '' : updateStoreContracts() + ]); + + accessControl = { + contract: contractAC, + project: projectAC + } + + // set everything for UI resetAllCheckBoxes(); expanded.value = _map(store.getters.contracts, 'id'); // expands all contracts in table await getContractEmployeesHeadcount(); // get headcounts for each contract @@ -646,8 +669,6 @@ async function clickedDelete() { * Fills in the contractHeadcounts variable. */ async function getContractEmployeesHeadcount() { - if (!store.getters.employees) await updateStoreEmployees(); - if (!store.getters.contracts) await updateStoreContracts(); for (let contract of store.getters.contracts) { let contractEmployees = new Set(); let projectEmployees; @@ -719,9 +740,6 @@ async function updateStatus(status) { * [{contract: "", prime: "", project: {...}, employees: [...]}, ...] */ async function getActiveEmployeeContractRelationships(contract, project = null) { - if (!store.getters.employees) { - await updateStoreEmployees(); - } let employees = store.getters.employees; let theRelationships = []; employees.forEach((e) => { @@ -778,9 +796,6 @@ async function getActiveEmployeeContractRelationships(contract, project = null) * [{contract: "", prime: "", project: {...}, employees: [...]}, ...] */ async function getEmployeeContractRelationships(contract, project = null) { - if (!store.getters.employees) { - await updateStoreEmployees(); - } let employees = store.getters.employees; let theRelationships = []; employees.forEach((e) => { @@ -1030,15 +1045,14 @@ const storeContracts = computed(() => { // get projects let projects = contract.projects.filter((p) => status.has(p.status)); - if (projects.length !== 0) { - for (let p of projects) p.checkBox = cbIndexProjects[p.id]; - contracts.push({ - ...contract, - ...cbIndexContracts[contract.id], - contractId: contract.id, // used for quick-edit - projects - }); - } + for (let p of (projects || [])) p.checkBox = cbIndexProjects[p.id]; + + contracts.push({ + ...contract, + ...cbIndexContracts[contract.id], + contractId: contract.id, // used for quick-edit + projects + }); } // :) diff --git a/src/components/reports/ReportsCertifications.vue b/src/components/reports/ReportsCertifications.vue index 39fac785f..8be3f4b38 100644 --- a/src/components/reports/ReportsCertifications.vue +++ b/src/components/reports/ReportsCertifications.vue @@ -170,7 +170,7 @@ function buildCertificationsColumns() { for (let emp of employeesInfo.value) { certIndex[emp.employeeNumber] = []; let activeCerts = certIndex[emp.employeeNumber]; // pointer-esque - for (let cert of (emp.certifications || [])) { + for (let cert of emp.certifications || []) { if (!cert.expirationDate || (isSameOrBefore(getTodaysDate(), cert.expirationDate))) { activeCerts.push(cert.name); } diff --git a/src/shared/api.js b/src/shared/api.js index 79ec40a2a..ba85a257a 100644 --- a/src/shared/api.js +++ b/src/shared/api.js @@ -16,7 +16,7 @@ const HIGH_FIVES = 'highFives'; const PTO_CASH_OUTS = 'ptoCashOuts'; const SETTINGS = 'settings'; const TAGS = 'tags'; -const ACCESS_GROUPS = 'accessGroups'; +const ACCESS_ROLES = 'accessRoles'; const API_HOSTNAME = API_CONFIG.apiHostname; const API_PORT = API_CONFIG.apiPort; const PORT = API_PORT === '443' ? '' : `:${API_PORT}`; @@ -506,7 +506,7 @@ async function getEmployeesFromAdp() { * @return - Array of IDs of employees who can see the user's data */ async function getAccessControlUsers(id) { - return await execute('get', `/${ACCESS_GROUPS}/employee/groupUsers/${id}`) + return await execute('get', `/${ACCESS_ROLES}/employee/roleUsers/${id}`) } /** @@ -516,7 +516,27 @@ async function getAccessControlUsers(id) { * @return - Array of IDs of employees who can see the user's data */ async function getUserProfileAccessControl(id) { - return await execute('get', `/${ACCESS_GROUPS}/employee/showOnProfile/${id}`) + return await execute('get', `/${ACCESS_ROLES}/link/showOnProfile/${id}`) +} + +/** + * Gets access control data linked to contracts and projects. + * Convenience wrapper for projects and contracts + * + * @return - Array of IDs of employees who can see the user's data + */ +async function getProjectAccessControl() { + return await execute('get', `/${ACCESS_ROLES}/link/projects`); +} + +/** + * Gets access control data linked to contracts and projects. + * Convenience wrapper for projects and contracts + * + * @return - Array of IDs of employees who can see the user's data + */ +async function getContractAccessControl() { + return await execute('get', `/${ACCESS_ROLES}/link/contracts`); } export default { @@ -562,13 +582,15 @@ export default { getEmployeesFromAdp, getAccessControlUsers, getUserProfileAccessControl, + getContractAccessControl, + getProjectAccessControl, EXPENSE_TYPES, EXPENSES, EMPLOYEES, CONTRACTS, PTO_CASH_OUTS, TAGS, - ACCESS_GROUPS, + ACCESS_ROLES, UTILITY, TIMESHEETS, AUDIT, diff --git a/src/utils/storeUtils.js b/src/utils/storeUtils.js index f339b4f16..2013802da 100644 --- a/src/utils/storeUtils.js +++ b/src/utils/storeUtils.js @@ -18,7 +18,7 @@ let isBudgetsLoading = false; let isPtoCashOutsLoading = false; let isExpenseTypesLoading = false; let isTagsLoading = false; -let isAccessGroupsLoading = false; +let isAccessRolesLoading = false; let employees = null; let user = null; @@ -29,7 +29,7 @@ let budgets = null; let ptoCashOuts = null; let expenseTypes = null; let tags = null; -let accessGroups = null; +let accessRoles = null; /** * Update store with latest user data @@ -202,17 +202,17 @@ export async function updateStoreTags() { } // updateStoreTags /** - * Update store with latest access group data + * Update store with latest access role data */ -export async function updateStoreAccessGroups() { +export async function updateStoreAccessRoles() { try { - if (!isAccessGroupsLoading) { - isAccessGroupsLoading = true; - accessGroups = api.getItems(api.TAGS); + if (!isAccessRolesLoading) { + isAccessRolesLoading = true; + accessRoles = api.getItems(api.TAGS); } - accessGroups = await accessGroups; - store.dispatch('setAcessGroups', { accessGroups }); - isAccessGroupsLoading = false; + accessRoles = await accessRoles; + store.dispatch('setAcessRoles', { accessRoles }); + isAccessRolesLoading = false; } catch (err) { console.error(err); } @@ -228,5 +228,5 @@ export default { updateStorePtoCashOuts, updateStoreExpenseTypes, updateStoreTags, - updateStoreAccessGroups + updateStoreAccessRoles }; diff --git a/src/views/AccessControl.vue b/src/views/AccessControl.vue index 332d99249..8bae21762 100644 --- a/src/views/AccessControl.vue +++ b/src/views/AccessControl.vue @@ -8,55 +8,55 @@

Roles

- +
-
- Select a group to edit, or press - +
+ Select a role to edit, or press + to make a new one.
- +

Assignments

- +
- +

Flags

- +

Permissions

- +

Admins always has full access to the Portal and all its data.

@@ -65,7 +65,7 @@ color="green-darken-1" class="ml-2" append-icon="mdi-content-save" - @click="saveCurrentGroup()" + @click="saveCurrentRole()" :disabled="saving" > {{ saveText }} @@ -74,8 +74,8 @@ :variant="userIsSure ? 'tonal' : 'outlined'" class="ml-2 delete-button" :append-icon="userIsSure ? '' : 'mdi-delete'" - :disabled="isAdmin(editGroup)" - @click="deleteCurrentGroup()" + :disabled="isAdmin(editRole)" + @click="deleteCurrentRole()" :color="userIsSure ? 'red-darken-1' : undefined" > {{ deleteText }} @@ -95,7 +95,7 @@ import { inject, ref, watch, computed, onBeforeMount, onUnmounted, nextTick } fr import { useStore } from 'vuex'; import { useDisplaySuccess, useDisplayError } from '@/components/shared/StatusSnackbar.vue'; import Assignments from '@/components/access-control/Assignments.vue'; -import GroupFlags from '@/components/access-control/GroupFlags.vue'; +import RoleFlags from '@/components/access-control/RoleFlags.vue'; import Permissions from '@/components/access-control/Permissions.vue'; // JS/utility imports import { updateStoreContracts, updateStoreEmployees, updateStoreTags } from '@/utils/storeUtils'; @@ -111,48 +111,48 @@ function aOrAn(word) { return 'aeiouAEIOU'.split('').includes(word.charAt(0)) ? // UI const loading = ref(false); -const groups = ref([]); -const editGroupIndex = ref(0); // index in array -const editGroup = computed(() => groups.value?.[editGroupIndex.value]); -const emptyGroup = { id: null, created: null, name: 'New Group', flags: {}, assignments: [] }; +const roles = ref([]); +const editRoleIndex = ref(0); // index in array +const editRole = computed(() => roles.value?.[editRoleIndex.value]); +const emptyRole = { id: null, created: null, name: 'New Role', flags: {}, assignments: [] }; const emptyAssignment = { id: null, name: 'New Assignment', users: {}, assignments: {} }; const projects = ref([]); /** - * Gets groups data + * Gets role data */ -async function buildGroups() { - // get groups from db and sort (db doesn't store in same order) - groups.value = await api.getItems(api.ACCESS_GROUPS); - groups.value.sort((a, b) => difference(a.created, b.created)); - // fill in group keys if there's anything new - for (let group of groups.value) - for (let k of Object.keys(emptyGroup)) - group[k] ??= emptyGroup[k]; +async function buildRoles() { + // get roles from db and sort (db doesn't store in same order) + roles.value = await api.getItems(api.ACCESS_ROLES); + roles.value.sort((a, b) => difference(a.created, b.created)); + // fill in role keys if there's anything new + for (let role of roles.value) + for (let k of Object.keys(emptyRole)) + role[k] ??= emptyRole[k]; } /** - * Adds a new group to the list of groups + * Adds a new role to the list of roles */ -async function addGroup() { - // copy into a new group with default assignment - groups.value.push({ ...deepClone(emptyGroup), id: generateUUID(), created: now() }); - editGroupIndex.value = groups.value.length - 1; - addAssignment(groups.value[editGroupIndex.value]); - // let group be created, then highlight the group name +async function addRole() { + // copy into a new role with default assignment + roles.value.push({ ...deepClone(emptyRole), id: generateUUID(), created: now() }); + editRoleIndex.value = roles.value.length - 1; + addAssignment(roles.value[editRoleIndex.value]); + // let role be created, then highlight the role name await nextTick(); - let el = document.getElementById('groupName'); + let el = document.getElementById('roleName'); el.focus(); - el.setSelectionRange(0, emptyGroup.name.length); + el.setSelectionRange(0, emptyRole.name.length); } /** - * Adds a new assignment to the given group + * Adds a new assignment to the given role */ -async function addAssignment(group) { +async function addAssignment(role) { // create default assignment - group.assignments ??= []; - group.assignments.push({ ...deepClone(emptyAssignment), id: generateUUID() }); + role.assignments ??= []; + role.assignments.push({ ...deepClone(emptyAssignment), id: generateUUID() }); // let assignment be created, then highlight the assignment name await nextTick(); let el = document.getElementsByClassName('assignmentNames'); @@ -163,12 +163,12 @@ async function addAssignment(group) { } /** - * Deletes the current group + * Deletes the current role */ const deleteText = ref('Delete'); const userIsSure = ref(false); let deleteTimeout; -async function deleteCurrentGroup() { +async function deleteCurrentRole() { // first click: make sure user is sure if (!userIsSure.value) { deleteText.value = 'Are you sure?'; @@ -182,10 +182,10 @@ async function deleteCurrentGroup() { } if (deleteTimeout) clearTimeout(deleteTimeout); // second click: delete and push to db - if (!isAdmin(editGroup.value)) { + if (!isAdmin(editRole.value)) { try { - let [{ id }] = groups.value.splice(editGroupIndex.value--, 1); // post decrement - await api.deleteItem(api.ACCESS_GROUPS, id); + let [{ id }] = roles.value.splice(editRoleIndex.value--, 1); // post decrement + await api.deleteItem(api.ACCESS_ROLES, id); deleteText.value = 'Delete'; userIsSure.value = false; useDisplaySuccess('Delete success!'); @@ -196,17 +196,17 @@ async function deleteCurrentGroup() { } /** - * Saves the current group + * Saves the current role * TODO: patch request instead of whole object */ const saveText = ref('Save'); const saving = ref(false); -async function saveCurrentGroup() { +async function saveCurrentRole() { // UI feedback saving.value = true; // save to db try { - await api.createItem(api.ACCESS_GROUPS, editGroup.value); + await api.createItem(api.ACCESS_ROLES, editRole.value); // reset delete UI vars if needed deleteText.value = 'Delete'; userIsSure.value = false; @@ -223,23 +223,23 @@ async function saveCurrentGroup() { } /** - * Returns whether or not the group is the admin group. + * Returns whether or not the role is the admin role. */ -function isAdmin(group) { - return group.name === 'Admin'; +function isAdmin(role) { + return role.name === 'Admin'; } /** - * Check for changes to the editing group + * Check for changes to the editing role */ watch( - () => [editGroupIndex.value, editGroup.value], - ([newIndex], [oldIndex, oldGroup]) => { + () => [editRoleIndex.value, editRole.value], + ([newIndex], [oldIndex, oldRole]) => { // reset deletion protection userIsSure.value = false; deleteText.value = 'Delete'; - // first run or changing groups - if (oldGroup === undefined || newIndex !== oldIndex) return; + // first run or changing roles + if (oldRole === undefined || newIndex !== oldIndex) return; }, { deep: true } ) @@ -266,8 +266,8 @@ onBeforeMount(async () => { projects.value = []; for (let c of store.getters.contracts) projects.value.push(...(c.projects || [])); - // put groups in UI - await buildGroups(); + // put roles in UI + await buildRoles(); // loading for feedback loading.value = false; diff --git a/store/index.js b/store/index.js index 63885ae02..7b085ff3d 100644 --- a/store/index.js +++ b/store/index.js @@ -31,7 +31,7 @@ export default createStore({ userRole: null, storeIsPopulated: false, tags: null, - accessGroups: null + accessRoles: null }; }, mutations: { @@ -73,8 +73,8 @@ export default createStore({ setTags(state, payload) { state.tags = payload.tags; }, - setAccessGroups(state, payload) { - state.accessGroups = payload.accessGroups; + setAccessRoles(state, payload) { + state.accessRoles = payload.accessRoles; } }, actions: { @@ -114,8 +114,8 @@ export default createStore({ setTags(context, payload) { context.commit('setTags', payload); }, - setAccessGroups(context, payload) { - context.commit('setAccessGroups', payload); + setAccessRoles(context, payload) { + context.commit('setAccessRoles', payload); } }, getters: { @@ -161,8 +161,8 @@ export default createStore({ tags(state) { return state.tags; }, - accessGroups(state) { - return state.accessGroups; + accessRoles(state) { + return state.accessRoles; } } }); From ff0818f30eed306f6af9b150728ba2881d0c1a6d Mon Sep 17 00:00:00 2001 From: Luke Ogburn <21106956+logburn@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:09:44 -0500 Subject: [PATCH 2/2] add ac users to contracts page --- src/components/contracts/ContractsTable.vue | 27 ++++++++++++++++--- .../contracts/ExpandedContractTableRow.vue | 7 +++++ src/shared/api.js | 4 +-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/contracts/ContractsTable.vue b/src/components/contracts/ContractsTable.vue index 788e4acac..b5a33c6dc 100644 --- a/src/components/contracts/ContractsTable.vue +++ b/src/components/contracts/ContractsTable.vue @@ -1,5 +1,5 @@