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:
commit
07b52c854f
BIN
src/assets/google_thumbnail.png
Normal file
BIN
src/assets/google_thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/info-pannes.png
Normal file
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 |
|
|
@ -4,8 +4,9 @@ export default {
|
|||
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!",
|
||||
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: {
|
||||
label: "Centre d'aide",
|
||||
|
|
@ -207,6 +208,7 @@ export default {
|
|||
modify: "modify",
|
||||
close: "close",
|
||||
download: "download",
|
||||
open: "open",
|
||||
},
|
||||
misc: {
|
||||
or: "or",
|
||||
|
|
@ -243,6 +245,9 @@ export default {
|
|||
page_header: "Timesheet",
|
||||
week: "week",
|
||||
total_hours: "total hours: ",
|
||||
total_expenses: "total expenses: ",
|
||||
vacation_available: "vacation time available: ",
|
||||
sick_available: "sick time available: ",
|
||||
current_shifts: "shifts worked",
|
||||
apply_preset: "auto-fill",
|
||||
apply_preset_day: "Apply schedule to day",
|
||||
|
|
@ -329,6 +334,7 @@ export default {
|
|||
},
|
||||
print_report: {
|
||||
title: "Download options",
|
||||
description: "Choose what to include in the report",
|
||||
company: "companies",
|
||||
type: "type",
|
||||
shifts: "shifts",
|
||||
|
|
@ -345,7 +351,7 @@ export default {
|
|||
unverified: "pending",
|
||||
inactive: "inactive",
|
||||
filter_active: "show only active employees",
|
||||
filter_team: "",
|
||||
filter_team: "show my team only",
|
||||
},
|
||||
tooltip: {
|
||||
button_detailed_view: "detailed view",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ export default {
|
|||
dashboard: {
|
||||
carousel: {
|
||||
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 l’essentiel avec un design rafraîchie.",
|
||||
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: "L’application a été pensée pour être plus intuitive et moderne. En cas de doute, la page d’aide est à votre disposition.",
|
||||
},
|
||||
useful_links: "liens utiles",
|
||||
},
|
||||
help: {
|
||||
label: "Centre d'aide",
|
||||
|
|
@ -207,7 +208,8 @@ export default {
|
|||
update: "mettre à jour",
|
||||
modify: "modifier",
|
||||
close: "fermer",
|
||||
download: "téléchargement",
|
||||
download: "télécharger",
|
||||
open: "ouvrir",
|
||||
},
|
||||
misc: {
|
||||
or: "ou",
|
||||
|
|
@ -244,6 +246,9 @@ export default {
|
|||
page_header: "Carte de temps",
|
||||
week: "semaine",
|
||||
total_hours: "heures totales: ",
|
||||
total_expenses: "dépenses totales: ",
|
||||
vacation_available: "vacances disponibles: ",
|
||||
sick_available: "congés maladie disponible: ",
|
||||
current_shifts: "quarts entrées",
|
||||
apply_preset: "auto-remplir",
|
||||
apply_preset_day: "Appliquer horaire pour la journée",
|
||||
|
|
@ -330,6 +335,7 @@ export default {
|
|||
},
|
||||
print_report: {
|
||||
title: "options de téléchargement",
|
||||
description: "Choisissez ce qui sera inclu dans le rapport",
|
||||
company: "compagnies",
|
||||
type: "types de données",
|
||||
shifts: "quarts de travail",
|
||||
|
|
@ -345,8 +351,8 @@ export default {
|
|||
verified: "approuvé",
|
||||
unverified: "à vérifier",
|
||||
inactive: "inactif",
|
||||
filter_active: "",
|
||||
filter_team: "",
|
||||
filter_active: "montrer les employés inactifs",
|
||||
filter_team: "montrer mon équipe seulement",
|
||||
},
|
||||
tooltip: {
|
||||
button_detailed_view: "vue détaillée",
|
||||
|
|
|
|||
|
|
@ -2,21 +2,34 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { ref } from 'vue';
|
||||
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 ui_store = useUiStore();
|
||||
const router = useRouter();
|
||||
const is_mini = ref(true);
|
||||
|
||||
const goToPageName = (page_name: string) => {
|
||||
router.push({ name: page_name }).catch(err => {
|
||||
console.error('Error with Vue Router: ', err);
|
||||
const onClickDrawerPage = (page_name: RouteNames) => {
|
||||
is_mini.value = true;
|
||||
|
||||
router.push({ name: page_name }).catch(error => {
|
||||
console.error('failed to reach page: ', error);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -26,7 +39,13 @@
|
|||
router.push({ name: 'login' }).catch(err => {
|
||||
console.error('could not log you out: ', err);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (q.platform.is.mobile) {
|
||||
ui_store.is_left_drawer_open = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -39,102 +58,56 @@
|
|||
:mini="is_mini"
|
||||
@mouseenter="is_mini = false"
|
||||
@mouseleave="is_mini = true"
|
||||
class="bg-dark z-max column no-wrap"
|
||||
:class="!$q.platform.is.mobile && is_mini ? 'items-center' : 'items-start'"
|
||||
class="bg-dark z-max"
|
||||
>
|
||||
<!-- Home -->
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="home"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.home')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@click="goToPageName(RouteNames.DASHBOARD)"
|
||||
/>
|
||||
<q-scroll-area class="column fit">
|
||||
<div
|
||||
v-for="button, index in DRAWER_BUTTONS"
|
||||
:key="index"
|
||||
v-show="button.required_module ?? true"
|
||||
@click="onClickDrawerPage(button.route)"
|
||||
>
|
||||
<div
|
||||
v-if="button.required_module ? auth_store.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') : ''"
|
||||
>
|
||||
<q-icon
|
||||
:name="button.icon"
|
||||
color="accent"
|
||||
size="lg"
|
||||
class="col-auto q-pl-sm"
|
||||
/>
|
||||
|
||||
<!-- Timesheet Validation -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS_APPROVAL)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="event_available"
|
||||
: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)"
|
||||
/>
|
||||
<div
|
||||
class="col text-uppercase text-weight-bold text-h6 q-pl-sm"
|
||||
:class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
|
||||
>
|
||||
{{ $t(button.i18n_key) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee List -->
|
||||
<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)"
|
||||
/>
|
||||
<q-separator spaced />
|
||||
|
||||
<!-- Employee Timesheet -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="punch_clock"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet')"
|
||||
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)"
|
||||
/>
|
||||
<div
|
||||
class="row items-center full-width cursor-pointer q-py-sm"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<q-icon
|
||||
name="exit_to_app"
|
||||
color="accent"
|
||||
size="lg"
|
||||
class="col-auto q-pl-sm"
|
||||
/>
|
||||
|
||||
<!-- Profile -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.PERSONAL_PROFILE)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="account_box"
|
||||
: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"
|
||||
/>
|
||||
<div
|
||||
class="col text-uppercase text-weight-bold text-h6 q-pl-sm"
|
||||
:class="$q.platform.is.mobile ? '' : 'q-mini-drawer-hide'"
|
||||
>
|
||||
{{ $t('nav_bar.logout') }}
|
||||
</div>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
|
@ -32,10 +32,13 @@
|
|||
<template>
|
||||
<q-layout view="hHh lpR fFf">
|
||||
<HeaderBar />
|
||||
|
||||
<LeftDrawer />
|
||||
|
||||
<q-page-container>
|
||||
<router-view class="q-pa-sm bg-secondary" />
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
|
||||
<FooterBar />
|
||||
</q-layout>
|
||||
</template>
|
||||
|
|
@ -1,25 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
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';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
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 is_remembered = ref<boolean>(false);
|
||||
const is_employee_email = computed(() => email.value.includes('@targ'));
|
||||
const is_game_time = computed(() => email.value.includes('allumette'));
|
||||
const email = defineModel<string>('email', { default: '', });
|
||||
// const is_remembered = ref<boolean>(false);
|
||||
const is_employee_email = computed(() => email.value.includes('@targ'));
|
||||
const is_game_time = computed(() => email.value.includes('allumette'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card class="rounded-15 shadow-10 full-width">
|
||||
<q-card-section class="text-center bg-primary q-pa-lg">
|
||||
<q-card
|
||||
bordered
|
||||
class="rounded-15 shadow-10 full-width"
|
||||
>
|
||||
<div class="text-center bg-primary q-pa-lg">
|
||||
<q-img
|
||||
src="/src/assets/logo-targo-white.svg"
|
||||
ratio="4.6"
|
||||
fit="contain"
|
||||
/>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<div class="q-pt-sm q-px-xl q-pb-lg ">
|
||||
<q-card-section class="text-center text-uppercase">
|
||||
|
|
|
|||
42
src/modules/dashboard/components/employee/shortcut-card.vue
Normal file
42
src/modules/dashboard/components/employee/shortcut-card.vue
Normal 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>
|
||||
72
src/modules/dashboard/components/main-carousel.vue
Normal file
72
src/modules/dashboard/components/main-carousel.vue
Normal 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>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -58,21 +58,21 @@
|
|||
name="form"
|
||||
icon="las la-id-card"
|
||||
: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);"
|
||||
/>
|
||||
<q-tab
|
||||
name="access"
|
||||
icon="las la-key"
|
||||
: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);"
|
||||
/>
|
||||
<q-tab
|
||||
name="schedule"
|
||||
icon="calendar_month"
|
||||
: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);"
|
||||
/>
|
||||
</q-tabs>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,30 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
||||
// // add logic here to see if user has an avatar image and return that instead of initials
|
||||
// return first_name.charAt(0) + last_name.charAt(0);
|
||||
// };
|
||||
const q = useQuasar();
|
||||
const is_mouseover = ref(false);
|
||||
|
||||
const { row, index = -1 } = defineProps<{
|
||||
const { row, index = -1, isManagement = false } = defineProps<{
|
||||
row: EmployeeProfile
|
||||
index?: number
|
||||
isManagement?: boolean;
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
|
||||
defineEmits<{
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -24,10 +34,13 @@
|
|||
:style="`animation-delay: ${index / 25}s;`"
|
||||
>
|
||||
<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="(row.last_work_day === null ? ' ' : 'opacity: 0.6; ') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||
@click="emit('onProfileClick', row.email)"
|
||||
:style="getItemStyle()"
|
||||
@click="$emit('onProfileClick', row.email)"
|
||||
@mouseenter="is_mouseover = true"
|
||||
@mouseleave="is_mouseover = false"
|
||||
>
|
||||
<div class="col-auto column flex-center q-pt-md">
|
||||
<q-avatar
|
||||
|
|
@ -66,4 +79,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.item-mouse-hover {
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,13 +7,18 @@
|
|||
import { onMounted, ref } from 'vue';
|
||||
import { date, type QTableColumn } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-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';
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const auth_store = useAuthStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
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 table_grid_container = ref<HTMLElement | null>(null);
|
||||
|
|
@ -86,16 +91,33 @@
|
|||
:visible-columns="visible_columns"
|
||||
>
|
||||
<template #top>
|
||||
<div class="row full-width q-mb-sm">
|
||||
<div class="row flex-center full-width q-mb-sm">
|
||||
<q-btn
|
||||
push
|
||||
v-if="is_management"
|
||||
rounded
|
||||
color="accent"
|
||||
icon="las la-user-edit"
|
||||
:label="$t('shared.label.add')"
|
||||
class="text-uppercase"
|
||||
class="text-uppercase q-py-sm"
|
||||
@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-btn-toggle
|
||||
|
|
@ -131,15 +153,6 @@
|
|||
</template>
|
||||
</q-input>
|
||||
</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 #header="props">
|
||||
|
|
@ -170,7 +183,8 @@
|
|||
:key="props.rowIndex"
|
||||
:row="props.row"
|
||||
:index="props.rowIndex"
|
||||
@on-profile-click="employee_store.openAddModifyDialog"
|
||||
:is-management="is_management"
|
||||
@on-profile-click="email => is_management ? employee_store.openAddModifyDialog(email) : ''"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
|
|
@ -178,7 +192,7 @@
|
|||
<template #body-cell="scope">
|
||||
<q-td
|
||||
:props="scope"
|
||||
@click="employee_store.openAddModifyDialog(scope.row.email)"
|
||||
@click="is_management ? employee_store.openAddModifyDialog(scope.row.email) : ''"
|
||||
>
|
||||
<transition
|
||||
appear
|
||||
|
|
|
|||
|
|
@ -66,6 +66,28 @@
|
|||
>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
//default images
|
||||
// import default_dashboard from 'src/assets/help-ss/default-dashboard.png';
|
||||
import default_personal_profile from 'src/assets/help-ss/default-personnal_profile.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';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
//default images
|
||||
// import default_dashboard from 'src/assets/help-ss/default-dashboard.png';
|
||||
import default_personal_profile from 'src/assets/help-ss/default-personnal_profile.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> = {
|
||||
dashboard: '',
|
||||
personal_profile: default_personal_profile,
|
||||
timesheets: default_timesheet,
|
||||
employee_list: default_employee_list,
|
||||
employee_management: default_employee_management,
|
||||
timesheets_approval: default_validation_page,
|
||||
};
|
||||
const default_images: Record<UserModuleAccess, string> = {
|
||||
dashboard: '',
|
||||
personal_profile: default_personal_profile,
|
||||
timesheets: default_timesheet,
|
||||
employee_list: default_employee_list,
|
||||
employee_management: default_employee_management,
|
||||
timesheets_approval: default_validation_page,
|
||||
};
|
||||
|
||||
import type { HelpModuleOptions } from 'src/modules/help/models/help-module.model';
|
||||
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||
import { ref } from 'vue';
|
||||
import type { HelpModuleOptions } from 'src/modules/help/models/help-module.model';
|
||||
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
help_module: UserModuleAccess;
|
||||
options: HelpModuleOptions[];
|
||||
moduleIndex: number;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
help_module: UserModuleAccess;
|
||||
options: HelpModuleOptions[];
|
||||
moduleIndex: number;
|
||||
}>();
|
||||
|
||||
const help_module = props.help_module;
|
||||
const current_path = ref<string>(default_images[help_module]);
|
||||
const help_module = props.help_module;
|
||||
const current_path = ref<string>(default_images[help_module]);
|
||||
|
||||
const switchSide = (index: number) => {
|
||||
if (index % 2 !== 0) {
|
||||
return false
|
||||
const switchSide = (index: number) => {
|
||||
if (index % 2 !== 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
|
@ -45,14 +48,15 @@ const switchSide = (index: number) => {
|
|||
>
|
||||
<!-- Card Header -->
|
||||
<div
|
||||
class="row col-auto text-h5 q-pa-md text-primary bg-secondary"
|
||||
:class="switchSide(moduleIndex) ? 'justify-start' : 'justify-end'"
|
||||
class="row col-auto text-h4 text-weight-thin q-py-md q-px-xl bg-secondary"
|
||||
:class="switchSide(moduleIndex) ? 'justify-end' : 'justify-start'"
|
||||
>
|
||||
{{ ($t(`help.tutorial.${help_module}.title`)).toUpperCase() }}
|
||||
</div>
|
||||
<!-- Card Body -->
|
||||
|
||||
<!-- Card Body. The visual support image will alternate left and right -->
|
||||
<div class="row col full-width q-px-none">
|
||||
<!-- Object and descriptions zone -->
|
||||
<!-- Left-sided visual support -->
|
||||
<div
|
||||
class="col flex-center row"
|
||||
v-if="moduleIndex % 2 !== 0"
|
||||
|
|
@ -63,16 +67,17 @@ const switchSide = (index: number) => {
|
|||
leave-active-class="animated fade-out"
|
||||
>
|
||||
<q-img
|
||||
class="image-wrapper"
|
||||
:src="current_path"
|
||||
loading="lazy"
|
||||
fit="contain"
|
||||
class="rounded-10 image-wrapper"
|
||||
>
|
||||
</q-img>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="col column q-mx-sm">
|
||||
|
||||
<!-- Area with expandable help items -->
|
||||
<div class="col column q-px-md">
|
||||
<q-expansion-item
|
||||
v-for="option, index in options"
|
||||
:key="index"
|
||||
|
|
@ -112,7 +117,8 @@ const switchSide = (index: number) => {
|
|||
</div>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
<!-- images of the related selected option -->
|
||||
|
||||
<!-- right-sided visual support -->
|
||||
<div
|
||||
class="col flex-center row"
|
||||
v-if="moduleIndex % 2 === 0"
|
||||
|
|
@ -123,11 +129,10 @@ const switchSide = (index: number) => {
|
|||
leave-active-class="animated fade-out"
|
||||
>
|
||||
<q-img
|
||||
class="image-wrapper"
|
||||
:src="current_path"
|
||||
loading="lazy"
|
||||
fit="contain"
|
||||
style="width: 150%;"
|
||||
class="rounded-10 image-wrapper"
|
||||
>
|
||||
</q-img>
|
||||
</transition>
|
||||
|
|
|
|||
|
|
@ -23,36 +23,40 @@
|
|||
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
<q-item
|
||||
<div
|
||||
v-for="mode of dark_mode_options"
|
||||
:key="mode.label"
|
||||
clickable
|
||||
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"
|
||||
class="col q-pa-sm"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="mode.icon"
|
||||
size="md"
|
||||
:color="mode.quasar_value === $q.dark.mode ? 'white' : ''"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item
|
||||
clickable
|
||||
dense
|
||||
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"
|
||||
>
|
||||
<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-label> {{ $t(mode.label) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase justify-center">{{ $t(mode.label) }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
v-if="mode.quasar_value === $q.dark.mode"
|
||||
name="check"
|
||||
color="white"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
v-if="mode.quasar_value === $q.dark.mode"
|
||||
name="check"
|
||||
color="white"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@
|
|||
<div class="column text-uppercase text-center text-weight-bolder text-h4">
|
||||
<span
|
||||
v-if="!$q.platform.is.mobile"
|
||||
class="col q-mt-lg"
|
||||
>{{ $t(title) }}</span>
|
||||
class="col q-pt-lg"
|
||||
>
|
||||
{{ $t(title) }}
|
||||
</span>
|
||||
|
||||
<transition
|
||||
enter-active-class="animated fadeInDown"
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
v-model="calendar_date"
|
||||
color="primary"
|
||||
today-btn
|
||||
no-unset
|
||||
mask="YYYY-MM-DD"
|
||||
:options="date => date >= PAY_PERIOD_DATE_LIMIT"
|
||||
@update:model-value="onDateSelected"
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<!-- employee pay period details using chart -->
|
||||
<div
|
||||
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'"
|
||||
>
|
||||
<DetailsDialogChartHoursWorked class="col" />
|
||||
|
|
@ -50,8 +50,8 @@
|
|||
</div>
|
||||
|
||||
<!-- list of shifts -->
|
||||
<div class="col column no-wrap">
|
||||
<TimesheetWrapper mode="approval" class="col"/>
|
||||
<div class="col-auto column no-wrap">
|
||||
<TimesheetWrapper mode="approval" class="col-auto"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
|
|
|
|||
|
|
@ -15,17 +15,18 @@
|
|||
keep-color
|
||||
size="lg"
|
||||
color="accent"
|
||||
label="show inactive"
|
||||
:label="$t('timesheet_approvals.table.filter_active')"
|
||||
class="col"
|
||||
:class="filters.is_showing_inactive ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
|
||||
/>
|
||||
|
||||
<q-checkbox
|
||||
v-model="filters.is_showing_team_only"
|
||||
keep-color
|
||||
size="lg"
|
||||
val="team"
|
||||
color="accent"
|
||||
label="show team only"
|
||||
:label="$t('timesheet_approvals.table.filter_team')"
|
||||
class="col"
|
||||
:class="filters.is_showing_team_only ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,6 @@ import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-
|
|||
>
|
||||
<div
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -35,15 +35,18 @@
|
|||
'is_approved',
|
||||
]);
|
||||
|
||||
const { maxHeight } = defineProps<{
|
||||
maxHeight: number;
|
||||
}>();
|
||||
|
||||
const is_showing_filters = ref(false);
|
||||
const search_string = ref('');
|
||||
|
||||
const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview));
|
||||
const overview_filters = ref<PayPeriodOverviewFilters>({
|
||||
is_showing_inactive: false,
|
||||
is_showing_team_only: false,
|
||||
supervisors: [],
|
||||
name_search_string: search_string.value,
|
||||
name_search_string: '',
|
||||
});
|
||||
|
||||
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
|
||||
|
|
@ -80,211 +83,213 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-px-md full-height">
|
||||
<div class="full-width">
|
||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||
|
||||
<q-table
|
||||
:key="timesheet_store.is_approval_grid_mode ? 'grid' : 'list'"
|
||||
:visible-columns="VISIBLE_COLUMNS"
|
||||
:rows="overview_rows"
|
||||
:columns="pay_period_overview_columns"
|
||||
row-key="email"
|
||||
:grid="timesheet_store.is_approval_grid_mode"
|
||||
:dense="timesheet_store.is_approval_grid_mode"
|
||||
hide-pagination
|
||||
:pagination="{ sortBy: 'is_active' }"
|
||||
:filter="overview_filters"
|
||||
:filter-method="filterEmployeeRows"
|
||||
color="accent"
|
||||
:rows-per-page-options="[0]"
|
||||
card-container-class="justify-center"
|
||||
class="bg-transparent"
|
||||
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
|
||||
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
|
||||
:no-data-label="$t('shared.error.no_data_found')"
|
||||
:no-results-label="$t('shared.error.no_search_results')"
|
||||
:loading-label="$t('shared.label.loading')"
|
||||
:style="$q.platform.is.mobile ? '' : 'max-height: 70vh;'"
|
||||
>
|
||||
<template #top>
|
||||
<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 />
|
||||
<q-table
|
||||
dense
|
||||
row-key="email"
|
||||
color="accent"
|
||||
hide-pagination
|
||||
:rows="overview_rows"
|
||||
:columns="pay_period_overview_columns"
|
||||
:visible-columns="VISIBLE_COLUMNS"
|
||||
:grid="timesheet_store.is_approval_grid_mode"
|
||||
:pagination="{ sortBy: 'is_active' }"
|
||||
:filter="overview_filters"
|
||||
:filter-method="filterEmployeeRows"
|
||||
:rows-per-page-options="[0]"
|
||||
class="bg-transparent"
|
||||
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
|
||||
card-container-class="justify-center"
|
||||
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
|
||||
:no-data-label="$t('shared.error.no_data_found')"
|
||||
:no-results-label="$t('shared.error.no_search_results')"
|
||||
:loading-label="$t('shared.label.loading')"
|
||||
:style="overview_rows.length > 0 ? `max-height: ${maxHeight - (timesheet_store.is_approval_grid_mode ? 0 : 20)}px;` : ''"
|
||||
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
|
||||
>
|
||||
<template #top>
|
||||
<div class="column full-width">
|
||||
|
||||
<div
|
||||
class="col-auto row no-wrap items-start"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||
class="col-auto row items-start full-width q-px-lg"
|
||||
:class="$q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md'"
|
||||
>
|
||||
<q-btn-toggle
|
||||
v-model="timesheet_store.is_approval_grid_mode"
|
||||
push
|
||||
rounded
|
||||
color="white"
|
||||
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 },
|
||||
]"
|
||||
<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-btn
|
||||
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"
|
||||
/>
|
||||
<q-space />
|
||||
|
||||
<QTableFilters
|
||||
v-model:search="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 / 30}s;`"
|
||||
>
|
||||
<transition
|
||||
v-if="props.col.name === 'is_approved'"
|
||||
enter-active-class="animated swing"
|
||||
mode="out-in"
|
||||
<div
|
||||
class="col-auto row no-wrap items-start"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||
>
|
||||
<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 ? 'bg-accent' : ''"
|
||||
@click.stop="onClickApproveAll(props.row.email, props.row.is_approved)"
|
||||
<q-btn-toggle
|
||||
v-model="timesheet_store.is_approval_grid_mode"
|
||||
push
|
||||
rounded
|
||||
color="white"
|
||||
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;"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
<div v-else-if="props.col.name === 'employee_first_name'">
|
||||
<span class="text-h5 text-uppercase text-accent q-mr-xs">
|
||||
{{ props.value }}
|
||||
</span>
|
||||
<span class="text-uppercase text-weight-light">
|
||||
{{ props.row.employee_last_name }}
|
||||
<q-btn
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</transition>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</transition>
|
||||
</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 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>
|
||||
|
||||
<span class="text-h6">
|
||||
{{ message }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
<!-- Template for custome failed-to-load state -->
|
||||
<template #no-data="{ message, filter }">
|
||||
<div v-if="!timesheet_store.is_loading" class="full-width column items-center text-accent">
|
||||
<q-icon
|
||||
size="4em"
|
||||
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
||||
/>
|
||||
|
||||
<span class="text-h6">
|
||||
{{ message }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -305,4 +310,7 @@
|
|||
|
||||
tbody
|
||||
scroll-margin-top: 48px
|
||||
|
||||
.q-table__grid-content
|
||||
overflow: auto
|
||||
</style>
|
||||
|
|
@ -1,143 +1,163 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { 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 report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
|
||||
|
||||
const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>(
|
||||
Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as 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)
|
||||
);
|
||||
|
||||
interface ReportOptions {
|
||||
label: string;
|
||||
value: keyof TimesheetApprovalCSVReportFilters;
|
||||
};
|
||||
interface ReportOptions {
|
||||
label: string;
|
||||
value: keyof TimesheetApprovalCSVReportFilters;
|
||||
};
|
||||
|
||||
const company_options: ReportOptions[] = [
|
||||
{ label: 'Targo', value: 'targo' },
|
||||
{ label: 'Solucom', value: 'solucom' },
|
||||
];
|
||||
const company_options: ReportOptions[] = [
|
||||
{ label: 'Targo', value: 'targo' },
|
||||
{ label: 'Solucom', value: 'solucom' },
|
||||
];
|
||||
|
||||
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 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))
|
||||
);
|
||||
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))
|
||||
);
|
||||
|
||||
const onClickedDownload = async () => {
|
||||
try {
|
||||
const data = await timesheet_store.getPayPeriodReport(report_filter_options.value);
|
||||
const onClickedDownload = async () => {
|
||||
try {
|
||||
const data = await timesheet_store.getPayPeriodReport(report_filter_options.value);
|
||||
|
||||
const companies = Object.entries(report_filter_options.value)
|
||||
.filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-');
|
||||
const companies = Object.entries(report_filter_options.value)
|
||||
.filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-');
|
||||
|
||||
const types = Object.entries(report_filter_options.value)
|
||||
.filter(([key, value]) => value && ['shifts', 'expenses', 'holiday', 'vacation'].includes(key)).map(([key]) => key).join('-');
|
||||
const types = Object.entries(report_filter_options.value)
|
||||
.filter(([key, value]) => value && ['shifts', 'expenses', 'holiday', 'vacation'].includes(key)).map(([key]) => key).join('-');
|
||||
|
||||
const file_name = `Desjardins_${companies}_${types}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
const file_name = `Desjardins_${companies}_${types}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
|
||||
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.setAttribute('download', file_name);
|
||||
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)
|
||||
link.href = url;
|
||||
link.setAttribute('download', file_name);
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog v-model="timesheet_store.is_report_dialog_open">
|
||||
<div class="bg-secondary full-width shadow-24 rounded-10 column">
|
||||
<div class="shadow-1 bg-primary text-accent text-weight-bold text-center text-uppercase">
|
||||
<span> {{ $t('timesheet_approvals.print_report.title') }}</span>
|
||||
<div
|
||||
class="column bg-secondary shadow-24 rounded-10"
|
||||
:style="$q.dark.isActive ? 'border: 2px solid var(--q-accent)' : ''"
|
||||
>
|
||||
<!-- main header -->
|
||||
<div class="col-auto bg-primary text-accent text-weight-bolder text-center text-uppercase text-h6 q-py-xs z-top">
|
||||
{{ $t('timesheet_approvals.print_report.title') }}
|
||||
</div>
|
||||
<div class="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">
|
||||
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3">
|
||||
{{ $t('timesheet_approvals.print_report.company') }}
|
||||
</span>
|
||||
<div class="row bordered-primary col-auto full-width">
|
||||
<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"
|
||||
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()"
|
||||
|
||||
<!-- 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">
|
||||
{{ $t('timesheet_approvals.print_report.description') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- company header -->
|
||||
<span class="col-auto q-px-sm q-pt-md text-weight-medium text-accent text-uppercase">
|
||||
{{ $t('timesheet_approvals.print_report.company') }}
|
||||
</span>
|
||||
|
||||
<!-- 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>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -209,6 +209,18 @@
|
|||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<template>
|
||||
<div
|
||||
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 -->
|
||||
<div
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
class="col row flex-center"
|
||||
>
|
||||
<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') : ''"
|
||||
:style="day.shifts.length > 0 ? '' : 'opacity: 0.5'"
|
||||
>
|
||||
|
|
@ -149,6 +149,20 @@
|
|||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@
|
|||
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.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 timesheet_api = useTimesheetApi();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_error_message = ref<string | undefined>();
|
||||
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
flat
|
||||
dense
|
||||
|
|
@ -86,7 +88,7 @@ import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
|||
:key="shift_index"
|
||||
class="col-auto"
|
||||
>
|
||||
<ShiftListDayRowMobile
|
||||
<ShiftListDayRowMobile
|
||||
v-if="$q.platform.is.mobile"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:is-timesheet-approved="approved"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -5,22 +5,25 @@
|
|||
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 } from 'quasar';
|
||||
import { computed, ref } from 'vue';
|
||||
import { date, useQuasar } from 'quasar';
|
||||
import { computed, ref, watch } 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 { QScrollArea } from 'quasar';
|
||||
import type { QScrollArea, TouchSwipeValue } from 'quasar';
|
||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||
|
||||
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const { extractDate } = date;
|
||||
const q = useQuasar();
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
const { mode = 'normal'} = defineProps<{
|
||||
const { mode = 'normal' } = defineProps<{
|
||||
mode: 'normal' | 'approval';
|
||||
}>();
|
||||
|
||||
|
|
@ -28,7 +31,12 @@
|
|||
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
||||
|
||||
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) => {
|
||||
ui_store.focus_next_component = true;
|
||||
|
|
@ -44,37 +52,54 @@
|
|||
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 handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: { x?: number, y?: number }) => {
|
||||
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||
if (distance.x && Math.abs(distance.x) > 10) {
|
||||
await timesheet_api.getTimesheetsBySwiping(direction === 'left' ? 1 : -1)
|
||||
const handleSwipe: TouchSwipeValue = (details) => {
|
||||
mobile_animation_direction.value = details.direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||
if (details.distance && details.distance.x && Math.abs(details.distance.x) > 10) {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="col column fit relative-position"
|
||||
:style="$q.platform.is.mobile ? 'margin-bottom: 40px' : ''"
|
||||
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })"
|
||||
class="column fit relative-position"
|
||||
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
|
||||
v-touch-swipe="handleSwipe"
|
||||
>
|
||||
<q-scroll-area
|
||||
ref="timesheet_page"
|
||||
: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' }"
|
||||
:bar-style="{ opacity: '0' }"
|
||||
>
|
||||
<!-- Show if no timesheets found (further than one month from present) -->
|
||||
<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"
|
||||
style="min-height: 20vh;"
|
||||
>
|
||||
|
|
@ -92,6 +117,7 @@
|
|||
<!-- Else show timesheets if found -->
|
||||
<div
|
||||
v-else
|
||||
ref="timesheet_container"
|
||||
class="col fit"
|
||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||
>
|
||||
|
|
@ -106,7 +132,7 @@
|
|||
leave-active-class="animated fadeOutUp"
|
||||
>
|
||||
<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)"
|
||||
flat
|
||||
dense
|
||||
|
|
@ -129,9 +155,11 @@
|
|||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day.date"
|
||||
:ref="getMobileDayRef(day.date)"
|
||||
class="col-auto row q-pa-sm fit"
|
||||
:style="`animation-delay: ${day_index / 15}s;`"
|
||||
>
|
||||
<!-- mobile version in portrait mode -->
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||
class="col column full-width q-px-md q-py-sm"
|
||||
|
|
@ -192,6 +220,7 @@
|
|||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- desktop version -->
|
||||
<div
|
||||
v-else
|
||||
class="col row full-width rounded-10 ellipsis shadow-10"
|
||||
|
|
@ -274,19 +303,21 @@
|
|||
|
||||
</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) {
|
||||
.mobile-rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
.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: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;
|
||||
}
|
||||
.mobile-rounded-#{$size}>div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -4,11 +4,13 @@
|
|||
>
|
||||
/* eslint-disable */
|
||||
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 PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.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/mobile/shift-list-weekly-overview.vue';
|
||||
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/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 { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
|
@ -37,9 +39,41 @@
|
|||
</script>
|
||||
|
||||
<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" />
|
||||
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="col-auto row items-center full-width"
|
||||
|
|
@ -49,7 +83,7 @@
|
|||
<PayPeriodNavigator
|
||||
v-if="mode === 'normal'"
|
||||
class="col-auto"
|
||||
@date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value)"
|
||||
@date-selected="timesheet_api.getTimesheetsByDate"
|
||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||
/>
|
||||
|
|
@ -66,9 +100,6 @@
|
|||
@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" />
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
|
|
@ -100,9 +131,13 @@
|
|||
<TimesheetErrorWidget class="col-auto" />
|
||||
|
||||
<!-- 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
|
||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
|
|
@ -112,7 +147,7 @@
|
|||
color="accent"
|
||||
icon="upload"
|
||||
: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;"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export const useShiftApi = () => {
|
|||
const saveShiftChanges = async () => {
|
||||
timesheet_store.is_loading = true;
|
||||
|
||||
const create_success = await shift_store.createNewShifts();
|
||||
const update_success = await shift_store.updateShifts();
|
||||
const create_success = await shift_store.createNewShifts();
|
||||
|
||||
if (create_success || update_success){
|
||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(auth_store.user?.email ?? '');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const useTimesheetApi = () => {
|
|||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
||||
timesheet_store.timesheets = [];
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 interface TimesheetResponse {
|
||||
has_preset_schedule: boolean;
|
||||
employee_fullname: string;
|
||||
timesheets: Timesheet[];
|
||||
}
|
||||
|
|
@ -41,79 +42,4 @@ export interface TotalExpenses {
|
|||
per_diem: number;
|
||||
on_call: 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,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
}
|
||||
|
|
@ -2,15 +2,14 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const slide = ref<string>('welcome');
|
||||
import MainCarousel from 'src/modules/dashboard/components/main-carousel.vue';
|
||||
import ShortcutCard from 'src/modules/dashboard/components/employee/shortcut-card.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page
|
||||
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'"
|
||||
>
|
||||
<!-- left column -->
|
||||
|
|
@ -19,71 +18,45 @@
|
|||
</div>
|
||||
|
||||
<!-- center column -->
|
||||
<div class="column col-xs-12 col-md-8 col-xl-6 flex-center q-pa-md">
|
||||
<q-carousel
|
||||
v-model="slide"
|
||||
transition-prev="jump-right"
|
||||
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-xs-12 col-md-8 col-xl-6 items-center q-pa-md">
|
||||
<div class="col-auto full-width q-py-md">
|
||||
<MainCarousel />
|
||||
</div>
|
||||
|
||||
<div class="column col q-mt-md q-px-md flex-center">
|
||||
<span class="col-auto">{{ $t('dashboard.carousel.welcome_message') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
<span class="col-auto text-uppercase text-weight-bold self-start q-pt-md">{{ $t('dashboard.useful_links') }}</span>
|
||||
|
||||
<!-- help page slide -->
|
||||
<q-carousel-slide
|
||||
name="tv"
|
||||
class="q-pa-none q-pb-xl"
|
||||
>
|
||||
<div class="column fit">
|
||||
<q-img
|
||||
src="src/assets/targo_help_banner.png"
|
||||
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 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 column justify-center q-mt-md q-px-md">
|
||||
<span class="col-auto">{{ $t('dashboard.carousel.help_message') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
<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 column">
|
||||
<span class="col-auto text-h6 text-uppercase"> </span>
|
||||
<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>
|
||||
|
||||
|
|
@ -97,7 +70,7 @@
|
|||
<iframe
|
||||
title="Environment Canada Weather"
|
||||
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"
|
||||
style="border: 0;"
|
||||
class="col-auto"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="column items-center justify-start">
|
||||
<q-page class="column items-center bg-secondary">
|
||||
<AddModifyDialog />
|
||||
|
||||
<PageHeaderTemplate title="employee_list.page_header" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<template>
|
||||
<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-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">
|
||||
|
|
|
|||
|
|
@ -9,13 +9,27 @@
|
|||
import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
|
||||
|
||||
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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||
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 () => {
|
||||
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||
});
|
||||
|
|
@ -23,16 +37,9 @@
|
|||
|
||||
<template>
|
||||
<q-page
|
||||
padding
|
||||
class="column q-pa-md bg-secondary"
|
||||
class="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
|
||||
:is-loading="timesheet_store.is_loading"
|
||||
:employee-overview="timesheet_store.current_pay_period_overview"
|
||||
|
|
@ -41,6 +48,24 @@
|
|||
|
||||
<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>
|
||||
</template>
|
||||
|
|
@ -2,29 +2,19 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const { user } = useAuthStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<q-page
|
||||
padding
|
||||
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
|
||||
class="col column fit"
|
||||
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
export enum RouteNames {
|
||||
LOGIN = 'login',
|
||||
LOGIN_SUCCESS = 'login-success',
|
||||
DASHBOARD = 'dashboard',
|
||||
TIMESHEET_APPROVALS = 'timesheets_approval',
|
||||
EMPLOYEE_LIST = 'employee_list',
|
||||
EMPLOYEE_MANAGEMENT = 'employee_management',
|
||||
PROFILE = 'personal_profile',
|
||||
TIMESHEET = 'timesheets',
|
||||
DASHBOARD = '/',
|
||||
TIMESHEET_APPROVALS = 'timesheet-approvals',
|
||||
EMPLOYEE_LIST = 'employees',
|
||||
PROFILE = 'profile',
|
||||
TIMESHEET = 'timesheet',
|
||||
HELP = 'help',
|
||||
|
||||
ERROR = 'error',
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const routes: RouteRecordRaw[] = [
|
|||
meta: { required_module: ModuleNames.TIMESHEETS },
|
||||
},
|
||||
{
|
||||
path: 'user/profile',
|
||||
path: 'profile',
|
||||
name: RouteNames.PROFILE,
|
||||
component: () => import('src/pages/profile-page.vue'),
|
||||
meta: { required_module: ModuleNames.PERSONAL_PROFILE },
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
|
||||
const is_details_dialog_open = ref(false);
|
||||
const selected_employee_name = ref<string>();
|
||||
const has_timesheet_preset = ref(false);
|
||||
const current_pay_period_overview = ref<TimesheetApprovalOverview>();
|
||||
const is_approval_grid_mode = ref<boolean>(true);
|
||||
const pay_period_report = ref();
|
||||
|
|
@ -98,6 +99,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
}
|
||||
|
||||
if (response.success && response.data) {
|
||||
has_timesheet_preset.value = response.data.has_preset_schedule;
|
||||
selected_employee_name.value = response.data.employee_fullname;
|
||||
timesheets.value = response.data.timesheets;
|
||||
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
||||
|
|
@ -174,6 +176,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
current_pay_period_overview,
|
||||
pay_period_infos,
|
||||
selected_employee_name,
|
||||
has_timesheet_preset,
|
||||
timesheets,
|
||||
all_current_shifts,
|
||||
initial_timesheets,
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@ import { computed, ref } from 'vue';
|
|||
import { LocalStorage, useQuasar, 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(false);
|
||||
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 toggleRightDrawer = () => {
|
||||
|
|
@ -69,6 +71,7 @@ export const useUiStore = defineStore('ui', () => {
|
|||
}
|
||||
|
||||
return {
|
||||
current_page,
|
||||
is_mobile_mode,
|
||||
focus_next_component,
|
||||
is_left_drawer_open,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user