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';
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

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

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',
@ -159,6 +159,10 @@ export default {
},
shared:{
searchBar: 'Search',
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
languageLabel: 'Language',
},
editUserPage: {
title: 'Edit Account',
@ -237,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',
@ -262,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',
@ -291,14 +296,13 @@ export default {
mileage: 'Mileage',
},
timeSheetValidations: {
tableHeader: 'List of employees',
tableColumnLabelFullname: 'Full name',
tableColumnLabelRegularHours: 'regular hours',
tableColumnLabelEveningHours: 'evening hours',
tableColumnLabelEmergencyHours: 'emergency hours',
tableColumnLabelOvertime: 'overtime hours',
tableColumnLabelExpenses: 'of expenses',
tableColumnLabelMileage: 'of mileage',
tableColumnLabelEveningHours: 'evening',
tableColumnLabelEmergencyHours: 'emergency',
tableColumnLabelOvertime: 'overtime',
tableColumnLabelExpenses: 'expenses',
tableColumnLabelMileage: 'mileage',
actionTitle: 'Please save the changes made.',
actionButton: 'Save',
timeSheetStatusVerified: 'approved',

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',
@ -228,6 +229,10 @@ export default {
},
shared:{
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: {
title: 'Quarts de travail',
@ -312,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',
@ -341,14 +346,13 @@ export default {
mileage: 'Kilometrage',
},
timeSheetValidations: {
tableHeader: 'Liste des employés',
tableColumnLabelFullname: 'nom complet',
tableColumnLabelRegularHours: 'heures régulières',
tableColumnLabelEveningHours: 'heures de soir',
tableColumnLabelEmergencyHours: 'heures durgence',
tableColumnLabelOvertime: 'heures supplémentaires',
tableColumnLabelExpenses: 'de dépenses',
tableColumnLabelMileage: 'de kilométrage',
tableColumnLabelEveningHours: 'soir',
tableColumnLabelEmergencyHours: 'urgence',
tableColumnLabelOvertime: 'supplémentaires',
tableColumnLabelExpenses: 'dépenses',
tableColumnLabelMileage: 'kilométrage',
actionTitle: 'Veuillez enregistrer les changements effectués.',
actionButton: 'Enregistrer',
timeSheetStatusVerified: 'validé',
@ -375,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

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

View File

@ -1,10 +1,8 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useAuthApi } from '../composables/use-auth-api';
import type { User } from 'src/modules/shared/types/user-interface';
import { useRouter } from 'vue-router';
const authApi = useAuthApi();
const email = ref('');
const isShowingEmployeeLoginButton = ref(false);
@ -12,12 +10,7 @@
const router = useRouter();
const setBypassUser = (bypassRole: string) => {
authApi.setUser({
firstName: "Testing",
lastName: bypassRole,
email: "testingT@targointernet.com",
role: bypassRole || "guest"
} as User);
authApi.setUser(bypassRole);
router.push({ name: 'dashboard' }).catch( err => {
console.error('Router navigation failed: ', err);
@ -94,7 +87,7 @@
<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="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-group>
</q-card-section>

View File

@ -25,7 +25,7 @@ import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-em
<template>
<q-card
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;"
@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;">
<div class="ellipsis">
{{ props.row.first_name }} {{ props.row.last_name }}
<q-separator color="primary" />
</div>
</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;">
<div class=" ellipsis-2-lines">
{{ props.row.job_title }}

View File

@ -1,29 +1,30 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { useEmployeeStore } from 'src/stores/employee-store';
import { useI18n } from 'vue-i18n';
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
import { computed, onMounted, ref } from 'vue';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { useEmployeeStore } from 'src/stores/employee-store';
import { useI18n } from 'vue-i18n';
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
import type { QTableColumn } from 'quasar';
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
import type { QTableColumn } from 'quasar';
const employeeListApi = useEmployeeListApi();
const employeeStore = useEmployeeStore();
const isLoadingList = ref<boolean>(true);
const employeeListApi = useEmployeeListApi();
const employeeStore = useEmployeeStore();
const isLoadingList = ref<boolean>(true);
const { t } = useI18n();
const filter = ref("");
const isGridMode = ref(true);
const { t } = useI18n();
const filter = ref("");
const isGridMode = ref(true);
const pagination = ref({ rowsPerPage: 0 });
const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [
{name: 'first_name', label: t('usersListPage.userListFirstName'), field: 'first_name'},
{name: 'last_name', label: t('usersListPage.userListLastName'), field: 'last_name', align: 'left'},
{name: 'email', label: t('usersListPage.userListEmail'), field: 'email', align:'center'},
{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: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title'},
]);
const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [
{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: 'email', label: t('usersListPage.userListEmail'), field: 'email', 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', align: 'left'},
{name: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title', align: 'left'},
]);
onMounted( async () => {
isLoadingList.value = true;
@ -35,28 +36,39 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
<template>
<div class="q-pa-lg col">
<q-table
title=" "
dense
flat
hide-pagination
virtual-scroll
title=" "
card-style="max-height: 70vh;"
:rows="employeeStore.employeeList"
:columns="employeeListColumns"
row-key="name"
v-model:pagination="pagination"
:rows-per-page-options="[0]"
:filter="filter"
class="q-pa-md"
class="q-pa-md bg-transparent"
color="primary"
table-header-class="text-primary text-uppercase"
card-container-class="justify-center q-gutter-md"
card-container-class="justify-center"
:grid="isGridMode"
:loading="isLoadingList"
flat
dense
:no-data-label="$t('shared.failedToLoad')"
: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">
<SupervisorCrewTableItem :row="props.row"/>
</template>
<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-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"
:options="[
{icon: 'grid_view', value: true},
@ -65,6 +77,7 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
<q-input
outlined
dense
rounded
v-model="filter"
:label="$t('shared.searchBar')"
label-color="primary" bg-color="white" color="primary"
@ -75,6 +88,16 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
</q-input>
</div>
</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>
</div>
</template>

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

@ -1,29 +1,28 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store';
import { hasRequiredRole } from 'src/utils/has-required-role';
import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants';
import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants';
const authStore = useAuthStore();
const uiStore = useUiStore();
const router = useRouter();
const miniState = ref(true);
const authStore = useAuthStore();
const uiStore = useUiStore();
const router = useRouter();
const miniState = ref(true);
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
const handleLogout = () => {
authStore.logout();
const handleLogout = () => {
authStore.logout();
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
</script>
<template>
@ -37,29 +36,29 @@ const handleLogout = () => {
<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>
<!-- Timesheet Validation -- Supervisor and Accounting only -->
<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-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>
<!-- Employee List -- Supervisor, Accounting and HR only -->
<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-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>
@ -69,7 +68,7 @@ const handleLogout = () => {
<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>
@ -79,7 +78,7 @@ const handleLogout = () => {
<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>
@ -90,7 +89,7 @@ const handleLogout = () => {
<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

@ -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 {
period_number: number;
start_date: string;
end_date: string;
year: number;
pay_period_no: number;
period_start: string;
period_end: string;
payday: string;
pay_year: number;
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,12 +1,31 @@
<script setup lang="ts">
/* eslint-disable */
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface';
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 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>[] => [
{ name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true },
@ -19,74 +38,80 @@
]);
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>
<template>
<div class="q-pa-md">
<q-table
:rows="rows"
:rows="timesheetStore.payPeriodEmployeeOverviews"
:columns="columns"
row-key="employee_id"
selection="multiple"
v-model:selected="selectedRows"
row-key="email"
:filter="filter"
grid
dense
hide-pagination
color="primary"
: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 -->
<template v-slot:top>
<q-card flat class="full-width bg-primary row q-px-md">
<!-- Table Title -->
<q-card-section class="q-py-xs">
<div class="text-h4 text-white text-weight-bold">{{$t('timeSheetValidations.tableHeader')}}</div>
</q-card-section>
<div :class="$q.screen.lt.md ? 'column justify-center items-center' : 'full-width row'">
<!-- Date Picker -->
<TimesheetApprovalPeriodPicker />
<q-space />
<!-- 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 -->
<q-card-section class="q-py-xs">
<q-input rounded standout="bg-white" dense debounce="300" v-model="filter" placeholder="Search" label-color="primary" bg-color="white">
<template v-slot:append>
<q-icon name="search" color="primary" />
</template>
</q-input>
</q-card-section>
</q-card>
<q-input
outlined
dense
rounded
v-model="filter"
:label="$t('shared.searchBar')"
label-color="primary" bg-color="white" color="primary"
>
<template v-slot:append>
<q-icon name="search" color="primary"/>
</template>
</q-input>
</div>
</template>
<!-- Template for individual employee cards -->
<template v-slot:item="props: { cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], row: PayPeriodEmployeeOverview, selected: boolean }">
<div class="q-px-sm q-pb-sm col-xs-6 col-sm-4 col-md-3 col-lg-2 grid-style-transition">
<q-card class="rounded-15">
<q-card-section class="q-pb-sm">
<div class="text-primary text-h5 text-weight-bolder ellipsis">{{ props.row.employee_name }}</div>
</q-card-section>
<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>
<q-card-section class="text-weight-bold q-pa-none col-9" >{{ col.label }}</q-card-section>
</div>
<q-card-section horizontal class="q-pa-sm q-mt-sm" :class="{ 'bg-primary text-white': props.selected}">
<q-space />
<!-- TODO: Replace checkbox with simple display of timesheet status (approved/pending/partial/complete/) -->
<q-checkbox
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>
<template v-slot:item="props: { cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], row: PayPeriodEmployeeOverview }">
<TimesheetApprovalEmployeeOverviewListItem
:cols="props.cols"
:row="props.row"
v-model="props.row.is_approved"/>
</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>

View File

@ -1,21 +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 timesheetStore = useTimesheetStore();
const updateCurrentPayPeriod = () => {
timesheetStore.getCurrentPayPeriod();
};
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">{{ timesheetStore.currentPayPeriod?.label }}</div>
<q-btn-group push rounded>
<q-btn push icon="keyboard_arrow_left" color="primary" class="q-px-xl" />
<q-btn push icon="date_range" color="primary" class="q-px-xl" @click="updateCurrentPayPeriod" />
<q-btn push icon="keyboard_arrow_right" color="primary" class="q-px-xl" />
</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

@ -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">
// 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

@ -1,37 +1,22 @@
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";
export const timesheetApprovalService = {
getCurrentPayPeriod: (): PayPeriod => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
//let current_pay_period: PayPeriod;
//
// 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;
getPayPeriodByDate: async (date: Date): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date.toISOString()}`);
return response.data;
},
getAllPayPeriods: async () => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
return await api.get(`/pay-periods/`) || mock_pay_periods;
getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/${year}/${period_number}`);
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
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 { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface"
// import type { PayPeriod } from "../shared/types/pay-period-interface";
// import type { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface"
export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
{
"employee_id": 'EMP-001',
"employee_name": 'Alice Johnson',
"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": 0,
"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 mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
// {
// "email": 'EMP-001',
// "employee_name": 'Alice Johnson',
// "regular_hours": 75,
// "evening_hours": 12,
// "emergency_hours": 3,
// "overtime_hours": 5,
// "expenses": 120.50,
// "mileage": 45,
// "is_approved": false
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": 'EMP-006',
// "employee_name": 'Maxime Murray Gendron',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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
// },
// {
// "email": '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 mock_pay_periods: PayPeriod[] = [
{
"period_number": 15,
"start_date": "2025-07-27",
"end_date": "2025-08-09",
"year": 2025,
"label": "2025-07-27 → 2025-08-09"
},
{
"period_number": 14,
"start_date": "2025-07-13",
"end_date": "2025-07-26",
"year": 2025,
"label": "2025-07-13 → 2025-07-26"
},
{
"period_number": 13,
"start_date": "2025-06-29",
"end_date": "2025-07-12",
"year": 2025,
"label": "2025-06-29 → 2025-07-12"
},
{
"period_number": 12,
"start_date": "2025-06-15",
"end_date": "2025-06-28",
"year": 2025,
"label": "2025-06-15 → 2025-06-28"
},
{
"period_number": 11,
"start_date": "2025-06-01",
"end_date": "2025-06-14",
"year": 2025,
"label": "2025-06-01 → 2025-06-14"
},
{
"period_number": 10,
"start_date": "2025-05-18",
"end_date": "2025-05-31",
"year": 2025,
"label": "2025-05-18 → 2025-05-31"
},
{
"period_number": 9,
"start_date": "2025-05-04",
"end_date": "2025-05-17",
"year": 2025,
"label": "2025-05-04 → 2025-05-17"
},
{
"period_number": 8,
"start_date": "2025-04-20",
"end_date": "2025-05-03",
"year": 2025,
"label": "2025-04-20 → 2025-05-03"
},
{
"period_number": 7,
"start_date": "2025-04-06",
"end_date": "2025-04-19",
"year": 2025,
"label": "2025-04-06 → 2025-04-19"
},
{
"period_number": 6,
"start_date": "2025-03-23",
"end_date": "2025-04-05",
"year": 2025,
"label": "2025-03-23 → 2025-04-05"
}
]
// export const mock_pay_periods: PayPeriod[] = [
// {
// "period_number": 15,
// "start_date": "2025-07-27",
// "end_date": "2025-08-09",
// "year": 2025,
// "label": "2025-07-27 → 2025-08-09"
// },
// {
// "period_number": 14,
// "start_date": "2025-07-13",
// "end_date": "2025-07-26",
// "year": 2025,
// "label": "2025-07-13 → 2025-07-26"
// },
// {
// "period_number": 13,
// "start_date": "2025-06-29",
// "end_date": "2025-07-12",
// "year": 2025,
// "label": "2025-06-29 → 2025-07-12"
// },
// {
// "period_number": 12,
// "start_date": "2025-06-15",
// "end_date": "2025-06-28",
// "year": 2025,
// "label": "2025-06-15 → 2025-06-28"
// },
// {
// "period_number": 11,
// "start_date": "2025-06-01",
// "end_date": "2025-06-14",
// "year": 2025,
// "label": "2025-06-01 → 2025-06-14"
// },
// {
// "period_number": 10,
// "start_date": "2025-05-18",
// "end_date": "2025-05-31",
// "year": 2025,
// "label": "2025-05-18 → 2025-05-31"
// },
// {
// "period_number": 9,
// "start_date": "2025-05-04",
// "end_date": "2025-05-17",
// "year": 2025,
// "label": "2025-05-04 → 2025-05-17"
// },
// {
// "period_number": 8,
// "start_date": "2025-04-20",
// "end_date": "2025-05-03",
// "year": 2025,
// "label": "2025-04-20 → 2025-05-03"
// },
// {
// "period_number": 7,
// "start_date": "2025-04-06",
// "end_date": "2025-04-19",
// "year": 2025,
// "label": "2025-04-06 → 2025-04-19"
// },
// {
// "period_number": 6,
// "start_date": "2025-03-23",
// "end_date": "2025-04-05",
// "year": 2025,
// "label": "2025-03-23 → 2025-04-05"
// }
// ]

View File

@ -1,5 +1,5 @@
export interface PayPeriodEmployeeOverview {
employee_id: string;
email: string;
employee_name: string;
regular_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 {
period_number: number;
start_date: string;
end_date: string;
pay_period_no: number;
pay_year: number;
payday: string;
period_start: string;
period_end: 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 type { User } from "src/modules/shared/types/user-interface";
const defaultUser: User = {
firstName: 'Unknown',
lastName: 'Unknown',
email: 'guest@guest.com',
role: 'guest'
};
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
const TestUsers: Record<CompanyRole, User> = {
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', 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', () => {
const user = ref(defaultUser);
const user = ref(TestUsers.guest);
const authError = ref("");
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
@ -27,11 +30,16 @@ export const useAuthStore = defineStore('auth', () => {
};
const logout = () => {
user.value = defaultUser;
user.value = TestUsers.guest;
};
const setUser = (currentUser: User) => {
user.value = currentUser;
const setUser = (bypassRole: string) => {
if (bypassRole in TestUsers) {
user.value = TestUsers[bypassRole as CompanyRole];
}
else {
user.value = TestUsers.guest;
}
};
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 { PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-overview-interface";
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"};
const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
{
"employee_id": 'EMP-001',
"employee_name": 'Alice Johnson',
"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
}
];
const default_pay_period: PayPeriod = {
pay_period_no: -1,
period_start: '',
period_end: '',
payday: '',
pay_year: -1,
label: ''
};
export const useTimesheetStore = defineStore('timesheet', () => {
const payPeriods = ref<PayPeriod[]>([]);
const currentPayPeriod = ref<PayPeriod>(default_current_pay_period);
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>(mock_pay_period_employee_overviews);
const currentPayPeriod = ref<PayPeriod>(default_pay_period);
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>([]);
const isLoading = ref<boolean>(false);
const getCurrentPayPeriod = () => {
currentPayPeriod.value = timesheetApprovalService.getCurrentPayPeriod();
}
const getPayPeriodByDate = async (date: Date): Promise<boolean> => {
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;
}