Merge pull request 'dev/nicolas/approvals-refactor' (#4) from dev/nicolas/approvals-refactor into main

Reviewed-on: Targo/targo_frontend#4
This commit is contained in:
Nicolas 2025-08-22 08:01:58 -04:00
commit b0dcfdc73c
28 changed files with 866 additions and 745 deletions

View File

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

View File

@ -1,7 +1,7 @@
// app global css in SCSS form // app global css in SCSS form
@each $size in (5, 10, 15, 20, 25) { @each $size in (5, 10, 15, 20, 25) {
.rounded-#{$size} { .rounded-#{$size} {
border-radius: #{$size}px; border-radius: #{$size}px !important;
} }
} }

View File

@ -24,7 +24,7 @@ export default {
}, },
navBar: { navBar: {
userMenuHome: 'Homepage', userMenuHome: 'Homepage',
userMenuEmployeeList: 'Employee list', userMenuEmployeeList: 'Employee Directory',
userMenuShiftValidation: 'Timesheet Approval', userMenuShiftValidation: 'Timesheet Approval',
userMenuProfile: 'Profile', userMenuProfile: 'Profile',
userMenuHelp: 'Help', userMenuHelp: 'Help',
@ -140,7 +140,7 @@ export default {
card_4: 'Customers', card_4: 'Customers',
}, },
usersListPage: { usersListPage: {
tableHeader: 'Employee list', tableHeader: 'Employee Directory',
searchInput: 'Search', searchInput: 'Search',
userListFirstName: 'First name', userListFirstName: 'First name',
userListLastName: 'Last name', userListLastName: 'Last name',
@ -159,6 +159,10 @@ export default {
}, },
shared:{ shared:{
searchBar: 'Search', searchBar: 'Search',
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
languageLabel: 'Language',
}, },
editUserPage: { editUserPage: {
title: 'Edit Account', title: 'Edit Account',
@ -237,11 +241,12 @@ export default {
passwordValidation: 'Password must meet all criteria.', passwordValidation: 'Password must meet all criteria.',
confirmPasswordValidation: 'Password must match new Password.', confirmPasswordValidation: 'Password must match new Password.',
}, },
pagesTitles: { pageTitles: {
newUserPageTitle: 'New user', employeeDirectory: 'Employee Directory',
updateUserPageTitle: 'Update user', newUsers: 'New user',
timeSheetPageTitle: 'Time sheet', updateUsers: 'Update user',
timeSheetValidationsIdPageTitle: 'Time sheet', timeSheets: 'Time sheet',
timeSheetValidations: 'Time sheet',
}, },
timeSheet: { timeSheet: {
timeSheetTab_1: 'Shifts', timeSheetTab_1: 'Shifts',
@ -262,7 +267,7 @@ export default {
sickDay: 'Sick working day', sickDay: 'Sick working day',
vacancyDay: 'vacation', vacancyDay: 'vacation',
holiday: 'Holiday', holiday: 'Holiday',
dateRangesWeek: 'Week from', dateRangesFrom: 'from',
dateRangesTo: 'to', dateRangesTo: 'to',
shiftBankedHours: 'Total hours to bank', shiftBankedHours: 'Total hours to bank',
bankedHoursHint_1: ' on', bankedHoursHint_1: ' on',
@ -291,14 +296,13 @@ export default {
mileage: 'Mileage', mileage: 'Mileage',
}, },
timeSheetValidations: { timeSheetValidations: {
tableHeader: 'List of employees',
tableColumnLabelFullname: 'Full name', tableColumnLabelFullname: 'Full name',
tableColumnLabelRegularHours: 'regular hours', tableColumnLabelRegularHours: 'regular hours',
tableColumnLabelEveningHours: 'evening hours', tableColumnLabelEveningHours: 'evening',
tableColumnLabelEmergencyHours: 'emergency hours', tableColumnLabelEmergencyHours: 'emergency',
tableColumnLabelOvertime: 'overtime hours', tableColumnLabelOvertime: 'overtime',
tableColumnLabelExpenses: 'of expenses', tableColumnLabelExpenses: 'expenses',
tableColumnLabelMileage: 'of mileage', tableColumnLabelMileage: 'mileage',
actionTitle: 'Please save the changes made.', actionTitle: 'Please save the changes made.',
actionButton: 'Save', actionButton: 'Save',
timeSheetStatusVerified: 'approved', timeSheetStatusVerified: 'approved',

View File

@ -163,7 +163,7 @@ export default {
}, },
navBar: { navBar: {
userMenuHome: 'Accueil', userMenuHome: 'Accueil',
userMenuEmployeeList: 'Liste employés', userMenuEmployeeList: 'Répertoire employés',
userMenuShiftValidation: 'Valider les heures', userMenuShiftValidation: 'Valider les heures',
userMenuProfile: 'Profil', userMenuProfile: 'Profil',
userMenuHelp: 'Aide', userMenuHelp: 'Aide',
@ -181,11 +181,12 @@ export default {
deleteAll: 'Supprimer tout', deleteAll: 'Supprimer tout',
close: 'Fermer', close: 'Fermer',
}, },
pagesTitles: { pageTitles: {
newUserPageTitle: 'Nouvel utilisateur', employeeDirectory: 'Répertoire des Employés',
updateUserPageTitle: 'Mettre à jour lutilisateur', newUsers: 'Nouvel utilisateur',
timeSheetPageTitle: 'Carte de temps', updateUsers: 'Mettre à jour lutilisateur',
timeSheetValidationsIdPageTitle: 'Carte de temps', timeSheets: 'Carte de temps',
timeSheetValidations: 'Validation cartes de temps',
}, },
profilePage: { profilePage: {
title: 'Profil', title: 'Profil',
@ -228,6 +229,10 @@ export default {
}, },
shared:{ shared:{
searchBar: 'Rechercher', searchBar: 'Rechercher',
loading: 'Téléchargement des données en cours...',
failedToLoad: 'Aucune donnée à afficher',
failedToSearch: 'Aucun résultat de recherche obtenu',
languageLabel: 'Langue',
}, },
shiftColumns: { shiftColumns: {
title: 'Quarts de travail', title: 'Quarts de travail',
@ -312,7 +317,7 @@ export default {
sickDay: 'Maladie', sickDay: 'Maladie',
vacancyDay: 'Vacances', vacancyDay: 'Vacances',
holiday: 'Férié', holiday: 'Férié',
dateRangesWeek: 'Semaine du', dateRangesFrom: 'du',
dateRangesTo: 'au', dateRangesTo: 'au',
shiftBankedHours: 'Totale dheures à banquer', shiftBankedHours: 'Totale dheures à banquer',
bankedHoursHint_1: ' sur', bankedHoursHint_1: ' sur',
@ -341,14 +346,13 @@ export default {
mileage: 'Kilometrage', mileage: 'Kilometrage',
}, },
timeSheetValidations: { timeSheetValidations: {
tableHeader: 'Liste des employés',
tableColumnLabelFullname: 'nom complet', tableColumnLabelFullname: 'nom complet',
tableColumnLabelRegularHours: 'heures régulières', tableColumnLabelRegularHours: 'heures régulières',
tableColumnLabelEveningHours: 'heures de soir', tableColumnLabelEveningHours: 'soir',
tableColumnLabelEmergencyHours: 'heures durgence', tableColumnLabelEmergencyHours: 'urgence',
tableColumnLabelOvertime: 'heures supplémentaires', tableColumnLabelOvertime: 'supplémentaires',
tableColumnLabelExpenses: 'de dépenses', tableColumnLabelExpenses: 'dépenses',
tableColumnLabelMileage: 'de kilométrage', tableColumnLabelMileage: 'kilométrage',
actionTitle: 'Veuillez enregistrer les changements effectués.', actionTitle: 'Veuillez enregistrer les changements effectués.',
actionButton: 'Enregistrer', actionButton: 'Enregistrer',
timeSheetStatusVerified: 'validé', timeSheetStatusVerified: 'validé',
@ -375,7 +379,7 @@ export default {
unlockToolTip: 'Déverrouiller la semaine', unlockToolTip: 'Déverrouiller la semaine',
}, },
usersListPage: { usersListPage: {
tableHeader: 'Liste demployées', tableHeader: 'Répertoire du personnel',
searchInput: 'rechercher', searchInput: 'rechercher',
userListFirstName: 'prénom', userListFirstName: 'prénom',
userListLastName: 'nom de famille', userListLastName: 'nom de famille',

View File

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

View File

@ -1,5 +1,4 @@
import { useAuthStore } from "../../../stores/auth-store"; import { useAuthStore } from "../../../stores/auth-store";
import type { User } from "src/modules/shared/types/user-interface";
export const useAuthApi = () => { export const useAuthApi = () => {
const authStore = useAuthStore(); const authStore = useAuthStore();
@ -22,8 +21,8 @@ export const useAuthApi = () => {
return authStore.isAuthorizedUser; return authStore.isAuthorizedUser;
}; };
const setUser = (currentUser: User) => { const setUser = (bypassRole: string) => {
authStore.user = currentUser; authStore.setUser(bypassRole);
} }
return { return {

View File

@ -1,10 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useAuthApi } from '../composables/use-auth-api'; import { useAuthApi } from '../composables/use-auth-api';
import type { User } from 'src/modules/shared/types/user-interface';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const authApi = useAuthApi(); const authApi = useAuthApi();
const email = ref(''); const email = ref('');
const isShowingEmployeeLoginButton = ref(false); const isShowingEmployeeLoginButton = ref(false);
@ -12,12 +10,7 @@
const router = useRouter(); const router = useRouter();
const setBypassUser = (bypassRole: string) => { const setBypassUser = (bypassRole: string) => {
authApi.setUser({ authApi.setUser(bypassRole);
firstName: "Testing",
lastName: bypassRole,
email: "testingT@targointernet.com",
role: bypassRole || "guest"
} as User);
router.push({ name: 'dashboard' }).catch( err => { router.push({ name: 'dashboard' }).catch( err => {
console.error('Router navigation failed: ', err); console.error('Router navigation failed: ', err);
@ -94,7 +87,7 @@
<q-btn-group push rounded> <q-btn-group push rounded>
<q-btn push color="primary" text-color="white" label="ACCOUNTING" icon="attach_money" @click="setBypassUser('accounting')"/> <q-btn push color="primary" text-color="white" label="ACCOUNTING" icon="attach_money" @click="setBypassUser('accounting')"/>
<q-btn push color="primary" text-color="white" label="SUPERVISOR" icon="supervisor_account" @click="setBypassUser('supervisor')"/> <q-btn push color="primary" text-color="white" label="SUPERVISOR" icon="supervisor_account" @click="setBypassUser('supervisor')"/>
<q-btn push color="primary" text-color="white" label="HR" icon="diversity_3" @click="setBypassUser('human resources')"/> <q-btn push color="primary" text-color="white" label="HR" icon="diversity_3" @click="setBypassUser('human_resources')"/>
<q-btn push color="primary" text-color="white" label="EMPLOYEE" icon="support_agent" @click="setBypassUser('employee')"/> <q-btn push color="primary" text-color="white" label="EMPLOYEE" icon="support_agent" @click="setBypassUser('employee')"/>
</q-btn-group> </q-btn-group>
</q-card-section> </q-card-section>

View File

@ -25,7 +25,7 @@ import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-em
<template> <template>
<q-card <q-card
v-ripple v-ripple
class="rounded-15 bg-white col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer" class="rounded-15 bg-white col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer q-ma-sm"
style="max-width: 230px;" style="max-width: 230px;"
@click="onProfileCardClick(props.row.email)" @click="onProfileCardClick(props.row.email)"
> >
@ -37,9 +37,9 @@ import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-em
<q-card-section class="text-center text-h6 text-primary text-weight-medium text-uppercase q-pb-none col-2 content-end" style="line-height: 0.7em;"> <q-card-section class="text-center text-h6 text-primary text-weight-medium text-uppercase q-pb-none col-2 content-end" style="line-height: 0.7em;">
<div class="ellipsis"> <div class="ellipsis">
{{ props.row.first_name }} {{ props.row.last_name }} {{ props.row.first_name }} {{ props.row.last_name }}
<q-separator color="primary" />
</div> </div>
</q-card-section> </q-card-section>
<q-separator color="primary" class="q-mx-sm q-mt-xs" />
<q-card-section class="text-caption text-grey-8 text-body2 text-uppercase q-pt-none text-center col content-start" style="min-height: 5em;"> <q-card-section class="text-caption text-grey-8 text-body2 text-uppercase q-pt-none text-center col content-start" style="min-height: 5em;">
<div class=" ellipsis-2-lines"> <div class=" ellipsis-2-lines">
{{ props.row.job_title }} {{ props.row.job_title }}

View File

@ -1,29 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api'; import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue'; import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface'; import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
import type { QTableColumn } from 'quasar'; import type { QTableColumn } from 'quasar';
const employeeListApi = useEmployeeListApi(); const employeeListApi = useEmployeeListApi();
const employeeStore = useEmployeeStore(); const employeeStore = useEmployeeStore();
const isLoadingList = ref<boolean>(true); const isLoadingList = ref<boolean>(true);
const { t } = useI18n(); const { t } = useI18n();
const filter = ref(""); const filter = ref("");
const isGridMode = ref(true); const isGridMode = ref(true);
const pagination = ref({ rowsPerPage: 0 });
const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [
{name: 'first_name', label: t('usersListPage.userListFirstName'), field: 'first_name'}, {name: 'first_name', label: t('usersListPage.userListFirstName'), field: 'first_name', align: 'left'},
{name: 'last_name', label: t('usersListPage.userListLastName'), field: 'last_name', align: 'left'}, {name: 'last_name', label: t('usersListPage.userListLastName'), field: 'last_name', align: 'left'},
{name: 'email', label: t('usersListPage.userListEmail'), field: 'email', align:'center'}, {name: 'email', label: t('usersListPage.userListEmail'), field: 'email', align: 'left'},
{name: 'supervisor_full_name', label: t('usersListPage.userListSupervisor'), field: 'supervisor_full_name', align: 'left'}, {name: 'supervisor_full_name', label: t('usersListPage.userListSupervisor'), field: 'supervisor_full_name', align: 'left'},
{name: 'company_name', label: t('usersListPage.userListCompany'), field: 'company_name'}, {name: 'company_name', label: t('usersListPage.userListCompany'), field: 'company_name', align: 'left'},
{name: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title'}, {name: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title', align: 'left'},
]); ]);
onMounted( async () => { onMounted( async () => {
isLoadingList.value = true; isLoadingList.value = true;
@ -35,28 +36,39 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
<template> <template>
<div class="q-pa-lg col"> <div class="q-pa-lg col">
<q-table <q-table
dense
flat
hide-pagination
virtual-scroll
title=" " title=" "
card-style="max-height: 70vh;"
:rows="employeeStore.employeeList" :rows="employeeStore.employeeList"
:columns="employeeListColumns" :columns="employeeListColumns"
row-key="name" row-key="name"
v-model:pagination="pagination"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
:filter="filter" :filter="filter"
class="q-pa-md" class="q-pa-md bg-transparent"
color="primary"
table-header-class="text-primary text-uppercase" table-header-class="text-primary text-uppercase"
card-container-class="justify-center q-gutter-md" card-container-class="justify-center"
:grid="isGridMode" :grid="isGridMode"
:loading="isLoadingList" :loading="isLoadingList"
flat :no-data-label="$t('shared.failedToLoad')"
dense :no-results-label="$t('shared.failedToSearch')"
:loading-label="$t('shared.loading')"
table-class="bg-white q-pa-md q-mx-md rounded-10 shadow-12"
table-style=""
@row-click="() => console.log('click!')"
> >
<template v-slot:item="props"> <template v-slot:item="props">
<SupervisorCrewTableItem :row="props.row"/> <SupervisorCrewTableItem :row="props.row"/>
</template> </template>
<template v-slot:top> <template v-slot:top>
<div class="row full-width q-mb-sm">
<q-btn push icon="person_add" color="primary" :label="$t('usersListPage.addButton')"/> <q-btn push icon="person_add" color="primary" :label="$t('usersListPage.addButton')"/>
<q-space /> <q-space />
<div class="row q-mb-lg">
<q-btn-toggle push class="q-mr-md" color="white" text-color="primary" toggle-color="primary" v-model="isGridMode" <q-btn-toggle push class="q-mr-md" color="white" text-color="primary" toggle-color="primary" v-model="isGridMode"
:options="[ :options="[
{icon: 'grid_view', value: true}, {icon: 'grid_view', value: true},
@ -65,6 +77,7 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
<q-input <q-input
outlined outlined
dense dense
rounded
v-model="filter" v-model="filter"
:label="$t('shared.searchBar')" :label="$t('shared.searchBar')"
label-color="primary" bg-color="white" color="primary" label-color="primary" bg-color="white" color="primary"
@ -75,6 +88,16 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
</q-input> </q-input>
</div> </div>
</template> </template>
<!-- Template for custome failed-to-load state -->
<template v-slot:no-data="{ message, filter }">
<div class="full-width column items-center text-primary q-gutter-sm">
<span class="text-h6 q-mt-xl">
{{ message }}
</span>
<q-icon size="4em" :name="filter ? 'filter_alt_off' : 'error_outline'" />
</div>
</template>
</q-table> </q-table>
</div> </div>
</template> </template>

View File

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

View File

@ -2,19 +2,33 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { locale } = useI18n(); const { locale } = useI18n();
const setLocale = (newLocale: string) => { locale.value = newLocale; };
const localeOptions = [ const localeOptions = [
{ value: 'en', label: 'English' }, { value: 'en-CA', label: 'English' },
{ value: 'fr', label: 'Francais' }, { value: 'fr-FR', label: 'Francais' },
]; ];
</script> </script>
<template> <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-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-section>{{ option.label }}</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </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> </template>

View File

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

View File

@ -1,29 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { hasRequiredRole } from 'src/utils/has-required-role'; import { useUiStore } from 'src/stores/ui-store';
import { useUiStore } from 'src/stores/ui-store'; import { ref } from 'vue';
import { ref } from 'vue'; import { RouteNames } from 'src/router/router-constants';
import { RouteNames } from 'src/router/router-constants';
const authStore = useAuthStore(); const authStore = useAuthStore();
const uiStore = useUiStore(); const uiStore = useUiStore();
const router = useRouter(); const router = useRouter();
const miniState = ref(true); const miniState = ref(true);
const goToPageName = (pageName: string) => { const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => { router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err); console.error('Error with Vue Router: ', err);
}); });
}; };
const handleLogout = () => { const handleLogout = () => {
authStore.logout(); authStore.logout();
router.push({ name: 'login' }).catch(err => { router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err); console.log('could not log you out: ', err);
}) })
} }
</script> </script>
<template> <template>
@ -37,29 +36,29 @@ const handleLogout = () => {
<q-icon name="home" color="primary" /> <q-icon name="home" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
<!-- Timesheet Validation -- Supervisor and Accounting only --> <!-- Timesheet Validation -- Supervisor and Accounting only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)" <q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
v-if="hasRequiredRole('supervisor', 'accounting')"> v-if="['supervisor', 'accounting'].includes(authStore.user.role)">
<q-item-section avatar> <q-item-section avatar>
<q-icon name="event_available" color="primary" /> <q-icon name="event_available" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
<!-- Employee List -- Supervisor, Accounting and HR only --> <!-- Employee List -- Supervisor, Accounting and HR only -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)" <q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
v-if="hasRequiredRole('supervisor', 'human resources', 'accounting')"> v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)">
<q-item-section avatar> <q-item-section avatar>
<q-icon name="view_list" color="primary" /> <q-icon name="view_list" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
@ -69,7 +68,7 @@ const handleLogout = () => {
<q-icon name="account_box" color="primary" /> <q-icon name="account_box" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
@ -79,7 +78,7 @@ const handleLogout = () => {
<q-icon name="contact_support" color="primary" /> <q-icon name="contact_support" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
</q-list> </q-list>
@ -90,7 +89,7 @@ const handleLogout = () => {
<q-icon name="exit_to_app" color="primary" /> <q-icon name="exit_to_app" color="primary" />
</q-item-section> </q-item-section>
<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-section>
</q-item> </q-item>
</q-scroll-area> </q-scroll-area>

View File

@ -0,0 +1,6 @@
import type { PayPeriod } from "./pay-period-interface";
export interface PayPeriodBundle {
current: PayPeriod;
periods: PayPeriod[];
}

View File

@ -1,7 +1,8 @@
export interface PayPeriod { export interface PayPeriod {
period_number: number; pay_period_no: number;
start_date: string; period_start: string;
end_date: string; period_end: string;
year: number; payday: string;
pay_year: number;
label: string; label: string;
}; }

View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface';
interface TableColumn {
name: string;
label: string;
value: unknown;
};
const props = defineProps<{
cols: TableColumn[];
row: PayPeriodEmployeeOverview;
modelValue: boolean;
}>();
</script>
<template>
<div class="q-px-sm q-pb-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
<q-card class="rounded-10">
<!-- Card header with employee name -->
<q-card-section horizontal class="q-py-none q-px-md">
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">{{ props.row.employee_name }}</div>
</q-card-section>
<q-separator color="accent" style="height: 2px;"/>
<!-- Main body of pay period card -->
<q-card-section class="q-pa-none q-mt-xs q-mb-sm">
<div class="row no-wrap">
<!-- left portion of pay period card -->
<div class="column no-wrap" :class="$q.screen.lt.md ? 'col' : 'col-8'">
<!-- Regular hours segment -->
<q-item dense class="column" :class="$q.screen.lt.md ? 'col' : 'col-8'">
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption">
{{ props.cols.find(c => c.name === 'regular_hours')?.label }}
</q-item-label>
<q-item-label class="text-weight-bolder text-h3 text-grey-8 q-py-none">
{{ props.cols.find(c => c.name === 'regular_hours')?.value }}
</q-item-label>
</q-item>
<q-separator color="accent" class="q-mx-sm"/>
<!-- Other hour types segment -->
<div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'">
<q-item dense class="column ellipsis " v-for="col in props.cols.slice(2, 5)" :key="col.label">
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption" style="font-size: 0.65em;">
{{ col.label }}
</q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8">
{{ col.value }}
</q-item-label>
</q-item>
</div>
</div>
<q-separator vertical color="accent" class="q-mt-xs q-mb-none"/>
<!-- Right portion of pay period card -->
<div class="no-wrap ellipsis col">
<q-item dense class="column" v-for="col in props.cols.slice(5, )" :key="col.label">
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption ellipsis" style="font-size: 0.8em;">
{{ col.label }}
</q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8">
{{ col.value }}
</q-item-label>
</q-item>
</div>
</div>
</q-card-section>
<q-separator color="primary" style="height: 2px;" />
<!-- Validate entire Pay Period section -->
<q-card-section
horizontal
class="q-pa-sm text-weight-bold"
:class="props.modelValue ? 'text-white bg-primary' : 'text-primary bg-white'"
>
<q-space />
<q-checkbox
dense
left-label
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="props.modelValue ? 'white' : 'primary'" keep-color
:model-value="props.modelValue"
@update:model-value="val => $emit('update:modelValue', val)"
:label="props.modelValue ? $t('timeSheetValidations.timeSheetStatusVerified') : $t('timeSheetValidations.timeSheetStatusUnverified')"
class="text-uppercase"
/>
</q-card-section>
</q-card>
</div>
</template>

View File

@ -1,13 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable */ /* eslint-disable */
import { computed, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface'; import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface';
import type { QTableColumn } from 'quasar'; import type { QTableColumn } from 'quasar';
import { mock_pay_period_employee_overviews } from '../timesheet-approval-test-constants'; import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
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(); const { t } = useI18n();
const currentPayPeriod = getCurrentPayPeriod();
const currentYear = (new Date()).getFullYear();
const originalApprovals = ref<Record<string, boolean>>({});
const hasChanges = computed(() => {
return timesheetStore.payPeriodEmployeeOverviews.some(emp => {
return emp.is_approved !== originalApprovals.value[emp.email];
});
});
const authStore = useAuthStore();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [ const columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [
{ name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true }, { name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true },
{ name: 'regular_hours', label: t('timeSheetValidations.tableColumnLabelRegularHours'), field: 'regular_hours', sortable: true }, { name: 'regular_hours', label: t('timeSheetValidations.tableColumnLabelRegularHours'), field: 'regular_hours', sortable: true },
@ -19,74 +38,80 @@
]); ]);
const filter = ref(''); const filter = ref('');
const selectedRows = ref<PayPeriodEmployeeOverview[]>();
const rows: PayPeriodEmployeeOverview[] = mock_pay_period_employee_overviews; onMounted( async () => {
await timesheetApprovalApi.getPayPeriodOverviewByDate(new Date());
originalApprovals.value = Object.fromEntries( timesheetStore.payPeriodEmployeeOverviews.map(emp => [emp.email, emp.is_approved]));
})
</script> </script>
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table <q-table
:rows="rows" :rows="timesheetStore.payPeriodEmployeeOverviews"
:columns="columns" :columns="columns"
row-key="employee_id" row-key="email"
selection="multiple"
v-model:selected="selectedRows"
:filter="filter" :filter="filter"
grid grid
dense dense
hide-pagination
color="primary"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
card-container-class="justify-center q-gutter-md" card-container-class="justify-center"
:loading="timesheetStore.isLoading"
:no-data-label="$t('shared.failedToLoad')"
:no-results-label="$t('shared.failedToSearch')"
:loading-label="$t('shared.loading')"
> >
<!-- Top Bar that contains Search, Title, Filters --> <!-- Top Bar that contains Search, Title, Filters -->
<template v-slot:top> <template v-slot:top>
<q-card flat class="full-width bg-primary row q-px-md"> <div :class="$q.screen.lt.md ? 'column justify-center items-center' : 'full-width row'">
<!-- Table Title --> <!-- Date Picker -->
<q-card-section class="q-py-xs"> <TimesheetApprovalPeriodPicker />
<div class="text-h4 text-white text-weight-bold">{{$t('timeSheetValidations.tableHeader')}}</div>
</q-card-section>
<q-space /> <q-space />
<!-- Filters toggle --> <!-- Filters toggle -->
<q-btn flat dense class="text-white" label="filters" icon-right="filter_alt" /> <q-btn-dropdown
rounded
push
class="q-mr-md bg-white text-primary"
label="filters"
icon="filter_alt"
/>
<!-- Search bar --> <!-- Search bar -->
<q-card-section class="q-py-xs"> <q-input
<q-input rounded standout="bg-white" dense debounce="300" v-model="filter" placeholder="Search" label-color="primary" bg-color="white"> outlined
<template v-slot:append> dense
<q-icon name="search" color="primary" /> rounded
</template> v-model="filter"
</q-input> :label="$t('shared.searchBar')"
</q-card-section> label-color="primary" bg-color="white" color="primary"
</q-card> >
<template v-slot:append>
<q-icon name="search" color="primary"/>
</template>
</q-input>
</div>
</template> </template>
<!-- Template for individual employee cards --> <!-- Template for individual employee cards -->
<template v-slot:item="props: { cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], row: PayPeriodEmployeeOverview, selected: boolean }"> <template v-slot:item="props: { cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], row: PayPeriodEmployeeOverview }">
<div class="q-px-sm q-pb-sm col-xs-6 col-sm-4 col-md-3 col-lg-2 grid-style-transition"> <TimesheetApprovalEmployeeOverviewListItem
<q-card class="rounded-15"> :cols="props.cols"
<q-card-section class="q-pb-sm"> :row="props.row"
<div class="text-primary text-h5 text-weight-bolder ellipsis">{{ props.row.employee_name }}</div> v-model="props.row.is_approved"/>
</q-card-section> </template>
<div v-for="col in props.cols.filter(col => col.name !== 'employee_name')" class="q-pa-none q-mx-sm items-center row" :class="{ 'bg-warning': col.name == 'overtime_hours' && col.value as number > 0 }" >
<q-card-section class="text-right text-weight-bolder text-subtitle1 text-primary q-pr-sm q-py-none col-3 ellipsis" style="line-height: 1.2em;">{{ col.value }}</q-card-section> <!-- Template for custome failed-to-load state -->
<q-card-section class="text-weight-bold q-pa-none col-9" >{{ col.label }}</q-card-section> <template v-slot:no-data="{ message, filter }">
</div> <div class="full-width column items-center text-primary q-gutter-sm">
<q-card-section horizontal class="q-pa-sm q-mt-sm" :class="{ 'bg-primary text-white': props.selected}"> <span class="text-h6 q-mt-xl">
<q-space /> {{ message }}
<!-- TODO: Replace checkbox with simple display of timesheet status (approved/pending/partial/complete/) --> </span>
<q-checkbox <q-icon size="4em" :name="filter ? 'filter_alt_off' : 'error_outline'" />
dense
left-label
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="props.selected ? 'white' : 'primary'" keep-color
v-model="props.selected"
:label="props.selected ? $t('timeSheetValidations.timeSheetStatusVerified') : ''" />
</q-card-section>
</q-card>
</div> </div>
</template> </template>
</q-table> </q-table>

View File

@ -1,21 +1,55 @@
<script setup lang="ts"> <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 { useTimesheetStore } from 'src/stores/timesheet-store';
import { date } from 'quasar';
const timesheetStore = useTimesheetStore(); const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore();
const updateCurrentPayPeriod = () => { const is_showing_calendar_picker = ref(false);
timesheetStore.getCurrentPayPeriod(); 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> </script>
<template> <template>
<div class="column items-center"> <div class="row">
<div class="text-primary text-h5">{{ timesheetStore.currentPayPeriod?.label }}</div> <q-btn
<q-btn-group push rounded> push rounded
<q-btn push icon="keyboard_arrow_left" color="primary" class="q-px-xl" /> icon="keyboard_arrow_left"
<q-btn push icon="date_range" color="primary" class="q-px-xl" @click="updateCurrentPayPeriod" /> color="primary"
<q-btn push icon="keyboard_arrow_right" color="primary" class="q-px-xl" /> @click="timesheet_approval_api.getNextPayPeriodOverview(-1)"
</q-btn-group> :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> </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> </template>

View File

@ -0,0 +1,47 @@
import { useTimesheetStore } from "src/stores/timesheet-store";
import { useAuthStore } from "src/stores/auth-store";
export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore();
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.
It checks if pay period number is within a certain range, adjusts pay period and year accordingly.
It then requests the matching pay period object to set as current pay period from server.
If successful, it then requests pay period overviews from that new pay period. */
const getNextPayPeriodOverview = async (direction: number) => {
const current_pay_period = timesheet_store.currentPayPeriod;
let new_pay_period_no = current_pay_period.pay_period_no + direction;
let new_pay_year = current_pay_period.pay_year;
if (new_pay_period_no > 26) {
new_pay_period_no = 1;
new_pay_year += 1;
}
if (new_pay_period_no < 1) {
new_pay_period_no = 26;
new_pay_year -= 1;
}
const success = await timesheet_store.getPayPeriodByYearAndPeriodNumber(new_pay_year, new_pay_period_no);
if (success) {
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(new_pay_year, new_pay_period_no, auth_store.user.email);
}
}
return {
getPayPeriodOverviewByDate,
getNextPayPeriodOverview,
}
};

View File

@ -1,12 +1,39 @@
<script setup lang="ts"> <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 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> </script>
<template> <template>
<q-page padding class="q-pa-md bg-secondary"> <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 /> <TimesheetApprovalEmployeeOverviewList />
</q-page> </q-page>
</template> </template>

View File

@ -1,37 +1,22 @@
import { api } from "src/boot/axios"; import { api } from "src/boot/axios";
import { mock_pay_periods } from "../timesheet-approval-test-constants"; import type { PayPeriodOverview } from "../types/timesheet-approval-pay-period-overview-interface";
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface"; import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
export const timesheetApprovalService = { export const timesheetApprovalService = {
getCurrentPayPeriod: (): PayPeriod => { getPayPeriodByDate: async (date: Date): Promise<PayPeriod> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD const response = await api.get(`pay-periods/date/${date.toISOString()}`);
//let current_pay_period: PayPeriod; return response.data;
//
// try {
// console.log("Trying to get current pay period");
// current_pay_period = await api.get(`/pay-periods/date/${(new Date()).toDateString()}`);
// return current_pay_period;
// } catch (err){
// console.log(err);
// }
// console.log("failed to retrieve current pay period");
return {
"period_number": 15,
"start_date": "2025-07-27",
"end_date": "2025-08-09",
"year": 2025,
"label": "2025-07-27 → 2025-08-09"
} as PayPeriod;
}, },
getAllPayPeriods: async () => { getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD const response = await api.get(`pay-periods/${year}/${period_number}`);
return await api.get(`/pay-periods/`) || mock_pay_periods; return response.data;
}, },
getPayPeriodEmployeeOverviews: async (period_number: number) => {
getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD // TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
return await api.get(`/pay-periods/${period_number}/overview`); const response = await api.get(`/pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data;
}, },
}; };

View File

@ -1,287 +1,287 @@
import type { PayPeriod } from "../shared/types/pay-period-interface"; // import type { PayPeriod } from "../shared/types/pay-period-interface";
import type { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface" // import type { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface"
export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [ // export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
{ // {
"employee_id": 'EMP-001', // "email": 'EMP-001',
"employee_name": 'Alice Johnson', // "employee_name": 'Alice Johnson',
"regular_hours": 75, // "regular_hours": 75,
"evening_hours": 12, // "evening_hours": 12,
"emergency_hours": 3, // "emergency_hours": 3,
"overtime_hours": 5, // "overtime_hours": 5,
"expenses": 120.50, // "expenses": 120.50,
"mileage": 45, // "mileage": 45,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-002', // "email": 'EMP-002',
"employee_name": 'Brian Smith', // "employee_name": 'Brian Smith',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 8, // "evening_hours": 8,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 2, // "overtime_hours": 2,
"expenses": 75.00, // "expenses": 75.00,
"mileage": 12, // "mileage": 12,
"is_approved": true // "is_approved": true
}, // },
{ // {
"employee_id": 'EMP-003', // "email": 'EMP-003',
"employee_name": 'Chloe Ramirez', // "employee_name": 'Chloe Ramirez',
"regular_hours": 68, // "regular_hours": 68,
"evening_hours": 15, // "evening_hours": 15,
"emergency_hours": 1, // "emergency_hours": 1,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 200.00, // "expenses": 200.00,
"mileage": 88, // "mileage": 88,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-004', // "email": 'EMP-004',
"employee_name": 'David Lee', // "employee_name": 'David Lee',
"regular_hours": 82, // "regular_hours": 82,
"evening_hours": 5, // "evening_hours": 5,
"emergency_hours": 4, // "emergency_hours": 4,
"overtime_hours": 6, // "overtime_hours": 6,
"expenses": 50.75, // "expenses": 50.75,
"mileage": 20, // "mileage": 20,
"is_approved": true // "is_approved": true
}, // },
{ // {
"employee_id": 'EMP-005', // "email": 'EMP-005',
"employee_name": 'Emily Carter', // "employee_name": 'Emily Carter',
"regular_hours": 78, // "regular_hours": 78,
"evening_hours": 10, // "evening_hours": 10,
"emergency_hours": 2, // "emergency_hours": 2,
"overtime_hours": 3, // "overtime_hours": 3,
"expenses": 95.25, // "expenses": 95.25,
"mileage": 60, // "mileage": 60,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-006', // "email": 'EMP-006',
"employee_name": 'Maxime Murray Gendron', // "employee_name": 'Maxime Murray Gendron',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-007', // "email": 'EMP-007',
"employee_name": 'Marc-André Henrico', // "employee_name": 'Marc-André Henrico',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-008', // "email": 'EMP-008',
"employee_name": 'Jessy Sharock', // "employee_name": 'Jessy Sharock',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-009', // "email": 'EMP-009',
"employee_name": 'David Richer', // "employee_name": 'David Richer',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-010', // "email": 'EMP-010',
"employee_name": 'Nicolas Drolet', // "employee_name": 'Nicolas Drolet',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-011', // "email": 'EMP-011',
"employee_name": 'Frederick Pruneau', // "employee_name": 'Frederick Pruneau',
"regular_hours": 16, // "regular_hours": 16,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-012', // "email": 'EMP-012',
"employee_name": 'Matthieu Haineault Gervais', // "employee_name": 'Matthieu Haineault Gervais',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-013', // "email": 'EMP-013',
"employee_name": 'Robinson Viaud', // "employee_name": 'Robinson Viaud',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-014', // "email": 'EMP-014',
"employee_name": 'Geneviève Bourdon', // "employee_name": 'Geneviève Bourdon',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-015', // "email": 'EMP-015',
"employee_name": 'Frédérique Soulard', // "employee_name": 'Frédérique Soulard',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-016', // "email": 'EMP-016',
"employee_name": 'Patrick Doucet', // "employee_name": 'Patrick Doucet',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-017', // "email": 'EMP-017',
"employee_name": 'Dahlia Tremblay', // "employee_name": 'Dahlia Tremblay',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-018', // "email": 'EMP-018',
"employee_name": 'Louis Morneau', // "employee_name": 'Louis Morneau',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
}, // },
{ // {
"employee_id": 'EMP-019', // "email": 'EMP-019',
"employee_name": 'Michel Blais', // "employee_name": 'Michel Blais',
"regular_hours": 80, // "regular_hours": 80,
"evening_hours": 0, // "evening_hours": 0,
"emergency_hours": 0, // "emergency_hours": 0,
"overtime_hours": 0, // "overtime_hours": 0,
"expenses": 0, // "expenses": 0,
"mileage": 0, // "mileage": 0,
"is_approved": false // "is_approved": false
} // }
]; // ];
export const mock_pay_periods: PayPeriod[] = [ // export const mock_pay_periods: PayPeriod[] = [
{ // {
"period_number": 15, // "period_number": 15,
"start_date": "2025-07-27", // "start_date": "2025-07-27",
"end_date": "2025-08-09", // "end_date": "2025-08-09",
"year": 2025, // "year": 2025,
"label": "2025-07-27 → 2025-08-09" // "label": "2025-07-27 → 2025-08-09"
}, // },
{ // {
"period_number": 14, // "period_number": 14,
"start_date": "2025-07-13", // "start_date": "2025-07-13",
"end_date": "2025-07-26", // "end_date": "2025-07-26",
"year": 2025, // "year": 2025,
"label": "2025-07-13 → 2025-07-26" // "label": "2025-07-13 → 2025-07-26"
}, // },
{ // {
"period_number": 13, // "period_number": 13,
"start_date": "2025-06-29", // "start_date": "2025-06-29",
"end_date": "2025-07-12", // "end_date": "2025-07-12",
"year": 2025, // "year": 2025,
"label": "2025-06-29 → 2025-07-12" // "label": "2025-06-29 → 2025-07-12"
}, // },
{ // {
"period_number": 12, // "period_number": 12,
"start_date": "2025-06-15", // "start_date": "2025-06-15",
"end_date": "2025-06-28", // "end_date": "2025-06-28",
"year": 2025, // "year": 2025,
"label": "2025-06-15 → 2025-06-28" // "label": "2025-06-15 → 2025-06-28"
}, // },
{ // {
"period_number": 11, // "period_number": 11,
"start_date": "2025-06-01", // "start_date": "2025-06-01",
"end_date": "2025-06-14", // "end_date": "2025-06-14",
"year": 2025, // "year": 2025,
"label": "2025-06-01 → 2025-06-14" // "label": "2025-06-01 → 2025-06-14"
}, // },
{ // {
"period_number": 10, // "period_number": 10,
"start_date": "2025-05-18", // "start_date": "2025-05-18",
"end_date": "2025-05-31", // "end_date": "2025-05-31",
"year": 2025, // "year": 2025,
"label": "2025-05-18 → 2025-05-31" // "label": "2025-05-18 → 2025-05-31"
}, // },
{ // {
"period_number": 9, // "period_number": 9,
"start_date": "2025-05-04", // "start_date": "2025-05-04",
"end_date": "2025-05-17", // "end_date": "2025-05-17",
"year": 2025, // "year": 2025,
"label": "2025-05-04 → 2025-05-17" // "label": "2025-05-04 → 2025-05-17"
}, // },
{ // {
"period_number": 8, // "period_number": 8,
"start_date": "2025-04-20", // "start_date": "2025-04-20",
"end_date": "2025-05-03", // "end_date": "2025-05-03",
"year": 2025, // "year": 2025,
"label": "2025-04-20 → 2025-05-03" // "label": "2025-04-20 → 2025-05-03"
}, // },
{ // {
"period_number": 7, // "period_number": 7,
"start_date": "2025-04-06", // "start_date": "2025-04-06",
"end_date": "2025-04-19", // "end_date": "2025-04-19",
"year": 2025, // "year": 2025,
"label": "2025-04-06 → 2025-04-19" // "label": "2025-04-06 → 2025-04-19"
}, // },
{ // {
"period_number": 6, // "period_number": 6,
"start_date": "2025-03-23", // "start_date": "2025-03-23",
"end_date": "2025-04-05", // "end_date": "2025-04-05",
"year": 2025, // "year": 2025,
"label": "2025-03-23 → 2025-04-05" // "label": "2025-03-23 → 2025-04-05"
} // }
] // ]

View File

@ -1,5 +1,5 @@
export interface PayPeriodEmployeeOverview { export interface PayPeriodEmployeeOverview {
employee_id: string; email: string;
employee_name: string; employee_name: string;
regular_hours: number; regular_hours: number;
evening_hours: number; evening_hours: number;

View File

@ -1,6 +1,11 @@
import type { PayPeriodEmployeeOverview } from "./timesheet-approval-pay-period-employee-overview-interface";
export interface PayPeriodOverview { export interface PayPeriodOverview {
period_number: number; pay_period_no: number;
start_date: string; pay_year: number;
end_date: string; payday: string;
period_start: string;
period_end: string;
label: string; label: string;
employees_overview: PayPeriodEmployeeOverview[];
}; };

View File

@ -3,15 +3,18 @@ import { defineStore } from "pinia";
import { AuthService } from "../modules/auth/services/services-auth"; import { AuthService } from "../modules/auth/services/services-auth";
import type { User } from "src/modules/shared/types/user-interface"; import type { User } from "src/modules/shared/types/user-interface";
const defaultUser: User = { export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
firstName: 'Unknown',
lastName: 'Unknown', const TestUsers: Record<CompanyRole, User> = {
email: 'guest@guest.com', guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
role: 'guest' supervisor: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
}; accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
}
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const user = ref(defaultUser); const user = ref(TestUsers.guest);
const authError = ref(""); const authError = ref("");
const isAuthorizedUser = computed(() => user.value.role !== 'guest'); const isAuthorizedUser = computed(() => user.value.role !== 'guest');
@ -27,11 +30,16 @@ export const useAuthStore = defineStore('auth', () => {
}; };
const logout = () => { const logout = () => {
user.value = defaultUser; user.value = TestUsers.guest;
}; };
const setUser = (currentUser: User) => { const setUser = (bypassRole: string) => {
user.value = currentUser; if (bypassRole in TestUsers) {
user.value = TestUsers[bypassRole as CompanyRole];
}
else {
user.value = TestUsers.guest;
}
}; };
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser }; return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };

View File

@ -4,229 +4,79 @@ import { timesheetApprovalService } from 'src/modules/timesheet-approval/service
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface'; import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
import type { PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-overview-interface"; import type { PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-overview-interface";
const default_pay_period: PayPeriod = {
const default_current_pay_period: PayPeriod = {"period_number": 1, "start_date": "1970-01-01", "end_date": "1970-01-15", "year": 1970, "label": "1970-01-01 → 1970-01-15"}; pay_period_no: -1,
period_start: '',
const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [ period_end: '',
{ payday: '',
"employee_id": 'EMP-001', pay_year: -1,
"employee_name": 'Alice Johnson', label: ''
"regular_hours": 75, };
"evening_hours": 12,
"emergency_hours": 3,
"overtime_hours": 5,
"expenses": 120.50,
"mileage": 45,
"is_approved": false
},
{
"employee_id": 'EMP-002',
"employee_name": 'Brian Smith',
"regular_hours": 80,
"evening_hours": 8,
"emergency_hours": 0,
"overtime_hours": 2,
"expenses": 75.00,
"mileage": 12,
"is_approved": true
},
{
"employee_id": 'EMP-003',
"employee_name": 'Chloe Ramirez',
"regular_hours": 68,
"evening_hours": 15,
"emergency_hours": 1,
"overtime_hours": 0,
"expenses": 200.00,
"mileage": 88,
"is_approved": false
},
{
"employee_id": 'EMP-004',
"employee_name": 'David Lee',
"regular_hours": 82,
"evening_hours": 5,
"emergency_hours": 4,
"overtime_hours": 6,
"expenses": 50.75,
"mileage": 20,
"is_approved": true
},
{
"employee_id": 'EMP-005',
"employee_name": 'Emily Carter',
"regular_hours": 78,
"evening_hours": 10,
"emergency_hours": 2,
"overtime_hours": 3,
"expenses": 95.25,
"mileage": 60,
"is_approved": false
},
{
"employee_id": 'EMP-006',
"employee_name": 'Maxime Murray Gendron',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 20000,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-007',
"employee_name": 'Marc-André Henrico',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-008',
"employee_name": 'Jessy Sharock',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-009',
"employee_name": 'David Richer',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-010',
"employee_name": 'Nicolas Drolet',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-011',
"employee_name": 'Frederick Pruneau',
"regular_hours": 16,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-012',
"employee_name": 'Matthieu Haineault Gervais',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-013',
"employee_name": 'Robinson Viaud',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-014',
"employee_name": 'Geneviève Bourdon',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-015',
"employee_name": 'Frédérique Soulard',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-016',
"employee_name": 'Patrick Doucet',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-017',
"employee_name": 'Dahlia Tremblay',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-018',
"employee_name": 'Louis Morneau',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
},
{
"employee_id": 'EMP-019',
"employee_name": 'Michel Blais',
"regular_hours": 80,
"evening_hours": 0,
"emergency_hours": 0,
"overtime_hours": 0,
"expenses": 0,
"mileage": 0,
"is_approved": false
}
];
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const payPeriods = ref<PayPeriod[]>([]); const currentPayPeriod = ref<PayPeriod>(default_pay_period);
const currentPayPeriod = ref<PayPeriod>(default_current_pay_period); const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>([]);
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>(mock_pay_period_employee_overviews); const isLoading = ref<boolean>(false);
const getCurrentPayPeriod = () => { const getPayPeriodByDate = async (date: Date): Promise<boolean> => {
currentPayPeriod.value = timesheetApprovalService.getCurrentPayPeriod(); isLoading.value = true;
}
return { payPeriods, currentPayPeriod, payPeriodEmployeeOverviews, getCurrentPayPeriod}; 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> => {
isLoading.value = true;
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 );
//TODO: More in-depth error-handling here
}
isLoading.value = false;
return false;
};
const getTimesheetApprovalPayPeriodEmployeeOverviews = async (pay_year: number, period_number: number, supervisor_email: string) => {
isLoading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews(pay_year, period_number, supervisor_email);
payPeriodEmployeeOverviews.value = response.employees_overview;
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
payPeriodEmployeeOverviews.value = [];
// TODO: trigger an alert window with an error message here!
}
isLoading.value = false;
};
return {
currentPayPeriod,
payPeriodEmployeeOverviews,
isLoading,
getPayPeriodByDate,
getPayPeriodByYearAndPeriodNumber,
getTimesheetApprovalPayPeriodEmployeeOverviews,
};
}); });

View File

@ -1,7 +0,0 @@
import { useAuthStore } from "src/stores/auth-store";
export const hasRequiredRole = (...requiredRoles: string[] ) => {
const currentUserRole = useAuthStore().user.role;
return requiredRoles.includes(currentUserRole);
};

View File

@ -0,0 +1,16 @@
import { date } from 'quasar';
const anchor_date: Date = new Date('2023-12-17');
export const getCurrentPayPeriod = (today = new Date()): number => {
const period_length = 14; // days
const periods_per_year = 26;
const days_since_anchor = date.getDateDiff(today, anchor_date, 'days');
const periods_since_anchor = Math.floor(days_since_anchor / period_length);
const current_period = (periods_since_anchor % periods_per_year) + 1;
console.log(current_period);
return current_period;
}