fix(all): many changes, see commit details. Add weekly overview data to timesheets

This commit is contained in:
Nicolas Drolet 2026-01-06 09:12:49 -05:00
parent f738a5872a
commit 1e16c8334b
16 changed files with 155 additions and 42 deletions

BIN
src/assets/info-pannes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -245,6 +245,9 @@ export default {
page_header: "Timesheet", page_header: "Timesheet",
week: "week", week: "week",
total_hours: "total hours: ", total_hours: "total hours: ",
total_expenses: "total expenses: ",
vacation_available: "vacation time available: ",
sick_available: "sick time available: ",
current_shifts: "shifts worked", current_shifts: "shifts worked",
apply_preset: "auto-fill", apply_preset: "auto-fill",
apply_preset_day: "Apply schedule to day", apply_preset_day: "Apply schedule to day",

View File

@ -246,6 +246,9 @@ export default {
page_header: "Carte de temps", page_header: "Carte de temps",
week: "semaine", week: "semaine",
total_hours: "heures totales: ", total_hours: "heures totales: ",
total_expenses: "dépenses totales: ",
vacation_available: "vacances disponibles: ",
sick_available: "congés maladie disponible: ",
current_shifts: "quarts entrées", current_shifts: "quarts entrées",
apply_preset: "auto-remplir", apply_preset: "auto-remplir",
apply_preset_day: "Appliquer horaire pour la journée", apply_preset_day: "Appliquer horaire pour la journée",

View File

@ -16,7 +16,7 @@
<template> <template>
<q-card <q-card
class="shortcut-card cursor-pointer" class="shortcut-card cursor-pointer shadow-12"
@click="onClickExternalShortcut" @click="onClickExternalShortcut"
> >
<q-img <q-img

View File

@ -22,7 +22,7 @@
const getPresetOptions = (): { label: string, value: number }[] => { const getPresetOptions = (): { label: string, value: number }[] => {
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } }); const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
options.push({ label: '', value: -1 }); options.push({ label: 'Aucun', value: -1 });
return options; return options;
}; };

View File

@ -58,21 +58,21 @@
name="form" name="form"
icon="las la-id-card" icon="las la-id-card"
:label="$q.screen.lt.sm ? '' : $t('employee_management.details_label')" :label="$q.screen.lt.sm ? '' : $t('employee_management.details_label')"
class="rounded-25 q-ma-xs" class="rounded-25 q-ma-xs bg-dark"
style="border: 2px solid var(--q-accent);" style="border: 2px solid var(--q-accent);"
/> />
<q-tab <q-tab
name="access" name="access"
icon="las la-key" icon="las la-key"
:label="$q.screen.lt.sm ? '' : $t('employee_management.access_label')" :label="$q.screen.lt.sm ? '' : $t('employee_management.access_label')"
class="rounded-25 q-ma-xs" class="rounded-25 q-ma-xs bg-dark"
style="border: 2px solid var(--q-accent);" style="border: 2px solid var(--q-accent);"
/> />
<q-tab <q-tab
name="schedule" name="schedule"
icon="calendar_month" icon="calendar_month"
:label="$q.screen.lt.sm ? '' : $t('employee_management.schedule_label')" :label="$q.screen.lt.sm ? '' : $t('employee_management.schedule_label')"
class="rounded-25 q-ma-xs" class="rounded-25 q-ma-xs bg-dark"
style="border: 2px solid var(--q-accent);" style="border: 2px solid var(--q-accent);"
/> />
</q-tabs> </q-tabs>

View File

@ -184,7 +184,7 @@
:row="props.row" :row="props.row"
:index="props.rowIndex" :index="props.rowIndex"
:is-management="is_management" :is-management="is_management"
@on-profile-click="is_management ? employee_store.openAddModifyDialog : ''" @on-profile-click="email => is_management ? employee_store.openAddModifyDialog(email) : ''"
/> />
</transition> </transition>
</template> </template>

View File

@ -72,7 +72,7 @@
v-model="shift.is_remote" v-model="shift.is_remote"
dense dense
keep-color keep-color
size="3em" size="2.5em"
color="accent" color="accent"
icon="las la-building" icon="las la-building"
checked-icon="las la-laptop" checked-icon="las la-laptop"

View File

@ -37,7 +37,7 @@
<!-- employee pay period details using chart --> <!-- employee pay period details using chart -->
<div <div
v-if="is_dialog_open && !$q.platform.is.mobile" v-if="is_dialog_open && !$q.platform.is.mobile"
class="col-4 q-px-md no-wrap" class="col-auto q-px-md no-wrap"
:class="$q.platform.is.mobile ? 'column' : 'row'" :class="$q.platform.is.mobile ? 'column' : 'row'"
> >
<DetailsDialogChartHoursWorked class="col" /> <DetailsDialogChartHoursWorked class="col" />
@ -50,8 +50,8 @@
</div> </div>
<!-- list of shifts --> <!-- list of shifts -->
<div class="col column no-wrap"> <div class="col-auto column no-wrap">
<TimesheetWrapper mode="approval" class="col"/> <TimesheetWrapper mode="approval" class="col-auto"/>
</div> </div>
</div> </div>
</q-dialog> </q-dialog>

View File

@ -151,7 +151,7 @@
</template> </template>
<template #option="scope"> <template #option="scope">
<q-item> <q-item clickable v-bind="scope.itemProps">
<q-item-section avatar> <q-item-section avatar>
<q-icon <q-icon
:name="scope.opt.icon" :name="scope.opt.icon"

View File

@ -0,0 +1,71 @@
<script
setup
lang="ts"
>
import { useAuthStore } from 'src/stores/auth-store';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
const { mode = 'totals', totalHours = 0, vacationHours = 0, sickHours = 0, totalExpenses = 0 } = defineProps<{
mode: 'total-hours' | 'off-hours';
totalHours?: number;
vacationHours?: number;
sickHours?: number;
totalExpenses?: number;
}>();
const auth_store = useAuthStore();
const is_management = auth_store.user?.user_module_access.includes('timesheets_approval');
</script>
<template>
<div
class="column full-width shadow-4 rounded-5 q-pa-sm"
style="border: 1px solid var(--q-accent);"
>
<div
v-if="mode === 'total-hours'"
class="col column full-width"
>
<div class="col row full-width">
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.total_hours') }}
</span>
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(totalHours) }}</span>
</div>
<div class="col row full-width">
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.total_expenses') }}
</span>
<span class="col text-right">{{ totalExpenses }}$</span>
</div>
</div>
<div
v-else
class="col column full-width"
>
<div class="col row full-width">
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.vacation_available') }}
</span>
<span class="col text-right">{{ Math.floor(vacationHours / 8) }}</span>
</div>
<div
v-if="is_management"
class="col row full-width"
>
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.sick_available') }}
</span>
<span class="col text-right">{{ Math.floor(sickHours / 8) }}</span>
</div>
</div>
</div>
</template>

View File

@ -11,7 +11,7 @@
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { Shift } from 'src/modules/timesheets/models/shift.models'; import { Shift } from 'src/modules/timesheets/models/shift.models';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { QScrollArea } from 'quasar'; import type { QScrollArea, TouchSwipeValue } from 'quasar';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models'; import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10); const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
@ -34,7 +34,9 @@
const currentDayComponent = ref<HTMLElement[] | null>(null); const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent); const currentDayComponentWatcher = ref(currentDayComponent);
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0) const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0);
const timesheet_container = ref<HTMLElement | null>(null);
const scroll_area_height = ref(0);
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => { const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true; ui_store.focus_next_component = true;
@ -57,10 +59,10 @@
return day.shifts.every(shift => shift.is_approved === true); return day.shifts.every(shift => shift.is_approved === true);
}; };
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: { x?: number, y?: number }) => { const handleSwipe: TouchSwipeValue = (details) => {
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft'; mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
if (distance.x && Math.abs(distance.x) > 10) { if (details.distance && details.distance.x && Math.abs(details.distance.x) > 10) {
await timesheet_api.getTimesheetsBySwiping(direction === 'left' ? 1 : -1) timesheet_api.getTimesheetsBySwiping(details.direction === 'left' ? 1 : -1).catch(error => console.error(error));
} }
}; };
@ -71,8 +73,12 @@
watch(currentDayComponentWatcher, () => { watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) { if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) {
console.log('setting scroll position to offsetTop of currentDayComponent: ', currentDayComponent.value[0]!.offsetTop); console.log('setting scroll position to offsetTop of currentDayComponent: ', currentDayComponent.value[0]!.offsetTop);
timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800); timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800);
return; return;
}
if (timesheet_container.value !== null && mode === 'approval') {
scroll_area_height.value = timesheet_container.value.offsetHeight
} }
}) })
</script> </script>
@ -81,12 +87,13 @@
<div <div
class="column fit relative-position" class="column fit relative-position"
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''" :style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })" v-touch-swipe="handleSwipe"
> >
<q-scroll-area <q-scroll-area
ref="timesheet_page" ref="timesheet_page"
:horizontal-offset="[0, 3]" :horizontal-offset="[0, 3]"
class="absolute-full hide-scrollbar" class="col absolute-full hide-scrollbar"
:style="mode === 'approval' ? `height: ${scroll_area_height}px;` : ''"
:thumb-style="{ opacity: '0' }" :thumb-style="{ opacity: '0' }"
:bar-style="{ opacity: '0' }" :bar-style="{ opacity: '0' }"
> >
@ -97,7 +104,7 @@
style="min-height: 20vh;" style="min-height: 20vh;"
> >
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found') <span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
}}</span> }}</span>
<q-icon <q-icon
name="las la-calendar" name="las la-calendar"
color="accent" color="accent"
@ -110,6 +117,7 @@
<!-- Else show timesheets if found --> <!-- Else show timesheets if found -->
<div <div
v-else v-else
ref="timesheet_container"
class="col fit" class="col fit"
:class="$q.platform.is.mobile ? 'column' : 'row'" :class="$q.platform.is.mobile ? 'column' : 'row'"
> >

View File

@ -4,10 +4,12 @@
> >
/* eslint-disable */ /* eslint-disable */
import ShiftList from 'src/modules/timesheets/components/shift-list.vue'; import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue'; import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue'; import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue'; import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue'; import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue'; import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
@ -37,9 +39,41 @@
</script> </script>
<template> <template>
<div class="column items-center full-height"> <div class="column items-center full-height relative-position no-wrap">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<!-- label for approval mode to delimit that this is the timesheet -->
<span
v-if="mode === 'approval'"
class="col-auto text-uppercase text-bold text-h5"
>
{{ $t('timesheet.page_header') }}
</span>
<!-- weekly overview -->
<div class="col-auto row q-px-lg full-width">
<!-- supervisor weekly overview -->
<div class="col-xs-6 col-md-4 col-xl-3 q-pa-md">
<ShiftListWeeklyOverview mode="total-hours" />
</div>
<PageHeaderTemplate
v-if="mode === 'normal'"
:title="'timesheet.page_header'"
:start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col"
/>
<q-space v-else />
<!-- employee weekly overview -->
<div class="col-xs-6 col-md-4 col-xl-3 q-pa-md">
<ShiftListWeeklyOverview mode="off-hours" />
</div>
</div>
<!-- top menu --> <!-- top menu -->
<div <div
class="col-auto row items-center full-width" class="col-auto row items-center full-width"
@ -66,12 +100,6 @@
@click="expenses_store.open" @click="expenses_store.open"
/> />
<!-- label for approval mode to delimit that this is the timesheet -->
<span
v-if="mode === 'approval'"
class="col-auto text-uppercase text-bold text-h5"
> {{ $t('timesheet.page_header') }}</span>
<q-space v-if="$q.screen.width > $q.screen.height" /> <q-space v-if="$q.screen.width > $q.screen.height" />
<!-- desktop expenses button --> <!-- desktop expenses button -->
@ -107,7 +135,8 @@
<ShiftList <ShiftList
:mode="mode" :mode="mode"
class="col" :class="mode === 'normal' ? 'col' : 'col-auto'"
:style="mode === 'normal' ? '' : 'min-height: 100vh'"
/> />
<q-btn <q-btn

View File

@ -49,6 +49,14 @@
route="https://map.targointernet.com/infrastructure/map.php" route="https://map.targointernet.com/infrastructure/map.php"
/> />
</div> </div>
<div class="col-3 q-pa-sm">
<ShortcutCard
image-source="src/assets/info-pannes.png"
title="Info Pannes"
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
/>
</div>
</div> </div>
</div> </div>

View File

@ -20,7 +20,7 @@
const headerComponent = ref<HTMLElement | null>(null); const headerComponent = ref<HTMLElement | null>(null);
const table_max_height = computed(() => { const table_max_height = computed(() => {
const height = page_height.value - (headerComponent.value?.offsetHeight ?? 0); const height = page_height.value - (headerComponent.value?.clientHeight ?? 0);
return height; return height;
}); });

View File

@ -2,14 +2,12 @@
setup setup
lang="ts" lang="ts"
> >
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue'; import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
const { user } = useAuthStore(); const { user } = useAuthStore();
const timesheet_store = useTimesheetStore();
</script> </script>
@ -17,13 +15,6 @@
<q-page <q-page
class="column bg-secondary items-center" class="column bg-secondary items-center"
> >
<PageHeaderTemplate
:title="'timesheet.page_header'"
:start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col-auto"
/>
<div <div
class="col column fit" class="col column fit"
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'" :style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"