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:{
searchBar: 'Search',
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
},
editUserPage: {
title: 'Edit Account',

View File

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

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

@ -1,29 +1,29 @@
<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 employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [
{name: 'first_name', label: t('usersListPage.userListFirstName'), field: 'first_name'},
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:'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: 'company_name', label: t('usersListPage.userListCompany'), field: 'company_name'},
{name: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title'},
]);
{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;
@ -41,22 +41,27 @@ const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] =
row-key="name"
: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"
:grid="isGridMode"
:loading="isLoadingList"
flat
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">
<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 +70,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 +81,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

@ -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) => {
const goToPageName = (pageName: string) => {
router.push({ name: pageName }).catch(err => {
console.error('Error with Vue Router: ', err);
});
};
};
const handleLogout = () => {
const handleLogout = () => {
authStore.logout();
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
}
</script>
<template>
@ -43,7 +42,7 @@ const handleLogout = () => {
<!-- 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>
@ -54,7 +53,7 @@ const handleLogout = () => {
<!-- 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>

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">
/* 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';
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>[] => [
{ name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true },
@ -19,74 +28,75 @@
]);
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>
<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
color="primary"
:rows-per-page-options="[0]"
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 -->
<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>
<q-card flat class="full-width bg-transparent row q-px-md">
<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">
<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" />
<q-icon name="search" color="primary"/>
</template>
</q-input>
</q-card-section>
</q-card>
</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

@ -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 { mock_pay_periods } from "../timesheet-approval-test-constants";
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 = {
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",
@ -30,8 +19,9 @@ export const timesheetApprovalService = {
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
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[] = [
{
"employee_id": 'EMP-001',
"email": 'EMP-001',
"employee_name": 'Alice Johnson',
"regular_hours": 75,
"evening_hours": 12,
@ -14,7 +14,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-002',
"email": 'EMP-002',
"employee_name": 'Brian Smith',
"regular_hours": 80,
"evening_hours": 8,
@ -25,7 +25,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": true
},
{
"employee_id": 'EMP-003',
"email": 'EMP-003',
"employee_name": 'Chloe Ramirez',
"regular_hours": 68,
"evening_hours": 15,
@ -36,7 +36,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-004',
"email": 'EMP-004',
"employee_name": 'David Lee',
"regular_hours": 82,
"evening_hours": 5,
@ -47,7 +47,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": true
},
{
"employee_id": 'EMP-005',
"email": 'EMP-005',
"employee_name": 'Emily Carter',
"regular_hours": 78,
"evening_hours": 10,
@ -58,7 +58,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-006',
"email": 'EMP-006',
"employee_name": 'Maxime Murray Gendron',
"regular_hours": 80,
"evening_hours": 0,
@ -69,7 +69,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-007',
"email": 'EMP-007',
"employee_name": 'Marc-André Henrico',
"regular_hours": 80,
"evening_hours": 0,
@ -80,7 +80,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-008',
"email": 'EMP-008',
"employee_name": 'Jessy Sharock',
"regular_hours": 80,
"evening_hours": 0,
@ -91,7 +91,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-009',
"email": 'EMP-009',
"employee_name": 'David Richer',
"regular_hours": 80,
"evening_hours": 0,
@ -102,7 +102,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-010',
"email": 'EMP-010',
"employee_name": 'Nicolas Drolet',
"regular_hours": 80,
"evening_hours": 0,
@ -113,7 +113,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-011',
"email": 'EMP-011',
"employee_name": 'Frederick Pruneau',
"regular_hours": 16,
"evening_hours": 0,
@ -124,7 +124,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-012',
"email": 'EMP-012',
"employee_name": 'Matthieu Haineault Gervais',
"regular_hours": 80,
"evening_hours": 0,
@ -135,7 +135,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-013',
"email": 'EMP-013',
"employee_name": 'Robinson Viaud',
"regular_hours": 80,
"evening_hours": 0,
@ -146,7 +146,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-014',
"email": 'EMP-014',
"employee_name": 'Geneviève Bourdon',
"regular_hours": 80,
"evening_hours": 0,
@ -157,7 +157,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-015',
"email": 'EMP-015',
"employee_name": 'Frédérique Soulard',
"regular_hours": 80,
"evening_hours": 0,
@ -168,7 +168,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-016',
"email": 'EMP-016',
"employee_name": 'Patrick Doucet',
"regular_hours": 80,
"evening_hours": 0,
@ -179,7 +179,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-017',
"email": 'EMP-017',
"employee_name": 'Dahlia Tremblay',
"regular_hours": 80,
"evening_hours": 0,
@ -190,7 +190,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-018',
"email": 'EMP-018',
"employee_name": 'Louis Morneau',
"regular_hours": 80,
"evening_hours": 0,
@ -201,7 +201,7 @@ export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
"is_approved": false
},
{
"employee_id": 'EMP-019',
"email": 'EMP-019',
"employee_name": 'Michel Blais',
"regular_hours": 80,
"evening_hours": 0,

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

@ -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 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', () => {
const payPeriods = ref<PayPeriod[]>([]);
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 = () => {
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;
}