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

Reviewed-on: Targo/targo_frontend#37
This commit is contained in:
Nicolas 2026-01-06 09:16:55 -05:00
commit 07b52c854f
53 changed files with 1036 additions and 748 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 KiB

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -4,8 +4,9 @@ export default {
welcome_title: "Welcome to the new Targo Application!", welcome_title: "Welcome to the new Targo Application!",
welcome_message: "Development is complete and the application is live! Things have remained mostly the same, but with a new coat of paint, a more streamlined user experience, and most importantly, drastically improved security and optimization!", welcome_message: "Development is complete and the application is live! Things have remained mostly the same, but with a new coat of paint, a more streamlined user experience, and most importantly, drastically improved security and optimization!",
help_title: "We have a help page!", help_title: "We have a help page!",
help_message: "We did our best to keep the app intuitive with as few clicks and changes as possible, but it's not always perfect! We made this page to explain every part of the app if you any of it ever feels confusing.", help_message: "We've modernized the app while trying to make as few functional changes as possible, but if there's ever any part of the site that leaves you scratching your head, feel free to check out the help page.",
}, },
useful_links: "useful links",
}, },
help: { help: {
label: "Centre d'aide", label: "Centre d'aide",
@ -207,6 +208,7 @@ export default {
modify: "modify", modify: "modify",
close: "close", close: "close",
download: "download", download: "download",
open: "open",
}, },
misc: { misc: {
or: "or", or: "or",
@ -243,6 +245,9 @@ export default {
page_header: "Timesheet", page_header: "Timesheet",
week: "week", week: "week",
total_hours: "total hours: ", total_hours: "total hours: ",
total_expenses: "total expenses: ",
vacation_available: "vacation time available: ",
sick_available: "sick time available: ",
current_shifts: "shifts worked", current_shifts: "shifts worked",
apply_preset: "auto-fill", apply_preset: "auto-fill",
apply_preset_day: "Apply schedule to day", apply_preset_day: "Apply schedule to day",
@ -329,6 +334,7 @@ export default {
}, },
print_report: { print_report: {
title: "Download options", title: "Download options",
description: "Choose what to include in the report",
company: "companies", company: "companies",
type: "type", type: "type",
shifts: "shifts", shifts: "shifts",
@ -345,7 +351,7 @@ export default {
unverified: "pending", unverified: "pending",
inactive: "inactive", inactive: "inactive",
filter_active: "show only active employees", filter_active: "show only active employees",
filter_team: "", filter_team: "show my team only",
}, },
tooltip: { tooltip: {
button_detailed_view: "detailed view", button_detailed_view: "detailed view",

View File

@ -2,10 +2,11 @@ export default {
dashboard: { dashboard: {
carousel: { carousel: {
welcome_title: "Bienvenue dans la nouvelle application Targo!", welcome_title: "Bienvenue dans la nouvelle application Targo!",
welcome_message: "Le développement est terminé et l'application est officiellement en ligne! Les fonctionnalités demeurent grandement intactes comparé à l'ancienne version, mise à part une nouvelle couche de peinture, une expérience utilisateur plus intuitive et surtout une sécurité et optimization drastiquement amélioriés!", welcome_message: "La nouvelle application est officiellement en ligne ! Plus performante et plus sécuritaire, elle conserve lessentiel avec un design rafraîchie.",
help_title: "Nous avons une page d'aide!", help_title: "Nous avons une page d'aide!",
help_message: "Nous avons fait notre possible pour rendre l'application plus intuitive et facile d'accès en suivant les tendances modernes, mais il y a toujours place à l'amélioration! La page d'aide est là pour vous si jamais nous avons raté la cible et qu'une partie du site semble nébuleux.", help_message: "Lapplication a été pensée pour être plus intuitive et moderne. En cas de doute, la page daide est à votre disposition.",
}, },
useful_links: "liens utiles",
}, },
help: { help: {
label: "Centre d'aide", label: "Centre d'aide",
@ -207,7 +208,8 @@ export default {
update: "mettre à jour", update: "mettre à jour",
modify: "modifier", modify: "modifier",
close: "fermer", close: "fermer",
download: "téléchargement", download: "télécharger",
open: "ouvrir",
}, },
misc: { misc: {
or: "ou", or: "ou",
@ -244,6 +246,9 @@ export default {
page_header: "Carte de temps", page_header: "Carte de temps",
week: "semaine", week: "semaine",
total_hours: "heures totales: ", total_hours: "heures totales: ",
total_expenses: "dépenses totales: ",
vacation_available: "vacances disponibles: ",
sick_available: "congés maladie disponible: ",
current_shifts: "quarts entrées", current_shifts: "quarts entrées",
apply_preset: "auto-remplir", apply_preset: "auto-remplir",
apply_preset_day: "Appliquer horaire pour la journée", apply_preset_day: "Appliquer horaire pour la journée",
@ -330,6 +335,7 @@ export default {
}, },
print_report: { print_report: {
title: "options de téléchargement", title: "options de téléchargement",
description: "Choisissez ce qui sera inclu dans le rapport",
company: "compagnies", company: "compagnies",
type: "types de données", type: "types de données",
shifts: "quarts de travail", shifts: "quarts de travail",
@ -345,8 +351,8 @@ export default {
verified: "approuvé", verified: "approuvé",
unverified: "à vérifier", unverified: "à vérifier",
inactive: "inactif", inactive: "inactif",
filter_active: "", filter_active: "montrer les employés inactifs",
filter_team: "", filter_team: "montrer mon équipe seulement",
}, },
tooltip: { tooltip: {
button_detailed_view: "vue détaillée", button_detailed_view: "vue détaillée",

View File

@ -2,21 +2,34 @@
setup setup
lang="ts" lang="ts"
> >
import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { ref } from 'vue';
import { RouteNames } from 'src/router/router-constants'; import { RouteNames } from 'src/router/router-constants';
import { ModuleNames } from 'src/modules/shared/models/user.models'; import { ModuleNames, type UserModuleAccess } from 'src/modules/shared/models/user.models';
const DRAWER_BUTTONS: { i18n_key: string, icon: string, route: RouteNames, required_module?: UserModuleAccess }[] = [
{ i18n_key: 'nav_bar.home', icon: "home", route: RouteNames.DASHBOARD, required_module: ModuleNames.DASHBOARD },
{ i18n_key: 'nav_bar.timesheet_approvals', icon: "event_available", route: RouteNames.TIMESHEET_APPROVALS, required_module: ModuleNames.TIMESHEETS_APPROVAL },
{ i18n_key: 'nav_bar.employee_list', icon: "groups", route: RouteNames.EMPLOYEE_LIST, required_module: ModuleNames.EMPLOYEE_LIST },
{ i18n_key: 'nav_bar.timesheet', icon: "punch_clock", route: RouteNames.TIMESHEET, required_module: ModuleNames.TIMESHEETS },
{ i18n_key: 'nav_bar.profile', icon: "account_box", route: RouteNames.PROFILE, required_module: ModuleNames.PERSONAL_PROFILE },
{ i18n_key: 'nav_bar.help', icon: "contact_support", route: RouteNames.HELP },
]
const q = useQuasar();
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const ui_store = useUiStore(); const ui_store = useUiStore();
const router = useRouter(); const router = useRouter();
const is_mini = ref(true); const is_mini = ref(true);
const goToPageName = (page_name: string) => { const onClickDrawerPage = (page_name: RouteNames) => {
router.push({ name: page_name }).catch(err => { is_mini.value = true;
console.error('Error with Vue Router: ', err);
router.push({ name: page_name }).catch(error => {
console.error('failed to reach page: ', error);
}); });
}; };
@ -26,7 +39,13 @@
router.push({ name: 'login' }).catch(err => { router.push({ name: 'login' }).catch(err => {
console.error('could not log you out: ', err); console.error('could not log you out: ', err);
}) })
} };
onMounted(() => {
if (q.platform.is.mobile) {
ui_store.is_left_drawer_open = false;
}
})
</script> </script>
<template> <template>
@ -39,102 +58,56 @@
:mini="is_mini" :mini="is_mini"
@mouseenter="is_mini = false" @mouseenter="is_mini = false"
@mouseleave="is_mini = true" @mouseleave="is_mini = true"
class="bg-dark z-max column no-wrap" class="bg-dark z-max"
:class="!$q.platform.is.mobile && is_mini ? 'items-center' : 'items-start'"
> >
<!-- Home --> <q-scroll-area class="column fit">
<q-btn <div
flat v-for="button, index in DRAWER_BUTTONS"
dense :key="index"
no-wrap v-show="button.required_module ?? true"
size="lg" @click="onClickDrawerPage(button.route)"
icon="home" >
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.home')" <div
class="col-auto text-uppercase text-weight-bold q-my-xs" v-if="button.required_module ? auth_store.user?.user_module_access.includes(button.required_module) : true"
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'" class="row items-center full-width q-py-sm cursor-pointer"
@click="goToPageName(RouteNames.DASHBOARD)" :class="$router.currentRoute.value.name === button.route ? ($q.dark.isActive ? 'bg-green-10' : 'bg-green-2') : ''"
/> >
<q-icon
:name="button.icon"
color="accent"
size="lg"
class="col-auto q-pl-sm"
/>
<!-- Timesheet Validation --> <div
<q-btn class="col text-uppercase text-weight-bold text-h6 q-pl-sm"
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS_APPROVAL)" :class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
flat >
dense {{ $t(button.i18n_key) }}
no-wrap </div>
size="lg" </div>
icon="event_available" </div>
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet_approvals')"
class="col-auto text-uppercase text-weight-bold q-my-xs"
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
/>
<!-- Employee List --> <q-separator spaced />
<q-btn
v-if="auth_store.user?.user_module_access.includes(ModuleNames.EMPLOYEE_LIST)"
flat
dense
no-wrap
size="lg"
icon="groups"
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.employee_list')"
class="col-auto text-uppercase text-weight-bold q-my-xs"
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
/>
<!-- Employee Timesheet --> <div
<q-btn class="row items-center full-width cursor-pointer q-py-sm"
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS)" @click="handleLogout"
flat >
dense <q-icon
no-wrap name="exit_to_app"
size="lg" color="accent"
icon="punch_clock" size="lg"
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet')" class="col-auto q-pl-sm"
class="col-auto text-uppercase text-weight-bold q-my-xs" />
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
@click="goToPageName(RouteNames.TIMESHEET)"
/>
<!-- Profile --> <div
<q-btn class="col text-uppercase text-weight-bold text-h6 q-pl-sm"
v-if="auth_store.user?.user_module_access.includes(ModuleNames.PERSONAL_PROFILE)" :class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
flat >
dense {{ $t('nav_bar.logout') }}
no-wrap </div>
size="lg" </div>
icon="account_box" </q-scroll-area>
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.profile')"
class="col-auto text-uppercase text-weight-bold q-my-xs"
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
@click="goToPageName(RouteNames.PROFILE)"
/>
<!-- Help -->
<q-btn
flat
dense
no-wrap
size="lg"
icon="contact_support"
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.help')"
class="col-auto text-uppercase text-weight-bold q-my-xs"
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
@click="goToPageName(RouteNames.HELP)"
/>
<!-- Logout -->
<q-btn
flat
dense
no-wrap
size="lg"
icon="exit_to_app"
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.logout')"
class="col-auto text-uppercase text-weight-bold q-my-xs"
:class="!$q.platform.is.mobile && is_mini ? 'absolute-bottom': 'absolute-bottom-left'"
@click="handleLogout"
/>
</q-drawer> </q-drawer>
</template> </template>

View File

@ -32,10 +32,13 @@
<template> <template>
<q-layout view="hHh lpR fFf"> <q-layout view="hHh lpR fFf">
<HeaderBar /> <HeaderBar />
<LeftDrawer /> <LeftDrawer />
<q-page-container> <q-page-container>
<router-view class="q-pa-sm bg-secondary" /> <router-view />
</q-page-container> </q-page-container>
<FooterBar /> <FooterBar />
</q-layout> </q-layout>
</template> </template>

View File

@ -1,25 +1,31 @@
<script setup lang="ts"> <script
import { computed } from 'vue'; setup
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api'; lang="ts"
import LoginRockPaperScissor from 'src/modules/auth/components/login-rock-paper-scissor.vue'; >
import { computed } from 'vue';
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
import LoginRockPaperScissor from 'src/modules/auth/components/login-rock-paper-scissor.vue';
const auth_api = useAuthApi(); const auth_api = useAuthApi();
const email = defineModel<string>('email', { default: '', }); const email = defineModel<string>('email', { default: '', });
// const is_remembered = ref<boolean>(false); // const is_remembered = ref<boolean>(false);
const is_employee_email = computed(() => email.value.includes('@targ')); const is_employee_email = computed(() => email.value.includes('@targ'));
const is_game_time = computed(() => email.value.includes('allumette')); const is_game_time = computed(() => email.value.includes('allumette'));
</script> </script>
<template> <template>
<q-card class="rounded-15 shadow-10 full-width"> <q-card
<q-card-section class="text-center bg-primary q-pa-lg"> bordered
class="rounded-15 shadow-10 full-width"
>
<div class="text-center bg-primary q-pa-lg">
<q-img <q-img
src="/src/assets/logo-targo-white.svg" src="/src/assets/logo-targo-white.svg"
ratio="4.6" ratio="4.6"
fit="contain" fit="contain"
/> />
</q-card-section> </div>
<div class="q-pt-sm q-px-xl q-pb-lg "> <div class="q-pt-sm q-px-xl q-pb-lg ">
<q-card-section class="text-center text-uppercase"> <q-card-section class="text-center text-uppercase">

View File

@ -0,0 +1,42 @@
<script
setup
lang="ts"
>
const { imageSource = "", title = "", description = "", route = "" } = defineProps<{
imageSource?: string,
title?: string,
description?: string,
route?: string,
}>();
const onClickExternalShortcut = () => {
window.open(route, '_blank')?.focus();
}
</script>
<template>
<q-card
class="shortcut-card cursor-pointer shadow-12"
@click="onClickExternalShortcut"
>
<q-img
:src="imageSource"
fit="contain"
>
<div class="absolute-bottom text-uppercase text-weight-bolder text-center">{{ title }}</div>
</q-img>
<q-card-section v-if="description">
<span>{{ description }}</span>
</q-card-section>
</q-card>
</template>
<style
lang="sass"
scoped
>
.shortcut-card
width: 100%
max-width: 250px
</style>

View File

@ -0,0 +1,72 @@
<script
setup
lang="ts"
>
import { RouteNames } from 'src/router/router-constants';
import { ref } from 'vue';
const slide = ref<string>('welcome');
</script>
<template>
<q-carousel
v-model="slide"
transition-prev="jump-right"
transition-next="jump-left"
swipeable
animated
infinite
arrows
:autoplay="9001"
control-color="accent"
control-type="outline"
class="bg-dark full-width rounded-15 shadow-18"
>
<!-- welcome slide -->
<q-carousel-slide
name="welcome"
class="q-pa-none fit"
>
<div class="column fit">
<q-img
src="src/assets/targo_building.png"
position="50% 25%"
fit="cover"
class="col-9"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.welcome_title') }}
</div>
</q-img>
<div class="col column flex-center q-px-md">
<span class="col-auto text-center">{{ $t('dashboard.carousel.welcome_message') }}</span>
</div>
</div>
</q-carousel-slide>
<!-- help page slide -->
<q-carousel-slide
name="tv"
class="q-pa-none cursor-pointer"
@click="$router.push(RouteNames.HELP)"
>
<div class="column fit">
<q-img
src="src/assets/targo_help_banner.png"
position="50% 25%"
fit="none"
class="col-9"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.help_title') }}
</div>
</q-img>
<div class="col column flex-center q-px-md">
<span class="col-auto text-center">{{ $t('dashboard.carousel.help_message') }}</span>
</div>
</div>
</q-carousel-slide>
</q-carousel>
</template>

View File

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

View File

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

View File

@ -2,20 +2,30 @@
setup setup
lang="ts" lang="ts"
> >
import { useQuasar } from 'quasar';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { ref } from 'vue';
// const getEmployeeAvatar = (first_name: string, last_name: string) => { const q = useQuasar();
// // add logic here to see if user has an avatar image and return that instead of initials const is_mouseover = ref(false);
// return first_name.charAt(0) + last_name.charAt(0);
// };
const { row, index = -1 } = defineProps<{ const { row, index = -1, isManagement = false } = defineProps<{
row: EmployeeProfile row: EmployeeProfile
index?: number index?: number
isManagement?: boolean;
}>() }>()
const emit = defineEmits<{
defineEmits<{
onProfileClick: [email: string] onProfileClick: [email: string]
}>(); }>();
const getItemStyle = (): string => {
const active_style = row.last_work_day === null ? '' : 'opacity: 0.6;';
const dark_style = q.dark.isActive ? 'border: 2px solid var(--q-accent);' : '';
const hover_style = isManagement ? (is_mouseover.value ? `transform: scale(1.1); z-index: 2;` :'transform: scale(1) skew(0)') : '';
return `${active_style} ${dark_style} ${hover_style}`;
}
</script> </script>
<template> <template>
@ -24,10 +34,13 @@
:style="`animation-delay: ${index / 25}s;`" :style="`animation-delay: ${index / 25}s;`"
> >
<div <div
class="column col no-wrap cursor-pointer bg-dark rounded-15 shadow-12" class="column col no-wrap bg-dark rounded-15 shadow-12"
:class="isManagement ? 'cursor-pointer item-mouse-hover' : ''"
style="max-width: 230px; height: 275px;" style="max-width: 230px; height: 275px;"
:style="(row.last_work_day === null ? ' ' : 'opacity: 0.6; ') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')" :style="getItemStyle()"
@click="emit('onProfileClick', row.email)" @click="$emit('onProfileClick', row.email)"
@mouseenter="is_mouseover = true"
@mouseleave="is_mouseover = false"
> >
<div class="col-auto column flex-center q-pt-md"> <div class="col-auto column flex-center q-pt-md">
<q-avatar <q-avatar
@ -66,4 +79,10 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="css" scoped>
.item-mouse-hover {
transition: all 0.2s ease-out;
}
</style>

View File

@ -7,13 +7,18 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { date, type QTableColumn } from 'quasar'; import { date, type QTableColumn } from 'quasar';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useAuthStore } from 'src/stores/auth-store';
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { employee_list_columns, type EmployeeProfile, type EmployeeListFilters } from 'src/modules/employee-list/models/employee-profile.models'; import { employee_list_columns, type EmployeeProfile, type EmployeeListFilters } from 'src/modules/employee-list/models/employee-profile.models';
const ui_store = useUiStore();
const auth_store = useAuthStore();
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const ui_store = useUiStore();
const is_management = auth_store.user?.user_module_access.includes('employee_management') ?? false;
const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'job_title', 'last_work_day']); const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'job_title', 'last_work_day']);
const table_grid_container = ref<HTMLElement | null>(null); const table_grid_container = ref<HTMLElement | null>(null);
@ -86,16 +91,33 @@
:visible-columns="visible_columns" :visible-columns="visible_columns"
> >
<template #top> <template #top>
<div class="row full-width q-mb-sm"> <div class="row flex-center full-width q-mb-sm">
<q-btn <q-btn
push v-if="is_management"
rounded
color="accent" color="accent"
icon="las la-user-edit" icon="las la-user-edit"
:label="$t('shared.label.add')" :label="$t('shared.label.add')"
class="text-uppercase" class="text-uppercase q-py-sm"
@click.stop="_evt => employee_store.openAddModifyDialog()" @click.stop="_evt => employee_store.openAddModifyDialog()"
/> />
<q-checkbox
v-if="is_management"
v-model="filters.hide_inactive_users"
color="accent"
:label="$t('employee_management.filter.hide_terminated')"
class="text-uppercase q-ml-md text-weight-medium q-px-sm"
>
<q-icon
name="las la-user-times"
color="negative"
size="sm"
class="q-px-sm"
/>
</q-checkbox>
<q-space /> <q-space />
<q-btn-toggle <q-btn-toggle
@ -131,15 +153,6 @@
</template> </template>
</q-input> </q-input>
</div> </div>
<div class="row">
<q-space />
<q-checkbox
v-model="filters.hide_inactive_users"
color="accent"
:label="$t('employee_management.filter.hide_terminated')"
/>
</div>
</template> </template>
<template #header="props"> <template #header="props">
@ -170,7 +183,8 @@
:key="props.rowIndex" :key="props.rowIndex"
:row="props.row" :row="props.row"
:index="props.rowIndex" :index="props.rowIndex"
@on-profile-click="employee_store.openAddModifyDialog" :is-management="is_management"
@on-profile-click="email => is_management ? employee_store.openAddModifyDialog(email) : ''"
/> />
</transition> </transition>
</template> </template>
@ -178,7 +192,7 @@
<template #body-cell="scope"> <template #body-cell="scope">
<q-td <q-td
:props="scope" :props="scope"
@click="employee_store.openAddModifyDialog(scope.row.email)" @click="is_management ? employee_store.openAddModifyDialog(scope.row.email) : ''"
> >
<transition <transition
appear appear

View File

@ -66,6 +66,28 @@
>{{ scope.opt.label }}</span> >{{ scope.opt.label }}</span>
</div> </div>
</template> </template>
<template #after>
<q-toggle
v-model="shift.is_remote"
dense
keep-color
size="2.5em"
color="accent"
icon="las la-building"
checked-icon="las la-laptop"
>
<q-tooltip
anchor="top middle"
self="bottom middle"
:offset="[0, 10]"
class="text-uppercase text-weight-medium text-white bg-accent"
>
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
$t('timesheet.shift.types.OFFICE') }}
</q-tooltip>
</q-toggle>
</template>
</q-select> </q-select>
</div> </div>

View File

@ -1,40 +1,43 @@
<script setup lang="ts"> <script
//default images setup
// import default_dashboard from 'src/assets/help-ss/default-dashboard.png'; lang="ts"
import default_personal_profile from 'src/assets/help-ss/default-personnal_profile.png'; >
import default_timesheet from 'src/assets/help-ss/default-timesheet.png'; //default images
import default_employee_list from 'src/assets/help-ss/default-employee-list.png'; // import default_dashboard from 'src/assets/help-ss/default-dashboard.png';
import default_employee_management from 'src/assets/help-ss/default-employee-management.png'; import default_personal_profile from 'src/assets/help-ss/default-personnal_profile.png';
import default_validation_page from 'src/assets/help-ss/default-validation-page.png'; import default_timesheet from 'src/assets/help-ss/default-timesheet.png';
import default_employee_list from 'src/assets/help-ss/default-employee-list.png';
import default_employee_management from 'src/assets/help-ss/default-employee-management.png';
import default_validation_page from 'src/assets/help-ss/default-validation-page.png';
const default_images: Record<UserModuleAccess, string> = { const default_images: Record<UserModuleAccess, string> = {
dashboard: '', dashboard: '',
personal_profile: default_personal_profile, personal_profile: default_personal_profile,
timesheets: default_timesheet, timesheets: default_timesheet,
employee_list: default_employee_list, employee_list: default_employee_list,
employee_management: default_employee_management, employee_management: default_employee_management,
timesheets_approval: default_validation_page, timesheets_approval: default_validation_page,
}; };
import type { HelpModuleOptions } from 'src/modules/help/models/help-module.model'; import type { HelpModuleOptions } from 'src/modules/help/models/help-module.model';
import type { UserModuleAccess } from 'src/modules/shared/models/user.models'; import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
import { ref } from 'vue'; import { ref } from 'vue';
const props = defineProps<{ const props = defineProps<{
help_module: UserModuleAccess; help_module: UserModuleAccess;
options: HelpModuleOptions[]; options: HelpModuleOptions[];
moduleIndex: number; moduleIndex: number;
}>(); }>();
const help_module = props.help_module; const help_module = props.help_module;
const current_path = ref<string>(default_images[help_module]); const current_path = ref<string>(default_images[help_module]);
const switchSide = (index: number) => { const switchSide = (index: number) => {
if (index % 2 !== 0) { if (index % 2 !== 0) {
return false return false
}
return true
} }
return true
}
</script> </script>
@ -45,14 +48,15 @@ const switchSide = (index: number) => {
> >
<!-- Card Header --> <!-- Card Header -->
<div <div
class="row col-auto text-h5 q-pa-md text-primary bg-secondary" class="row col-auto text-h4 text-weight-thin q-py-md q-px-xl bg-secondary"
:class="switchSide(moduleIndex) ? 'justify-start' : 'justify-end'" :class="switchSide(moduleIndex) ? 'justify-end' : 'justify-start'"
> >
{{ ($t(`help.tutorial.${help_module}.title`)).toUpperCase() }} {{ ($t(`help.tutorial.${help_module}.title`)).toUpperCase() }}
</div> </div>
<!-- Card Body -->
<!-- Card Body. The visual support image will alternate left and right -->
<div class="row col full-width q-px-none"> <div class="row col full-width q-px-none">
<!-- Object and descriptions zone --> <!-- Left-sided visual support -->
<div <div
class="col flex-center row" class="col flex-center row"
v-if="moduleIndex % 2 !== 0" v-if="moduleIndex % 2 !== 0"
@ -63,16 +67,17 @@ const switchSide = (index: number) => {
leave-active-class="animated fade-out" leave-active-class="animated fade-out"
> >
<q-img <q-img
class="image-wrapper"
:src="current_path" :src="current_path"
loading="lazy" loading="lazy"
fit="contain" fit="contain"
class="rounded-10 image-wrapper"
> >
</q-img> </q-img>
</transition> </transition>
</div> </div>
<div class="col column q-mx-sm">
<!-- Area with expandable help items -->
<div class="col column q-px-md">
<q-expansion-item <q-expansion-item
v-for="option, index in options" v-for="option, index in options"
:key="index" :key="index"
@ -112,7 +117,8 @@ const switchSide = (index: number) => {
</div> </div>
</q-expansion-item> </q-expansion-item>
</div> </div>
<!-- images of the related selected option -->
<!-- right-sided visual support -->
<div <div
class="col flex-center row" class="col flex-center row"
v-if="moduleIndex % 2 === 0" v-if="moduleIndex % 2 === 0"
@ -123,11 +129,10 @@ const switchSide = (index: number) => {
leave-active-class="animated fade-out" leave-active-class="animated fade-out"
> >
<q-img <q-img
class="image-wrapper"
:src="current_path" :src="current_path"
loading="lazy" loading="lazy"
fit="contain" fit="contain"
style="width: 150%;" class="rounded-10 image-wrapper"
> >
</q-img> </q-img>
</transition> </transition>

View File

@ -23,36 +23,40 @@
:class="ui_store.is_mobile_mode ? 'column' : 'row'" :class="ui_store.is_mobile_mode ? 'column' : 'row'"
style="border: 1px solid var(--q-accent);" style="border: 1px solid var(--q-accent);"
> >
<q-item <div
v-for="mode of dark_mode_options" v-for="mode of dark_mode_options"
:key="mode.label" :key="mode.label"
clickable class="col q-pa-sm"
dense
v-ripple
class="col row rounded-5 q-ma-sm shadow-4"
: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"
> >
<q-item-section avatar> <q-item
<q-icon clickable
:name="mode.icon" dense
size="md" v-ripple
:color="mode.quasar_value === $q.dark.mode ? 'white' : ''" 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' : '')"
</q-item-section> @click="ui_store.user_preferences.is_dark_mode = mode.value"
>
<q-item-section side>
<q-icon
:name="mode.icon"
size="md"
:color="mode.quasar_value === $q.dark.mode ? 'white' : ''"
/>
</q-item-section>
<q-item-section class="text-uppercase justify-center"> <q-item-section>
<q-item-label> {{ $t(mode.label) }}</q-item-label> <q-item-label class="text-uppercase justify-center">{{ $t(mode.label) }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-icon <q-icon
v-if="mode.quasar_value === $q.dark.mode" v-if="mode.quasar_value === $q.dark.mode"
name="check" name="check"
color="white" color="white"
/> />
</q-item-section> </q-item-section>
</q-item> </q-item>
</div>
</div> </div>
<div <div

View File

@ -20,8 +20,10 @@
<div class="column text-uppercase text-center text-weight-bolder text-h4"> <div class="column text-uppercase text-center text-weight-bolder text-h4">
<span <span
v-if="!$q.platform.is.mobile" v-if="!$q.platform.is.mobile"
class="col q-mt-lg" class="col q-pt-lg"
>{{ $t(title) }}</span> >
{{ $t(title) }}
</span>
<transition <transition
enter-active-class="animated fadeInDown" enter-active-class="animated fadeInDown"

View File

@ -96,6 +96,7 @@
v-model="calendar_date" v-model="calendar_date"
color="primary" color="primary"
today-btn today-btn
no-unset
mask="YYYY-MM-DD" mask="YYYY-MM-DD"
:options="date => date >= PAY_PERIOD_DATE_LIMIT" :options="date => date >= PAY_PERIOD_DATE_LIMIT"
@update:model-value="onDateSelected" @update:model-value="onDateSelected"

View File

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

View File

@ -15,17 +15,18 @@
keep-color keep-color
size="lg" size="lg"
color="accent" color="accent"
label="show inactive" :label="$t('timesheet_approvals.table.filter_active')"
class="col" class="col"
:class="filters.is_showing_inactive ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'" :class="filters.is_showing_inactive ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
/> />
<q-checkbox <q-checkbox
v-model="filters.is_showing_team_only" v-model="filters.is_showing_team_only"
keep-color keep-color
size="lg" size="lg"
val="team" val="team"
color="accent" color="accent"
label="show team only" :label="$t('timesheet_approvals.table.filter_team')"
class="col" class="col"
:class="filters.is_showing_team_only ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'" :class="filters.is_showing_team_only ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
/> />

View File

@ -154,7 +154,6 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
> >
<div <div
class="col text-uppercase" class="col text-uppercase"
:class="row.total_hours > 80 || !row.is_active ? 'text-negative' : ''"
> >
<span class="text-h6 q-ml-sm text-weight-bolder">{{ 'Total : ' + Math.floor(row.total_hours) <span class="text-h6 q-ml-sm text-weight-bolder">{{ 'Total : ' + Math.floor(row.total_hours)
}}</span> }}</span>

View File

@ -35,15 +35,18 @@
'is_approved', 'is_approved',
]); ]);
const { maxHeight } = defineProps<{
maxHeight: number;
}>();
const is_showing_filters = ref(false); const is_showing_filters = ref(false);
const search_string = ref('');
const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview)); const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview));
const overview_filters = ref<PayPeriodOverviewFilters>({ const overview_filters = ref<PayPeriodOverviewFilters>({
is_showing_inactive: false, is_showing_inactive: false,
is_showing_team_only: false, is_showing_team_only: false,
supervisors: [], supervisors: [],
name_search_string: search_string.value, name_search_string: '',
}); });
const onClickedDetails = async (row: TimesheetApprovalOverview) => { const onClickedDetails = async (row: TimesheetApprovalOverview) => {
@ -80,211 +83,213 @@
</script> </script>
<template> <template>
<div class="q-px-md full-height"> <div class="full-width">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<q-table
<q-table dense
:key="timesheet_store.is_approval_grid_mode ? 'grid' : 'list'" row-key="email"
:visible-columns="VISIBLE_COLUMNS" color="accent"
:rows="overview_rows" hide-pagination
:columns="pay_period_overview_columns" :rows="overview_rows"
row-key="email" :columns="pay_period_overview_columns"
:grid="timesheet_store.is_approval_grid_mode" :visible-columns="VISIBLE_COLUMNS"
:dense="timesheet_store.is_approval_grid_mode" :grid="timesheet_store.is_approval_grid_mode"
hide-pagination :pagination="{ sortBy: 'is_active' }"
:pagination="{ sortBy: 'is_active' }" :filter="overview_filters"
:filter="overview_filters" :filter-method="filterEmployeeRows"
:filter-method="filterEmployeeRows" :rows-per-page-options="[0]"
color="accent" class="bg-transparent"
:rows-per-page-options="[0]" :class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
card-container-class="justify-center" card-container-class="justify-center"
class="bg-transparent" table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'" :no-data-label="$t('shared.error.no_data_found')"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar" :no-results-label="$t('shared.error.no_search_results')"
:no-data-label="$t('shared.error.no_data_found')" :loading-label="$t('shared.label.loading')"
:no-results-label="$t('shared.error.no_search_results')" :style="overview_rows.length > 0 ? `max-height: ${maxHeight - (timesheet_store.is_approval_grid_mode ? 0 : 20)}px;` : ''"
:loading-label="$t('shared.label.loading')" @row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
:style="$q.platform.is.mobile ? '' : 'max-height: 70vh;'" >
> <template #top>
<template #top> <div class="column full-width">
<div class="column full-width">
<div
class="col-auto row items-start full-width q-px-lg"
:class="$q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md'"
>
<PayPeriodNavigator
@date-selected="timesheet_approval_api.getTimesheetOverviews"
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
:class="$q.platform.is.mobile ? 'q-mb-sm' : ''"
style="height: 40px;"
/>
<q-space />
<div <div
class="col-auto row no-wrap items-start" class="col-auto row items-start full-width q-px-lg"
:class="$q.platform.is.mobile ? 'q-mb-md' : ''" :class="$q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md'"
> >
<q-btn-toggle <PayPeriodNavigator
v-model="timesheet_store.is_approval_grid_mode" @date-selected="timesheet_approval_api.getTimesheetOverviews"
push @pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
rounded @pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
color="white" :class="$q.platform.is.mobile ? 'q-mb-sm' : ''"
text-color="accent"
toggle-color="accent"
class="col-auto"
:class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-sm'"
:options="[
{ icon: 'grid_view', value: true },
{ icon: 'view_list', value: false },
]"
style="height: 40px;" style="height: 40px;"
/> />
<q-btn <q-space />
push
rounded
icon="download"
color="accent"
:label="$q.screen.lt.md ? '' : $t('shared.label.download')"
class="col-auto q-mr-sm"
style="height: 40px;"
@click="timesheet_store.is_report_dialog_open = true"
/>
<QTableFilters <div
v-model:search="search_string" class="col-auto row no-wrap items-start"
class="col-auto q-mb-sm" :class="$q.platform.is.mobile ? 'q-mb-md' : ''"
/>
<q-btn
flat
icon="filter_alt"
color="white"
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
class="col q-ml-sm self-stretch bg-primary"
style="border-radius: 5px 5px 0 0;"
@click="is_showing_filters = !is_showing_filters"
/>
</div>
</div>
<q-slide-transition>
<OverviewListFilters
v-if="is_showing_filters"
v-model:filters="overview_filters"
class="q-mx-lg col-auto"
/>
</q-slide-transition>
<q-separator
color="primary"
size="5px"
class="q-mx-lg q-my-none q-pa-none"
/>
</div>
</template>
<template #header="props">
<q-tr
:props="props"
class="bg-primary"
>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="text-uppercase text-weight-bolder text-white">
{{ $t(col.label) }}
</span>
</q-th>
</q-tr>
</template>
<template #body-cell="props">
<q-td
:props="props"
class="text-weight-medium"
>
<transition
appear
enter-active-class="animated fadeInUp slow"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<div
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
style="font-size: 1.2em;"
:style="`animation-delay: ${props.rowIndex / 30}s;`"
>
<transition
v-if="props.col.name === 'is_approved'"
enter-active-class="animated swing"
mode="out-in"
> >
<q-btn <q-btn-toggle
:key="props.row.is_approved" v-model="timesheet_store.is_approval_grid_mode"
flat push
dense rounded
:icon="props.value ? 'lock' : 'lock_open'" color="white"
:color="props.value ? 'white' : 'grey-5'" text-color="accent"
class="rounded-5 " toggle-color="accent"
:class="props.value ? 'bg-accent' : ''" class="col-auto"
@click.stop="onClickApproveAll(props.row.email, props.row.is_approved)" :class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-sm'"
:options="[
{ icon: 'grid_view', value: true },
{ icon: 'view_list', value: false },
]"
style="height: 40px;"
/> />
</transition>
<div v-else-if="props.col.name === 'employee_first_name'"> <q-btn
<span class="text-h5 text-uppercase text-accent q-mr-xs"> push
{{ props.value }} rounded
</span> icon="download"
<span class="text-uppercase text-weight-light"> color="accent"
{{ props.row.employee_last_name }} :label="$q.screen.lt.md ? '' : $t('shared.label.download')"
class="col-auto q-mr-sm"
style="height: 40px;"
@click="timesheet_store.is_report_dialog_open = true"
/>
<QTableFilters
v-model:search="overview_filters.name_search_string"
class="col-auto q-mb-sm"
/>
<q-btn
flat
icon="filter_alt"
color="white"
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
class="col q-ml-sm self-stretch bg-primary"
style="border-radius: 5px 5px 0 0;"
@click="is_showing_filters = !is_showing_filters"
/>
</div>
</div>
<q-slide-transition>
<OverviewListFilters
v-if="is_showing_filters"
v-model:filters="overview_filters"
class="q-mx-lg col-auto"
/>
</q-slide-transition>
<q-separator
color="primary"
size="5px"
class="q-mx-lg q-my-none q-pa-none"
/>
</div>
</template>
<template #header="props">
<q-tr
:props="props"
class="bg-primary"
>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="text-uppercase text-weight-bolder text-white">
{{ $t(col.label) }}
</span>
</q-th>
</q-tr>
</template>
<template #body-cell="props">
<q-td
:props="props"
class="text-weight-medium"
>
<transition
appear
enter-active-class="animated fadeInUp slow"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<div
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
style="font-size: 1.2em;"
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
>
<transition
v-if="props.col.name === 'is_approved'"
enter-active-class="animated swing"
mode="out-in"
>
<q-btn
:key="props.row.is_approved"
flat
dense
:icon="props.value ? 'lock' : 'lock_open'"
:color="props.value ? 'white' : 'grey-5'"
class="rounded-5 "
:class="props.value ? (props.row.is_active ? 'bg-accent' : 'bg-negative') : ''"
@click.stop="onClickApproveAll(props.row.email, !props.row.is_approved)"
/>
</transition>
<div v-else-if="props.col.name === 'employee_first_name'">
<span
class="text-h5 text-uppercase q-mr-xs"
:class="props.row.is_active ? 'text-accent' : 'text-negative'"
>
{{ props.value }}
</span>
<span class="text-uppercase text-weight-light">
{{ props.row.employee_last_name }}
</span>
</div>
<span
v-else
:class="props.col.name === overview_column_names.REGULAR && props.row.overtime > 0 ? 'text-negative text-weight-bolder' : 'text-weight-regular'"
>
{{ TIME_COLUMNS.includes(props.col.name) ?
getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
</span> </span>
</div> </div>
</transition>
</q-td>
</template>
<span <!-- Template for individual employee cards -->
v-else <template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
:class="props.col.name === overview_column_names.REGULAR && props.row.overtime > 0 ? 'text-negative text-weight-bolder' : 'text-weight-regular'" <OverviewListItem
> v-model="props.row.is_approved"
{{ TIME_COLUMNS.includes(props.col.name) ? :key="props.row.email + timesheet_store.pay_period?.pay_period_no"
getHoursMinutesStringFromHoursFloat(props.value) : props.value }} :index="props.rowIndex"
</span> :row="props.row"
</div> @click-details="onClickedDetails"
</transition> @click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
</q-td>
</template>
<!-- Template for individual employee cards -->
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
<OverviewListItem
v-model="props.row.is_approved"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
:index="props.rowIndex"
:row="props.row"
@click-details="onClickedDetails"
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
/>
</template>
<!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }">
<div class="full-width column items-center text-accent q-gutter-sm">
<q-icon
size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'"
/> />
</template>
<span class="text-h6"> <!-- Template for custome failed-to-load state -->
{{ message }} <template #no-data="{ message, filter }">
</span> <div v-if="!timesheet_store.is_loading" class="full-width column items-center text-accent">
</div> <q-icon
</template> size="4em"
</q-table> :name="filter ? 'filter_alt_off' : 'error_outline'"
/>
<span class="text-h6">
{{ message }}
</span>
</div>
</template>
</q-table>
</div> </div>
</template> </template>
@ -305,4 +310,7 @@
tbody tbody
scroll-margin-top: 48px scroll-margin-top: 48px
.q-table__grid-content
overflow: auto
</style> </style>

View File

@ -1,143 +1,163 @@
<script setup lang="ts"> <script
import { computed, ref, watch } from 'vue'; setup
import { useTimesheetStore } from 'src/stores/timesheet-store'; lang="ts"
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models'; >
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';
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters); const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>( const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>(
Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as keyof TimesheetApprovalCSVReportFilters) Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as keyof TimesheetApprovalCSVReportFilters)
); );
interface ReportOptions { interface ReportOptions {
label: string; label: string;
value: keyof TimesheetApprovalCSVReportFilters; value: keyof TimesheetApprovalCSVReportFilters;
}; };
const company_options: ReportOptions[] = [ const company_options: ReportOptions[] = [
{ label: 'Targo', value: 'targo' }, { label: 'Targo', value: 'targo' },
{ label: 'Solucom', value: 'solucom' }, { label: 'Solucom', value: 'solucom' },
]; ];
const type_options: ReportOptions[] = [ const type_options: ReportOptions[] = [
{ label: 'timesheet_approvals.print_report.shifts', value: 'shifts' }, { label: 'timesheet_approvals.print_report.shifts', value: 'shifts' },
{ label: 'timesheet_approvals.print_report.expenses', value: 'expenses' }, { label: 'timesheet_approvals.print_report.expenses', value: 'expenses' },
{ label: 'shared.shift_type.holiday', value: 'holiday' }, { label: 'shared.shift_type.holiday', value: 'holiday' },
{ label: 'shared.shift_type.vacation', value: 'vacation' }, { label: 'shared.shift_type.vacation', value: 'vacation' },
]; ];
const is_download_button_enable = computed(() => const is_download_button_enable = computed(() =>
company_options.map(option => option.value).some(option => selected_report_filters.value.includes(option)) && 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)) type_options.map(option => option.value).some(option => selected_report_filters.value.includes(option))
); );
const onClickedDownload = async () => { const onClickedDownload = async () => {
try { try {
const data = await timesheet_store.getPayPeriodReport(report_filter_options.value); const data = await timesheet_store.getPayPeriodReport(report_filter_options.value);
const companies = Object.entries(report_filter_options.value) const companies = Object.entries(report_filter_options.value)
.filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-'); .filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-');
const types = Object.entries(report_filter_options.value) const types = Object.entries(report_filter_options.value)
.filter(([key, value]) => value && ['shifts', 'expenses', 'holiday', 'vacation'].includes(key)).map(([key]) => key).join('-'); .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 file_name = `Desjardins_${companies}_${types}_${new Date().toISOString().split('T')[0]}.csv`;
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.setAttribute('download', file_name); link.setAttribute('download', file_name);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
console.error(`An error occured during the CSV download: `, error) console.error(`An error occured during the CSV download: `, error)
}
} }
}
watch(selected_report_filters, (new_values) => { watch(selected_report_filters, (new_values) => {
Object.keys(report_filter_options.value).forEach(key => { Object.keys(report_filter_options.value).forEach(key => {
const typed_key = key as keyof TimesheetApprovalCSVReportFilters; const typed_key = key as keyof TimesheetApprovalCSVReportFilters;
report_filter_options.value[typed_key] = new_values.includes(key as keyof TimesheetApprovalCSVReportFilters); report_filter_options.value[typed_key] = new_values.includes(key as keyof TimesheetApprovalCSVReportFilters);
});
}); });
});
</script> </script>
<template> <template>
<q-dialog v-model="timesheet_store.is_report_dialog_open"> <q-dialog v-model="timesheet_store.is_report_dialog_open">
<div class="bg-secondary full-width shadow-24 rounded-10 column"> <div
<div class="shadow-1 bg-primary text-accent text-weight-bold text-center text-uppercase"> class="column bg-secondary shadow-24 rounded-10"
<span> {{ $t('timesheet_approvals.print_report.title') }}</span> :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>
<div class="row q-py-md q-px-lg">
<div class="col-auto full-width shadow-1 row bg-dark q-py-xs q-px-lg rounded-10"> <!-- info blurb -->
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3"> <div class="col-auto row flex-center full-width bg-dark shadow-2">
{{ $t('timesheet_approvals.print_report.company') }} <q-icon
</span> name="info_outline"
<div class="row bordered-primary col-auto full-width"> size="sm"
<div class="col-auto q-mr-xs"
v-for="company, index in company_options"
:key="index"
class="q-pa-xs col-6"
>
<q-checkbox
v-model="selected_report_filters"
left-label
color="white"
class="q-px-md shadow-4 rounded-25 full-width"
dense
:class="selected_report_filters.includes(company.value) ? 'bg-accent text-white' : 'bg-dark'"
:label="$t(company.label)"
:val="company.value"
checked-icon="download"
unchecked-icon="highlight_off"
/>
</div>
</div>
</div>
<div class="row q-py-md">
<div class="col-auto full-width shadow-1 row bg-dark q-px-lg rounded-10 q-pb-sm">
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3">
{{ $t('timesheet_approvals.print_report.options') }}
</span>
<div class=" row bordered-primary col-auto full-width">
<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"
class="q-px-md shadow-4 rounded-25 full-width"
dense
:class="selected_report_filters.includes(type.value) ? 'bg-accent text-white' : 'bg-dark'"
:label="$t(type.label)"
:val="type.value"
checked-icon="download"
unchecked-icon="highlight_off"
/>
</div>
</div>
</div>
</div>
</div>
<div class="column">
<q-btn
:disable="!is_download_button_enable"
square
size="md"
icon="download"
:color="is_download_button_enable ? 'accent' : 'grey-5'"
:label="$t('shared.label.download')"
@click="onClickedDownload()"
/> />
<span class="col-auto text-weight-light q-mr-sm">
{{ $t('timesheet_approvals.print_report.description') }}
</span>
</div> </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>
<!-- 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-checkbox
v-model="selected_report_filters"
left-label
color="white"
dense
:label="$t(company.label)"
:val="company.value"
checked-icon="download"
unchecked-icon="highlight_off"
class="q-px-md q-py-xs shadow-4 rounded-25 full-width"
:class="selected_report_filters.includes(company.value) ? 'bg-accent text-white text-bold' : 'bg-dark'"
/>
</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>
<!-- 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="download"
unchecked-icon="highlight_off"
: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'"
/>
</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> </div>
</q-dialog> </q-dialog>
</template> </template>

View File

@ -209,6 +209,18 @@
</q-tooltip> </q-tooltip>
</q-toggle> </q-toggle>
</template> </template>
<template #option="scope">
<q-item>
<q-item-section avatar>
<q-icon :name="scope.opt.icon" />
</q-item-section>
<q-item-section class="text-left">
{{ $t(scope.label) }}
</q-item-section>
</q-item>
</template>
</q-select> </q-select>
</div> </div>

View File

@ -12,7 +12,7 @@
<template> <template>
<div <div
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height" v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
class="col-auto row items-start q-px-sm q-pt-sm full-width" class="row items-start q-px-sm q-pt-sm full-width"
> >
<!-- per timesheet --> <!-- per timesheet -->
<div <div
@ -60,7 +60,7 @@
class="col row flex-center" class="col row flex-center"
> >
<q-badge <q-badge
:color="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'accent shadow-2' : 'dark shadow-2') : 'blue-grey-5'" :color="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'accent shadow-2' : 'white shadow-2') : 'blue-grey-5'"
:class="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'q-px-xs' : 'q-pa-sm') : ''" :class="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'q-px-xs' : 'q-pa-sm') : ''"
:style="day.shifts.length > 0 ? '' : 'opacity: 0.5'" :style="day.shifts.length > 0 ? '' : 'opacity: 0.5'"
> >

View File

@ -149,6 +149,20 @@
</q-tooltip> </q-tooltip>
</q-toggle> </q-toggle>
</template> </template>
<template #option="scope">
<q-item clickable v-bind="scope.itemProps">
<q-item-section avatar>
<q-icon
:name="scope.opt.icon"
/>
</q-item-section>
<q-item-section class="text-left">
{{ $t(scope.label) }}
</q-item-section>
</q-item>
</template>
</q-select> </q-select>
</div> </div>

View File

@ -6,14 +6,16 @@
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue'; import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { Shift } from 'src/modules/timesheets/models/shift.models'; import type { Shift } from 'src/modules/timesheets/models/shift.models';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models'; import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util'; import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
const shift_api = useShiftApi(); const shift_api = useShiftApi();
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
const timesheet_store = useTimesheetStore();
const shift_error_message = ref<string | undefined>(); const shift_error_message = ref<string | undefined>();
const { day, dense = false, approved = false } = defineProps<{ const { day, dense = false, approved = false } = defineProps<{
@ -63,7 +65,7 @@ import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
leave-active-class="animated zoomOut fast" leave-active-class="animated zoomOut fast"
> >
<q-btn <q-btn
v-if="!$q.platform.is.mobile && day.shifts.length < 1 && preset_mouseover" v-if="!$q.platform.is.mobile && day.shifts.length < 1 && preset_mouseover && timesheet_store.has_timesheet_preset"
:disable="day.shifts.length > 0" :disable="day.shifts.length > 0"
flat flat
dense dense
@ -86,7 +88,7 @@ import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
:key="shift_index" :key="shift_index"
class="col-auto" class="col-auto"
> >
<ShiftListDayRowMobile <ShiftListDayRowMobile
v-if="$q.platform.is.mobile" v-if="$q.platform.is.mobile"
v-model:shift="day.shifts[shift_index]!" v-model:shift="day.shifts[shift_index]!"
:is-timesheet-approved="approved" :is-timesheet-approved="approved"

View File

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

View File

@ -5,22 +5,25 @@
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue'; import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue'; import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
import { date } from 'quasar'; import { date, useQuasar } from 'quasar';
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { Shift } from 'src/modules/timesheets/models/shift.models'; import { Shift } from 'src/modules/timesheets/models/shift.models';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { QScrollArea } from 'quasar'; import type { QScrollArea, TouchSwipeValue } from 'quasar';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models'; import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
const { extractDate } = date; const { extractDate } = date;
const q = useQuasar();
const ui_store = useUiStore(); const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
const { mode = 'normal'} = defineProps<{ const { mode = 'normal' } = defineProps<{
mode: 'normal' | 'approval'; mode: 'normal' | 'approval';
}>(); }>();
@ -28,7 +31,12 @@
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown'); const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
const timesheet_page = ref<QScrollArea | null>(null); const timesheet_page = ref<QScrollArea | null>(null);
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0) const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent);
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0);
const timesheet_container = ref<HTMLElement | null>(null);
const scroll_area_height = ref(0);
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => { const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true; ui_store.focus_next_component = true;
@ -44,37 +52,54 @@
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0); const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
day.shifts = shifts_without_deleted_shift; day.shifts = shifts_without_deleted_shift;
} }
} };
const getDayApproval = (day: TimesheetDay) => { const getDayApproval = (day: TimesheetDay) => {
if (day.shifts.length < 1) return false; if (day.shifts.length < 1) return false;
return day.shifts.every(shift => shift.is_approved === true); return day.shifts.every(shift => shift.is_approved === true);
} };
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: { x?: number, y?: number }) => { const handleSwipe: TouchSwipeValue = (details) => {
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft'; mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
if (distance.x && Math.abs(distance.x) > 10) { if (details.distance && details.distance.x && Math.abs(details.distance.x) > 10) {
await timesheet_api.getTimesheetsBySwiping(direction === 'left' ? 1 : -1) timesheet_api.getTimesheetsBySwiping(details.direction === 'left' ? 1 : -1).catch(error => console.error(error));
} }
} };
const getMobileDayRef = (iso_date_string: string): string => {
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
};
watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) {
console.log('setting scroll position to offsetTop of currentDayComponent: ', currentDayComponent.value[0]!.offsetTop);
timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800);
return;
}
if (timesheet_container.value !== null && mode === 'approval') {
scroll_area_height.value = timesheet_container.value.offsetHeight
}
})
</script> </script>
<template> <template>
<div <div
class="col column fit relative-position" class="column fit relative-position"
:style="$q.platform.is.mobile ? 'margin-bottom: 40px' : ''" :style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })" v-touch-swipe="handleSwipe"
> >
<q-scroll-area <q-scroll-area
ref="timesheet_page" ref="timesheet_page"
:horizontal-offset="[0, 3]" :horizontal-offset="[0, 3]"
class="absolute-full hide-scrollbar" class="col absolute-full hide-scrollbar"
:style="mode === 'approval' ? `height: ${scroll_area_height}px;` : ''"
:thumb-style="{ opacity: '0' }" :thumb-style="{ opacity: '0' }"
:bar-style="{ opacity: '0' }" :bar-style="{ opacity: '0' }"
> >
<!-- Show if no timesheets found (further than one month from present) --> <!-- Show if no timesheets found (further than one month from present) -->
<div <div
v-if="timesheet_store.timesheets.length < 1" v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
class="col-auto column flex-center fit q-py-lg" class="col-auto column flex-center fit q-py-lg"
style="min-height: 20vh;" style="min-height: 20vh;"
> >
@ -92,6 +117,7 @@
<!-- Else show timesheets if found --> <!-- Else show timesheets if found -->
<div <div
v-else v-else
ref="timesheet_container"
class="col fit" class="col fit"
:class="$q.platform.is.mobile ? 'column' : 'row'" :class="$q.platform.is.mobile ? 'column' : 'row'"
> >
@ -106,7 +132,7 @@
leave-active-class="animated fadeOutUp" leave-active-class="animated fadeOutUp"
> >
<q-btn <q-btn
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1)" v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheet_store.has_timesheet_preset"
:disable="!timesheet.days.every(day => day.shifts.length < 1)" :disable="!timesheet.days.every(day => day.shifts.length < 1)"
flat flat
dense dense
@ -129,9 +155,11 @@
<div <div
v-for="day, day_index in timesheet.days" v-for="day, day_index in timesheet.days"
:key="day.date" :key="day.date"
:ref="getMobileDayRef(day.date)"
class="col-auto row q-pa-sm fit" class="col-auto row q-pa-sm fit"
:style="`animation-delay: ${day_index / 15}s;`" :style="`animation-delay: ${day_index / 15}s;`"
> >
<!-- mobile version in portrait mode -->
<div <div
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)" v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
class="col column full-width q-px-md q-py-sm" class="col column full-width q-px-md q-py-sm"
@ -192,6 +220,7 @@
</q-card> </q-card>
</div> </div>
<!-- desktop version -->
<div <div
v-else v-else
class="col row full-width rounded-10 ellipsis shadow-10" class="col row full-width rounded-10 ellipsis shadow-10"
@ -274,19 +303,21 @@
</template> </template>
<style scoped lang="scss"> <style
scoped
lang="scss"
>
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) { @each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
.mobile-rounded-#{$size} { .mobile-rounded-#{$size} {
border-radius: #{$size}px !important; border-radius: #{$size}px !important;
} }
.mobile-rounded-#{$size} > div:first-child { .mobile-rounded-#{$size}>div:first-child {
border-radius: #{$size}px #{$size}px 0 0 !important; border-radius: #{$size}px #{$size}px 0 0 !important;
} }
.mobile-rounded-#{$size} > div:last-child { .mobile-rounded-#{$size}>div:last-child {
border-radius: 0 0 #{$size}px #{$size}px !important; border-radius: 0 0 #{$size}px #{$size}px !important;
} }
} }
</style> </style>

View File

@ -4,11 +4,13 @@
> >
/* eslint-disable */ /* eslint-disable */
import ShiftList from 'src/modules/timesheets/components/shift-list.vue'; import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue'; import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue'; import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue'; import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue'; import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview.vue'; import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
@ -37,9 +39,41 @@
</script> </script>
<template> <template>
<div class="column items-center full-height"> <div class="column items-center full-height relative-position no-wrap">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<!-- label for approval mode to delimit that this is the timesheet -->
<span
v-if="mode === 'approval'"
class="col-auto text-uppercase text-bold text-h5"
>
{{ $t('timesheet.page_header') }}
</span>
<!-- weekly overview -->
<div class="col-auto row q-px-lg full-width">
<!-- supervisor weekly overview -->
<div class="col-xs-6 col-md-4 col-xl-3 q-pa-md">
<ShiftListWeeklyOverview mode="total-hours" />
</div>
<PageHeaderTemplate
v-if="mode === 'normal'"
:title="'timesheet.page_header'"
:start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col"
/>
<q-space v-else />
<!-- employee weekly overview -->
<div class="col-xs-6 col-md-4 col-xl-3 q-pa-md">
<ShiftListWeeklyOverview mode="off-hours" />
</div>
</div>
<!-- top menu --> <!-- top menu -->
<div <div
class="col-auto row items-center full-width" class="col-auto row items-center full-width"
@ -49,7 +83,7 @@
<PayPeriodNavigator <PayPeriodNavigator
v-if="mode === 'normal'" v-if="mode === 'normal'"
class="col-auto" class="col-auto"
@date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value)" @date-selected="timesheet_api.getTimesheetsByDate"
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod" @pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod" @pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
/> />
@ -66,9 +100,6 @@
@click="expenses_store.open" @click="expenses_store.open"
/> />
<!-- label for approval mode to delimit that this is the timesheet -->
<span v-if="mode === 'approval'" class="col-auto text-uppercase text-bold text-h5"> {{ $t('timesheet.page_header') }}</span>
<q-space v-if="$q.screen.width > $q.screen.height" /> <q-space v-if="$q.screen.width > $q.screen.height" />
<!-- desktop expenses button --> <!-- desktop expenses button -->
@ -100,9 +131,13 @@
<TimesheetErrorWidget class="col-auto" /> <TimesheetErrorWidget class="col-auto" />
<!-- mobile weekly overview widget --> <!-- mobile weekly overview widget -->
<ShiftListWeeklyOverview /> <ShiftListWeeklyOverviewMobile class="col-auto" />
<ShiftList :mode="mode" /> <ShiftList
:mode="mode"
:class="mode === 'normal' ? 'col' : 'col-auto'"
:style="mode === 'normal' ? '' : 'min-height: 100vh'"
/>
<q-btn <q-btn
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height" v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
@ -112,7 +147,7 @@
color="accent" color="accent"
icon="upload" icon="upload"
:label="$t('shared.label.save')" :label="$t('shared.label.save')"
class="col-auto absolute-bottom shadow-up-10 z-top" class="col-auto absolute-bottom shadow-up-10"
style="height: 50px;" style="height: 50px;"
@click="shift_api.saveShiftChanges" @click="shift_api.saveShiftChanges"
/> />

View File

@ -21,8 +21,8 @@ export const useShiftApi = () => {
const saveShiftChanges = async () => { const saveShiftChanges = async () => {
timesheet_store.is_loading = true; timesheet_store.is_loading = true;
const create_success = await shift_store.createNewShifts();
const update_success = await shift_store.updateShifts(); const update_success = await shift_store.updateShifts();
const create_success = await shift_store.createNewShifts();
if (create_success || update_success){ if (create_success || update_success){
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(auth_store.user?.email ?? '');

View File

@ -5,6 +5,7 @@ export const useTimesheetApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => { const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
timesheet_store.timesheets = [];
timesheet_store.is_loading = true; timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);

View File

@ -5,6 +5,7 @@ export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/; export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export interface TimesheetResponse { export interface TimesheetResponse {
has_preset_schedule: boolean;
employee_fullname: string; employee_fullname: string;
timesheets: Timesheet[]; timesheets: Timesheet[];
} }
@ -41,79 +42,4 @@ export interface TotalExpenses {
per_diem: number; per_diem: number;
on_call: number; on_call: number;
mileage: number; mileage: number;
} }
// export const test_timesheets: Timesheet[] = [
// {
// timehsid: 1,
// is_approved: false,
// weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
// weekly_expenses: { expenses: 15.5, mileage: 0 },
// days: [
// {
// date: '2025-10-18',
// daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
// daily_expenses: { expenses: 15.5, mileage: 0 },
// shifts: [
// { id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, },
// { id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
// ],
// expenses: [
// { id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
// ],
// },
// ],
// },
// {
// id: 2,
// is_approved: true,
// weekly_hours: {
// regular: 0,
// evening: 0,
// emergency: 0,
// overtime: 8,
// vacation: 0,
// holiday: 0,
// sick: 0,
// absent: 0,
// },
// weekly_expenses: {
// expenses: 0,
// mileage: 32.4,
// },
// days: [
// {
// date: '2025-10-27',
// daily_hours: {
// regular: 0,
// evening: 0,
// emergency: 0,
// overtime: 8,
// vacation: 0,
// holiday: 0,
// sick: 0,
// absent: 0,
// },
// daily_expenses: {
// expenses: 0,
// mileage: 32.4,
// },
// shifts: [
// { id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, },
// { id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
// ],
// expenses: [
// {
// id: 202,
// date: '2025-10-27',
// type: 'MILEAGE',
// amount: 0,
// mileage: 32.4,
// comment: 'Travel to client site',
// is_approved: true,
// },
// ],
// },
// ],
// },
// ];

View File

@ -2,15 +2,14 @@
setup setup
lang="ts" lang="ts"
> >
import { ref } from 'vue'; import MainCarousel from 'src/modules/dashboard/components/main-carousel.vue';
import ShortcutCard from 'src/modules/dashboard/components/employee/shortcut-card.vue';
const slide = ref<string>('welcome');
</script> </script>
<template> <template>
<q-page <q-page
padding padding
class="q-pa-md justify-center items-stretch" class="q-pa-md justify-center items-stretch bg-secondary"
:class="$q.platform.is.mobile ? 'column' : 'row'" :class="$q.platform.is.mobile ? 'column' : 'row'"
> >
<!-- left column --> <!-- left column -->
@ -19,71 +18,45 @@
</div> </div>
<!-- center column --> <!-- center column -->
<div class="column col-xs-12 col-md-8 col-xl-6 flex-center q-pa-md"> <div class="column col-xs-12 col-md-8 col-xl-6 items-center q-pa-md">
<q-carousel <div class="col-auto full-width q-py-md">
v-model="slide" <MainCarousel />
transition-prev="jump-right" </div>
transition-next="jump-left"
swipeable
animated
navigation
infinite
:autoplay="9001"
control-color="accent"
class="col-auto bg-dark rounded-15 shadow-18"
:style="$q.platform.is.mobile ? 'height: 60vh;' : 'height: 50vh;'"
>
<!-- welcome slide -->
<q-carousel-slide
name="welcome"
class="q-pa-none q-pb-xl fit"
>
<div class="column fit">
<q-img
src="src/assets/targo_building.png"
height="30vh"
position="50% 25%"
fit="cover"
class="col-auto"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.welcome_title') }}
</div>
</q-img>
<div class="column col q-mt-md q-px-md flex-center"> <span class="col-auto text-uppercase text-weight-bold self-start q-pt-md">{{ $t('dashboard.useful_links') }}</span>
<span class="col-auto">{{ $t('dashboard.carousel.welcome_message') }}</span>
</div>
</div>
</q-carousel-slide>
<!-- help page slide --> <div class="col row full-width justify-evenly items-start q-py-md">
<q-carousel-slide <div class="col-3 q-pa-sm">
name="tv" <ShortcutCard
class="q-pa-none q-pb-xl" image-source="src/assets/google_thumbnail.png"
> title="Google Workspace"
<div class="column fit"> route="https://mail.google.com/mail/u/0/#inbox"
<q-img />
src="src/assets/targo_help_banner.png" </div>
height="30vh"
position="50% 25%"
fit="none"
class="col-auto"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.help_title') }}
</div>
</q-img>
<div class="col column justify-center q-mt-md q-px-md"> <div class="col-3 q-pa-sm">
<span class="col-auto">{{ $t('dashboard.carousel.help_message') }}</span> <ShortcutCard
</div> image-source="src/assets/facturation_thumbnail.png"
</div> title="Facturation"
</q-carousel-slide> route="https://facturation.targo.ca/facturation/accueil.php?menu=ticket_open"
</q-carousel> />
</div>
<div class="col column"> <div class="col-3 q-pa-sm">
<span class="col-auto text-h6 text-uppercase"> </span> <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>
</div> </div>
@ -97,7 +70,7 @@
<iframe <iframe
title="Environment Canada Weather" title="Environment Canada Weather"
height="400px" height="400px"
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=e" src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=f"
allowtransparency="true" allowtransparency="true"
style="border: 0;" style="border: 0;"
class="col-auto" class="col-auto"

View File

@ -17,7 +17,7 @@
</script> </script>
<template> <template>
<q-page class="column items-center justify-start"> <q-page class="column items-center bg-secondary">
<AddModifyDialog /> <AddModifyDialog />
<PageHeaderTemplate title="employee_list.page_header" /> <PageHeaderTemplate title="employee_list.page_header" />

View File

@ -4,7 +4,7 @@
<template> <template>
<q-layout view="hHh lpR fFf"> <q-layout view="hHh lpR fFf">
<q-page-container class="bg-dark"> <q-page-container class="bg-blue-grey-10">
<q-page class="row"> <q-page class="row">
<q-img src="src/assets/village.png" fit="cover" :class="$q.screen.lt.md ? 'absolute-bottom' : 'absolute-right'" /> <q-img src="src/assets/village.png" fit="cover" :class="$q.screen.lt.md ? 'absolute-bottom' : 'absolute-right'" />
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" class="col-xs-10 absolute-center"> <transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" class="col-xs-10 absolute-center">

View File

@ -9,13 +9,27 @@
import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue'; import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { onMounted } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api'; import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const page_height = ref(0);
const headerComponent = ref<HTMLElement | null>(null);
const table_max_height = computed(() => {
const height = page_height.value - (headerComponent.value?.clientHeight ?? 0);
return height;
});
const tableStyleFunction = (offset: number, height: number) => {
page_height.value = height - offset;
return { minHeight: height - offset + 'px' };
};
onMounted(async () => { onMounted(async () => {
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD')); await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
}); });
@ -23,16 +37,9 @@
<template> <template>
<q-page <q-page
padding class="bg-secondary"
class="column q-pa-md bg-secondary" :style-fn="tableStyleFunction"
> >
<PageHeaderTemplate
title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col-auto"
/>
<DetailsDialog <DetailsDialog
:is-loading="timesheet_store.is_loading" :is-loading="timesheet_store.is_loading"
:employee-overview="timesheet_store.current_pay_period_overview" :employee-overview="timesheet_store.current_pay_period_overview"
@ -41,6 +48,24 @@
<OverviewReport /> <OverviewReport />
<OverviewList class="col" /> <div
class="column items-center scroll q-px-sm full-width"
style="min-height: inherit;"
>
<div
ref="headerComponent"
class="col-auto"
>
<PageHeaderTemplate
title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''"
/>
</div>
<div class="col-grow full-width">
<OverviewList :max-height="table_max_height" />
</div>
</div>
</q-page> </q-page>
</template> </template>

View File

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

View File

@ -1,12 +1,11 @@
export enum RouteNames { export enum RouteNames {
LOGIN = 'login', LOGIN = 'login',
LOGIN_SUCCESS = 'login-success', LOGIN_SUCCESS = 'login-success',
DASHBOARD = 'dashboard', DASHBOARD = '/',
TIMESHEET_APPROVALS = 'timesheets_approval', TIMESHEET_APPROVALS = 'timesheet-approvals',
EMPLOYEE_LIST = 'employee_list', EMPLOYEE_LIST = 'employees',
EMPLOYEE_MANAGEMENT = 'employee_management', PROFILE = 'profile',
PROFILE = 'personal_profile', TIMESHEET = 'timesheet',
TIMESHEET = 'timesheets',
HELP = 'help', HELP = 'help',
ERROR = 'error', ERROR = 'error',

View File

@ -33,7 +33,7 @@ const routes: RouteRecordRaw[] = [
meta: { required_module: ModuleNames.TIMESHEETS }, meta: { required_module: ModuleNames.TIMESHEETS },
}, },
{ {
path: 'user/profile', path: 'profile',
name: RouteNames.PROFILE, name: RouteNames.PROFILE,
component: () => import('src/pages/profile-page.vue'), component: () => import('src/pages/profile-page.vue'),
meta: { required_module: ModuleNames.PERSONAL_PROFILE }, meta: { required_module: ModuleNames.PERSONAL_PROFILE },

View File

@ -22,6 +22,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const is_details_dialog_open = ref(false); const is_details_dialog_open = ref(false);
const selected_employee_name = ref<string>(); const selected_employee_name = ref<string>();
const has_timesheet_preset = ref(false);
const current_pay_period_overview = ref<TimesheetApprovalOverview>(); const current_pay_period_overview = ref<TimesheetApprovalOverview>();
const is_approval_grid_mode = ref<boolean>(true); const is_approval_grid_mode = ref<boolean>(true);
const pay_period_report = ref(); const pay_period_report = ref();
@ -98,6 +99,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
if (response.success && response.data) { if (response.success && response.data) {
has_timesheet_preset.value = response.data.has_preset_schedule;
selected_employee_name.value = response.data.employee_fullname; selected_employee_name.value = response.data.employee_fullname;
timesheets.value = response.data.timesheets; timesheets.value = response.data.timesheets;
initial_timesheets.value = unwrapAndClone(timesheets.value); initial_timesheets.value = unwrapAndClone(timesheets.value);
@ -174,6 +176,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
current_pay_period_overview, current_pay_period_overview,
pay_period_infos, pay_period_infos,
selected_employee_name, selected_employee_name,
has_timesheet_preset,
timesheets, timesheets,
all_current_shifts, all_current_shifts,
initial_timesheets, initial_timesheets,

View File

@ -4,15 +4,17 @@ import { computed, ref } from 'vue';
import { LocalStorage, useQuasar, Dark } from 'quasar'; import { LocalStorage, useQuasar, Dark } from 'quasar';
import { Preferences } from 'src/modules/profile/models/preferences.models'; import { Preferences } from 'src/modules/profile/models/preferences.models';
import { ProfileService } from 'src/modules/profile/services/profile-service'; import { ProfileService } from 'src/modules/profile/services/profile-service';
import { RouteNames } from 'src/router/router-constants';
export const useUiStore = defineStore('ui', () => { export const useUiStore = defineStore('ui', () => {
const q = useQuasar(); const q = useQuasar();
const { locale } = useI18n(); const { locale } = useI18n();
const is_left_drawer_open = ref(false); const is_left_drawer_open = ref(true);
const focus_next_component = ref(false); const focus_next_component = ref(false);
const is_mobile_mode = computed(() => q.screen.lt.md); const is_mobile_mode = computed(() => q.screen.lt.md);
const user_preferences = ref<Preferences>(new Preferences); const user_preferences = ref<Preferences>(new Preferences);
const current_page = ref<RouteNames>(RouteNames.DASHBOARD);
const toggleRightDrawer = () => { const toggleRightDrawer = () => {
@ -69,6 +71,7 @@ export const useUiStore = defineStore('ui', () => {
} }
return { return {
current_page,
is_mobile_mode, is_mobile_mode,
focus_next_component, focus_next_component,
is_left_drawer_open, is_left_drawer_open,