fix(approvals): progress on layout, dynamic resizing of table scroll area, UI/UX improvements, redo of left drawer

This commit is contained in:
Nicolas Drolet 2025-12-30 17:15:47 -05:00
parent 720417ab16
commit 8989a7d9c0
18 changed files with 501 additions and 472 deletions

View File

@ -97,7 +97,8 @@ export default defineConfig((ctx) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: { devServer: {
// https: true, // https: true,
open: true // opens browser window automatically open: true, // opens browser window automatically
allowedHosts: true
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework

Binary file not shown.

Before

Width:  |  Height:  |  Size: 938 KiB

After

Width:  |  Height:  |  Size: 578 KiB

View File

@ -329,6 +329,7 @@ export default {
}, },
print_report: { print_report: {
title: "Download options", title: "Download options",
description: "Choose what to include in the report",
company: "companies", company: "companies",
type: "type", type: "type",
shifts: "shifts", shifts: "shifts",
@ -345,7 +346,7 @@ export default {
unverified: "pending", unverified: "pending",
inactive: "inactive", inactive: "inactive",
filter_active: "show only active employees", filter_active: "show only active employees",
filter_team: "", filter_team: "show my team only",
}, },
tooltip: { tooltip: {
button_detailed_view: "detailed view", button_detailed_view: "detailed view",

View File

@ -207,7 +207,7 @@ export default {
update: "mettre à jour", update: "mettre à jour",
modify: "modifier", modify: "modifier",
close: "fermer", close: "fermer",
download: "téléchargement", download: "télécharger",
}, },
misc: { misc: {
or: "ou", or: "ou",
@ -330,6 +330,7 @@ export default {
}, },
print_report: { print_report: {
title: "options de téléchargement", title: "options de téléchargement",
description: "Choisissez ce qui sera inclu dans le rapport",
company: "compagnies", company: "compagnies",
type: "types de données", type: "types de données",
shifts: "quarts de travail", shifts: "quarts de travail",
@ -345,8 +346,8 @@ export default {
verified: "approuvé", verified: "approuvé",
unverified: "à vérifier", unverified: "à vérifier",
inactive: "inactif", inactive: "inactif",
filter_active: "", filter_active: "montrer les employés inactifs",
filter_team: "", filter_team: "montrer mon équipe seulement",
}, },
tooltip: { tooltip: {
button_detailed_view: "vue détaillée", button_detailed_view: "vue détaillée",

View File

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

View File

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

View File

@ -86,16 +86,32 @@
:visible-columns="visible_columns" :visible-columns="visible_columns"
> >
<template #top> <template #top>
<div class="row full-width q-mb-sm"> <div class="row flex-center full-width q-mb-sm">
<q-btn <q-btn
push rounded
color="accent" color="accent"
icon="las la-user-edit" icon="las la-user-edit"
:label="$t('shared.label.add')" :label="$t('shared.label.add')"
class="text-uppercase" class="text-uppercase q-py-sm"
@click.stop="_evt => employee_store.openAddModifyDialog()" @click.stop="_evt => employee_store.openAddModifyDialog()"
/> />
<q-checkbox
v-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"
:class="filters.hide_inactive_users ? 'rounded-25 bg-accent' : ''"
>
<q-icon
name="las la-user-times"
:color="filters.hide_inactive_users ? 'white' : 'negative'"
size="sm"
class="q-px-sm"
/>
</q-checkbox>
<q-space /> <q-space />
<q-btn-toggle <q-btn-toggle
@ -131,15 +147,6 @@
</template> </template>
</q-input> </q-input>
</div> </div>
<div class="row">
<q-space />
<q-checkbox
v-model="filters.hide_inactive_users"
color="accent"
:label="$t('employee_management.filter.hide_terminated')"
/>
</div>
</template> </template>
<template #header="props"> <template #header="props">

View File

@ -23,17 +23,20 @@
:class="ui_store.is_mobile_mode ? 'column' : 'row'" :class="ui_store.is_mobile_mode ? 'column' : 'row'"
style="border: 1px solid var(--q-accent);" style="border: 1px solid var(--q-accent);"
> >
<q-item <div
v-for="mode of dark_mode_options" v-for="mode of dark_mode_options"
:key="mode.label" :key="mode.label"
class="col q-pa-sm"
>
<q-item
clickable clickable
dense dense
v-ripple v-ripple
class="col row rounded-5 q-ma-sm shadow-4" 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' : '')" :class="(mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : '') + ($q.platform.is.mobile ? ' full-width q-py-xs' : '')"
@click="ui_store.user_preferences.is_dark_mode = mode.value" @click="ui_store.user_preferences.is_dark_mode = mode.value"
> >
<q-item-section avatar> <q-item-section side>
<q-icon <q-icon
:name="mode.icon" :name="mode.icon"
size="md" size="md"
@ -41,8 +44,8 @@
/> />
</q-item-section> </q-item-section>
<q-item-section class="text-uppercase justify-center"> <q-item-section>
<q-item-label> {{ $t(mode.label) }}</q-item-label> <q-item-label class="text-uppercase justify-center">{{ $t(mode.label) }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
@ -54,6 +57,7 @@
</q-item-section> </q-item-section>
</q-item> </q-item>
</div> </div>
</div>
<div <div
class="col-auto row text-uppercase text-weight-bold text-accent" class="col-auto row text-uppercase text-weight-bold text-accent"

View File

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

View File

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

View File

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

View File

@ -35,6 +35,10 @@
'is_approved', 'is_approved',
]); ]);
const { maxHeight } = defineProps<{
maxHeight: number;
}>();
const is_showing_filters = ref(false); const is_showing_filters = ref(false);
const search_string = ref(''); const search_string = ref('');
@ -80,31 +84,30 @@
</script> </script>
<template> <template>
<div class="q-px-md full-height"> <div class="full-width">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<q-table <q-table
:key="timesheet_store.is_approval_grid_mode ? 'grid' : 'list'" dense
:visible-columns="VISIBLE_COLUMNS" row-key="email"
color="accent"
hide-pagination
:rows="overview_rows" :rows="overview_rows"
:columns="pay_period_overview_columns" :columns="pay_period_overview_columns"
row-key="email" :visible-columns="VISIBLE_COLUMNS"
:grid="timesheet_store.is_approval_grid_mode" :grid="timesheet_store.is_approval_grid_mode"
:dense="timesheet_store.is_approval_grid_mode"
hide-pagination
:pagination="{ sortBy: 'is_active' }" :pagination="{ sortBy: 'is_active' }"
:filter="overview_filters" :filter="overview_filters"
:filter-method="filterEmployeeRows" :filter-method="filterEmployeeRows"
color="accent"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
card-container-class="justify-center"
class="bg-transparent" class="bg-transparent"
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'" :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" 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-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')" :loading-label="$t('shared.label.loading')"
:style="$q.platform.is.mobile ? '' : 'max-height: 70vh;'" :style="`max-height: ${maxHeight}px;`"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
> >
<template #top> <template #top>
<div class="column full-width"> <div class="column full-width">
@ -219,7 +222,7 @@
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)" :key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5" class="rounded-5"
style="font-size: 1.2em;" style="font-size: 1.2em;"
:style="`animation-delay: ${props.rowIndex / 30}s;`" :style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
> >
<transition <transition
v-if="props.col.name === 'is_approved'" v-if="props.col.name === 'is_approved'"
@ -233,13 +236,16 @@
:icon="props.value ? 'lock' : 'lock_open'" :icon="props.value ? 'lock' : 'lock_open'"
:color="props.value ? 'white' : 'grey-5'" :color="props.value ? 'white' : 'grey-5'"
class="rounded-5 " class="rounded-5 "
:class="props.value ? 'bg-accent' : ''" :class="props.value ? (props.row.is_active ? 'bg-accent' : 'bg-negative') : ''"
@click.stop="onClickApproveAll(props.row.email, props.row.is_approved)" @click.stop="onClickApproveAll(props.row.email, !props.row.is_approved)"
/> />
</transition> </transition>
<div v-else-if="props.col.name === 'employee_first_name'"> <div v-else-if="props.col.name === 'employee_first_name'">
<span class="text-h5 text-uppercase text-accent q-mr-xs"> <span
class="text-h5 text-uppercase q-mr-xs"
:class="props.row.is_active ? 'text-accent' : 'text-negative'"
>
{{ props.value }} {{ props.value }}
</span> </span>
<span class="text-uppercase text-weight-light"> <span class="text-uppercase text-weight-light">
@ -305,4 +311,7 @@
tbody tbody
scroll-margin-top: 48px scroll-margin-top: 48px
.q-table__grid-content
overflow: auto
</style> </style>

View File

@ -1,4 +1,7 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models'; import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
@ -69,16 +72,35 @@ watch(selected_report_filters, (new_values) => {
<template> <template>
<q-dialog v-model="timesheet_store.is_report_dialog_open"> <q-dialog v-model="timesheet_store.is_report_dialog_open">
<div class="bg-secondary full-width shadow-24 rounded-10 column"> <div
<div class="shadow-1 bg-primary text-accent text-weight-bold text-center text-uppercase"> class="column bg-secondary shadow-24 rounded-10"
<span> {{ $t('timesheet_approvals.print_report.title') }}</span> :style="$q.dark.isActive ? 'border: 2px solid var(--q-accent)' : ''"
>
<!-- main header -->
<div class="col-auto bg-primary text-accent text-weight-bolder text-center text-uppercase text-h6 q-py-xs z-top">
{{ $t('timesheet_approvals.print_report.title') }}
</div> </div>
<div class="row q-py-md q-px-lg">
<div class="col-auto full-width shadow-1 row bg-dark q-py-xs q-px-lg rounded-10"> <!-- info blurb -->
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3"> <div class="col-auto row flex-center full-width bg-dark shadow-2">
<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') }} {{ $t('timesheet_approvals.print_report.company') }}
</span> </span>
<div class="row bordered-primary col-auto full-width">
<!-- company options -->
<div class="col row text-uppercase full-width q-px-md">
<div <div
v-for="company, index in company_options" v-for="company, index in company_options"
:key="index" :key="index"
@ -88,23 +110,24 @@ watch(selected_report_filters, (new_values) => {
v-model="selected_report_filters" v-model="selected_report_filters"
left-label left-label
color="white" color="white"
class="q-px-md shadow-4 rounded-25 full-width"
dense dense
:class="selected_report_filters.includes(company.value) ? 'bg-accent text-white' : 'bg-dark'"
:label="$t(company.label)" :label="$t(company.label)"
:val="company.value" :val="company.value"
checked-icon="download" checked-icon="download"
unchecked-icon="highlight_off" 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>
</div> </div>
</div>
<div class="row q-py-md"> <!-- shift type header -->
<div class="col-auto full-width shadow-1 row bg-dark q-px-lg rounded-10 q-pb-sm"> <span class="col-auto q-px-sm q-pt-md text-weight-medium text-uppercase text-accent">
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3">
{{ $t('timesheet_approvals.print_report.options') }} {{ $t('timesheet_approvals.print_report.options') }}
</span> </span>
<div class=" row bordered-primary col-auto full-width">
<!-- shift type options -->
<div class="col row text-uppercase full-width q-px-md q-pb-md">
<div <div
v-for="type, index in type_options" v-for="type, index in type_options"
:key="index" :key="index"
@ -114,30 +137,27 @@ watch(selected_report_filters, (new_values) => {
v-model="selected_report_filters" v-model="selected_report_filters"
left-label left-label
color="white" color="white"
class="q-px-md shadow-4 rounded-25 full-width"
dense dense
:class="selected_report_filters.includes(type.value) ? 'bg-accent text-white' : 'bg-dark'"
:label="$t(type.label)"
:val="type.value" :val="type.value"
checked-icon="download" checked-icon="download"
unchecked-icon="highlight_off" 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>
</div> </div>
</div>
</div> <!-- download button -->
</div>
<div class="column">
<q-btn <q-btn
:disable="!is_download_button_enable" :disable="!is_download_button_enable"
square square
size="md"
icon="download" icon="download"
:color="is_download_button_enable ? 'accent' : 'grey-5'" :color="is_download_button_enable ? 'accent' : 'grey-5'"
:label="$t('shared.label.download')" :label="$t('shared.label.download')"
class="col-auto q-py-sm shadow-up-2"
@click="onClickedDownload()" @click="onClickedDownload()"
/> />
</div> </div>
</div>
</q-dialog> </q-dialog>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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