refactor(approvals): move functionalities for separation of concern, page formatting, i18n refactoring, consideration for mobile appearance.

This commit is contained in:
Nicolas Drolet 2025-08-21 17:06:57 -04:00
parent 27e73f8f51
commit d0e64bdffc
13 changed files with 153 additions and 72 deletions

View File

@ -4,26 +4,19 @@ import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
export type MessageLanguages = keyof typeof messages;
// Type-define 'en-US' as the master schema for the resource
export type MessageSchema = typeof messages['en'];
export type MessageSchema = typeof messages['en-CA'];
// See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition
/* eslint-disable @typescript-eslint/no-empty-object-type */
declare module 'vue-i18n' {
// define the locale messages schema
export interface DefineLocaleMessage extends MessageSchema {}
// define the datetime format schema
export interface DefineDateTimeFormat {}
// define the number format schema
export interface DefineNumberFormat {}
}
/* eslint-enable @typescript-eslint/no-empty-object-type */
/* eslint-enable @typescript-eslint/no-empty-object-type */
export default defineBoot(({ app }) => {
const i18n = createI18n<{ message: MessageSchema }, MessageLanguages>({
locale: 'fr',
locale: 'fr-FR',
legacy: false,
messages,
});

View File

@ -24,7 +24,7 @@ export default {
},
navBar: {
userMenuHome: 'Homepage',
userMenuEmployeeList: 'Employee list',
userMenuEmployeeList: 'Employee Directory',
userMenuShiftValidation: 'Timesheet Approval',
userMenuProfile: 'Profile',
userMenuHelp: 'Help',
@ -140,7 +140,7 @@ export default {
card_4: 'Customers',
},
usersListPage: {
tableHeader: 'Employee list',
tableHeader: 'Employee Directory',
searchInput: 'Search',
userListFirstName: 'First name',
userListLastName: 'Last name',
@ -162,6 +162,7 @@ export default {
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
languageLabel: 'Language',
},
editUserPage: {
title: 'Edit Account',
@ -240,11 +241,12 @@ export default {
passwordValidation: 'Password must meet all criteria.',
confirmPasswordValidation: 'Password must match new Password.',
},
pagesTitles: {
newUserPageTitle: 'New user',
updateUserPageTitle: 'Update user',
timeSheetPageTitle: 'Time sheet',
timeSheetValidationsIdPageTitle: 'Time sheet',
pageTitles: {
employeeDirectory: 'Employee Directory',
newUsers: 'New user',
updateUsers: 'Update user',
timeSheets: 'Time sheet',
timeSheetValidations: 'Time sheet',
},
timeSheet: {
timeSheetTab_1: 'Shifts',
@ -265,7 +267,7 @@ export default {
sickDay: 'Sick working day',
vacancyDay: 'vacation',
holiday: 'Holiday',
dateRangesWeek: 'Week from',
dateRangesFrom: 'from',
dateRangesTo: 'to',
shiftBankedHours: 'Total hours to bank',
bankedHoursHint_1: ' on',
@ -294,7 +296,6 @@ export default {
mileage: 'Mileage',
},
timeSheetValidations: {
tableHeader: 'List of employees',
tableColumnLabelFullname: 'Full name',
tableColumnLabelRegularHours: 'regular hours',
tableColumnLabelEveningHours: 'evening',

View File

@ -163,7 +163,7 @@ export default {
},
navBar: {
userMenuHome: 'Accueil',
userMenuEmployeeList: 'Liste employés',
userMenuEmployeeList: 'Répertoire employés',
userMenuShiftValidation: 'Valider les heures',
userMenuProfile: 'Profil',
userMenuHelp: 'Aide',
@ -181,11 +181,12 @@ export default {
deleteAll: 'Supprimer tout',
close: 'Fermer',
},
pagesTitles: {
newUserPageTitle: 'Nouvel utilisateur',
updateUserPageTitle: 'Mettre à jour lutilisateur',
timeSheetPageTitle: 'Carte de temps',
timeSheetValidationsIdPageTitle: 'Carte de temps',
pageTitles: {
employeeDirectory: 'Répertoire des Employés',
newUsers: 'Nouvel utilisateur',
updateUsers: 'Mettre à jour lutilisateur',
timeSheets: 'Carte de temps',
timeSheetValidations: 'Validation cartes de temps',
},
profilePage: {
title: 'Profil',
@ -231,6 +232,7 @@ export default {
loading: 'Téléchargement des données en cours...',
failedToLoad: 'Aucune donnée à afficher',
failedToSearch: 'Aucun résultat de recherche obtenu',
languageLabel: 'Langue',
},
shiftColumns: {
title: 'Quarts de travail',
@ -315,7 +317,7 @@ export default {
sickDay: 'Maladie',
vacancyDay: 'Vacances',
holiday: 'Férié',
dateRangesWeek: 'Semaine du',
dateRangesFrom: 'du',
dateRangesTo: 'au',
shiftBankedHours: 'Totale dheures à banquer',
bankedHoursHint_1: ' sur',
@ -344,7 +346,6 @@ export default {
mileage: 'Kilometrage',
},
timeSheetValidations: {
tableHeader: 'Liste des employés',
tableColumnLabelFullname: 'nom complet',
tableColumnLabelRegularHours: 'heures régulières',
tableColumnLabelEveningHours: 'soir',
@ -378,7 +379,7 @@ export default {
unlockToolTip: 'Déverrouiller la semaine',
},
usersListPage: {
tableHeader: 'Liste demployées',
tableHeader: 'Répertoire du personnel',
searchInput: 'rechercher',
userListFirstName: 'prénom',
userListLastName: 'nom de famille',

View File

@ -2,6 +2,6 @@ import enCA from './en-ca';
import frCA from './fr-ca';
export default {
'en': enCA,
'fr': frCA,
'en-CA': enCA,
'fr-FR': frCA,
};

View File

@ -6,8 +6,8 @@
<template>
<q-page>
<EmployeeListAddModifyDialog />
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder text-primary">
{{ $t('usersListPage.tableHeader') }}
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder text-grey-8">
{{ $t('pageTitles.employeeDirectory') }}
</div>
<SupervisorCrewTable />
</q-page>

View File

@ -2,19 +2,33 @@
import { useI18n } from 'vue-i18n';
const { locale } = useI18n();
const setLocale = (newLocale: string) => { locale.value = newLocale; };
const localeOptions = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Francais' },
{ value: 'en-CA', label: 'English' },
{ value: 'fr-FR', label: 'Francais' },
];
</script>
<template>
<q-btn-dropdown flat :label=locale class="rounded-borders" icon="language">
<q-btn-dropdown flat :label="$t('shared.languageLabel')" class="rounded-borders" icon="language">
<q-list>
<q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="setLocale(option.value)">
<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>
<!-- <q-select
v-model="locale"
:options="localeOptions"
dense
borderless
emit-value
map-options
hide-dropdown-icon
class="text-white"
>
<template v-slot:prepend>
<q-icon name="language" color="white"/>
</template>
</q-select> -->
</template>

View File

@ -1,10 +1,8 @@
<script setup lang="ts">
import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { ref } from 'vue';
const authStore = useAuthStore();
const uiStore = useUiStore();
const currentUser = authStore.user;
// Will need to implement this eventually, just testing the look for now
@ -12,15 +10,15 @@ import { ref } from 'vue';
</script>
<template>
<q-item clickable v-ripple dark @click="uiStore.toggleRightDrawer">
<q-item-section side>
<q-item clickable v-ripple dark class="q-pa-none">
<q-item-section :side="$q.screen.gt.sm">
<q-avatar rounded >
<q-img src="src/assets/targo-default-avatar.png" />
<q-badge floating color="red" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-section v-if="$q.screen.gt.sm">
<q-item-label>{{ currentUser.firstName }} {{ currentUser.lastName }}</q-item-label>
<q-item-label caption>{{ notifAmount }} new messages</q-item-label>
</q-item-section>

View File

@ -36,7 +36,7 @@
<q-icon name="home" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuHome') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHome') }}</q-item-label>
</q-item-section>
</q-item>
@ -47,7 +47,7 @@
<q-icon name="event_available" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuShiftValidation') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuShiftValidation') }}</q-item-label>
</q-item-section>
</q-item>
@ -58,7 +58,7 @@
<q-icon name="view_list" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuEmployeeList') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuEmployeeList') }}</q-item-label>
</q-item-section>
</q-item>
@ -68,7 +68,7 @@
<q-icon name="account_box" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuProfile') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuProfile') }}</q-item-label>
</q-item-section>
</q-item>
@ -78,7 +78,7 @@
<q-icon name="contact_support" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuHelp') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHelp') }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
@ -89,7 +89,7 @@
<q-icon name="exit_to_app" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuLogout') }}</q-item-label>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuLogout') }}</q-item-label>
</q-item-section>
</q-item>
</q-scroll-area>

View File

@ -9,6 +9,7 @@
import { getCurrentPayPeriod } from 'src/utils/pay-period-calculator';
import { useAuthStore } from 'src/stores/auth-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue';
const { t } = useI18n();
@ -39,8 +40,7 @@
const filter = ref('');
onMounted( async () => {
await timesheetApprovalApi.getPayPeriodByDate(new Date());
await timesheetApprovalApi.getTimesheetApprovalPayPeriodEmployeeOverviews(currentYear, currentPayPeriod, authStore.user.email);
await timesheetApprovalApi.getPayPeriodOverviewByDate(new Date());
originalApprovals.value = Object.fromEntries( timesheetStore.payPeriodEmployeeOverviews.map(emp => [emp.email, emp.is_approved]));
})
</script>
@ -66,7 +66,10 @@
>
<!-- Top Bar that contains Search, Title, Filters -->
<template v-slot:top>
<div class="full-width row">
<div :class="$q.screen.lt.md ? 'column justify-center items-center' : 'full-width row'">
<!-- Date Picker -->
<TimesheetApprovalPeriodPicker />
<q-space />
<!-- Filters toggle -->

View File

@ -1,18 +1,55 @@
<script setup lang="ts">
/* eslint-disable */
import { ref, computed } from 'vue';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { date } from 'quasar';
const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore();
const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY/MM/DD' ));
const is_calendar_limit = computed( () => {
return timesheet_store.currentPayPeriod.pay_year === 2024 && timesheet_store.currentPayPeriod.pay_period_no <= 1;
});
</script>
<template>
<div class="column items-center">
<div class="text-primary text-h5">{{ timesheet_store.currentPayPeriod?.label || '' }}</div>
<q-btn-group push rounded>
<q-btn push icon="keyboard_arrow_left" color="primary" class="q-px-xl" @click="timesheet_approval_api.getNextPayPeriodOverview(-1)"/>
<q-btn push icon="date_range" color="primary" class="q-px-xl" />
<q-btn push icon="keyboard_arrow_right" color="primary" class="q-px-xl" @click="timesheet_approval_api.getNextPayPeriodOverview(1)"/>
</q-btn-group>
<div class="row">
<q-btn
push rounded
icon="keyboard_arrow_left"
color="primary"
@click="timesheet_approval_api.getNextPayPeriodOverview(-1)"
:disable="is_calendar_limit || timesheet_store.isLoading"
class="q-mr-sm q-px-sm"
/>
<q-btn
push rounded
icon="date_range"
color="primary"
@click="is_showing_calendar_picker = true"
:disable="timesheet_store.isLoading"
class="q-px-lg"
/>
<q-btn
push rounded
icon="keyboard_arrow_right"
color="primary"
@click="timesheet_approval_api.getNextPayPeriodOverview(1)"
:disable="timesheet_store.isLoading"
class="q-ml-sm q-px-sm"
/>
</div>
<q-dialog v-model="is_showing_calendar_picker" transition-show="slide-down" transition-hide="slide-up" position="top">
<q-date
v-model="calendar_date"
color="primary"
class="q-mt-xl"
today-btn
:options="date => date > '2023/12/16'"
/>
</q-dialog>
</template>

View File

@ -5,8 +5,13 @@ export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore();
const getPayPeriodByDate = async (date: Date) => {
await timesheet_store.getPayPeriodByDate(date);
const getPayPeriodOverviewByDate = async (date: Date) => {
const success = await timesheet_store.getPayPeriodByDate(date);
if (success) {
const current_pay_period = timesheet_store.currentPayPeriod;
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(current_pay_period.pay_year, current_pay_period.pay_period_no, auth_store.user.email);
}
}
/* This method attempts to get the next or previous pay period.
@ -35,13 +40,8 @@ export const useTimesheetApprovalApi = () => {
}
}
const getTimesheetApprovalPayPeriodEmployeeOverviews = async (year: number, period_number: number, supervisor_email: string): Promise<void> => {
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(year, period_number, supervisor_email);
}
return {
getPayPeriodByDate,
getPayPeriodOverviewByDate,
getNextPayPeriodOverview,
getTimesheetApprovalPayPeriodEmployeeOverviews,
}
};

View File

@ -1,12 +1,39 @@
<script setup lang="ts">
// const testDates: string[] = ['25 Juin 2025', '12 Juillet 2025', '13 Juillet 2025', '26 Juillet 2025', '27 Juillet 2025', '9 Aout 2025']
import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue';
import TimesheetApprovalEmployeeOverviewList from '../components/timesheet-approval-employee-overview-list.vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { date } from 'quasar';
const { locale } = useI18n();
const timesheet_store = useTimesheetStore();
const date_options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: "long",
year: 'numeric',
};
const pay_period_label = computed(() => {
const dates = timesheet_store.currentPayPeriod.label.split('.');
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
const end_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
return {
start_date: start_date,
end_date: end_date,
};
});
</script>
<template>
<q-page padding class="q-pa-md bg-secondary">
<TimesheetApprovalPeriodPicker />
<div class="text-h4 row justify-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">{{ $t('pageTitles.timeSheetValidations') }}</div>
<div class="row items-center justify-center q-py-none q-my-none">
<div class="text-primary text-h6 text-uppercase">{{ pay_period_label.start_date }}</div>
<div class="text-grey-8 text-weight-bold text-uppercase q-mx-md">{{ $t('timeSheet.dateRangesTo') }}</div>
<div class="text-primary text-h6 text-uppercase">{{ pay_period_label.end_date }}</div>
</div>
<TimesheetApprovalEmployeeOverviewList />
</q-page>
</template>

View File

@ -18,18 +18,23 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>([]);
const isLoading = ref<boolean>(false);
const getPayPeriodByDate = async (date: Date) => {
const getPayPeriodByDate = async (date: Date): Promise<boolean> => {
isLoading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodByDate(date);
currentPayPeriod.value = response;
isLoading.value = false;
return true;
} catch(error){
console.error('Could not get current pay period: ', error );
//TODO: More in-depth error-handling here
}
isLoading.value = false;
return false;
};
const getPayPeriodByYearAndPeriodNumber = async (year: number, period_number: number): Promise<boolean> => {
@ -38,6 +43,8 @@ export const useTimesheetStore = defineStore('timesheet', () => {
try {
const response = await timesheetApprovalService.getPayPeriodByYearAndPeriodNumber(year, period_number);
currentPayPeriod.value = response;
isLoading.value = false;
return true;
} catch(error){
console.error('Could not get current pay period: ', error );
@ -60,7 +67,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
payPeriodEmployeeOverviews.value = [];
// TODO: trigger an alert window with an error message here!
}
isLoading.value = false;
};