fix(many): refactor timesheet approval download menu, details window fixes, store refactor

- Timesheet Approval's download menu has had its UI overhauled and the script has been streamlined to better match backend structure and logic

- Details window in timesheet approval has a few bug and oversight fixes.

- Refactored UI store to work with camelCase instead of snake_case
This commit is contained in:
Nic D 2026-03-18 09:18:06 -04:00
parent 9213a42d6b
commit e37ec79827
28 changed files with 666 additions and 308 deletions

View File

@ -401,10 +401,14 @@ export default {
title: "Download options",
description: "Choose what to include in the report",
company: "companies",
targo: "Targo",
solucom: "Solucom",
type: "type",
shifts: "shifts",
expenses: "expenses",
options: "options",
download_failed: "download failed",
download_failed_caption: "an unexpected error occured",
},
table: {
full_name: "full name",

View File

@ -401,10 +401,14 @@ export default {
title: "options de téléchargement",
description: "Choisissez ce qui sera inclu dans le rapport",
company: "compagnies",
targo: "Targo",
solucom: "Solucom",
type: "types de données",
shifts: "quarts de travail",
expenses: "dépenses",
options: "options",
download_failed: "téléchargement échoué",
download_failed_caption: "une erreur inconnue est survenue",
},
table: {
full_name: "nom complet",

View File

@ -21,14 +21,14 @@ import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
]
const q = useQuasar();
const auth_store = useAuthStore();
const authStore = useAuthStore();
const authApi = useAuthApi();
const ui_store = useUiStore();
const uiStore = useUiStore();
const router = useRouter();
const is_mini = ref(true);
const isMini = ref(true);
const onClickDrawerPage = (page_name: RouteNames) => {
is_mini.value = true;
isMini.value = true;
router.push({ name: page_name }).catch(error => {
console.error('failed to reach page: ', error);
@ -41,21 +41,21 @@ import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
onMounted(() => {
if (q.platform.is.mobile) {
ui_store.is_left_drawer_open = false;
uiStore.isLeftDrawerOpen = false;
}
})
</script>
<template>
<q-drawer
v-model="ui_store.is_left_drawer_open"
v-model="uiStore.isLeftDrawerOpen"
:persistent="!$q.platform.is.mobile"
mini-to-overlay
elevated
side="left"
:mini="is_mini"
@mouseenter="is_mini = false"
@mouseleave="is_mini = true"
:mini="isMini"
@mouseenter="isMini = false"
@mouseleave="isMini = true"
class="bg-dark z-max"
>
<q-scroll-area class="column fit">
@ -66,7 +66,7 @@ import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
@click="onClickDrawerPage(button.route)"
>
<div
v-if="button.required_module ? auth_store.user?.user_module_access.includes(button.required_module) : true"
v-if="button.required_module ? authStore.user?.user_module_access.includes(button.required_module) : true"
class="row items-center full-width q-py-sm cursor-pointer"
:class="$router.currentRoute.value.name === button.route ? ($q.dark.isActive ? 'bg-green-10' : 'bg-green-2') : ''"
>

View File

@ -15,16 +15,16 @@
const ui_store = useUiStore();
const auth_store = useAuthStore();
const user_preferences = ref(ui_store.user_preferences);
const userPreferences = ref(ui_store.userPreferences);
onMounted(async () => {
if (ui_store.user_preferences.id === -1) {
if (ui_store.userPreferences.id === -1) {
await ui_store.getUserPreferences();
}
});
watch(user_preferences, async () => {
if (ui_store.user_preferences.id !== -1) {
watch(userPreferences, async () => {
if (ui_store.userPreferences.id !== -1) {
await ui_store.updateUserPreferences();
return
}

View File

@ -82,13 +82,13 @@
:filter="filters"
:filter-method="filterEmployeeRows"
class="bg-transparent no-shadow sticky-header-table full-width q-pt-lg"
:style="employee_store.employee_list.length > 0 ? `max-height: ${maxHeight - (ui_store.user_preferences.is_employee_list_grid ? 0 : 20)}px;` : ''"
:style="employee_store.employee_list.length > 0 ? `max-height: ${maxHeight - (ui_store.userPreferences.is_employee_list_grid ? 0 : 20)}px;` : ''"
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10 hide-scrollbar' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10 hide-scrollbar'"
color="accent"
separator="none"
table-header-class="text-accent text-uppercase"
card-container-class="justify-center"
:grid="ui_store.user_preferences.is_employee_list_grid"
:grid="ui_store.userPreferences.is_employee_list_grid"
:loading="employee_store.is_loading"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
@ -126,7 +126,7 @@
<q-space />
<q-btn-toggle
v-model="ui_store.user_preferences.is_employee_list_grid"
v-model="ui_store.userPreferences.is_employee_list_grid"
push
rounded
color="white"

View File

@ -20,7 +20,7 @@
<div
class="col-auto justify-center content-center q-mb-sm q-pa-md rounded-5"
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
:class="$q.platform.is.mobile ? 'column' : 'row'"
style="border: 1px solid var(--q-accent);"
>
<div
@ -34,7 +34,7 @@
v-ripple
class="rounded-5 shadow-4 q-py-xs"
:class="(mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : '') + ($q.platform.is.mobile ? ' full-width q-py-xs' : '')"
@click="ui_store.user_preferences.is_dark_mode = mode.value"
@click="ui_store.userPreferences.is_dark_mode = mode.value"
>
<q-item-section side>
<q-icon

View File

@ -8,8 +8,8 @@
const ui_store = useUiStore();
const setDisplayLanguage = (locale: MessageLanguages) => {
if (ui_store.user_preferences !== undefined) {
ui_store.user_preferences.display_language = locale;
if (ui_store.userPreferences !== undefined) {
ui_store.userPreferences.display_language = locale;
}
}
</script>
@ -18,7 +18,7 @@
<q-list
dense
class="full-width"
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
<q-item
v-for="locale in $i18n.availableLocales"

View File

@ -57,10 +57,12 @@
datasets: [{
data: shift_type_data,
backgroundColor: [
colors.getPaletteColor('accent'), // Regular
colors.getPaletteColor('green-10'), // Evening
colors.getPaletteColor('warning'), // Emergency
colors.getPaletteColor('negative'), // Overtime
colors.getPaletteColor('accent'), // Regular
colors.getPaletteColor('green-10'), // Evening
colors.getPaletteColor('warning'), // Emergency
colors.getPaletteColor('negative'), // Overtime
colors.getPaletteColor('purple-5'), // Holiday
colors.getPaletteColor('deep-orange-5') // Vacation
]
}],
}"

View File

@ -75,7 +75,7 @@
}
const onShowDetailsDialog = () => {
isDialogOpen.value = true;
isDialogOpen.value = true;
expenseStore.is_showing_create_form = false;
}
</script>
@ -158,7 +158,7 @@
/>
</div>
<div class="col-auto column">
<div class="col-auto column no-wrap">
<q-separator
spaced
size="4px"

View File

@ -40,7 +40,7 @@
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
>
<q-btn-toggle
v-model="uiStore.user_preferences.is_timesheet_approval_grid"
v-model="uiStore.userPreferences.is_timesheet_approval_grid"
push
rounded
color="white"

View File

@ -62,7 +62,7 @@
// ========== computed ========================================
const isGridMode = computed(() => q.platform.is.mobile ? true :
uiStore.user_preferences.is_timesheet_approval_grid
uiStore.userPreferences.is_timesheet_approval_grid
);
const overviewRows = computed(() =>
@ -134,7 +134,7 @@
:filter-method="filterEmployeeRows"
:rows-per-page-options="[0]"
class="bg-transparent"
:class="uiStore.user_preferences.is_timesheet_approval_grid ? '' : 'sticky-header-table sticky-first-column-table sticky-last-column-table no-shadow q-pb-sm'"
:class="uiStore.userPreferences.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"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15"
:no-data-label="$t('shared.error.no_data_found')"

View File

@ -0,0 +1,37 @@
<script
setup
lang="ts"
>
defineProps<{
isSelected: boolean;
label: string;
}>();
defineEmits<{
'clickOption': [void];
}>();
</script>
<template>
<transition
enter-active-class="animated pulse faster"
mode="out-in"
>
<div
:key="isSelected ? '1' : '0'"
class="row items-center q-pa-xs cursor-pointer rounded-25 shadow-4 relative-position non-selectable"
:class="isSelected ? 'bg-accent text-white text-bold' : 'text-weight-medium bg-dark'"
@click.stop="$emit('clickOption')"
>
<span class="col text-uppercase q-pl-md">
{{ $t(label) }}
</span>
<q-icon
:name="isSelected ? 'las la-check-circle' : 'las la-circle'"
:color="isSelected ? 'white' : ''"
size="sm"
/>
</div>
</transition>
</template>

View File

@ -1,168 +1,166 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
<script
setup
lang="ts"
>
import OverviewReportOption from 'src/modules/timesheet-approval/components/overview-report-option.vue';
const timesheet_store = useTimesheetStore();
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
const selected_company = ref<'targo' | 'solucom'>('targo');
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { companyOptions, CSVReportFilters, typeOptions } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
import type { ShiftType } from 'src/modules/timesheets/models/shift.models';
import type { CompanyNames } from 'src/modules/employee-list/models/employee-profile.models';
const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>(
Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as keyof TimesheetApprovalCSVReportFilters)
);
// ========== STATE ========================================
interface ReportOptions {
label: string;
value: keyof TimesheetApprovalCSVReportFilters;
};
const { t } = useI18n();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const reportFilters = ref<CSVReportFilters>(new CSVReportFilters);
const company_options: ReportOptions[] = [
{ label: 'Targo', value: 'targo' },
{ label: 'Solucom', value: 'solucom' },
];
// ========== COMPUTED ========================================
const type_options: ReportOptions[] = [
{ label: 'timesheet_approvals.print_report.shifts', value: 'shifts' },
{ label: 'timesheet_approvals.print_report.expenses', value: 'expenses' },
{ label: 'shared.shift_type.holiday', value: 'holiday' },
{ label: 'shared.shift_type.vacation', value: 'vacation' },
];
const isDownloadButtonEnabled = computed(() => reportFilters.value.shiftTypes.length > 0);
const is_download_button_enable = computed(() =>
company_options.map(option => option.value).some(option => selected_report_filters.value.includes(option)) &&
type_options.map(option => option.value).some(option => selected_report_filters.value.includes(option))
);
// ========== METHODS ========================================
const onClickedDownload = async () => {
try {
const data = await timesheet_store.getPayPeriodReport(report_filter_options.value);
const onClickedDownload = async () => {
const response = await timesheetApprovalApi.getTimesheetApprovalCSVReport(reportFilters.value);
console.log(response);
if (!response) {
Notify.create({
message: t('timesheet_approvals.print_report.download_failed'),
caption: t('timesheet_approvals.print_report.download_failed_caption'),
color: 'negative',
textColor: 'white',
icon: 'file_download_off',
iconSize: 'lg',
classes: 'text-uppercase text-center'
});
const companies = Object.entries(report_filter_options.value)
.filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-');
return;
}
const types = Object.entries(report_filter_options.value)
.filter(([key, value]) => value && ['shifts', 'expenses', 'holiday', 'vacation'].includes(key)).map(([key]) => key).join('-');
const file_name = `Desjardins_${companies}_${types}_${new Date().toISOString().split('T')[0]}.csv`;
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const url = window.URL.createObjectURL(response.file);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', file_name);
link.setAttribute('download', response.fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error(`An error occured during the CSV download: `, error)
timesheetStore.is_report_dialog_open = false;
}
}
watch(selected_report_filters, (new_values) => {
Object.keys(report_filter_options.value).forEach(key => {
const typed_key = key as keyof TimesheetApprovalCSVReportFilters;
report_filter_options.value[typed_key] = new_values.includes(key as keyof TimesheetApprovalCSVReportFilters);
});
});
const onClickCompanyOption = (companyName: CompanyNames) => {
if (reportFilters.value.companyName === companyName) return;
reportFilters.value.companyName = companyName;
}
watch(selected_company, (company) => {
report_filter_options.value.targo = company === 'targo';
report_filter_options.value.solucom = company === 'solucom';
});
const onClickShiftOption = (clickedOption: ShiftType) => {
const index = reportFilters.value.shiftTypes.findIndex(option => option === clickedOption);
if (index === -1)
reportFilters.value.shiftTypes.push(clickedOption);
else
reportFilters.value.shiftTypes.splice(index, 1);
}
const onClickExpenseOption = () => {
reportFilters.value.includeExpenses = !reportFilters.value.includeExpenses;
}
</script>
<template>
<q-dialog v-model="timesheet_store.is_report_dialog_open">
<q-dialog
v-model="timesheetStore.is_report_dialog_open"
@hide="reportFilters = new CSVReportFilters;"
backdrop-filter="blur(4px)"
>
<div
class="column bg-secondary shadow-24 rounded-10"
:style="$q.dark.isActive ? 'border: 2px solid var(--q-accent)' : ''"
>
<!-- main header -->
<div
class="col-auto bg-primary text-accent text-weight-bolder text-center text-uppercase text-h6 q-py-xs z-top">
{{ $t('timesheet_approvals.print_report.title') }}
</div>
<div class="col-auto column bg-primary text-center text-uppercase">
<span class="text-white text-weight-bolder q-py-sm text-h5">
{{ $t('timesheet_approvals.print_report.title') }}
</span>
<!-- info blurb -->
<div class="col-auto row flex-center full-width bg-dark shadow-2">
<q-icon
name="info_outline"
size="sm"
class="col-auto q-mr-xs"
/>
<span class="col-auto text-weight-light q-mr-sm">
<span class="col-auto q-py-xs bg-dark full-width shadow-4">
{{ $t('timesheet_approvals.print_report.description') }}
</span>
</div>
<!-- company header -->
<span class="col-auto q-px-sm q-pt-md text-weight-medium text-accent text-uppercase">
{{ $t('timesheet_approvals.print_report.company') }}
</span>
<!-- groups -->
<div class="col column full-width q-px-md">
<!-- company header -->
<span class="col-auto q-px-sm q-pt-md text-bold text-uppercase">
{{ $t('timesheet_approvals.print_report.company') }}
</span>
<!-- company options -->
<div class="col row text-uppercase full-width q-px-md">
<div
v-for="company, index in company_options"
:key="index"
class="q-pa-xs col-6"
>
<q-radio
v-model="selected_company"
left-label
color="white"
dense
:label="company.label"
:val="company.value"
checked-icon="radio_button_checked"
unchecked-icon="radio_button_unchecked"
class="q-px-md q-py-xs shadow-4 rounded-25 full-width"
:class="selected_company.includes(company.value) ? 'bg-accent text-white text-bold' : 'bg-dark'"
/>
<!-- company options -->
<div class="row q-pb-sm">
<div
v-for="company, companyIndex in companyOptions"
:key="companyIndex"
class="col-6 q-pa-sm"
>
<OverviewReportOption
:is-selected="reportFilters.companyName === company.value"
:label="company.label"
@click-option="onClickCompanyOption(company.value)"
/>
</div>
</div>
</div>
<!-- shift type header -->
<span class="col-auto q-px-sm q-pt-md text-weight-medium text-uppercase text-accent">
{{ $t('timesheet_approvals.print_report.options') }}
</span>
<!-- data type header -->
<span class="col-auto q-px-sm q-pt-md text-bold text-uppercase">
{{ $t('timesheet_approvals.print_report.type') }}
</span>
<!-- shift type options -->
<div class="col row text-uppercase full-width q-px-md q-pb-md">
<div
v-for="type, index in type_options"
:key="index"
class="q-pa-xs col-6"
>
<q-checkbox
v-model="selected_report_filters"
left-label
color="white"
dense
:val="type.value"
checked-icon="check_box"
unchecked-icon="check_box_outline_blank"
:label="$t(type.label)"
class="q-px-md q-py-xs shadow-4 rounded-25 full-width"
:class="selected_report_filters.includes(type.value) ? 'bg-accent text-white text-bold' : 'bg-white text-primary'"
/>
<!-- data type options -->
<div class="row">
<!-- shift types -->
<div
v-for="shiftType, typeIndex in typeOptions"
:key="typeIndex"
class="col-6 q-pa-sm"
>
<OverviewReportOption
:is-selected="reportFilters.shiftTypes.includes(shiftType.value)"
:label="shiftType.label"
@click-option="onClickShiftOption(shiftType.value)"
/>
</div>
<!-- expenses -->
<div class="col-6 q-pa-sm">
<OverviewReportOption
:is-selected="reportFilters.includeExpenses"
:label="'timesheet_approvals.print_report.expenses'"
@click-option="onClickExpenseOption"
/>
</div>
</div>
</div>
<!-- download button -->
<q-btn
:disable="!is_download_button_enable"
square
icon="download"
:color="is_download_button_enable ? 'accent' : 'grey-5'"
:label="$t('shared.label.download')"
class="col-auto q-py-sm shadow-up-2"
@click="onClickedDownload()"
/>
<div class="col-auto row justify-end q-px-md q-pb-md q-pt-xl">
<q-btn
push
:disable="!isDownloadButtonEnabled"
icon="download"
:color="isDownloadButtonEnabled ? 'accent' : 'grey-5'"
:label="$t('shared.label.download')"
class="col-auto q-py-sm q-px-xl"
@click="onClickedDownload"
/>
</div>
</div>
</q-dialog>
</template>

View File

@ -1,58 +1,72 @@
import { useTimesheetStore } from "src/stores/timesheet-store";
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
import type { CSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
import { fromEnToFrShiftType } from "src/utils/translator";
export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore();
const timesheetStore = useTimesheetStore();
const DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
const getTimesheetOverviews = async (date?: string) => {
timesheet_store.is_loading = true;
timesheetStore.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheet_store.getTimesheetOverviews();
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheetStore.getTimesheetOverviews();
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
}
const getTimesheetOverviewsByDate = async (date: string) => {
const valid_date = DATE_REGEX.test(date);
timesheet_store.is_loading = true;
timesheetStore.is_loading = true;
if (valid_date) {
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheet_store.getTimesheetOverviews();
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheetStore.getTimesheetOverviews();
}
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
};
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean) => {
timesheet_store.is_loading = true;
timesheetStore.is_loading = true;
const success = await timesheet_store.getTimesheetsByOptionalEmployeeEmail(email);
const success = await timesheetStore.getTimesheetsByOptionalEmployeeEmail(email);
if (success) {
const approval_success = await timesheet_store.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
const overview = timesheet_store.pay_period_overviews.find(overview => overview.email === email);
const approval_success = await timesheetStore.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
const overview = timesheetStore.pay_period_overviews.find(overview => overview.email === email);
if (overview && approval_success) {
overview.is_approved = approval_status;
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(email);
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(email);
}
}
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
};
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[]) => {
if (timesheet_store.pay_period === undefined) return;
const getTimesheetApprovalCSVReport = async (selectedFilters: CSVReportFilters): Promise<{file: Blob, fileName: string} | undefined> => {
if (timesheetStore.pay_period === undefined) return;
const [targo, solucom] = report_filter_company;
const [shifts, expenses, holiday, vacation] = report_filter_type;
const options = {
shifts, expenses, holiday, vacation, targo, solucom
} as TimesheetApprovalCSVReportFilters;
const success = await timesheetStore.getPayPeriodReport(selectedFilters);
if (!success) return;
await timesheet_store.getPayPeriodReport(options);
// const year = timesheetStore.pay_period?.pay_year;
// const number = timesheetStore.pay_period?.pay_period_no;
const startDate = timesheetStore.pay_period?.period_start;
const endDate = timesheetStore.pay_period?.period_end;
const content: string[] = [];
selectedFilters.shiftTypes.forEach(type => content.push(fromEnToFrShiftType(type)));
content.push(selectedFilters.companyName);
if (selectedFilters.includeExpenses) content.push('Dépenses');
// const fileName = `${year}-${number}_${content.join('_')}.csv`;
const fileName = `${startDate}_AU_${endDate}_${content.join('_')}.csv`;
if (!timesheetStore.payPeriodReport) return;
return {file: timesheetStore.payPeriodReport, fileName};
};
return {

View File

@ -1,18 +1,28 @@
export class TimesheetApprovalCSVReportFilters {
shifts: boolean;
expenses: boolean;
holiday: boolean;
vacation: boolean;
targo: boolean;
solucom: boolean;
import type { ShiftType } from "src/modules/timesheets/models/shift.models";
import type { CompanyNames } from "src/modules/employee-list/models/employee-profile.models";
export class CSVReportFilters {
companyName: CompanyNames;
includeExpenses: boolean;
shiftTypes: ShiftType[];
constructor() {
this.shifts = true;
this.expenses = true;
this.holiday = true;
this.vacation = true;
this.targo = true;
this.solucom = false;
};
}
this.companyName = 'Targo';
this.includeExpenses = false;
this.shiftTypes = ['REGULAR', 'EVENING', 'EMERGENCY', 'SICK']
}
};
export const companyOptions: {label: string, value: CompanyNames}[] = [
{ label: 'timesheet_approvals.print_report.targo', value: 'Targo' },
{ label: 'timesheet_approvals.print_report.solucom', value: 'Solucom' },
];
export const typeOptions: {label: string, value: ShiftType}[] = [
{ label: 'shared.shift_type.regular', value: 'REGULAR' },
{ label: 'shared.shift_type.evening', value: 'EVENING' },
{ label: 'shared.shift_type.emergency', value: 'EMERGENCY' },
{ label: 'shared.shift_type.sick', value: 'SICK' },
{ label: 'shared.shift_type.holiday', value: 'HOLIDAY' },
{ label: 'shared.shift_type.vacation', value: 'VACATION' },
];

View File

@ -1,6 +1,6 @@
import { api } from "src/boot/axios";
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
import type { CSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
export const timesheetApprovalService = {
@ -9,13 +9,13 @@ export const timesheetApprovalService = {
return response.data.data;
},
getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, filters?: TimesheetApprovalCSVReportFilters) => {
const response = await api.get(`exports/csv/${year}/${period_number}`, { params: filters, responseType: 'arraybuffer' });
return response;
getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, filters?: CSVReportFilters): Promise<Blob> => {
const response = await api.post(`exports/csv/${year}/${period_number}`, filters, { responseType: 'blob' });
return response.data;
},
updateTimesheetsApprovalStatus: async (email: string, timesheet_ids: number[], is_approved: boolean): Promise<BackendResponse<{shifts: number, expenses: number}>> => {
const response = await api.patch<BackendResponse<{shifts: number, expenses: number}>>('pay-periods/pay-period-approval', { email, timesheet_ids, is_approved});
updateTimesheetsApprovalStatus: async (email: string, timesheet_ids: number[], is_approved: boolean): Promise<BackendResponse<{ shifts: number, expenses: number }>> => {
const response = await api.patch<BackendResponse<{ shifts: number, expenses: number }>>('pay-periods/pay-period-approval', { email, timesheet_ids, is_approved });
return response.data;
},

View File

@ -6,7 +6,6 @@
import { useI18n } from 'vue-i18n';
import { computed, onMounted, ref } from 'vue';
import { useUiStore } from 'src/stores/ui-store';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
@ -28,7 +27,6 @@
}>();
const { t } = useI18n();
const ui_store = useUiStore();
const timesheetStore = useTimesheetStore();
const expenseStore = useExpensesStore();
const expensesApi = useExpensesApi();
@ -168,7 +166,6 @@
<template #selected-item="scope">
<div
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
:tabindex="scope.tabindex"
>
<q-icon

View File

@ -4,7 +4,6 @@
>
import { computed, ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useUiStore } from 'src/stores/ui-store';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
@ -26,7 +25,6 @@
}>();
const { t } = useI18n();
const ui_store = useUiStore();
const timesheet_store = useTimesheetStore();
const expenses_store = useExpensesStore();
const auth_store = useAuthStore();
@ -168,8 +166,7 @@
<template #selected-item="scope">
<div
class="row items-center text-weight-bold q-ma-none q-pa-none full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
class="row items-center text-weight-bold q-ma-none q-pa-none fit"
:tabindex="scope.tabindex"
>
<q-icon

View File

@ -37,11 +37,11 @@
'onTimeFieldBlur': [void];
}>();
const ui_store = useUiStore();
const uiStore = useUiStore();
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
const select_ref = ref<QSelect | null>(null);
const is_showing_comment_popup = ref(false);
const error_message = ref('');
const selectRef = ref<QSelect | null>(null);
const isShowingCommentPopup = ref(false);
const errorMessageRow = ref('');
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
const predefinedHoursString = ref('');
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
@ -63,10 +63,10 @@
const onTimeFieldBlur = (time_string: string) => {
if (time_string.length < 1 || !time_string) {
shift.value.has_error = true;
error_message.value = 'timesheet.errors.SHIFT_TIME_REQUIRED';
errorMessageRow.value = 'timesheet.errors.SHIFT_TIME_REQUIRED';
} else {
shift.value.has_error = false;
error_message.value = '';
errorMessageRow.value = '';
emit('onTimeFieldBlur');
}
}
@ -102,15 +102,15 @@
}
onMounted(() => {
if (ui_store.focus_next_component) {
select_ref.value?.focus();
select_ref.value?.showPopup();
if (uiStore.focusNextComponent) {
selectRef.value?.focus();
selectRef.value?.showPopup();
shiftTypeSelected.value = undefined;
ui_store.focus_next_component = false;
uiStore.focusNextComponent = false;
}
if (errorMessage)
error_message.value = errorMessage;
errorMessageRow.value = errorMessage;
});
</script>
@ -120,13 +120,13 @@
<div class="col row items-center text-uppercase q-px-xs rounded-5">
<!-- comment button -->
<q-btn
v-if="ui_store.is_mobile_mode && !dense"
v-if="!dense"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
@click="is_showing_comment_popup = true"
@click="isShowingCommentPopup = true"
>
<q-dialog v-model="is_showing_comment_popup">
<q-dialog v-model="isShowingCommentPopup">
<q-input
color="white"
v-model="shift.comment"
@ -190,8 +190,7 @@
>
<template #selected-item="scope">
<div
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis fit"
:tabindex="scope.tabindex"
>
<q-icon
@ -305,7 +304,7 @@
no-error-icon
hide-bottom-space
:error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:error-message="errorMessage || errorMessageRow !== '' ? $t(errorMessage ?? errorMessageRow) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'"
class="rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
@ -338,7 +337,7 @@
no-error-icon
hide-bottom-space
:error="shift.has_error"
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
:error-message="errorMessage || errorMessageRow !== '' ? $t(errorMessage ?? errorMessageRow) : ''"
:label-color="!shift.is_approved ? 'accent' : 'white'"
: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;"

View File

@ -0,0 +1,287 @@
<script
setup
lang="ts"
>
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
import { date, useQuasar } from 'quasar';
import { ref, computed, watch, onMounted, inject } from 'vue';
import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { Shift } from 'src/modules/timesheets/models/shift.models';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
import { useI18n } from 'vue-i18n';
// ========== constants ========================================
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
// ========== state ========================================
const emit = defineEmits<{
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
}>();
const q = useQuasar();
const { extractDate } = date;
const { locale } = useI18n();
const uiStore = useUiStore();
const timesheetApi = useTimesheetApi();
const timesheetStore = useTimesheetStore();
const mobileAnimationDirection = ref('fadeInLeft');
const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent);
const employeeEmail = inject<string>('employeeEmail');
// ========== computed ========================================
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
// ========== methods ========================================
// const timesheetRows = computed(() => timesheetStore.timesheets);
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
uiStore.focusNextComponent = true;
const newShift = new Shift;
newShift.date = date;
newShift.timesheet_id = timesheet_id;
day_shifts.push(newShift);
};
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
if (timesheetStore.timesheets !== undefined) {
const day = timesheetStore.timesheets[timesheet_index]!.days[day_index]!;
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
day.shifts = shifts_without_deleted_shift;
}
};
const getDayApproval = (day: TimesheetDay) => {
if (day.shifts.length < 1) return false;
return day.shifts.every(shift => shift.is_approved === true);
};
const getMobileDayRef = (iso_date_string: string): string => {
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
};
const getHolidayName = (date: string) => {
const holiday = timesheetStore.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;
};
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
}
onMounted(async () => {
await timesheetStore.getCurrentFederalHolidays();
});
watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && q.platform.is.mobile) {
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
}
});
</script>
<template>
<div
class="fit"
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
>
<div
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
:key="timesheet.timesheet_id"
class="no-wrap"
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
>
<transition
appear
enter-active-class="animated fadeInDown"
leave-active-class="animated fadeOutUp"
>
<q-btn
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheetStore.has_timesheet_preset"
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
flat
dense
:label="$t('timesheet.apply_preset_week')"
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
@click="onClickApplyWeeklyPreset(timesheet.timesheet_id)"
>
<q-icon
name="las la-calendar-week"
color="accent"
size="md"
/>
</q-btn>
</transition>
<transition-group
appear
:enter-active-class="`animated ${animationStyle}`"
>
<div
v-for="day, day_index in timesheet.days"
:key="day.date"
:ref="getMobileDayRef(day.date)"
class="col-auto row q-pa-sm full-width relative-position"
:style="`animation-delay: ${day_index / 15}s;`"
>
<!-- optional label indicating which holiday if today is a holiday -->
<span
v-if="timesheetStore.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 -->
<div
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
class="col-auto full-width q-px-md q-py-sm"
>
<q-card
class="shadow-12"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent rounded-10' : 'bg-dark mobile-rounded-10'"
>
<q-card-section
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
style="line-height: 1em;"
>
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
weekday: 'long', day: 'numeric', month:
'long'
}) }}</span>
<q-icon
v-if="(getDayApproval(day) || timesheet.is_approved)"
name="verified"
size="3em"
color="white"
class="absolute-top-left z-top"
style="top: -0.2em; left: 0px;"
/>
</q-card-section>
<q-card-section
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
class="q-pa-none transparent"
>
<ShiftListDay
outlined
:timesheet-id="timesheet.timesheet_id"
:week-day-index="day_index"
:animation-delay-multiplier="day_index"
:approved="(getDayApproval(day) || timesheet.is_approved)"
:day="day"
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
/>
</q-card-section>
<q-card-section class="q-pa-none">
<q-btn
v-if="!(getDayApproval(day) || timesheet.is_approved)"
square
dense
size="xl"
color="accent"
icon="more_time"
class="full-width"
style="border-radius: 0 0 10px 10px;"
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
/>
</q-card-section>
</q-card>
</div>
<!-- desktop version -->
<div
v-else
class="col row full-width rounded-10 ellipsis shadow-10"
:style="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'border: 2px solid #ab47bc' : ''"
>
<div
class="col row"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
>
<!-- Date block -->
<ShiftListDateWidget
:display-date="day.date"
:approved="(getDayApproval(day) || timesheet.is_approved)"
class="col-auto"
/>
<ShiftListDay
:timesheet-id="timesheet.timesheet_id"
:week-day-index="day_index"
:day="day"
:holiday="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
:approved="getDayApproval(day) || timesheet.is_approved"
class="col"
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
/>
</div>
<div class="col-auto self-stretch">
<q-icon
v-if="(getDayApproval(day) || timesheet.is_approved)"
name="verified"
color="white"
size="xl"
class="full-height"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
/>
<q-btn
v-else
:dense="!$q.platform.is.mobile"
square
icon="more_time"
size="lg"
:color="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
text-color="white"
class="full-height"
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
/>
</div>
</div>
</div>
</transition-group>
</div>
</div>
</template>
<style
scoped
lang="scss"
>
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
.mobile-rounded-#{$size} {
border-radius: #{$size}px !important;
}
.mobile-rounded-#{$size}>div:first-child {
border-radius: #{$size}px #{$size}px 0 0 !important;
}
.mobile-rounded-#{$size}>div:last-child {
border-radius: 0 0 #{$size}px #{$size}px !important;
}
}
</style>

View File

@ -32,7 +32,7 @@
:class="approved ? 'text-white' : ''"
:style="'font-size: ' + weekday_font_size"
>
{{ $d(display_date, { weekday: $q.screen.lt.md ? 'short' : 'long'}) }}
{{ $d(display_date, { weekday: $q.platform.is.mobile ? 'short' : 'long'}) }}
</span>
<span
class="col-auto text-weight-bolder"
@ -46,7 +46,7 @@
:class="approved ? 'text-white' : ''"
:style="'font-size: ' + weekday_font_size"
>
{{ $d(display_date, { month: $q.screen.lt.md ? 'short' : 'long' }) }}
{{ $d(display_date, { month: $q.platform.is.mobile ? 'short' : 'long' }) }}
</span>
</div>
</template>

View File

@ -41,7 +41,7 @@
const q = useQuasar();
const { t } = useI18n();
const ui_store = useUiStore();
const uiStore = useUiStore();
const authStore = useAuthStore();
const mode = inject<'normal' | 'approval'>('mode');
@ -88,7 +88,7 @@
dense: true,
borderless: shift.value.is_approved && isTimesheetApproved,
readonly: shift.value.is_approved && isTimesheetApproved,
optionsDense: !ui_store.is_mobile_mode,
optionsDense: !q.platform.is.mobile,
hideDropdownIcon: true,
menuOffset: [0, 10],
menuAnchor: "bottom middle",
@ -169,11 +169,11 @@
}
onMounted(() => {
if (ui_store.focus_next_component) {
if (uiStore.focusNextComponent) {
selectRef.value?.focus();
selectRef.value?.showPopup();
shiftTypeSelected.value = undefined;
ui_store.focus_next_component = false;
uiStore.focusNextComponent = false;
}
});
</script>
@ -238,7 +238,7 @@
<div
class="row items-center text-uppercase rounded-5"
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
:class="$q.platform.is.mobile ? 'col q-mb-xs q-px-xs' : 'col-4'"
>
<!-- shift type -->
<q-select
@ -251,7 +251,7 @@
<template #selected-item="scope">
<div
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
:class="$q.platform.is.mobile ? 'full-height' : ''"
:tabindex="scope.tabindex"
>
<q-icon
@ -386,18 +386,18 @@
<div
class="row full-height"
:class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'"
:class="$q.platform.is.mobile ? 'col-12' : 'col-auto flex-center'"
>
<!-- comment button -->
<q-btn
v-if="!ui_store.is_mobile_mode"
v-if="!$q.platform.is.mobile"
push
dense
:color="shift.is_approved ? 'white' : (shift.comment ? 'accent' : (holiday ? 'purple-5' : 'blue-grey-5'))"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.is_approved ? (holiday ? 'purple-5' : 'accent') : 'white'"
class="col"
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
:class="$q.platform.is.mobile ? 'q-mt-xs bg-dark' : ''"
>
<q-badge
v-if="shift.comment"

View File

@ -27,32 +27,32 @@
const q = useQuasar();
const { extractDate } = date;
const { locale } = useI18n();
const ui_store = useUiStore();
const timesheet_api = useTimesheetApi();
const timesheet_store = useTimesheetStore();
const uiStore = useUiStore();
const timesheetApi = useTimesheetApi();
const timesheetStore = useTimesheetStore();
const mobile_animation_direction = ref('fadeInLeft');
const mobileAnimationDirection = ref('fadeInLeft');
const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent);
const employeeEmail = inject<string>('employeeEmail');
// ========== computed ========================================
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
const animationStyle = computed(() => q.platform.is.mobile ? mobileAnimationDirection.value : 'fadeInDown');
// ========== methods ========================================
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true;
const new_shift = new Shift;
new_shift.date = date;
new_shift.timesheet_id = timesheet_id;
day_shifts.push(new_shift);
uiStore.focusNextComponent = true;
const newShift = new Shift;
newShift.date = date;
newShift.timesheet_id = timesheet_id;
day_shifts.push(newShift);
};
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
if (timesheet_store.timesheets !== undefined) {
const day = timesheet_store.timesheets[timesheet_index]!.days[day_index]!;
if (timesheetStore.timesheets !== undefined) {
const day = timesheetStore.timesheets[timesheet_index]!.days[day_index]!;
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
day.shifts = shifts_without_deleted_shift;
}
@ -68,7 +68,7 @@
};
const getHolidayName = (date: string) => {
const holiday = timesheet_store.federal_holidays.find(holiday => holiday.date === date);
const holiday = timesheetStore.federal_holidays.find(holiday => holiday.date === date);
if (!holiday) return;
if (locale.value === 'fr-FR')
@ -79,11 +79,11 @@
};
const onClickApplyWeeklyPreset = async (timesheet_id: number) => {
await timesheet_api.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
await timesheetApi.applyPreset(timesheet_id, undefined, undefined, employeeEmail);
}
onMounted(async () => {
await timesheet_store.getCurrentFederalHolidays();
await timesheetStore.getCurrentFederalHolidays();
});
watch(currentDayComponentWatcher, () => {
@ -99,9 +99,10 @@
:class="$q.platform.is.mobile ? 'column no-wrap q-pb-lg' : 'row'"
>
<div
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
v-for="timesheet, timesheet_index of timesheetStore.timesheets"
:key="timesheet.timesheet_id"
:class="$q.platform.is.mobile ? 'col-auto column no-wrap' : 'col column fit items-center'"
class="no-wrap"
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
>
<transition
appear
@ -109,7 +110,7 @@
leave-active-class="animated fadeOutUp"
>
<q-btn
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheet_store.has_timesheet_preset"
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheetStore.has_timesheet_preset"
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
flat
dense
@ -127,7 +128,7 @@
<transition-group
appear
:enter-active-class="`animated ${animation_style}`"
:enter-active-class="`animated ${animationStyle}`"
>
<div
v-for="day, day_index in timesheet.days"
@ -138,7 +139,7 @@
>
<!-- optional label indicating which holiday if today is a holiday -->
<span
v-if="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date)"
v-if="timesheetStore.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);"
>
@ -209,11 +210,11 @@
<div
v-else
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' : ''"
:style="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'border: 2px solid #ab47bc' : ''"
>
<div
class="col row"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : 'bg-dark'"
>
<!-- Date block -->
<ShiftListDateWidget
@ -226,7 +227,7 @@
:timesheet-id="timesheet.timesheet_id"
:week-day-index="day_index"
:day="day"
:holiday="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date)"
:holiday="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date)"
:approved="getDayApproval(day) || timesheet.is_approved"
class="col"
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
@ -241,7 +242,7 @@
color="white"
size="xl"
class="full-height"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
:class="(getDayApproval(day) || timesheet.is_approved) ? (timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'bg-purple-5' : 'bg-accent') : ''"
/>
<q-btn
@ -250,7 +251,7 @@
square
icon="more_time"
size="lg"
:color="timesheet_store.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
:color="timesheetStore.federal_holidays.some(holiday => holiday.date === day.date) ? 'purple-5' : 'accent'"
text-color="white"
class="full-height"
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"

View File

@ -40,21 +40,14 @@ import { RouteNames } from 'src/router/router-constants';
const hasShiftErrors = computed(() => timesheetStore.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
const isTimesheetsApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
// const timesheetStore.canSaveTimesheets = computed(() => {
// /* eslint-disable-next-line */
// const currentShifts = timesheetStore.timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
// const initialShifts = timesheetStore.initial_timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
// return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
// });
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum += timesheet.weekly_hours.regular
+ timesheet.weekly_hours.evening
+ timesheet.weekly_hours.emergency
+ timesheet.weekly_hours.overtime,
0) //initial value
);
0 //initial value
));
const totalExpenses = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_expenses.expenses

View File

@ -44,3 +44,8 @@ export interface TotalExpenses {
on_call: number;
mileage: number;
}
export interface TimesheetDayDisplay extends TimesheetDay {
timesheet_id: number;
i18WeekdayKey: string;
}

View File

@ -10,7 +10,7 @@ import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
import type { PaidTimeOff } from 'src/modules/employee-list/models/employee-profile.models';
import type { PayPeriodEvent } from 'src/modules/timesheet-approval/models/pay-period-event.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
import type { CSVReportFilters } 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';
import type { RouteNames } from 'src/router/router-constants';
import type { RouteRecordNameGeneric } from 'vue-router';
@ -42,7 +42,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const selected_employee_name = ref<string>();
const has_timesheet_preset = ref(false);
const current_pay_period_overview = ref<TimesheetApprovalOverview>();
const pay_period_report = ref();
const payPeriodReport = ref<Blob>();
const pay_period_observer = ref<EventSource | undefined>();
const federal_holidays = ref<FederalHoliday[]>([]);
@ -191,12 +191,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return false;
}
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
const getPayPeriodReport = async (filters: CSVReportFilters): Promise<boolean> => {
try {
if (!pay_period.value) return false;
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no, report_filters);
pay_period_report.value = response;
return response.data;
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no, filters);
if (response){
payPeriodReport.value = response;
return true;
}
} catch (error) {
console.error('There was an error retrieving the report CSV: ', error);
// TODO: More in-depth error-handling here
@ -256,6 +260,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return {
is_loading,
is_report_dialog_open,
payPeriodReport,
is_details_dialog_open,
pay_period,
pay_period_overviews,

View File

@ -1,33 +1,27 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { LocalStorage, useQuasar, Dark } from 'quasar';
import { LocalStorage, Dark } from 'quasar';
import { Preferences } from 'src/modules/profile/models/preferences.models';
import { ProfileService } from 'src/modules/profile/services/profile-service';
import { RouteNames } from 'src/router/router-constants';
export const useUiStore = defineStore('ui', () => {
const q = useQuasar();
const { locale } = useI18n();
const is_left_drawer_open = ref(true);
const focus_next_component = ref(false);
const is_mobile_mode = computed(() => q.screen.lt.md);
const user_preferences = ref<Preferences>(new Preferences);
const current_page = ref<RouteNames>(RouteNames.DASHBOARD);
const isLeftDrawerOpen = ref(true);
const focusNextComponent = ref(false);
const userPreferences = ref<Preferences>(new Preferences);
const toggleRightDrawer = () => {
is_left_drawer_open.value = !is_left_drawer_open.value;
isLeftDrawerOpen.value = !isLeftDrawerOpen.value;
};
const getUserPreferences = async () => {
try {
const local_user_preferences = LocalStorage.getItem<Preferences>('user_preferences');
const local_userPreferences = LocalStorage.getItem<Preferences>('userPreferences');
if (local_user_preferences !== null) {
if (local_user_preferences.id !== -1) {
Object.assign(user_preferences.value, local_user_preferences);
if (local_userPreferences !== null) {
if (local_userPreferences.id !== -1) {
Object.assign(userPreferences.value, local_userPreferences);
setPreferences();
return;
}
@ -36,24 +30,24 @@ export const useUiStore = defineStore('ui', () => {
const response = await ProfileService.getUserPreferences();
if (response.success && response.data) {
LocalStorage.setItem('user_preferences', response.data);
Object.assign(user_preferences.value, response.data);
LocalStorage.setItem('userPreferences', response.data);
Object.assign(userPreferences.value, response.data);
setPreferences();
}
} catch (error) {
user_preferences.value = new Preferences;
userPreferences.value = new Preferences;
console.error('Could not retrieve user preferences: ', error);
}
};
const updateUserPreferences = async () => {
try {
if (user_preferences.value.id === -1) return;
if (userPreferences.value.id === -1) return;
const response = await ProfileService.updateUserPreferences(user_preferences.value);
const response = await ProfileService.updateUserPreferences(userPreferences.value);
if (response.success && response.data) {
Object.assign(user_preferences.value, response.data);
LocalStorage.setItem('user_preferences', response.data);
Object.assign(userPreferences.value, response.data);
LocalStorage.setItem('userPreferences', response.data);
setPreferences();
return;
}
@ -63,19 +57,17 @@ export const useUiStore = defineStore('ui', () => {
};
const setPreferences = () => {
if (user_preferences.value !== undefined) {
// if user_preferences.value.is_dark_mode === null
Dark.set(user_preferences.value.is_dark_mode ?? "auto");
locale.value = user_preferences.value.display_language;
if (userPreferences.value !== undefined) {
// if userPreferences.value.is_dark_mode === null
Dark.set(userPreferences.value.is_dark_mode ?? "auto");
locale.value = userPreferences.value.display_language;
}
}
return {
current_page,
is_mobile_mode,
focus_next_component,
is_left_drawer_open,
user_preferences,
focusNextComponent,
isLeftDrawerOpen,
userPreferences,
toggleRightDrawer,
getUserPreferences,
updateUserPreferences,

13
src/utils/translator.ts Normal file
View File

@ -0,0 +1,13 @@
import type { ShiftType } from "src/modules/timesheets/models/shift.models";
export const fromEnToFrShiftType = (shiftType: ShiftType): string => {
switch(shiftType) {
case "REGULAR": return 'Régulier';
case "EVENING": return 'Soir';
case "EMERGENCY": return 'Urgence';
case "HOLIDAY": return 'Férié';
case "VACATION": return 'Vacances';
case "SICK": return 'Maladie';
case "WITHDRAW_BANKED": return 'Heures-en-Banque';
}
}