feat(timesheet-approval): add display of overtime to overview and details dialog. Minor changes to timesheet appearance, holidays
This commit is contained in:
parent
b8c112f149
commit
7913c58680
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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'"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import type { BackendResponse } from "src/modules/shared/models/backend-response
|
||||||
import type { FederalHoliday } from "src/modules/timesheets/models/federal-holidays.models";
|
import type { FederalHoliday } from "src/modules/timesheets/models/federal-holidays.models";
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getAllFederalHolidays: async (): Promise<FederalHoliday[]> => {
|
getAllFederalHolidays: async (year?: number): Promise<FederalHoliday[]> => {
|
||||||
const response = await api.get<{ holidays: FederalHoliday[] }>('https://canada-holidays.ca/api/v1/holidays', { withCredentials: false });
|
const response = await api.get<{ holidays: FederalHoliday[] }>(`https://canada-holidays.ca/api/v1/holidays${year ? ('?year=' + year) : ''}`, { withCredentials: false });
|
||||||
return response.data.holidays;
|
return response.data.holidays;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,13 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
|
|
||||||
const getCurrentFederalHolidays = async(): Promise<boolean> => {
|
const getCurrentFederalHolidays = async(): Promise<boolean> => {
|
||||||
const all_federal_holidays = await timesheetService.getAllFederalHolidays();
|
const all_federal_holidays = await timesheetService.getAllFederalHolidays();
|
||||||
if (!all_federal_holidays) return false;
|
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));
|
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;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user