refactor(approvals): fully overhaul timesheet approvals to work with backend, begin to implement approval logic.

This commit is contained in:
Nicolas Drolet 2025-08-19 16:49:49 -04:00
parent 62aec8f597
commit 0c1d214420
17 changed files with 275 additions and 373 deletions

View File

@ -159,6 +159,9 @@ export default {
}, },
shared:{ shared:{
searchBar: 'Search', searchBar: 'Search',
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
}, },
editUserPage: { editUserPage: {
title: 'Edit Account', title: 'Edit Account',

View File

@ -228,6 +228,9 @@ 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',
}, },
shiftColumns: { shiftColumns: {
title: 'Quarts de travail', title: 'Quarts de travail',

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

@ -1,29 +1,29 @@
<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 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;
@ -41,22 +41,27 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
row-key="name" row-key="name"
: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 q-gutter-md"
:grid="isGridMode" :grid="isGridMode"
:loading="isLoadingList" :loading="isLoadingList"
flat
dense dense
flat
: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-15 shadow-12"
> >
<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 +70,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 +81,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

@ -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>
@ -43,7 +42,7 @@ const handleLogout = () => {
<!-- 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>
@ -54,7 +53,7 @@ const handleLogout = () => {
<!-- 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>

View File

@ -0,0 +1,53 @@
<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-6 col-sm-4 col-md-3 col-lg-2 grid-style-transition">
<q-card class="rounded-10">
<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')"
:key="col.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 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,21 @@
<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';
const { t } = useI18n(); const { t } = useI18n();
const timesheetApprovalApi = useTimesheetApprovalApi();
const currentPayPeriod = getCurrentPayPeriod();
const currentYear = (new Date()).getFullYear();
const authStore = useAuthStore();
const timesheetStore = useTimesheetStore();
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 },
@ -19,74 +28,75 @@
]); ]);
const filter = ref(''); const filter = ref('');
const selectedRows = ref<PayPeriodEmployeeOverview[]>();
const rows: PayPeriodEmployeeOverview[] = mock_pay_period_employee_overviews; onMounted( async () => {
await timesheetApprovalApi.getTimesheetApprovalPayPeriodEmployeeOverviews(currentYear, currentPayPeriod, authStore.user.email);
})
</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
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 q-gutter-md"
: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"> <q-card flat class="full-width bg-transparent 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>
<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"
>
<template v-slot:append>
<q-icon name="search" color="primary"/>
</template>
</q-input>
</q-card> </q-card>
</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

@ -0,0 +1,13 @@
import { useTimesheetStore } from "src/stores/timesheet-store";
export const useTimesheetApprovalApi = () => {
const timesheetStore = useTimesheetStore();
const getTimesheetApprovalPayPeriodEmployeeOverviews = async (year: number, period_number: number, supervisor_email: string): Promise<void> => {
await timesheetStore.getTimesheetApprovalPayPeriodEmployeeOverviews(year, period_number, supervisor_email);
}
return {
getTimesheetApprovalPayPeriodEmployeeOverviews,
}
};

View File

@ -1,21 +1,10 @@
import { api } from "src/boot/axios"; import { api } from "src/boot/axios";
import { mock_pay_periods } from "../timesheet-approval-test-constants"; import { mock_pay_periods } from "../timesheet-approval-test-constants";
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface"; import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
import type { PayPeriodOverview } from "../types/timesheet-approval-pay-period-overview-interface";
export const timesheetApprovalService = { export const timesheetApprovalService = {
getCurrentPayPeriod: (): PayPeriod => { 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 { return {
"period_number": 15, "period_number": 15,
"start_date": "2025-07-27", "start_date": "2025-07-27",
@ -30,8 +19,9 @@ export const timesheetApprovalService = {
return await api.get(`/pay-periods/`) || mock_pay_periods; return await api.get(`/pay-periods/`) || mock_pay_periods;
}, },
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

@ -3,7 +3,7 @@ import type { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-p
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,
@ -14,7 +14,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -25,7 +25,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -36,7 +36,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -47,7 +47,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -58,7 +58,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -69,7 +69,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -80,7 +80,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -91,7 +91,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -102,7 +102,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -113,7 +113,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -124,7 +124,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -135,7 +135,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -146,7 +146,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -157,7 +157,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -168,7 +168,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -179,7 +179,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -190,7 +190,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,
@ -201,7 +201,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"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,

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

@ -7,226 +7,27 @@ import type { PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/t
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 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
}
];
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const payPeriods = ref<PayPeriod[]>([]); const payPeriods = ref<PayPeriod[]>([]);
const currentPayPeriod = ref<PayPeriod>(default_current_pay_period); const currentPayPeriod = ref<PayPeriod>(default_current_pay_period);
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>(mock_pay_period_employee_overviews); const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>([]);
const isLoading = ref<boolean>(false);
const getCurrentPayPeriod = () => { const getCurrentPayPeriod = () => {
currentPayPeriod.value = timesheetApprovalService.getCurrentPayPeriod(); currentPayPeriod.value = timesheetApprovalService.getCurrentPayPeriod();
} }
return { payPeriods, currentPayPeriod, payPeriodEmployeeOverviews, getCurrentPayPeriod}; const getTimesheetApprovalPayPeriodEmployeeOverviews = async (year: number, period_number: number, supervisor_email: string) => {
isLoading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews(year, period_number, supervisor_email);
payPeriodEmployeeOverviews.value = response.employees_overview;
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
// TODO: trigger an alert window with an error message here!
}
isLoading.value = false;
}
return { payPeriods, currentPayPeriod, payPeriodEmployeeOverviews, isLoading, getCurrentPayPeriod, 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;
}