Merge pull request 'dev/nicolas/timesheet-validation' (#1) from dev/nicolas/timesheet-validation into main

Reviewed-on: Targo/targo_frontend#1
This commit is contained in:
Nicolas 2025-08-13 13:11:13 -04:00
commit cf6f411ac6
47 changed files with 1010 additions and 68 deletions

BIN
src/assets/ChromeSetup.exe Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
src/assets/event-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -13,15 +13,18 @@
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #019547;
$secondary: #EFFFEF;
$accent: #4ada86;
$secondary: #DAE0E7;
$accent: #AAD5C4;
$dark-font: #305530;
$dark: #323232;
$verdigris: #6EBAB0;
$mint: #56B586;
$dark-font: #1f3a1f;
$dark: #000;
$dark-page: #323232;
$positive: #21ba45;
$negative: #c10015;
$negative: #ff586c71;
$info: #31ccec;
$warning: #eeb10a;
$warning: #ffde82c2;
$white: white;

View File

@ -23,6 +23,7 @@ export default {
clearFilter: 'Clear filter',
},
navBar: {
userMenuHome: 'Homepage',
userMenuEmployeeList: 'Employee list',
userMenuShiftValidation: 'Timesheet Approval',
userMenuProfile: 'Profile',
@ -32,8 +33,8 @@ export default {
userMenuCalendar: 'Calendar',
},
notFoundPage: {
pageTitle: 'Oops. Nothing here...',
backButton: 'Go to the home page',
pageText: 'We cannot seem to find the page you are looking for, sorry!',
backButton: 'Take me back!',
},
loginPage: {
title: 'Log in to Targo',
@ -292,23 +293,21 @@ export default {
},
timeSheetValidations: {
tableHeader: 'List of employees',
tableCol_1: 'Full name',
tableCol_2: 'Regular hours',
tableCol_3: 'Evening hours',
tableCol_4: 'Emergency hours',
tableCol_5: 'Overtime hours',
tableCol_6: 'Expenses',
tableCol_7: 'Mileage',
tableCol_8: 'Status',
tableCol_9: 'Supervisor',
tableColumnLabelFullname: 'Full name',
tableColumnLabelRegularHours: 'regular hours',
tableColumnLabelEveningHours: 'evening hours',
tableColumnLabelEmergencyHours: 'emergency hours',
tableColumnLabelOvertime: 'overtime hours',
tableColumnLabelExpenses: 'of expenses',
tableColumnLabelMileage: 'of mileage',
actionTitle: 'Please save the changes made.',
actionButton: 'Save',
timeSheetStatus_verified: 'Verified',
timeSheetStatus_unverified: 'Unverified',
timeSheetStatus_partial: 'Partial',
timeSheetStatus_complete: 'Complete',
timeSheetStatus_empty: 'Empty',
timeSheetStatus_blocked: 'Blocked',
timeSheetStatusVerified: 'approved',
timeSheetStatusUnverified: 'pending',
timeSheetStatusPartial: 'partially approved',
timeSheetStatusComplete: 'complete',
timeSheetStatusEmpty: 'empty',
timeSheetStatusBlocked: 'blocked',
showAllCheckbox: 'Show all',
accumulatedSicknessTotal: 'Accumulated illnesses',
consumedSicknessTotal: 'Consumed with illnesses',

View File

@ -162,6 +162,7 @@ export default {
clearFilter: 'Effacer le filtre',
},
navBar: {
userMenuHome: 'Accueil',
userMenuEmployeeList: 'Liste employés',
userMenuShiftValidation: 'Valider les heures',
userMenuProfile: 'Profil',
@ -171,8 +172,8 @@ export default {
userMenuCalendar: 'Calendrier annuel',
},
notFoundPage: {
pageTitle: 'Oops. Rien ici...',
backButton: 'Aller à la page daccueil',
pageText: 'On ne semble pas trouver la page que vous cherchez, désolé!',
backButton: 'Je veux retourner en arrière!',
},
notificationDialog: {
notice: 'Notification',
@ -338,23 +339,21 @@ export default {
},
timeSheetValidations: {
tableHeader: 'Liste des employés',
tableCol_1: 'Nom et prénom',
tableCol_2: 'Heures régulières',
tableCol_3: 'Heures de soir',
tableCol_4: 'Heures durgence',
tableCol_5: 'Heures supplémentaires',
tableCol_6: 'Dépenses ',
tableCol_7: 'Kilométrage ',
tableCol_8: 'État',
tableCol_9: 'Superviseur',
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',
actionTitle: 'Veuillez enregistrer les changements effectués.',
actionButton: 'Enregistrer',
timeSheetStatus_verified: 'Vérifié',
timeSheetStatus_unverified: 'Invérifié',
timeSheetStatus_partial: 'Partiel',
timeSheetStatus_complete: 'Complet',
timeSheetStatus_empty: 'Vide',
timeSheetStatus_blocked: 'Bloqué',
timeSheetStatusVerified: 'validé',
timeSheetStatusUnverified: 'à valider',
timeSheetStatusPartial: 'partiellement validé',
timeSheetStatusComplete: 'complet',
timeSheetStatusEmpty: 'vide',
timeSheetStatusBlocked: 'bloqué',
showAllCheckbox: 'Afficher tous',
accumulatedSicknessTotal: 'Accumulées de maladies',
consumedSicknessTotal: 'Consommées de maladies',

View File

@ -1,5 +1,5 @@
import { useAuthStore } from "../../../stores/auth-store";
import type { User } from "src/modules/users/types/user-interface";
import type { User } from "src/modules/shared/types/user-interface";
export const useAuthApi = () => {
const authStore = useAuthStore();

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useAuthApi } from '../composables/use-auth-api';
import type { User } from 'src/modules/users/types/user-interface';
import { useRouter } from 'vue-router';
import type { User } from 'src/modules/shared/types/user-interface';
import { useRouter } from 'vue-router';
const authApi = useAuthApi();

View File

@ -1,4 +1,4 @@
import type { User } from "src/modules/users/types/user-interface";
import type { User } from "src/modules/shared/types/user-interface";
export interface AuthState {
token: string;

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import LanguageSwitch from '../language-switch.vue';
</script>
<template>
<q-footer elevated class="bg-primary text-white">
<q-toolbar>
<q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title>
<LanguageSwitch class="q-mr-xs text-white" />
</q-toolbar>
</q-footer>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store';
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
const notifAmount = ref(7);
</script>
<template>
<q-item clickable v-ripple dark @click="uiStore.toggleRightDrawer">
<q-item-section side>
<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-label>{{ currentUser.firstName }} {{ currentUser.lastName }}</q-item-label>
<q-item-label caption>{{ notifAmount }} new messages</q-item-label>
</q-item-section>
</q-item>
</template>

View File

@ -0,0 +1,22 @@
<script lang="ts" setup>
import { useUiStore } from 'src/stores/ui-store';
import HeaderBarAvatar from './header-bar-avatar.vue';
const uiStore = useUiStore();
</script>
<template>
<q-header elevated>
<q-toolbar>
<q-toolbar-title>
<q-btn flat dense color="white" icon="menu" @click="uiStore.toggleRightDrawer">
<q-img src="src/assets/logo-targo-white.svg" fit="contain" width="150px" height="30px"/>
</q-btn>
</q-toolbar-title>
<q-item class="q-pa-none">
<HeaderBarAvatar />
</q-item>
</q-toolbar>
</q-header>
</template>

View File

@ -0,0 +1,98 @@
<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';
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 handleLogout = () => {
authStore.logout();
router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err);
})
}
</script>
<template>
<q-drawer overlay elevated side="left" :mini="miniState" @mouseenter="miniState = false"
@mouseleave="miniState = true" v-model="uiStore.isRightDrawerOpen">
<q-scroll-area class="fit">
<q-list>
<!-- Home -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.DASHBOARD)">
<q-item-section avatar>
<q-icon name="home" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $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')">
<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-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')">
<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-section>
</q-item>
<!-- Profile -->
<q-item v-ripple clickable side @click="goToPageName(RouteNames.PROFILE)">
<q-item-section avatar>
<q-icon name="account_box" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuProfile') }}</q-item-label>
</q-item-section>
</q-item>
<!-- Help -->
<q-item v-ripple clickable @click="goToPageName('help')">
<q-item-section avatar>
<q-icon name="contact_support" color="primary" />
</q-item-section>
<q-item-section>
<q-item-label>{{ $t('navBar.userMenuHelp') }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<!-- Logout -->
<q-item v-ripple clickable @click="handleLogout" class="absolute-bottom">
<q-item-section avatar>
<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-section>
</q-item>
</q-scroll-area>
</q-drawer>
</template>

View File

@ -0,0 +1,7 @@
export interface PayPeriod {
period_number: number;
start_date: string;
end_date: string;
year: number;
label: string;
};

View File

@ -0,0 +1,6 @@
export interface User {
firstName: string;
lastName: string;
email: string;
role: string;
}

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
/* eslint-disable */
import { computed, 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';
const { t } = useI18n();
const columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [
{ name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true },
{ name: 'regular_hours', label: t('timeSheetValidations.tableColumnLabelRegularHours'), field: 'regular_hours', sortable: true },
{ name: 'evening_hours', label: t('timeSheetValidations.tableColumnLabelEveningHours'), field: 'evening_hours' },
{ name: 'emergency_hours', label: t('timeSheetValidations.tableColumnLabelEmergencyHours'), field: 'emergency_hours' },
{ name: 'overtime_hours', label: t('timeSheetValidations.tableColumnLabelOvertime'), field: 'overtime_hours' },
{ name: 'expenses', label: t('timeSheetValidations.tableColumnLabelExpenses'), field: 'expenses', sortable: true },
{ name: 'mileage', label: t('timeSheetValidations.tableColumnLabelMileage'), field: 'mileage', sortable: true }
]);
const filter = ref('');
const selectedRows = ref<PayPeriodEmployeeOverview[]>();
const rows: PayPeriodEmployeeOverview[] = mock_pay_period_employee_overviews;
</script>
<template>
<div class="q-pa-md">
<q-table
:rows="rows"
:columns="columns"
row-key="employee_id"
selection="multiple"
v-model:selected="selectedRows"
:filter="filter"
grid
dense
:rows-per-page-options="[0]"
card-container-class="justify-center q-gutter-md"
>
<!-- 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-space />
<!-- Filters toggle -->
<q-btn flat dense class="text-white" label="filters" icon-right="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>
</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" 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>
</div>
</template>
</q-table>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useTimesheetStore } from 'src/stores/timesheet-store';
const timesheetStore = useTimesheetStore();
const updateCurrentPayPeriod = () => {
timesheetStore.getCurrentPayPeriod();
};
</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>
</template>

View File

@ -0,0 +1,12 @@
<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';
</script>
<template>
<q-page padding class="q-pa-md bg-secondary">
<TimesheetApprovalPeriodPicker />
<TimesheetApprovalEmployeeOverviewList />
</q-page>
</template>

View File

@ -0,0 +1,37 @@
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";
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;
},
getAllPayPeriods: async () => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
return await api.get(`/pay-periods/`) || mock_pay_periods;
},
getPayPeriodEmployeeOverviews: async (period_number: number) => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
return await api.get(`/pay-periods/${period_number}/overview`);
},
};

View File

@ -0,0 +1,287 @@
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_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

@ -0,0 +1,11 @@
export interface PayPeriodEmployeeOverview {
employee_id: string;
employee_name: string;
regular_hours: number;
evening_hours: number;
emergency_hours: number;
overtime_hours: number;
expenses: number;
mileage: number;
is_approved: boolean;
};

View File

@ -0,0 +1,6 @@
export interface PayPeriodOverview {
period_number: number;
start_date: string;
end_date: string;
label: string;
};

View File

@ -3,25 +3,21 @@
</script>
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
<q-layout view="hHh lpR fFf">
<q-page-container>
<q-page padding class="column justify-center items-center bg-secondary">
<q-card class="col-shrink rounded-20">
<q-img src="src/assets/line-truck-1.jpg" height="20vh">
<div class="absolute-bottom text-h4 text-center text-weight-bolder justify-center items-center row">
<div class="q-pr-md text-primary text-h3 text-weight-bolder">404</div>
PAGE NOT FOUND
</div>
</q-img>
<q-card-section class="text-center text-h5 text-primary">
{{$t('notFoundPage.pageText')}}
</q-card-section>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>

View File

@ -0,0 +1,9 @@
export enum RouteNames {
/* eslint-disable */
LOGIN = 'login',
LOGIN_SUCCESS = 'login-success',
DASHBOARD = 'dashboard',
TIMESHEET_APPROVALS = 'timesheet-approvals',
EMPLOYEE_LIST = 'employee-list',
PROFILE = 'user/profile',
}

View File

@ -1,4 +1,5 @@
import type { RouteRecordRaw } from 'vue-router';
import { RouteNames } from './router-constants';
const routes: RouteRecordRaw[] = [
{
@ -8,22 +9,27 @@ const routes: RouteRecordRaw[] = [
children: [
{
path: '',
name: 'dashboard',
name: RouteNames.DASHBOARD,
component: () => import('src/pages/test-page.vue'),
},
{
path: 'timesheet-approvals',
name: RouteNames.TIMESHEET_APPROVALS,
component: () => import('src/modules/timesheet-approval/pages/timesheet-approval.vue'),
},
],
},
{
path: '/v1/login',
name: 'login',
name: RouteNames.LOGIN,
component: () => import('src/modules/auth/pages/auth-login.vue'),
meta: { requiresAuth: false },
},
{
path: '/login-success',
name: 'login-success',
name: RouteNames.LOGIN_SUCCESS,
component: () => import('src/modules/auth/pages/auth-login-popup-success.vue'),
meta: { requiresAuth: false },
},

39
src/stores/auth-store.ts Normal file
View File

@ -0,0 +1,39 @@
import { computed, ref } from "vue";
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 const useAuthStore = defineStore('auth', () => {
const user = ref (defaultUser);
const authError = ref("");
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
const login = () => {
//TODO: manage customer login process
};
const oidcLogin = () => {
const oidcPopup = AuthService.oidcLogin();
if (!oidcPopup) {
authError.value = "You have popups blocked on this website!";
}
};
const logout = () => {
user.value = defaultUser;
};
const setUser = (currentUser: User) => {
user.value = currentUser;
};
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
});

View File

@ -0,0 +1,232 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
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
}
];
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 getCurrentPayPeriod = () => {
currentPayPeriod.value = timesheetApprovalService.getCurrentPayPeriod();
}
return { payPeriods, currentPayPeriod, payPeriodEmployeeOverviews, getCurrentPayPeriod};
});

11
src/stores/ui-store.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useUiStore = defineStore('ui', () => {
const isRightDrawerOpen = ref(true);
const toggleRightDrawer = () => {
isRightDrawerOpen.value = !isRightDrawerOpen.value;
}
return { isRightDrawerOpen, toggleRightDrawer };
});

View File

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