feat(user-management): add popup with basic functionality to add or edit user info and module access

This commit is contained in:
Nicolas Drolet 2025-12-02 09:20:05 -05:00
parent ff66a457d9
commit a1b6748d95
11 changed files with 357 additions and 31 deletions

View File

@ -58,6 +58,21 @@ export default {
company: "company", company: "company",
supervisor: "supervisor", supervisor: "supervisor",
hired_date: "hiring date", hired_date: "hiring date",
bankroll_id: "payroll ID",
module_access: {
dashboard: "Dashboard",
employee_list: "employee list",
employee_management: "employee management",
personal_profile: "profile",
timesheets: "timesheets",
timesheets_approval: "timesheet approval",
user_access: "module access",
presets: "access presets",
preset_admin: "admin",
preset_employee: "employee",
uncheck_all: "remove all",
},
}, },
preferences: { preferences: {
tab_title: "preferences", tab_title: "preferences",

View File

@ -58,6 +58,20 @@ export default {
company: "compagnie", company: "compagnie",
supervisor: "nom du superviseur", supervisor: "nom du superviseur",
hired_date: "date d'embauche", hired_date: "date d'embauche",
bankroll_id: "identifiant de paie",
module_access: {
dashboard: "accueil",
employee_list: "liste employés",
employee_management: "gestion employés",
personal_profile: "profil",
timesheets: "feuilles de temps",
timesheets_approval: "valider feuilles de temps",
user_access: "accès aux modules",
presets: "accès prédéfinis",
preset_admin: "administrateur",
preset_employee: "employé",
uncheck_all: "aucun accès",
},
}, },
preferences: { preferences: {
tab_title: "préférences", tab_title: "préférences",

View File

@ -16,20 +16,16 @@
const user_preferences = ref(ui_store.user_preferences); const user_preferences = ref(ui_store.user_preferences);
onMounted(async () => { onMounted(async () => {
console.log('current preferences on load: ', ui_store.user_preferences);
if (ui_store.user_preferences.id === -1) { if (ui_store.user_preferences.id === -1) {
console.log('fetching preferences');
await ui_store.getUserPreferences(); await ui_store.getUserPreferences();
} }
}); });
watch(user_preferences, async () => { watch(user_preferences, async () => {
if (ui_store.user_preferences.id !== -1) { if (ui_store.user_preferences.id !== -1) {
console.log('triggered watcher');
await ui_store.updateUserPreferences(t); await ui_store.updateUserPreferences(t);
return return
} }
console.log('watcher triggered but store has no preferences')
await ui_store.getUserPreferences(); await ui_store.getUserPreferences();
}, {deep: true}); }, {deep: true});
</script> </script>

View File

@ -49,12 +49,17 @@
class="col-grow text-center text-h6 text-weight-medium text-uppercase q-pb-none" class="col-grow text-center text-h6 text-weight-medium text-uppercase q-pb-none"
style="line-height: 0.8em;" style="line-height: 0.8em;"
> >
<div class="ellipsis" :class="row.last_work_day === undefined ? 'text-accent' : 'text-negative'"> {{ row.first_name }} {{ row.last_name }} </div> <div
class="ellipsis"
:class="row.last_work_day === undefined ? 'text-accent' : 'text-negative'"
>
{{ row.first_name }} {{ row.last_name }}
</div>
<q-separator <q-separator
color="accent" color="accent"
class="q-mx-sm q-mt-xs" class="q-mx-sm q-mt-xs"
/> />
<div class=" ellipsis-2-lines text-caption"> {{ row.job_title }} </div> <div class=" ellipsis-2-lines text-caption">{{ row.job_title }}</div>
</q-card-section> </q-card-section>
<q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center"> <q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center">

View File

@ -71,6 +71,7 @@
<EmployeeListTableItem <EmployeeListTableItem
:row="props.row" :row="props.row"
:index="props.rowIndex" :index="props.rowIndex"
@on-profile-click="employee_store.openAddModifyDialog"
/> />
</template> </template>
@ -82,6 +83,7 @@
icon="person_add" icon="person_add"
:label="$t('shared.label.add')" :label="$t('shared.label.add')"
class="text-uppercase" class="text-uppercase"
@click.stop="_evt => employee_store.openAddModifyDialog()"
/> />
<q-space /> <q-space />

View File

@ -0,0 +1,244 @@
<script
setup
lang="ts"
>
import { useEmployeeStore } from 'src/stores/employee-store';
import { employee_access_options, type ModuleAccessPreset, type ModuleAccessName, employee_access_presets } from 'src/modules/employee-list/models/employee-profile.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
const employee_store = useEmployeeStore();
const toggleInSelected = (value: ModuleAccessName) => {
const i = employee_store.employee.user_module_access.indexOf(value);
if (i === -1) employee_store.employee.user_module_access.push(value);
else employee_store.employee.user_module_access.splice(i, 1);
}
const applyAccessPreset = (preset: ModuleAccessPreset) => {
employee_store.employee.user_module_access = unwrapAndClone(employee_access_presets[preset]);
}
</script>
<template>
<div>
<q-form>
<div
class="q-ma-xs"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-input
v-model="employee_store.employee.first_name"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.personal.first_name') }}
</span>
</template>
</q-input>
<q-input
v-model="employee_store.employee.last_name"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.personal.last_name') }}
</span>
</template>
</q-input>
</div>
<div
class="q-ma-xs"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-input
v-model="employee_store.employee.email"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.email') }}
</span>
</template>
</q-input>
<q-input
v-model="employee_store.employee.phone_number"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.personal.phone_number') }}
</span>
</template>
</q-input>
</div>
<div
class="q-ma-xs"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-input
v-model="employee_store.employee.job_title"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.job_title') }}
</span>
</template>
</q-input>
<q-input
v-model="employee_store.employee.company_name"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.company') }}
</span>
</template>
</q-input>
</div>
<div
class="q-ma-xs"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-input
v-model="employee_store.employee.supervisor_full_name"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.supervisor') }}
</span>
</template>
</q-input>
<q-input
v-model="employee_store.employee.phone_number"
standout="bg-accent"
dense
stack-label
label-slot
class="col q-ma-xs"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.bankroll_id') }}
</span>
</template>
</q-input>
</div>
<q-field
v-model="employee_store.employee.user_module_access"
dense
stack-label
label-slot
standout="transparent"
class="col-12 q-ma-sm"
>
<template #label>
<span class="text-weight-bolder text-uppercase" style="font-size: 0.85em;">
{{ $t('profile.employee.module_access.user_access') }}
</span>
</template>
<div class="col-12 column full-width">
<div class="row col-auto flex-center">
<q-btn
dense
flat
color="accent"
:label="$t('profile.employee.module_access.preset_admin')"
class="col-3 q-mx-sm q-pa-xs text-weight-bold"
@click="applyAccessPreset('admin')"
/>
<q-btn
dense
flat
color="accent"
:label="$t('profile.employee.module_access.preset_employee')"
class="col-3 q-mx-sm q-pa-xs text-weight-bold"
@click="applyAccessPreset('employee')"
/>
<q-btn
dense
flat
color="negative"
:label="$t('profile.employee.module_access.uncheck_all')"
class="col-3 q-mx-sm q-pa-xs text-weight-bold"
@click="applyAccessPreset('none')"
/>
</div>
<div class="row">
<div
v-for="option in employee_access_options"
:key="option.label"
class="col-xl-4 col-lg-6 col-sm-12 col-xs-12 q-pa-xs"
>
<div
class="row full-width cursor-pointer flex-center q-pa-sm rounded-5 no-wrap shadow-1"
:class="employee_store.employee.user_module_access.includes(option.value) ? 'bg-accent text-white' : ''"
@click="toggleInSelected(option.value)"
>
<span class="text-uppercase text-caption">
{{ $t('profile.employee.module_access.' + option.value) }}
</span>
<q-space />
<q-icon
:name="employee_store.employee.user_module_access.includes(option.value) ? 'check' : ''"
size="sm"
/>
</div>
</div>
</div>
</div>
<!-- <q-option-group
v-model="selected_permissions"
dense
inline
left-label
type="checkbox"
color="accent"
:options="employee_access_options"
/> -->
</q-field>
</q-form>
</div>
</template>

View File

@ -0,0 +1,23 @@
<script
setup
lang="ts"
>
import AddModifyDialogForm from 'src/modules/employee-list/components/employee/add-modify-dialog-form.vue';
import { useEmployeeStore } from 'src/stores/employee-store';
const employee_store = useEmployeeStore();
</script>
<template>
<q-dialog v-model="employee_store.is_add_modify_dialog_open">
<q-card>
<q-card-section class="text-bolder text-white bg-primary text-center">
<span >ADD EMPLOYEE</span>
</q-card-section>
<AddModifyDialogForm />
<q-inner-loading :showing="employee_store.is_loading" />
</q-card>
</q-dialog>
</template>

View File

@ -1,16 +0,0 @@
<script setup lang="ts">
import { useEmployeeStore } from 'src/stores/employee-store';
const employee_store = useEmployeeStore();
</script>
<template>
<q-dialog v-model="employee_store.isShowingEmployeeAddModifyWindow">
<q-card>
<q-card-section>
LOL
</q-card-section>
<q-inner-loading :showing="employee_store.is_loading"/>
</q-card>
</q-dialog>
</template>

View File

@ -1,4 +1,7 @@
import type { QTableColumn } from "quasar"; import type { QSelectOption, QTableColumn } from "quasar";
export type ModuleAccessName = 'dashboard' | 'employee_list' | 'employee_management' | 'personal_profile' | 'timesheets' | 'timesheets_approval';
export type ModuleAccessPreset = 'admin' | 'employee' | 'none';
export class EmployeeProfile { export class EmployeeProfile {
first_name: string; first_name: string;
@ -10,8 +13,10 @@ export class EmployeeProfile {
phone_number: string; phone_number: string;
first_work_day: string; first_work_day: string;
last_work_day: string; last_work_day: string;
external_payroll_id: number;
residence: string; residence: string;
birth_date: string; birth_date: string;
user_module_access: ModuleAccessName[];
constructor() { constructor() {
this.first_name = ''; this.first_name = '';
@ -25,6 +30,8 @@ export class EmployeeProfile {
this.last_work_day = ''; this.last_work_day = '';
this.residence = ''; this.residence = '';
this.birth_date = ''; this.birth_date = '';
this.external_payroll_id = -1;
this.user_module_access = ['dashboard',];
} }
} }
@ -67,6 +74,21 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
}, },
]; ];
export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
{ label: 'dashboard', value: 'dashboard' },
{ label: 'employee_list', value: 'employee_list' },
{ label: 'employee_management', value: 'employee_management' },
{ label: 'personal_profile', value: 'personal_profile' },
{ label: 'timesheets', value: 'timesheets' },
{ label: 'timesheets_approval', value: 'timesheets_approval' },
]
export const employee_access_presets: Record<ModuleAccessPreset, ModuleAccessName[]> = {
'admin' : ['dashboard', 'employee_list', 'employee_management', 'personal_profile', 'timesheets', 'timesheets_approval'],
'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'],
'none' : [],
}
export const getCompanyName = (company_code: number) => { export const getCompanyName = (company_code: number) => {
switch (company_code) { switch (company_code) {
case 271583: return 'Targo'; case 271583: return 'Targo';

View File

@ -3,13 +3,13 @@
lang="ts" lang="ts"
> >
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue'; import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
import EmployeeListAddModifyDialog from 'src/modules/employee-list/components/employee/employee-list-add-modify-dialog.vue'; import AddModifyDialog from 'src/modules/employee-list/components/employee/add-modify-dialog.vue';
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue'; import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
</script> </script>
<template> <template>
<q-page class="column flex-center"> <q-page class="column flex-center">
<EmployeeListAddModifyDialog /> <AddModifyDialog />
<PageHeaderTemplate title="employee_list.page_header" /> <PageHeaderTemplate title="employee_list.page_header" />

View File

@ -6,12 +6,25 @@ import { EmployeeProfile } from "src/modules/employee-list/models/employee-profi
export const useEmployeeStore = defineStore('employee', () => { export const useEmployeeStore = defineStore('employee', () => {
const employee = ref<EmployeeProfile>(new EmployeeProfile); const employee = ref<EmployeeProfile>(new EmployeeProfile);
const employee_list = ref<EmployeeProfile[]>([]); const employee_list = ref<EmployeeProfile[]>([]);
const isShowingEmployeeAddModifyWindow = ref<boolean>(false); const is_add_modify_dialog_open = ref<boolean>(false);
const is_loading = ref(false); const is_loading = ref(false);
const isLoadingEmployeeList = ref(false);
const openAddModifyDialog = async (employee_email?: string) =>{
console.log('open window triggered');
is_add_modify_dialog_open.value = true;
if (employee_email === undefined) {
employee.value = new EmployeeProfile();
return;
}
is_loading.value = true;
await getEmployeeDetails(employee_email);
is_loading.value = false;
}
const getEmployeeList = async () => { const getEmployeeList = async () => {
isLoadingEmployeeList.value = true; is_loading.value = true;
try { try {
const response = await EmployeeListService.getEmployeeList(); const response = await EmployeeListService.getEmployeeList();
employee_list.value = response; employee_list.value = response;
@ -19,7 +32,7 @@ export const useEmployeeStore = defineStore('employee', () => {
console.error("Ran into an error fetching employee list: ", error); console.error("Ran into an error fetching employee list: ", error);
//TODO: trigger an alert window with an error message here! //TODO: trigger an alert window with an error message here!
} }
isLoadingEmployeeList.value = false; is_loading.value = false;
}; };
const getEmployeeDetails = async (email?: string) => { const getEmployeeDetails = async (email?: string) => {
@ -40,6 +53,14 @@ export const useEmployeeStore = defineStore('employee', () => {
is_loading.value = false; is_loading.value = false;
}; };
return { employee, employee_list, isShowingEmployeeAddModifyWindow, isLoadingEmployeeList, is_loading, getEmployeeList, getEmployeeDetails }; return {
employee,
employee_list,
is_add_modify_dialog_open,
is_loading,
getEmployeeList,
getEmployeeDetails,
openAddModifyDialog,
};
}); });