feat(profile): finalize get/update of user preferences, begin planning for employee-management module

This commit is contained in:
Nicolas Drolet 2025-11-28 10:54:38 -05:00
parent 4c79820128
commit a4904ee80d
24 changed files with 493 additions and 296 deletions

View File

@ -1,5 +1,7 @@
<script setup lang="ts">
//
<script
setup
lang="ts"
>
</script>
<template>

View File

@ -61,10 +61,14 @@ export default {
},
preferences: {
tab_title: "preferences",
display_options: "display options",
display_options: "Color mode",
language_options: "language options",
'fr-FR': "French",
'en-CA': "English",
dark_mode: "dark",
light_mode: "light",
update_successful: "Preferences saved",
update_failed: "Failed to save preferences",
},
schedule_presets: {
tab_title: "Schedule",

View File

@ -61,10 +61,14 @@ export default {
},
preferences: {
tab_title: "préférences",
display_options: "Options d'affichage",
display_options: "Mode d'affichage",
language_options: "Options de langue",
'fr-FR': "Français",
'en-CA': "Anglais",
dark_mode: "sombre",
light_mode: "clair",
update_successful: "Préférences enregistrées",
update_failed: "Échec de sauvegarde",
},
schedule_presets: {
tab_title: "horaire",

View File

@ -1,8 +1,37 @@
<script lang="ts" setup>
<script
lang="ts"
setup
>
import { RouterView } from 'vue-router';
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
import { useUiStore } from 'src/stores/ui-store';
import { onMounted, watch, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const ui_store = useUiStore();
const user_preferences = ref(ui_store.user_preferences);
onMounted(async () => {
console.log('current preferences on load: ', ui_store.user_preferences);
if (ui_store.user_preferences.id === -1) {
console.log('fetching preferences');
await ui_store.getUserPreferences();
}
});
watch(user_preferences, async () => {
if (ui_store.user_preferences.id !== -1) {
console.log('triggered watcher');
await ui_store.updateUserPreferences(t);
return
}
console.log('watcher triggered but store has no preferences')
await ui_store.getUserPreferences();
}, {deep: true});
</script>
<template>

View File

@ -55,18 +55,14 @@
color="accent"
class="col-auto"
/>
<transition
enter-active-class="animated rubberBand fast"
leave-active-class=""
mode="out-in"
>
<span
:key="is_remembered ? 'yep' : 'nope'"
class="col-auto text-weight-bold self-center q-ml-sm"
:class="is_remembered ? 'text-accent' : ''"
>{{ $t('login.button.remember_me') }}</span>
</transition>
>
{{ $t('login.button.remember_me') }}
</span>
</q-card-section>
<q-card-actions>
@ -90,14 +86,14 @@
<q-card-section class="row q-pt-sm">
<q-separator
size="2px"
color="primary"
color="accent"
class="col self-center"
/>
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
<span class="col text-accent text-weight-bolder text-center text-uppercase self-center">{{
$t('shared.misc.or') }}</span>
<q-separator
size="2px"
color="primary"
color="accent"
class="col self-center"
/>
</q-card-section>

View File

@ -2,18 +2,20 @@
setup
lang="ts"
>
import { onMounted, ref } from 'vue';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { useEmployeeStore } from 'src/stores/employee-store';
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
import { employee_list_columns } from 'src/modules/employee-list/models/employee-profile.models';
import { onMounted, ref } from 'vue';
import { useUiStore } from 'src/stores/ui-store';
import { useEmployeeStore } from 'src/stores/employee-store';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { employee_list_columns, getCompanyName } from 'src/modules/employee-list/models/employee-profile.models';
const employee_list_api = useEmployeeListApi();
const employee_store = useEmployeeStore();
const ui_store = useUiStore();
const is_loading_list = ref<boolean>(true);
const filter = ref("");
const is_grid_mode = ref(true);
onMounted(async () => {
is_loading_list.value = true;
@ -26,7 +28,6 @@
<div class="q-pa-lg">
<q-table
dense
flat
hide-pagination
virtual-scroll
title=" "
@ -36,14 +37,13 @@
row-key="name"
:rows-per-page-options="[0]"
:filter="filter"
class="q-pa-md bg-transparent"
:class="is_grid_mode ? '' : 'sticky-header-table'"
class="bg-transparent no-shadow sticky-header-table"
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10'"
color="accent"
table-header-class="text-accent text-uppercase"
card-container-class="justify-center"
:grid="is_grid_mode"
:grid="ui_store.user_preferences.is_employee_list_grid"
:loading="is_loading_list"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
@ -87,7 +87,7 @@
<q-space />
<q-btn-toggle
v-model="is_grid_mode"
v-model="ui_store.user_preferences.is_employee_list_grid"
push
rounded
color="white"
@ -119,6 +119,16 @@
</div>
</template>
<template #body-cell="scope">
<q-td
:props="scope"
class="text-weight-medium"
>
<span v-if="scope.col.name === 'company_name'"> {{ getCompanyName(scope.value) }}</span>
<span v-else>{{ scope.value }}</span>
</q-td>
</template>
<!-- Template for custome failed-to-load state -->
<template v-slot:no-data="{ message, filter }">
<div class="full-width column items-center text-accent q-gutter-sm">
@ -138,14 +148,14 @@
<style lang="sass">
.sticky-header-table
thead tr:first-child th
background-color: var(--q-dark)
background-color: var(--q-accent)
margin-top: none
thead tr th
position: sticky
z-index: 1
thead tr:first-child th
top: 0
top: 0px
&.q-table--loading thead tr:last-child th
top: 48px

View File

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

View File

@ -1,6 +1,6 @@
import type { QTableColumn } from "quasar";
export interface EmployeeProfile {
export class EmployeeProfile {
first_name: string;
last_name: string;
supervisor_full_name: string;
@ -12,20 +12,20 @@ export interface EmployeeProfile {
last_work_day: string;
residence: string;
birth_date: string;
}
export const default_employee_profile: EmployeeProfile = {
first_name: '',
last_name: '',
supervisor_full_name: '',
company_name: -1,
job_title: '',
email: '',
phone_number: '',
first_work_day: '',
last_work_day: '',
residence: '',
birth_date: '',
constructor() {
this.first_name = '';
this.last_name = '';
this.supervisor_full_name = '';
this.company_name = 271583;
this.job_title = '';
this.email = '';
this.phone_number = '';
this.first_work_day = '';
this.last_work_day = '';
this.residence = '';
this.birth_date = '';
}
}
export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
@ -66,3 +66,11 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
align: 'left'
},
];
export const getCompanyName = (company_code: number) => {
switch (company_code) {
case 271583: return 'Targo';
case 271585: return 'Solucom';
default: return 'N / A';
}
}

View File

@ -7,8 +7,13 @@ export const EmployeeListService = {
return response.data.data;
},
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
return response.data;
getEmployeeDetails: async (): Promise<EmployeeProfile> => {
const response = await api.get<{success: boolean, data: EmployeeProfile, error?: string}>('employees/profile');
return response.data.data;
},
getEmployeeDetailsWithEmployeeEmail: async (employee_email: string): Promise<EmployeeProfile> => {
const response = await api.get<{success: boolean, data: EmployeeProfile, error?: string}>(`employees/profile?employee_email=${employee_email}`);
return response.data.data;
}
};

View File

@ -7,7 +7,7 @@
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
import MenuPanelSchedulePresets from 'src/modules/profile/components/shared/menu-panel-schedule-presets.vue';
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.vue';
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { useAuthStore } from 'src/stores/auth-store';
const auth_store = useAuthStore();
@ -19,7 +19,7 @@
SCHEDULE_PRESETS: 'schedule_presets',
};
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
const employee_profile = defineModel<EmployeeProfile>({ default: new EmployeeProfile });
</script>

View File

@ -2,38 +2,49 @@
setup
lang="ts"
>
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
import MenuPanelSelectField from 'src/modules/profile/components/shared/menu-panel-select-field.vue';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
const employee_profile = defineModel<EmployeeProfile>({required: true});
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useAuthStore } from 'src/stores/auth-store';
import { useEmployeeStore } from 'src/stores/employee-store';
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const COMPANY_OPTIONS = [
{ label: 'Targo', value: 271583 },
{ label: 'Solucom', value: 271585 }
];
const SUPERVISOR_OPTIONS = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
const auth_store = useAuthStore();
const employee_store = useEmployeeStore();
const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
const current_company_option = ref(COMPANY_OPTIONS.find(option => option.value === employee_store.employee.company_name) ?? { label: '', value: 0 })
let initial_info: EmployeeProfile = unwrapAndClone(employee_store.employee);
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
console.log('clicky!');
return;
}
is_editing.value = false;
initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
initial_info = unwrapAndClone(employee_store.employee); // update initial value for future possible resets
employee_store.employee.company_name = current_company_option.value.value;
if (!deepEqual(employee_profile.value, initial_info)) {
if (!deepEqual(employee_store.employee, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
employee_profile.value = unwrapAndClone(initial_info);
employee_store.employee = unwrapAndClone(initial_info);
is_editing.value = false;
}
</script>
@ -46,23 +57,24 @@
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<MenuPanelInputField
v-model="employee_profile.job_title"
v-model="employee_store.employee.job_title"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.job_title')"
/>
<MenuPanelInputField
v-model="employee_profile.company_name"
<MenuPanelSelectField
v-model="current_company_option"
:options="COMPANY_OPTIONS"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.company')"
/>
</div>
<div class="q-mx-xs">
<div>
<MenuPanelSelectField
v-model="employee_profile.supervisor_full_name"
:options="supervisor_options"
v-model="employee_store.employee.supervisor_full_name"
:options="SUPERVISOR_OPTIONS"
:label-string="$t('profile.employee.supervisor')"
:is-editing="is_editing"
/>
@ -71,15 +83,15 @@
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<MenuPanelInputField
v-model="employee_profile.email"
v-model="employee_store.employee.email"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.email')"
/>
<MenuPanelInputField
v-model="employee_profile.first_work_day"
v-model="employee_store.employee.first_work_day"
readonly
class="col"
class="col-auto"
type="date"
:is-editing="is_editing"
:label-string="$t('profile.employee.hired_date')"
@ -87,6 +99,7 @@
</div>
<div
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
class="absolute-bottom"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
@ -94,20 +107,18 @@
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
class="q-ma-sm q-py-xs"
:label="$t('shared.label.cancel')"
/>
<q-btn
push
size="sm"
color="primary"
color="accent"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
class="q-ma-sm q-py-xs"
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
/>
</div>

View File

@ -2,35 +2,36 @@
setup
lang="ts"
>
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useEmployeeStore } from 'src/stores/employee-store';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const employee_profile = defineModel<EmployeeProfile>({required: true});
const employee_store = useEmployeeStore();
const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
const initial_info = ref<EmployeeProfile>(unwrapAndClone(employee_store.employee));
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
initial_info.value = unwrapAndClone(employee_store.employee);
return;
}
is_editing.value = false;
initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
initial_info.value = unwrapAndClone(employee_store.employee); // update initial value for future possible resets
if (!deepEqual(employee_profile.value, initial_info)) {
if (!deepEqual(employee_store.employee, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
employee_profile.value = unwrapAndClone(initial_info);
employee_store.employee = unwrapAndClone(initial_info.value);
is_editing.value = false;
}
</script>
@ -43,14 +44,14 @@
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<MenuPanelInputField
v-model="employee_profile.first_name"
v-model="employee_store.employee.first_name"
type="text"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.personal.first_name')"
/>
<MenuPanelInputField
v-model="employee_profile.last_name"
v-model="employee_store.employee.last_name"
class="col"
type="text"
:is-editing="is_editing"
@ -60,14 +61,14 @@
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<MenuPanelInputField
v-model="employee_profile.phone_number"
v-model="employee_store.employee.phone_number"
class="col"
type="text"
:is-editing="is_editing"
:label-string="$t('profile.personal.phone_number')"
/>
<MenuPanelInputField
v-model="employee_profile.birth_date"
v-model="employee_store.employee.birth_date"
class="col"
mask="#### / ## / ##"
hint="ex: 1970 / 01 / 01"
@ -78,7 +79,7 @@
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<MenuPanelInputField
v-model="employee_profile.residence"
v-model="employee_store.employee.residence"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.personal.address')"
@ -93,21 +94,19 @@
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
flat
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
class="q-ma-sm q-py-xs"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
color="accent"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
class="q-ma-sm q-py-xs"
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
/>
</div>

View File

@ -1,7 +1,10 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import type { ValidationRule } from 'quasar';
const model = defineModel<string | number>({ required: true });
const model = defineModel<string | number | undefined>({ required: true });
const { readonly = false, hint = '' } = defineProps<{
labelString: string;
@ -23,14 +26,19 @@
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
debounce="500"
label-color="accent"
class="q-ma-xs text-uppercase"
input-class="text-weight-medium text-h6"
label-slot
class="q-ma-xs"
input-class="text-weight-light"
input-style="font-size: 1.2em"
:hide-hint="hint === ''"
:hint="isEditing ? hint : ''"
:mask="mask"
:readonly="readonly || !isEditing"
:type="type"
:label="labelString"
:rules="rules"
/>
>
<template #label>
<span class="text-weight-bolder text-accent text-uppercase">{{ labelString }}</span>
</template>
</q-input>
</template>

View File

@ -1,46 +1,39 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import LanguageSwitch from 'src/modules/shared/components/language-switch.vue';
import { ref } from 'vue';
import { Dark } from 'quasar';
import LanguageSwitch from 'src/modules/shared/components/language-switch.vue';
import { useUiStore } from 'src/stores/ui-store';
const ui_store = useUiStore();
const initial_dark_mode_value = Dark.isActive;
const is_dark_mode = ref<boolean>(initial_dark_mode_value);
const toggle_dark_mode = (value: boolean) => {
is_dark_mode.value = value;
if (ui_store.user_preferences) ui_store.user_preferences.is_dark_mode = value;
Dark.set(value);
}
</script>
<template>
<q-form class="q-pa-md column fit">
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profile.preferences.display_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
class="col-auto justify-center content-center q-mb-lg q-pa-none"
:style="$q.dark.isActive ? 'background-color: #FFF1;' : 'background-color: #0001;'"
>
<q-card-section class="q-py-none">
<span class="text-uppercase text-weight-bold text-accent">{{ $t('profile.preferences.display_options')
}}</span>
</q-card-section>
<div class="row col">
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #DAE0E7;"
<q-card-section
horizontal
class="flex-center text-uppercase"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: white;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profile.preferences.light_mode')}}</span>
</q-card>
<span class="q-mx-md text-weight-medium">{{ $t('profile.preferences.light_mode') }}</span>
<q-toggle
v-model="is_dark_mode"
@update:model-value="value => toggle_dark_mode(value)"
@ -49,36 +42,26 @@
checked-icon="dark_mode"
unchecked-icon="light_mode"
/>
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #0f1114;"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: #333;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profile.preferences.dark_mode')}}</span>
</q-card>
</div>
<span class="q-mx-md text-weight-medium">{{ $t('profile.preferences.dark_mode') }}</span>
</q-card-section>
</q-card>
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profile.preferences.language_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
class="col-auto justify-center content-center q-mb-lg q-pa-none"
:style="$q.dark.isActive ? 'background-color: #FFF1;' : 'background-color: #0001;'"
>
<q-card-section class="q-py-none">
<span class="text-uppercase text-weight-bold text-accent">{{ $t('profile.preferences.language_options')
}}</span>
</q-card-section>
<LanguageSwitch class="q-mr-xs col-auto" />
<q-card-section
horizontal
class="flex-center"
>
<LanguageSwitch class="col-auto" />
</q-card-section>
</q-card>
</q-form>
</template>

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
const model = defineModel<string>();
<script
setup
lang="ts"
>
const model = defineModel<string | number | {label: string, value: unknown} | undefined>();
const { readonly = false, localizeOptions = false } = defineProps<{
options: { label: string, value: string }[];
options: { label: string, value: string | number }[];
labelString: string;
isEditing: boolean;
readonly?: boolean;
@ -18,14 +21,23 @@
:stack-label="!isEditing"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
label-color="accent"
class="q-ma-xs text-h6 text-uppercase"
popup-content-class="text-weight-medium text-h6"
input-class="text-weight-medium"
class="q-ma-xs"
popup-content-class="text-weight-medium text-h6 rounded-5"
options-selected-class="bg-accent text-white"
:menu-offset="[0, 10]"
:options="options"
:readonly="readonly || !isEditing"
:hide-dropdown-icon="!isEditing"
:label="labelString"
:option-label="opt => localizeOptions ? $t(opt) : opt"
:option-label="opt => localizeOptions ? $t(opt.label) : opt.label ?? opt"
hint=''
/>
>
<template #label>
<span class="text-weight-bolder text-accent text-uppercase">{{ labelString }}</span>
</template>
<template #selected-item="scope">
<span class="text-weight-light" style="font-size: 1.2em;">{{ scope.opt.label }}</span>
</template>
</q-select>
</template>

View File

@ -1,4 +1,7 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import { ref } from 'vue';
import MenuHeader from 'src/modules/profile/components/shared/menu-header.vue';
@ -13,7 +16,7 @@
<template>
<div
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
:class="$q.screen.lt.md ? 'column no-wrap flex-center' : 'row'"
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
>
<MenuHeader
@ -21,10 +24,6 @@
:user-last-name="lastName"
/>
<div
class="col-3 no-wrap"
:class="$q.screen.lt.md ? '' : 'column'"
>
<q-card
class="col-auto q-pa-xs"
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
@ -32,19 +31,16 @@
<q-tabs
v-model="current_menu"
:vertical="$q.screen.gt.sm"
dense
active-color="accent"
indicator-color="accent"
>
<slot name="tabs"></slot>
</q-tabs>
</q-card>
<div class="col"></div>
</div>
<q-card
class="col"
:class="$q.screen.lt.md ? '' : 'q-ml-sm'"
:class="$q.screen.lt.md ? 'full-width' : 'q-ml-sm'"
>
<q-tab-panels
v-model="current_menu"

View File

@ -0,0 +1,21 @@
import type { MessageLanguages } from "src/boot/i18n";
export class Preferences {
id: number;
notifications: boolean;
is_dark_mode: boolean | null;
display_language: MessageLanguages;
is_lefty_mode: boolean;
is_employee_list_grid: boolean;
is_timesheet_approval_grid: boolean;
constructor() {
this.id = -1;
this.notifications = true;
this.is_dark_mode = null;
this.display_language = 'fr-FR';
this.is_lefty_mode = false;
this.is_employee_list_grid = true;
this.is_timesheet_approval_grid = true;
}
}

View File

@ -0,0 +1,15 @@
import { api } from "src/boot/axios";
import type { Preferences } from "src/modules/profile/models/preferences.models";
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
export const ProfileService = {
getUserPreferences: async (): Promise<BackendResponse<Preferences>> => {
const response = await api.get<BackendResponse<Preferences>>(`/preferences`);
return response.data;
},
updateUserPreferences: async (new_preferences: Preferences): Promise<BackendResponse<Preferences>> => {
const response = await api.patch<BackendResponse<Preferences>>(`/preferences/update`, new_preferences);
return response.data;
},
};

View File

@ -1,36 +1,45 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
<script
lang="ts"
setup
>
import { useUiStore } from 'src/stores/ui-store';
import type { MessageLanguages } from 'src/boot/i18n';
const { locale } = useI18n();
const localeOptions = [
{ value: 'en-CA', label: 'English' },
{ value: 'fr-FR', label: 'Francais' },
];
const ui_store = useUiStore();
const setDisplayLanguage = (locale: MessageLanguages) => {
if (ui_store.user_preferences !== undefined) {
ui_store.user_preferences.display_language = locale;
console.log('triggered language change: ', ui_store.user_preferences.display_language);
}
}
</script>
<template>
<div>
<q-list dense class="row">
<q-item v-for="option in localeOptions"
:key="option.value"
tag="label"
v-ripple
<q-list
dense
class="row full-width"
>
<q-item-section avatar>
<q-radio v-model="locale" :val="option.value" />
<q-item
v-for="locale in $i18n.availableLocales"
:key="locale"
clickable
dense
v-ripple
class="col rounded-5 q-ma-sm shadow-1 "
:class="locale === $i18n.locale ? 'bg-accent text-white text-weight-bolder' : ''"
@click="setDisplayLanguage(locale as MessageLanguages)"
>
<q-item-section class="text-uppercase justify-center">
<q-item-label> {{ $t(`profile.preferences.${locale}`) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label>{{ option.label }}</q-item-label>
<q-item-section side>
<q-icon
v-if="locale === $i18n.locale"
name="check"
color="white"
/>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- <q-btn-dropdown push color="primary" :label="$t('shared.languageLabel')" icon="language">
<q-list>
<q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="locale = option.value">
<q-item-section>{{ option.label }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown> -->
</template>

View File

@ -0,0 +1,5 @@
export interface BackendResponse<T> {
success: boolean;
data?: T | undefined;
error?: string | undefined;
};

View File

@ -4,19 +4,34 @@
>
import MenuEmployee from 'src/modules/profile/components/employee/menu-employee.vue';
import { useAuthStore } from 'src/stores/auth-store';
// import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { useEmployeeStore } from 'src/stores/employee-store';
import { onMounted } from 'vue';
const auth_store = useAuthStore();
const employee_store = useEmployeeStore();
const employee_roles = ['SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING'];
// const employee_profile = defineModel<EmployeeProfile>({ required: true });
const is_employee = employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST');
onMounted(async () => {
if (is_employee) {
await employee_store.getEmployeeDetails();
}
})
</script>
<template>
<q-page class="bg-secondary column items-center justify-center">
<MenuEmployee
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
class="col-auto"
class="col-sm-12 col-md-10 col-lg-9 col-xl-8"
/>
</q-page>
</template>
<style scoped>
:deep(.q-field--standout.q-field--readonly .q-field__control:before) {
border: none !important;
background-color: transparent;
}
</style>

View File

@ -3,8 +3,9 @@ export enum RouteNames {
LOGIN = 'login',
LOGIN_SUCCESS = 'login-success',
DASHBOARD = 'dashboard',
TIMESHEET_APPROVALS = 'timesheet-approvals',
EMPLOYEE_LIST = 'employee-list',
PROFILE = 'user/profile',
TIMESHEET = 'timesheet'
TIMESHEET_APPROVALS = 'timesheets_approval',
EMPLOYEE_LIST = 'employee_list',
EMPLOYEE_MANAGEMENT = 'employee_management',
PROFILE = 'personal_profile',
TIMESHEET = 'timesheets'
}

View File

@ -1,13 +1,13 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
import { EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
export const useEmployeeStore = defineStore('employee', () => {
const employee = ref<EmployeeProfile>( default_employee_profile );
const employee = ref<EmployeeProfile>(new EmployeeProfile);
const employee_list = ref<EmployeeProfile[]>([]);
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
const isLoadingEmployeeProfile = ref(false);
const is_loading = ref(false);
const isLoadingEmployeeList = ref(false);
const getEmployeeList = async () => {
@ -22,19 +22,24 @@ export const useEmployeeStore = defineStore('employee', () => {
isLoadingEmployeeList.value = false;
};
const getEmployeeDetails = async (email: string) => {
isLoadingEmployeeProfile.value = true;
const getEmployeeDetails = async (email?: string) => {
is_loading.value = true;
try {
const response = await EmployeeListService.getEmployeeDetails(email);
if (email === undefined) {
const response = await EmployeeListService.getEmployeeDetails();
employee.value = response;
} else{
const response = await EmployeeListService.getEmployeeDetailsWithEmployeeEmail(email);
employee.value = response;
}
} catch (error) {
console.error('There was an error retrieving employee info: ', error);
//TODO: trigger an alert window with an error message here!
}
isLoadingEmployeeProfile.value = false;
is_loading.value = false;
};
return { employee, employee_list, isShowingEmployeeAddModifyWindow, isLoadingEmployeeList, isLoadingEmployeeProfile, getEmployeeList, getEmployeeDetails };
return { employee, employee_list, isShowingEmployeeAddModifyWindow, isLoadingEmployeeList, is_loading, getEmployeeList, getEmployeeDetails };
});

View File

@ -1,22 +1,81 @@
import { defineStore } from 'pinia';
import { useQuasar } from 'quasar';
import { Notify, LocalStorage, useQuasar, Dark } from 'quasar';
import { computed, ref } from 'vue';
import { Preferences } from 'src/modules/profile/models/preferences.models';
import { ProfileService } from 'src/modules/profile/services/profile-service';
import { useI18n, type ComposerTranslation } from 'vue-i18n';
export const useUiStore = defineStore('ui', () => {
const q = useQuasar();
const is_left_drawer_open = ref(true);
const { locale } = useI18n();
const is_left_drawer_open = ref(false);
const focus_next_component = ref(false);
const is_mobile_mode = computed(() => q.screen.lt.md);
const user_preferences = ref<Preferences>(new Preferences);
const toggleRightDrawer = () => {
is_left_drawer_open.value = !is_left_drawer_open.value;
};
const getUserPreferences = async () => {
try {
const local_user_preferences = LocalStorage.getItem<Preferences>('user_preferences');
if (local_user_preferences !== null) {
if (local_user_preferences.id !== -1) {
Object.assign(user_preferences.value, local_user_preferences);
setPreferences();
return;
}
}
const response = await ProfileService.getUserPreferences();
if (response.success && response.data) {
LocalStorage.setItem('user_preferences', response.data);
Object.assign(user_preferences.value, response.data);
setPreferences();
}
} catch (error) {
user_preferences.value = new Preferences;
console.error('Could not retrieve user preferences: ', error);
}
};
const updateUserPreferences = async (t: ComposerTranslation) => {
try {
if (user_preferences.value.id === -1) return;
const response = await ProfileService.updateUserPreferences(user_preferences.value);
if (response.success && response.data) {
Object.assign(user_preferences.value, response.data);
LocalStorage.setItem('user_preferences', response.data);
setPreferences();
Notify.create({ message: t('profile.preferences.update_successful'), color: 'accent' });
return;
}
} catch (error) {
console.error('Could not update user preferences: ', error);
}
Notify.create({ message: t('profile.preferences.update_failed'), color: 'negative' })
};
const setPreferences = () => {
if (user_preferences.value !== undefined) {
Dark.set(user_preferences.value.is_dark_mode ?? 'auto');
locale.value = user_preferences.value.display_language;
}
}
return {
is_mobile_mode,
focus_next_component,
is_left_drawer_open,
toggleRightDrawer
user_preferences,
toggleRightDrawer,
getUserPreferences,
updateUserPreferences,
};
});