Merge pull request 'dev/nicolas/staging-prep' (#50) from dev/nicolas/staging-prep into main

Reviewed-on: Targo/targo_frontend#50
This commit is contained in:
Nicolas 2026-01-13 16:29:50 -05:00
commit 550dcbc3ba
16 changed files with 214 additions and 35 deletions

View File

@ -239,6 +239,7 @@ export default {
close: "close", close: "close",
download: "download", download: "download",
open: "open", open: "open",
day: "day",
}, },
misc: { misc: {
or: "or", or: "or",
@ -278,6 +279,7 @@ export default {
total_expenses: "total expenses: ", total_expenses: "total expenses: ",
vacation_available: "vacation time available: ", vacation_available: "vacation time available: ",
sick_available: "sick time available: ", sick_available: "sick time available: ",
banked_available: "available banked hours: ",
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

@ -239,6 +239,7 @@ export default {
close: "fermer", close: "fermer",
download: "télécharger", download: "télécharger",
open: "ouvrir", open: "ouvrir",
day: "jour",
}, },
misc: { misc: {
or: "ou", or: "ou",
@ -278,6 +279,7 @@ export default {
total_expenses: "dépenses totales: ", total_expenses: "dépenses totales: ",
vacation_available: "vacances disponibles: ", vacation_available: "vacances disponibles: ",
sick_available: "congés maladie disponible: ", sick_available: "congés maladie disponible: ",
banked_available: "heures en banque disponibles: ",
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

@ -4,7 +4,7 @@
> >
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { ref } from 'vue'; import { computed, ref } from 'vue';
const q = useQuasar(); const q = useQuasar();
const is_mouseover = ref(false); const is_mouseover = ref(false);
@ -15,17 +15,23 @@
isManagement?: boolean; isManagement?: boolean;
}>() }>()
defineEmits<{ const is_showing_highlight = computed(() => isManagement && is_mouseover.value)
const emit = defineEmits<{
onProfileClick: [email: string] onProfileClick: [email: string]
}>(); }>();
const getItemStyle = (): string => { const getItemStyle = (): string => {
const active_style = row.last_work_day === null ? '' : 'opacity: 0.6;'; const active_style = row.last_work_day === null ? '' : 'opacity: 0.6;';
const dark_style = q.dark.isActive ? 'border: 2px solid var(--q-accent);' : ''; const dark_style = q.dark.isActive ? 'border: 2px solid var(--q-accent);' : '';
const hover_style = isManagement ? (is_mouseover.value ? `transform: scale(1.1); z-index: 2;` : 'transform: scale(1) skew(0)') : '';
return `${active_style} ${dark_style} ${hover_style}`; return `${active_style} ${dark_style}`;
} }
const onProfileClick = (email: string) => {
is_mouseover.value = false;
emit('onProfileClick', email)
};
</script> </script>
<template> <template>
@ -34,14 +40,17 @@
:style="`animation-delay: ${index / 25}s;`" :style="`animation-delay: ${index / 25}s;`"
> >
<div <div
class="column col no-wrap bg-dark rounded-15 shadow-12" class="column col no-wrap bg-dark rounded-15 shadow-12 relative-position"
:class="isManagement ? 'cursor-pointer item-mouse-hover' : ''" :class="isManagement ? 'cursor-pointer item-mouse-hover' : ''"
style="height: 275px;" style="height: 275px;"
:style="getItemStyle()" :style="getItemStyle()"
@click="$emit('onProfileClick', row.email)" @click="onProfileClick(row.email)"
@mouseenter="is_mouseover = true" @mouseenter="is_mouseover = true"
@mouseleave="is_mouseover = false" @mouseleave="is_mouseover = false"
> >
<!-- highlight component for mouseover -->
<div v-if="is_showing_highlight" class="absolute-full bg-accent rounded-15 z-top" style="opacity: 0.3;"></div>
<div class="col-auto column flex-center q-pt-md"> <div class="col-auto column flex-center q-pt-md">
<q-avatar <q-avatar
:color="row.last_work_day === null ? 'accent' : 'negative'" :color="row.last_work_day === null ? 'accent' : 'negative'"

View File

@ -4,11 +4,12 @@
> >
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { colors, useQuasar } from 'quasar'; import { colors, date, useQuasar } from 'quasar';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartDataset } from 'chart.js';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import type { TotalHours } from 'src/modules/timesheets/models/timesheet.models'; import type { TotalHours } from 'src/modules/timesheets/models/timesheet.models';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
interface ChartConfigHoursWorked { interface ChartConfigHoursWorked {
key: keyof Pick<TotalHours, 'regular' | 'evening' | 'emergency' | 'overtime'>; key: keyof Pick<TotalHours, 'regular' | 'evening' | 'emergency' | 'overtime'>;
@ -78,6 +79,17 @@
:options="({ :options="({
indexAxis: $q.screen.lt.md ? 'y' : 'x', indexAxis: $q.screen.lt.md ? 'y' : 'x',
plugins: { plugins: {
tooltip: {
callbacks: {
title: function (context) {
return $d(date.extractDate(`2025-${context[0]!.label}`, 'YYYY-MM-DD'), {month: 'long', day: 'numeric'});
},
label: function (context) {
return getHoursMinutesStringFromHoursFloat(context.parsed.y);
}
},
},
legend: { legend: {
labels: { labels: {
boxWidth: 15, boxWidth: 15,

View File

@ -10,6 +10,7 @@
import { Doughnut } from 'vue-chartjs'; import { Doughnut } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
const $q = useQuasar(); const $q = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -63,6 +64,14 @@
}" }"
:options="({ :options="({
plugins: { plugins: {
tooltip: {
callbacks: {
label: function (context) {
context.formattedValue = getHoursMinutesStringFromHoursFloat(context.parsed);
}
},
},
legend: { legend: {
labels: { labels: {
boxWidth: 15, boxWidth: 15,

View File

@ -118,14 +118,14 @@
:filter-method="filterEmployeeRows" :filter-method="filterEmployeeRows"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
class="bg-transparent" class="bg-transparent"
:class="ui_store.user_preferences.is_timesheet_approval_grid ? '' : 'sticky-header-table no-shadow'" :class="ui_store.user_preferences.is_timesheet_approval_grid ? '' : 'sticky-header-table sticky-first-column-table sticky-last-column-table no-shadow q-pb-sm'"
card-container-class="justify-center" card-container-class="justify-center"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar" table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15"
:no-data-label="$t('shared.error.no_data_found')" :no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')" :loading-label="$t('shared.label.loading')"
table-header-style="min-width: 80xp; max-width: 80px;" table-header-style="min-width: 80xp; max-width: 80px;"
:style="overview_rows.length > 0 ? `max-height: ${maxHeight - (ui_store.user_preferences.is_timesheet_approval_grid ? 0 : 20)}px;` : ''" :style="overview_rows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
:table-style="{ tableLayout: 'fixed' }" :table-style="{ tableLayout: 'fixed' }"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)" @row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
> >
@ -373,4 +373,46 @@
td td
min-width: 80px min-width: 80px
.sticky-last-column-table
thead tr:last-child th:last-child
background-color: $accent
position: sticky
z-index: 3
thead tr:last-child th
top: 0px
td:last-child
background-color: var(--q-dark)
th:last-child,
td:last-child
position: sticky
right: 0
z-index: 1
border-left: 3px solid $accent
.sticky-first-column-table
thead tr:first-child th:first-child
background-color: $accent
position: sticky
z-index: 3
thead tr:first-child th
top: 0px
td:first-child
background-color: var(--q-dark)
th:first-child,
td:first-child
position: sticky
left: 0
z-index: 1
border-right: 3px solid $accent
:deep(tbody)
overflow-x: scroll
</style> </style>

View File

@ -14,10 +14,9 @@
approved?: boolean; approved?: boolean;
}>(); }>();
const is_mobile = computed(() => q.screen.lt.md);
const date_font_size = computed(() => dense ? '1.5em' : '2.5em'); const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;'); const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
const date_box_size = computed(() => dense || is_mobile.value ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;'); const date_box_size = computed(() => dense || q.platform.is.mobile ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
const display_date = extractDate(displayDate, 'YYYY-MM-DD'); const display_date = extractDate(displayDate, 'YYYY-MM-DD');
</script> </script>

View File

@ -17,10 +17,11 @@
const select_ref = ref<QSelect | null>(null); const select_ref = ref<QSelect | null>(null);
const error_message = ref(''); const error_message = ref('');
const { errorMessage = undefined, isTimesheetApproved = false } = defineProps<{ const { errorMessage = undefined, isTimesheetApproved = false, holiday = false } = defineProps<{
dense?: boolean; dense?: boolean;
isTimesheetApproved?: boolean; isTimesheetApproved?: boolean;
errorMessage?: string | undefined; errorMessage?: string | undefined;
holiday?: boolean | undefined;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -89,7 +90,7 @@
class="col rounded-5 q-mx-xs bg-dark" class="col rounded-5 q-mx-xs bg-dark"
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'" :class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5" popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : ''"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect" @blur="onBlurShiftTypeSelect"
@update:model-value="option => shift.type = option.value" @update:model-value="option => shift.type = option.value"
@ -102,7 +103,7 @@
> >
<q-icon <q-icon
:name="scope.opt.icon" :name="scope.opt.icon"
:color="scope.opt.icon_color" :color="shift.is_approved ? 'white' : scope.opt.icon_color"
size="sm" size="sm"
class="col-auto" class="col-auto"
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'" :class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
@ -143,7 +144,7 @@
dense dense
keep-color keep-color
size="3em" size="3em"
color="accent" :color="holiday? 'purple-5' : 'accent'"
icon="las la-building" icon="las la-building"
checked-icon="las la-laptop" checked-icon="las la-laptop"
> >
@ -191,12 +192,12 @@
hide-bottom-space hide-bottom-space
:error="shift.has_error" :error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''" :error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'" :label-color="!shift.is_approved ? (holiday? 'purple-5' : 'accent') : 'white'"
class="col rounded-5 bg-dark q-mx-xs" class="col rounded-5 bg-dark q-mx-xs"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : ''"
@blur="onTimeFieldBlur(shift.start_time)" @blur="onTimeFieldBlur(shift.start_time)"
> >
<template #label> <template #label>
@ -222,12 +223,12 @@
hide-bottom-space hide-bottom-space
:error="shift.has_error" :error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''" :error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'" :label-color="!shift.is_approved ? (holiday? 'purple-5' : 'accent') : 'white'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark q-mx-xs" class="col rounded-5 bg-dark q-mx-xs"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? (holiday ? 'background-color: #7b1fa2 !important' : 'background-color: #0a7d32 !important;') : ''"
@blur="onTimeFieldBlur(shift.end_time)" @blur="onTimeFieldBlur(shift.end_time)"
> >
<template #label> <template #label>
@ -249,9 +250,9 @@
v-if="!ui_store.is_mobile_mode" v-if="!ui_store.is_mobile_mode"
push push
dense dense
:color="shift.is_approved ? 'white' : 'accent'" :color="shift.is_approved ? 'white' : (holiday? 'purple-5' : 'accent')"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'" :icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.is_approved ? 'accent' : 'white'" :text-color="shift.is_approved ? (holiday? 'purple-5' : 'accent') : 'white'"
class="col" class="col"
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''" :class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
> >

View File

@ -18,12 +18,13 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const shift_error_message = ref<string | undefined>(); const shift_error_message = ref<string | undefined>();
const { day, dense = false, approved = false } = defineProps<{ const { day, dense = false, approved = false, holiday = false } = defineProps<{
timesheetId: number; timesheetId: number;
weekDayIndex: number; weekDayIndex: number;
day: TimesheetDay; day: TimesheetDay;
dense?: boolean; dense?: boolean;
approved?: boolean; approved?: boolean;
holiday?: boolean;
}>(); }>();
const preset_mouseover = ref(false); const preset_mouseover = ref(false);
@ -102,6 +103,7 @@
<ShiftListDayRow <ShiftListDayRow
v-else v-else
v-model:shift="day.shifts[shift_index]!" v-model:shift="day.shifts[shift_index]!"
:holiday="holiday"
:is-timesheet-approved="approved" :is-timesheet-approved="approved"
:error-message="shift_error_message" :error-message="shift_error_message"
@request-delete="deleteCurrentShift(shift)" @request-delete="deleteCurrentShift(shift)"

View File

@ -3,13 +3,15 @@
lang="ts" lang="ts"
> >
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils'; import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
const { mode = 'totals', totalHours = 0, vacationHours = 0, sickHours = 0, totalExpenses = 0 } = defineProps<{ const { mode = 'totals', timesheetMode = 'normal', totalHours = 0, vacationHours = 0, sickHours = 0, bankedHours = 0, totalExpenses = 0 } = defineProps<{
mode: 'total-hours' | 'off-hours'; mode: 'total-hours' | 'off-hours';
timesheetMode: 'approval' | 'normal';
totalHours?: number; totalHours?: number;
vacationHours?: number; vacationHours?: number;
sickHours?: number; sickHours?: number;
bankedHours?: number;
totalExpenses?: number; totalExpenses?: number;
}>(); }>();
@ -53,7 +55,12 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
{{ $t('timesheet.vacation_available') }} {{ $t('timesheet.vacation_available') }}
</span> </span>
<span class="col text-right">{{ Math.floor(vacationHours / 8) }}</span> <div class="col row">
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(vacationHours) }}</span>
<span class="col-auto text-right q-pl-xs">{{ ' (' + Math.floor(vacationHours / 8) }}</span>
<span class="col-auto q-pl-xs">{{ $t('shared.label.day') }}{{ (Math.floor(vacationHours / 8) !== 1 ?
's' : '') + ')' }}</span>
</div>
</div> </div>
<div <div
@ -64,7 +71,18 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
{{ $t('timesheet.sick_available') }} {{ $t('timesheet.sick_available') }}
</span> </span>
<span class="col text-right">{{ Math.floor(sickHours / 8) }}</span> <span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(sickHours) }}</span>
</div>
<div
v-if="timesheetMode === 'normal'"
class="col row full-width"
>
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.banked_available') }}
</span>
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(bankedHours) }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,17 +6,19 @@
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue'; import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
import { date, useQuasar } from 'quasar'; import { date, useQuasar } from 'quasar';
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
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 { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models'; import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
import { useI18n } from 'vue-i18n';
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10); const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
const { extractDate } = date; const { extractDate } = date;
const { locale } = useI18n();
const q = useQuasar(); const q = useQuasar();
const ui_store = useUiStore(); const ui_store = useUiStore();
@ -58,11 +60,26 @@
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : ''; return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
}; };
const getHolidayName = (date: string) => {
const holiday = timesheet_store.federal_holidays.find(holiday => holiday.date === date);
if (!holiday) return;
if (locale.value === 'fr-FR')
return holiday.nameFr;
else if (locale.value === 'en-CA')
return holiday.nameEn;
};
onMounted(async () => {
await timesheet_store.getCurrentFederalHolidays();
});
watch(currentDayComponentWatcher, () => { watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && q.platform.is.mobile) { if (currentDayComponent.value && q.platform.is.mobile) {
emit('onCurrentDayComponentFound', currentDayComponent.value[0]) emit('onCurrentDayComponentFound', currentDayComponent.value[0])
} }
}) });
</script> </script>
<template> <template>
@ -105,9 +122,18 @@
v-for="day, day_index in timesheet.days" v-for="day, day_index in timesheet.days"
:key="day.date" :key="day.date"
:ref="getMobileDayRef(day.date)" :ref="getMobileDayRef(day.date)"
class="col-auto row q-pa-sm full-width" class="col-auto row q-pa-sm full-width relative-position"
:style="`animation-delay: ${day_index / 15}s;`" :style="`animation-delay: ${day_index / 15}s;`"
> >
<!-- optional label indicating which holiday if today is a holiday -->
<span
v-if="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date)"
class="absolute-top-left text-uppercase text-weight-bolder text-purple-5"
style="transform: translate(25px, -7px);"
>
{{ getHolidayName(day.date) }}
</span>
<!-- mobile version in portrait mode --> <!-- mobile version in portrait mode -->
<div <div
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)" v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
@ -173,10 +199,11 @@
<div <div
v-else v-else
class="col row full-width rounded-10 ellipsis shadow-10" class="col row full-width rounded-10 ellipsis shadow-10"
:style="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'border: 2px solid #ab47bc' : ''"
> >
<div <div
class="col row" class="col row"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'" :class="(getDayApproval(day) || timesheet.is_approved) ? (timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
> >
<!-- Date block --> <!-- Date block -->
<ShiftListDateWidget <ShiftListDateWidget
@ -189,6 +216,7 @@
:timesheet-id="timesheet.timesheet_id" :timesheet-id="timesheet.timesheet_id"
:week-day-index="day_index" :week-day-index="day_index"
:day="day" :day="day"
:holiday="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date)"
:approved="getDayApproval(day) || timesheet.is_approved" :approved="getDayApproval(day) || timesheet.is_approved"
class="col" class="col"
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)" @delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
@ -203,7 +231,7 @@
color="white" color="white"
size="xl" size="xl"
class="full-height" class="full-height"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : ''" :class="(getDayApproval(day) || timesheet.is_approved) ? (timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
/> />
<q-btn <q-btn
@ -212,7 +240,7 @@
square square
icon="more_time" icon="more_time"
size="lg" size="lg"
color="accent" :color="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
text-color="white" text-color="white"
class="full-height" class="full-height"
:class="$q.platform.is.mobile ? 'q-px-xs' : ''" :class="$q.platform.is.mobile ? 'q-px-xs' : ''"

View File

@ -76,6 +76,7 @@
> >
<ShiftListWeeklyOverview <ShiftListWeeklyOverview
mode="total-hours" mode="total-hours"
:timesheet-mode="mode"
:total-hours="total_hours" :total-hours="total_hours"
:total-expenses="total_expenses" :total-expenses="total_expenses"
/> />
@ -96,7 +97,7 @@
v-if="!$q.platform.is.mobile" v-if="!$q.platform.is.mobile"
class="col-xs-6 col-md-4 col-xl-3 q-pa-md" class="col-xs-6 col-md-4 col-xl-3 q-pa-md"
> >
<ShiftListWeeklyOverview mode="off-hours" /> <ShiftListWeeklyOverview mode="off-hours" :timesheet-mode="mode" />
</div> </div>
</div> </div>

View File

@ -0,0 +1,31 @@
export interface FederalHoliday {
id: number;
date: string;
nameEn: string;
nameFr: string;
federal: number;
observedDate: string;
provinces: FederalHolidayProvince[];
}
export interface FederalHolidayProvince {
id: number;
nameEn: string;
nameFr: string;
sourceLink: string;
sourceEn: string;
}
export const TARGO_HOLIDAY_NAMES_FR: string[] = [
"Jour de lAn",
"Vendredi saint",
"Lundi de Pâques",
"Journée nationale des patriotes",
"Saint-Jean-Baptiste / Fête nationale du Québec",
"Fête du Canada",
"Fête du travail",
"Journée nationale de la vérité et de la réconciliation",
"Action de grâce",
"Noël",
"Lendemain de Noël",
]

View File

@ -3,8 +3,14 @@ import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models"; import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
import type { TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models"; import type { TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { BackendResponse } from "src/modules/shared/models/backend-response.models"; import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
import type { FederalHoliday } from "src/modules/timesheets/models/federal-holidays.models";
export const timesheetService = { export const timesheetService = {
getAllFederalHolidays: async (year?: number): Promise<FederalHoliday[]> => {
const response = await api.get<{ holidays: FederalHoliday[] }>(`https://canada-holidays.ca/api/v1/holidays${year ? ('?year=' + year) : ''}`, { withCredentials: false });
return response.data.holidays;
},
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => { getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
const response = await api.get<{ success: boolean, data: PayPeriod, error?: string }>(`pay-periods/date/${date_string}`); const response = await api.get<{ success: boolean, data: PayPeriod, error?: string }>(`pay-periods/date/${date_string}`);
return response.data.data; return response.data.data;

View File

@ -7,6 +7,7 @@ import type { PayPeriodOverviewResponse, TimesheetApprovalOverview } from "src/m
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models'; import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models'; import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models'; import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
@ -26,6 +27,20 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const current_pay_period_overview = ref<TimesheetApprovalOverview>(); const current_pay_period_overview = ref<TimesheetApprovalOverview>();
const pay_period_report = ref(); const pay_period_report = ref();
const federal_holidays = ref<FederalHoliday[]>([]);
const getCurrentFederalHolidays = async(): Promise<boolean> => {
const all_federal_holidays = await timesheetService.getAllFederalHolidays();
const all_federal_holidays_2025 = await timesheetService.getAllFederalHolidays(2025);
if (!all_federal_holidays || !all_federal_holidays_2025) return false;
federal_holidays.value = all_federal_holidays.filter(holiday => TARGO_HOLIDAY_NAMES_FR.includes(holiday.nameFr));
const targo_fed_holidays_2025 = all_federal_holidays_2025.filter(holiday => TARGO_HOLIDAY_NAMES_FR.includes(holiday.nameFr));
targo_fed_holidays_2025.forEach(holiday => federal_holidays.value.push(holiday));
return true;
};
const getNextOrPreviousPayPeriod = (direction: number) => { const getNextOrPreviousPayPeriod = (direction: number) => {
if (!pay_period.value) return; if (!pay_period.value) return;
@ -178,6 +193,8 @@ export const useTimesheetStore = defineStore('timesheet', () => {
timesheets, timesheets,
all_current_shifts, all_current_shifts,
initial_timesheets, initial_timesheets,
federal_holidays,
getCurrentFederalHolidays,
getNextOrPreviousPayPeriod, getNextOrPreviousPayPeriod,
getPayPeriodByDateOrYearAndNumber, getPayPeriodByDateOrYearAndNumber,
getTimesheetOverviews, getTimesheetOverviews,