From b28f8768d22f6db15958b26849ab22b47128575b Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Fri, 19 Dec 2025 15:36:15 -0500 Subject: [PATCH] fix(timesheet): more refactors and fixes to timesheet, mostly error handling, mobile UI/UX adjustments --- src/css/app.scss | 8 + src/i18n/en-ca/index.ts | 8 +- src/i18n/fr-ca/index.ts | 8 +- .../components/main-layout-left-drawer.vue | 230 ++++---- .../components/add-modify-dialog-access.vue | 17 +- .../models/employee-profile.models.ts | 10 +- src/modules/shared/models/user.models.ts | 19 +- .../components/expense-dialog-form.vue | 1 + .../timesheets/components/expense-dialog.vue | 11 +- .../mobile/expense-dialog-form-mobile.vue | 16 +- .../mobile/shift-list-day-row-mobile.vue | 28 +- .../mobile/shift-list-weekly-overview.vue | 91 ++++ .../components/shift-list-day-row.vue | 502 ++++++++---------- .../timesheets/components/shift-list-day.vue | 19 +- .../timesheets/components/shift-list.vue | 15 +- .../components/timesheet-wrapper.vue | 79 +-- src/pages/error-page.vue | 48 +- src/router/index.ts | 16 +- src/router/router-constants.ts | 5 +- src/router/routes.ts | 17 +- src/stores/auth-store.ts | 16 +- 21 files changed, 587 insertions(+), 577 deletions(-) create mode 100644 src/modules/timesheets/components/mobile/shift-list-weekly-overview.vue diff --git a/src/css/app.scss b/src/css/app.scss index 2e7ae8b..d16f13e 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -2,6 +2,14 @@ @each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) { .rounded-#{$size} { border-radius: #{$size}px !important; + } + + .rounded-#{$size} > div:first-child { + border-radius: #{$size}px #{$size}px 0 0 !important; + } + + .rounded-#{$size} > div:last-child { + border-radius: 0 0 #{$size}px #{$size}px !important; } } diff --git a/src/i18n/en-ca/index.ts b/src/i18n/en-ca/index.ts index c02d48e..3a60600 100644 --- a/src/i18n/en-ca/index.ts +++ b/src/i18n/en-ca/index.ts @@ -53,6 +53,12 @@ export default { }, }, + error :{ + not_found_header: "page not found", + not_found_description: "You may have entered the wrong URL, or you may not have access to this page", + go_back: "go back", + }, + login: { page_header: "account login", email: "e-mail", @@ -244,7 +250,7 @@ export default { SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts", SHIFT_OVERLAP_SHORT: "Overlap", INVALID_SHIFT: "A shift contains missing or corrupted data", - SHIFT_TIME_REQUIRED: "Time required", + SHIFT_TIME_REQUIRED: "Time missing", SHIFT_TYPE_REQUIRED: "Shift type required", SHIFT_NOT_FOUND: "Shift missing or deleted", PAY_PERIOD_NOT_FOUND: "No pay period matching given dates", diff --git a/src/i18n/fr-ca/index.ts b/src/i18n/fr-ca/index.ts index e402331..54c3b61 100644 --- a/src/i18n/fr-ca/index.ts +++ b/src/i18n/fr-ca/index.ts @@ -53,6 +53,12 @@ export default { }, }, + error :{ + not_found_header: "page introuvable", + not_found_description: "Vous avez possiblement entré une mauvaise addresse URL, ou vous n'avez pas accès à cette section du site", + go_back: "retour en arrière", + }, + login: { page_header: "connexion au compte", email: "courriel", @@ -245,7 +251,7 @@ export default { SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts", SHIFT_OVERLAP_SHORT: "Chevauchement", INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues", - SHIFT_TIME_REQUIRED: "Heure requise", + SHIFT_TIME_REQUIRED: "Heures manquantes", SHIFT_TYPE_REQUIRED: "Type requis", SHIFT_NOT_FOUND: "Quart de travail manquant ou supprimé", PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies", diff --git a/src/layouts/components/main-layout-left-drawer.vue b/src/layouts/components/main-layout-left-drawer.vue index 5d5de81..01472c3 100644 --- a/src/layouts/components/main-layout-left-drawer.vue +++ b/src/layouts/components/main-layout-left-drawer.vue @@ -7,7 +7,7 @@ import { useUiStore } from 'src/stores/ui-store'; import { ref } from 'vue'; import { RouteNames } from 'src/router/router-constants'; - import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models'; + import { ModuleNames } from 'src/modules/shared/models/user.models'; const auth_store = useAuthStore(); const ui_store = useUiStore(); @@ -32,155 +32,109 @@ \ No newline at end of file diff --git a/src/modules/employee-list/components/add-modify-dialog-access.vue b/src/modules/employee-list/components/add-modify-dialog-access.vue index 1dbb15c..516e5b1 100644 --- a/src/modules/employee-list/components/add-modify-dialog-access.vue +++ b/src/modules/employee-list/components/add-modify-dialog-access.vue @@ -5,12 +5,13 @@ import { ref } from 'vue'; import { unwrapAndClone } from 'src/utils/unwrap-and-clone'; import { useEmployeeStore } from 'src/stores/employee-store'; - import { employee_access_options, type ModuleAccessPreset, type ModuleAccessName, employee_access_presets, getEmployeeAccessOptionIcon } from 'src/modules/employee-list/models/employee-profile.models'; + import { employee_access_options, type ModuleAccessPreset, employee_access_presets, getEmployeeAccessOptionIcon } from 'src/modules/employee-list/models/employee-profile.models'; + import type { UserModuleAccess } from 'src/modules/shared/models/user.models'; const employee_store = useEmployeeStore(); const preset_preview = ref(); - const toggleInSelected = (value: ModuleAccessName) => { + const toggleInSelected = (value: UserModuleAccess) => { 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); @@ -21,7 +22,7 @@ } - const getPreviewBackgroundColor = (name: ModuleAccessName) => { + const getPreviewBackgroundColor = (name: UserModuleAccess) => { if (employee_access_presets[preset_preview.value!].includes(name)) { if (!employee_store.employee.user_module_access.includes(name)) return 'bg-info text-white'; @@ -33,7 +34,7 @@ return 'bg-dark'; }; - const getBackgroundColor = (name: ModuleAccessName) => { + const getBackgroundColor = (name: UserModuleAccess) => { if (employee_store.employee.user_module_access.includes(name)) return 'bg-accent text-white'; return 'bg-dark'; @@ -142,8 +143,12 @@ class="row full-width cursor-pointer flex-center q-pa-sm rounded-5 no-wrap shadow-5" :class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)" @click="toggleInSelected(option.value)" - > - + > + {{ $t('employee_management.module_access.' + option.value) }} diff --git a/src/modules/employee-list/models/employee-profile.models.ts b/src/modules/employee-list/models/employee-profile.models.ts index b53dc9a..ff92bc9 100644 --- a/src/modules/employee-list/models/employee-profile.models.ts +++ b/src/modules/employee-list/models/employee-profile.models.ts @@ -1,6 +1,6 @@ import type { QSelectOption, QTableColumn } from "quasar"; +import type { UserModuleAccess } from "src/modules/shared/models/user.models"; -export type ModuleAccessName = 'dashboard' | 'employee_list' | 'employee_management' | 'personal_profile' | 'timesheets' | 'timesheets_approval'; export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none'; export type CompanyNames = 'Targo' | 'Solucom'; @@ -18,7 +18,7 @@ export class EmployeeProfile { residence: string; birth_date: string; is_supervisor: boolean; - user_module_access: ModuleAccessName[]; + user_module_access: UserModuleAccess[]; preset_id?: number | null; constructor() { @@ -100,7 +100,7 @@ export const employee_list_columns: QTableColumn[] = [ }, ]; -export const employee_access_options: QSelectOption[] = [ +export const employee_access_options: QSelectOption[] = [ { label: 'dashboard', value: 'dashboard' }, { label: 'employee_list', value: 'employee_list' }, { label: 'personal_profile', value: 'personal_profile' }, @@ -109,14 +109,14 @@ export const employee_access_options: QSelectOption[] = [ { label: 'timesheets_approval', value: 'timesheets_approval' }, ] -export const employee_access_presets: Record = { +export const employee_access_presets: Record = { 'admin' : ['dashboard', 'employee_list', 'employee_management', 'personal_profile', 'timesheets', 'timesheets_approval'], 'supervisor' : ['dashboard', 'employee_list', 'personal_profile', 'timesheets', 'timesheets_approval'], 'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'], 'none' : [], } -export const getEmployeeAccessOptionIcon = (module: ModuleAccessName): string => { +export const getEmployeeAccessOptionIcon = (module: UserModuleAccess): string => { switch (module) { case 'dashboard': return 'home'; case 'employee_list' : return 'groups'; diff --git a/src/modules/shared/models/user.models.ts b/src/modules/shared/models/user.models.ts index 743108e..d97ae27 100644 --- a/src/modules/shared/models/user.models.ts +++ b/src/modules/shared/models/user.models.ts @@ -3,13 +3,18 @@ export interface User { last_name: string; email: string; role: UserRole; + user_module_access: UserModuleAccess; } -export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST'; +export type UserRole = 'ADMIN' | 'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST'; -export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [ - 'ADMIN', - 'SUPERVISOR', - 'HR', - 'ACCOUNTING', -] \ No newline at end of file +export const ModuleNames = { + DASHBOARD: 'dashboard', + EMPLOYEE_LIST: 'employee_list', + EMPLOYEE_MANAGEMENT: 'employee_management', + PERSONAL_PROFILE: 'personal_profile', + TIMESHEETS: 'timesheets', + TIMESHEETS_APPROVAL: 'timesheets_approval', +} as const; + +export type UserModuleAccess = typeof ModuleNames[keyof typeof ModuleNames]; \ No newline at end of file diff --git a/src/modules/timesheets/components/expense-dialog-form.vue b/src/modules/timesheets/components/expense-dialog-form.vue index ffec64f..03b2e39 100644 --- a/src/modules/timesheets/components/expense-dialog-form.vue +++ b/src/modules/timesheets/components/expense-dialog-form.vue @@ -101,6 +101,7 @@ v-model="is_navigator_open" transition-show="jump-right" transition-hide="jump-right" + class="z-top" > diff --git a/src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue b/src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue index 1a7ee66..2c1fdc7 100644 --- a/src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue +++ b/src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue @@ -94,6 +94,7 @@ v-model="is_navigator_open" transition-show="jump-right" transition-hide="jump-right" + class="z-top" > - - + + {{ $t('timesheet.expense.employee_comment') }} @@ -231,14 +232,13 @@ diff --git a/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue b/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue index 50039d6..cc1101e 100644 --- a/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue +++ b/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue @@ -17,11 +17,13 @@ const select_ref = ref(null); const is_showing_comment_popup = ref(false); const comment_length = computed(() => shift.value.comment?.length ?? 0); + const error_message = ref(''); - const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{ + const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{ dense?: boolean; hasShiftAfter?: boolean; isTimesheetApproved?: boolean; + errorMessage?: string | undefined; }>(); const emit = defineEmits<{ @@ -37,6 +39,17 @@ } }; + const onTimeFieldBlur = (time_string: string) => { + if (time_string.length < 1 || !time_string) { + shift.value.has_error = true; + error_message.value = 'timesheet.errors.SHIFT_TIME_REQUIRED'; + } else { + shift.value.has_error = false; + error_message.value = ''; + emit('onTimeFieldBlur'); + } + } + const getCommentCounterColor = (comment_length: number) => { if (comment_length < 200) return 'primary'; if (comment_length < 250) return 'warning'; @@ -50,6 +63,9 @@ shift_type_selected.value = undefined; ui_store.focus_next_component = false; } + + if (errorMessage) + error_message.value = errorMessage; }); @@ -162,6 +178,7 @@ anchor="top middle" self="bottom middle" :offset="[0, 10]" + :hide-delay="1000" class="text-uppercase text-weight-bold text-white bg-primary" > {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') : @@ -184,6 +201,7 @@ anchor="top middle" self="bottom middle" :offset="[0, 10]" + :hide-delay="1000" class="text-uppercase text-weight-medium text-white bg-accent" > {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') : @@ -208,13 +226,15 @@ lazy-rules no-error-icon hide-bottom-space + :error="shift.has_error" + :error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''" :label-color="!shift.is_approved ? 'accent' : 'white'" class="rounded-5 bg-dark" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')" input-style="font-size: 1.2em;" :style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" - @blur="emit('onTimeFieldBlur')" + @blur="onTimeFieldBlur(shift.start_time)" >