Merge pull request 'release/nicolas/v1.2' (#94) from release/nicolas/v1.2 into main

Reviewed-on: Targo/targo_frontend#94
This commit is contained in:
Nicolas 2026-03-18 15:39:37 -04:00
commit 80c88ae749
51 changed files with 961 additions and 430 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

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

@ -14,7 +14,6 @@
const is_game_time = computed(() => email.value.includes('allumette'));
const onSubmitConnectionRequest = () => {
console.log('submit requested');
if (is_employee_email.value) return;
}

View File

@ -18,7 +18,6 @@
const handleSend = async () => {
const message = text.value.trim();
text.value = '';
console.log('message: ', message, ', length: ', message.length);
await chatbot_api.sendMessage(message);
};

View File

@ -2,10 +2,10 @@
setup
lang="ts"
>
const { imageSource = "", title = "", description = "", route = "" } = defineProps<{
imageSource?: string,
title?: string,
description?: string,
const { route = "" } = defineProps<{
iconImageSource: string,
bgImageSource: string,
name: string,
route?: string,
}>();
@ -15,28 +15,50 @@
</script>
<template>
<q-card
class="shortcut-card cursor-pointer shadow-12"
<div
class="full-width cursor-pointer bg-dark shadow-2 rounded-15 q-pa-xs position-relative"
style="border: solid 1px var(--q-accent);"
@click="onClickExternalShortcut"
>
<q-img
:src="imageSource"
fit="contain"
<span
v-if="$q.platform.is.mobile"
class="col text-uppercase text-bold text-accent absolute"
style="transform: translate(20px, -20px);"
>
<div class="absolute-bottom text-uppercase text-weight-bolder text-center">{{ title }}</div>
</q-img>
{{ name }}
</span>
<q-card-section v-if="description">
<span>{{ description }}</span>
</q-card-section>
</q-card>
<div
class="row items-center q-px-lg q-py-sm link-card rounded-10 inset-shadow"
:style="`background-image: url(${bgImageSource}); background-size: ${$q.platform.is.mobile ? 'cover' : 'contain'};`"
>
<q-icon
round
color="dark"
size="md"
:name="`img:${iconImageSource}`"
class="col-auto q-pr-md"
/>
<span
v-if="!$q.platform.is.mobile"
class="col text-uppercase text-bold"
>
{{ name }}
</span>
</div>
</div>
</template>
<style
lang="sass"
scoped
lang="css"
>
.shortcut-card
width: 100%
max-width: 250px
.link-card {
background-blend-mode: multiply;
background-position: bottom right;
background-repeat: no-repeat;
background-color: var(--q-dark);
background-size: contain;
}
</style>

View File

@ -2,10 +2,18 @@
setup
lang="ts"
>
import { RouteNames } from 'src/router/router-constants';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants';
const slide = ref<string>('welcome');
const autoplayTimer = ref(9001);
const onCarouselMouseEvent = (state: 'enter' | 'exit') => {
if (state === 'enter')
autoplayTimer.value = 0
else
autoplayTimer.value = 9001
}
</script>
<template>
@ -17,10 +25,12 @@ import { ref } from 'vue';
animated
infinite
arrows
:autoplay="9001"
:autoplay="autoplayTimer"
control-color="accent"
control-type="outline"
class="bg-dark full-width rounded-15 shadow-18"
class="bg-dark fit rounded-15 shadow-18"
@mouseenter="onCarouselMouseEvent('enter')"
@mouseleave="onCarouselMouseEvent('exit')"
>
<!-- welcome slide -->
<q-carousel-slide
@ -39,7 +49,7 @@ import { ref } from 'vue';
</div>
</q-img>
<div class="col column flex-center q-px-md">
<div class="col column flex-center q-px-md text-h5 text-weight-light">
<span class="col-auto text-center">{{ $t('dashboard.carousel.welcome_message') }}</span>
</div>
</div>
@ -63,7 +73,7 @@ import { ref } from 'vue';
</div>
</q-img>
<div class="col column flex-center q-px-md">
<div class="col column flex-center q-px-md text-h5 text-weight-light">
<span class="col-auto text-center">{{ $t('dashboard.carousel.help_message') }}</span>
</div>
</div>

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

@ -12,6 +12,7 @@
noTopPadding?: boolean;
backgroundColor?: 'bg-secondary' | 'bg-dark';
appendContent?: string | number;
autoFocus?: boolean;
}>();
defineOptions({
@ -28,6 +29,7 @@
v-model="model"
v-bind="$attrs"
dense
:autofocus="autoFocus"
borderless
color="accent"
label-color="white"

View File

@ -61,6 +61,8 @@
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

@ -0,0 +1,130 @@
<script
setup
lang="ts"
>
import TargoInput from 'src/modules/shared/components/targo-input.vue';
import { computed, ref } from 'vue';
import type { Shift } from 'src/modules/timesheets/models/shift.models';
const shift = defineModel<Shift>({ required: true });
defineEmits<{
'clickToggleApproval': [void];
'clickDelete': [void];
}>();
const isCommentDialogOpen = ref(false);
const approvalOptionState = computed<{ icon: string, label: string }>(() => shift.value.is_approved ?
{ icon: 'las la-unlock', label: 'shared.label.unlock' } :
{ icon: 'las la-lock', label: 'shared.label.lock' }
)
const hasComment = computed(() => shift.value.comment && shift.value.comment.length > 0)
const onClickViewComments = () => {
isCommentDialogOpen.value = true;
}
</script>
<template>
<div class="row full-height flex-center">
<q-dialog
v-model="isCommentDialogOpen"
full-width
backdrop-filter="blur(4px)"
>
<div class="row flex-center full-width">
<div class="col-xs-12 col-sm-10 col-md-8 col-lg-6">
<TargoInput
v-model="shift.comment"
auto-focus
:label="$t('timesheet.expense.employee_comment')"
/>
</div>
</div>
</q-dialog>
<q-btn
flat
dense
icon="more_vert"
color="accent"
class="col-auto q-px-md"
>
<q-badge
v-if="hasComment"
rounded
floating
color="negative"
style="transform:translate(-10px, 0px)"
/>
<q-menu
auto-close
transition-show="jump-down"
transition-hide="jump-up"
transition-duration="200"
>
<q-list dense>
<q-item
clickable
@click="$emit('clickToggleApproval')"
>
<q-item-section avatar>
<q-avatar :icon="approvalOptionState.icon" />
</q-item-section>
<q-item-section>
{{ $t(approvalOptionState.label) }}
</q-item-section>
</q-item>
<q-item
clickable
@click="onClickViewComments"
>
<q-item-section avatar>
<q-avatar icon="las la-power-off" />
</q-item-section>
<q-item-section>
<div class="row items-center">
<span class="col">{{ $t('timesheet.expense.employee_comment') }}</span>
<div class="col-auto q-pl-sm">
<q-badge
v-if="hasComment"
rounded
color="negative"
/>
</div>
</div>
</q-item-section>
</q-item>
<q-separator />
<q-item
clickable
@click="$emit('clickDelete')"
>
<q-item-section avatar>
<q-avatar
icon="las la-trash"
text-color="negative"
/>
</q-item-section>
<q-item-section>
{{ $t('shared.label.remove') }}
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</template>

View File

@ -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,165 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
<script
setup
lang="ts"
>
import OverviewReportOption from 'src/modules/timesheet-approval/components/overview-report-option.vue';
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
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 timesheet_store = useTimesheetStore();
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
const selected_company = ref<'targo' | 'solucom'>('targo');
// ========== STATE ========================================
const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>(
Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as keyof TimesheetApprovalCSVReportFilters)
);
const { t } = useI18n();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const reportFilters = ref<CSVReportFilters>(new CSVReportFilters);
interface ReportOptions {
label: string;
value: keyof TimesheetApprovalCSVReportFilters;
};
// ========== COMPUTED ========================================
const company_options: ReportOptions[] = [
{ label: 'Targo', value: 'targo' },
{ label: 'Solucom', value: 'solucom' },
];
const isDownloadButtonEnabled = computed(() => reportFilters.value.shiftTypes.length > 0);
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 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 response = await timesheetApprovalApi.getTimesheetApprovalCSVReport(reportFilters.value);
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 = `${timesheet_store.pay_period?.pay_year}-${timesheet_store.pay_period?.pay_period_no}_${companies}_${types}_${timesheet_store.pay_period?.period_start}-${timesheet_store.pay_period?.period_end}.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">
<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') }}
</div>
</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>
<!-- groups -->
<div class="col column full-width q-px-md">
<!-- company header -->
<span class="col-auto q-px-sm q-pt-md text-weight-medium text-accent text-uppercase">
<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 class="row q-pb-sm">
<div
v-for="company, index in company_options"
:key="index"
class="q-pa-xs col-6"
v-for="company, companyIndex in companyOptions"
:key="companyIndex"
class="col-6 q-pa-sm"
>
<q-radio
v-model="selected_company"
left-label
color="white"
dense
<OverviewReportOption
:is-selected="reportFilters.companyName === company.value"
: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'"
@click-option="onClickCompanyOption(company.value)"
/>
</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') }}
<!-- 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">
<!-- data type options -->
<div class="row">
<!-- shift types -->
<div
v-for="type, index in type_options"
:key="index"
class="q-pa-xs col-6"
v-for="shiftType, typeIndex in typeOptions"
:key="typeIndex"
class="col-6 q-pa-sm"
>
<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'"
<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 -->
<div class="col-auto row justify-end q-px-md q-pb-md q-pt-xl">
<q-btn
:disable="!is_download_button_enable"
square
push
:disable="!isDownloadButtonEnabled"
icon="download"
:color="is_download_button_enable ? 'accent' : 'grey-5'"
:color="isDownloadButtonEnabled ? 'accent' : 'grey-5'"
:label="$t('shared.label.download')"
class="col-auto q-py-sm shadow-up-2"
@click="onClickedDownload()"
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,9 +9,9 @@ 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 }>> => {

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

@ -75,7 +75,6 @@
const onClickAttachment = async () => {
expenseStore.isShowingAttachmentDialog = true;
await expenseStore.getAttachmentURL(expense.value.attachment_key);
console.log('image url: ', expenseStore.attachmentURL);
}
const hideUpdateForm = () => {

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');
}
}
@ -86,7 +86,6 @@
if (option.value === 'SICK' || option.value === 'VACATION') {
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
console.log('worked minutes: ', workedMinutes);
const expectedWorkedMinutes = expectedDailyHours * 60;
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
@ -102,15 +101,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 +119,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 +189,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 +303,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 +336,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

@ -2,9 +2,11 @@
setup
lang="ts"
>
import DetailsDialogShiftMenu from 'src/modules/timesheet-approval/components/details-dialog-shift-menu.vue';
import { useI18n } from 'vue-i18n';
import { computed, inject, onMounted, ref } from 'vue';
import { QSelect, QInput, useQuasar, type QSelectProps, QPopupProxy } from 'quasar';
import { QSelect, QInput, useQuasar, type QSelectProps } from 'quasar';
import { useUiStore } from 'src/stores/ui-store';
import { useAuthStore } from 'src/stores/auth-store';
import { getCurrentDailyMinutesWorked, getShiftOptions, getTimeStringFromMinutes, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
@ -41,7 +43,7 @@
const q = useQuasar();
const { t } = useI18n();
const ui_store = useUiStore();
const uiStore = useUiStore();
const authStore = useAuthStore();
const mode = inject<'normal' | 'approval'>('mode');
@ -51,7 +53,6 @@
const shiftErrorMessage = ref<string | undefined>();
const is_showing_delete_confirm = ref(false);
const isShowingPredefinedTime = ref(shift.value.type === 'HOLIDAY');
const popupProxyRef = ref<QPopupProxy | null>(null);
const predefinedHoursString = ref('');
const predefinedHoursBgColor = ref(`bg-${shiftTypeSelected.value?.icon_color ?? ''}`);
@ -59,12 +60,6 @@
const hasPTO = computed(() => currentShifts.some(shift => SHIFT_TYPES_WITH_PREDEFINED_TIMES.includes(shift.type)));
const rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
t('timesheet_approvals.tooltip.unapprove') :
t('timesheet_approvals.tooltip.approve'));
const timeInputProps = computed(() => ({
dense: true,
borderless: shift.value.is_approved && isTimesheetApproved,
@ -88,7 +83,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",
@ -136,12 +131,9 @@
is_showing_delete_confirm.value = state;
}
const onRightClickApprove = () => {
const onclickToogleApproval = () => {
if (authStore.user?.user_module_access.includes('timesheets_approval'))
shift.value.is_approved = !shift.value.is_approved;
if (popupProxyRef.value)
popupProxyRef.value.hide();
}
const onShiftTypeChange = (option: ShiftOption) => {
@ -153,7 +145,6 @@
if (option.value === 'SICK' || option.value === 'VACATION') {
const workedMinutes = getCurrentDailyMinutesWorked(currentShifts);
console.log('worked minutes: ', workedMinutes);
const expectedWorkedMinutes = expectedDailyHours * 60;
const leftOverMinutes = expectedWorkedMinutes - workedMinutes;
@ -169,42 +160,17 @@
}
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>
<template>
<div class="row">
<!-- right-click to approve shift only (if in approval mode) -->
<q-popup-proxy
v-if="mode === 'approval'"
ref="popupProxyRef"
context-menu
class="rounded-5 q-px-md shadow-24 cursor-pointer"
style="border: 3px solid var(--q-primary);"
>
<q-banner
dense
class="cursor-pointer q-px-lg"
@click="onRightClickApprove"
>
<template v-slot:avatar>
<q-icon
:name="rightClickMenuIcon"
color="accent"
/>
</template>
<span class="text-weight-bold text-accent text-uppercase">
{{ rightClickMenuLabel }}
</span>
</q-banner>
</q-popup-proxy>
<!-- delete shift confirmation dialog -->
<q-dialog
v-model="is_showing_delete_confirm"
@ -238,7 +204,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 +217,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
@ -349,7 +315,10 @@
</div>
<!-- Else show input fields for in-out timestamps -->
<div v-else class="col row items-start text-uppercase rounded-5 q-pa-xs">
<div
v-else
class="col row items-start text-uppercase rounded-5 q-pa-xs"
>
<q-input
ref="start_time"
v-model="shift.start_time"
@ -385,19 +354,19 @@
</div>
<div
class="row full-height"
:class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'"
v-if="mode === 'normal'"
class="row col-auto flex-center full-height"
>
<!-- 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"
@ -471,6 +440,17 @@
@click="toggleIsShowingDeleteConfirm(true)"
/>
</div>
<div
v-else
class="col-auto"
>
<DetailsDialogShiftMenu
v-model="shift"
@click-toggle-approval="onclickToogleApproval"
@click-delete="toggleIsShowingDeleteConfirm(true)"
/>
</div>
</div>
</div>
</template>

View File

@ -10,6 +10,7 @@
const { mode = 'totals', timesheetMode = 'normal', totalHours = 0, totalExpenses = 0 } = defineProps<{
mode: 'total-hours' | 'off-hours';
timesheetMode: 'approval' | 'normal';
weeklyHours?: number[];
totalHours?: number;
totalExpenses?: number;
}>();
@ -37,6 +38,18 @@
v-if="mode === 'total-hours'"
class="col column full-width"
>
<div
v-for="hours, index in weeklyHours"
:key="index"
class="col row full-width"
>
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t(`timesheet_approvals.table.weekly_hours_${index + 1}`) }}
</span>
<span class="col text-right">{{ getHoursMinutesStringFromHoursFloat(hours) }}</span>
</div>
<div class="col row full-width">
<span class="col-auto text-uppercase text-caption text-bold text-accent">
{{ $t('timesheet.total_hours') }}

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,7 +99,7 @@
: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="no-wrap"
:class="$q.platform.is.mobile ? 'col-auto column' : 'col column fit items-center'"
@ -110,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
@ -128,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"
@ -139,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);"
>
@ -210,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
@ -227,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)"
@ -242,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
@ -251,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,19 @@ 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 weeklyHours = computed(() => timesheetStore.timesheets.map(timesheet =>
Object.values(timesheet.weekly_hours).reduce((sum, hoursPerType) => sum += hoursPerType, 0) - timesheet.weekly_hours.sick
));
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum += timesheet.weekly_hours.regular
+ timesheet.weekly_hours.evening
+ timesheet.weekly_hours.emergency
+ timesheet.weekly_hours.vacation
+ timesheet.weekly_hours.holiday
+ timesheet.weekly_hours.overtime,
0) //initial value
);
0 //initial value
));
const totalExpenses = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_expenses.expenses
@ -131,6 +129,7 @@ import { RouteNames } from 'src/router/router-constants';
<ShiftListWeeklyOverview
mode="total-hours"
:timesheet-mode="mode"
:weekly-hours="weeklyHours"
:total-hours="totalHours"
:total-expenses="totalExpenses"
/>

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

@ -12,68 +12,84 @@
class="q-pa-md justify-center items-stretch bg-secondary"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
<!-- left column -->
<div class="column col flex-center q-pa-md">
</div>
<!-- center column -->
<div class="column col-xs-12 col-md-8 col-xl-6 items-center q-pa-md">
<div class="col-auto full-width q-py-md">
<div class="column col items-center q-pa-md">
<div class="col-8 fit q-py-md">
<MainCarousel />
</div>
<span class="col-auto text-uppercase text-weight-bold self-start q-pt-md">{{ $t('dashboard.useful_links') }}</span>
<div class="col row full-width justify-evenly items-start q-py-md">
<div class="col-3 q-pa-sm">
<ShortcutCard
image-source="src/assets/google_thumbnail.png"
title="Google Workspace"
route="https://mail.google.com/mail/u/0/#inbox"
/>
</div>
<div class="col-3 q-pa-sm">
<ShortcutCard
image-source="src/assets/facturation_thumbnail.png"
title="Facturation"
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
/>
</div>
<div class="col-3 q-pa-sm">
<ShortcutCard
image-source="src/assets/map_targo_banner.png"
title="Map Targo"
route="https://map.targointernet.com/infrastructure/map.php"
/>
</div>
<div class="col-3 q-pa-sm">
<ShortcutCard
image-source="src/assets/info-pannes.png"
title="Info Pannes"
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
/>
</div>
</div>
</div>
<!-- right column -->
<div class="column col items-center">
<div class="column col-lg-4 items-center" :class="$q.platform.is.mobile ? 'q-px-md' : 'q-px-xl'">
<span
v-if="!$q.platform.is.mobile"
class="col-auto text-uppercase text-weight-bold self-start q-px-md q-pt-lg"
>
{{ $t('dashboard.useful_links') }}
</span>
<div class="col-auto full-width"
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
<ShortcutCard
icon-image-source="src/assets/links/logo_gmail.png"
bg-image-source="src/assets/links/google_bg.png"
name="Messagerie"
route="https://mail.google.com/mail/u/0/#inbox"
/>
</div>
<div class="col-auto full-width"
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
<ShortcutCard
icon-image-source="src/assets/links/facturation-transparent.png"
bg-image-source="src/assets/links/facturation_bg.png"
name="Facturation"
route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
/>
</div>
<div class="col-auto full-width"
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
<ShortcutCard
icon-image-source="src/assets/links/map-icon.png"
bg-image-source="src/assets/links/map_targo_banner.png"
name="Map Targo"
route="https://map.targointernet.com/infrastructure/map"
/>
</div>
<div class="col-auto full-width"
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
<ShortcutCard
icon-image-source="src/assets/links/hydroQC_icon.png"
bg-image-source="src/assets/links/hydroQC_bg.png"
name="Info Pannes"
route="https://infopannes.solutions.hydroquebec.com/info-pannes/pannes/pannes-en-cours"
/>
</div>
<div class="col-auto full-width"
:class="$q.platform.is.mobile ? 'q-py-md' : 'q-py-sm'">
<ShortcutCard
icon-image-source="src/assets/links/intranet_logo.png"
bg-image-source="src/assets/links/intranet_targo_bg.png"
name="Intranet"
route="https://intranet.facturation.targo.ca/"
/>
</div>
<div class="col"></div>
<div
class="col-auto row full-width within-iframe"
:class="$q.platform.is.mobile ? 'justify-center' : 'justify-end q-pl-md'"
style="height: 50vh;"
:class="$q.platform.is.mobile ? 'justify-center q-pt-lg' : 'justify-end'"
>
<iframe
title="Environment Canada Weather"
height="400px"
height="200px"
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=f"
allowtransparency="true"
style="border: 0;"
class="col-auto"
></iframe>
</div>
</div>

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';
}
}