fix(approvals): fix sizing issue, multiple ui bugs in details window, separate scrollable timesheet from static one

This commit is contained in:
Nicolas Drolet 2026-01-06 15:10:27 -05:00
parent 07b52c854f
commit ec466bf6f2
4 changed files with 397 additions and 331 deletions

View File

@ -0,0 +1,102 @@
<script
setup
lang="ts"
>
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import { useQuasar } from 'quasar';
import { computed, ref, watch } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { QScrollArea, TouchSwipeValue } from 'quasar';
const q = useQuasar();
const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi();
const { mode = 'normal' } = defineProps<{
mode: 'normal' | 'approval';
}>();
const mobile_animation_direction = ref('fadeInLeft');
const timesheet_page = ref<QScrollArea | null>(null);
const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent);
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0);
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));
}
};
watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) {
timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800);
return;
}
})
</script>
<template>
<div
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="col absolute-full hide-scrollbar"
: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 && !timesheet_store.is_loading"
class="col-auto column flex-center fit q-py-lg"
style="min-height: 20vh;"
>
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
}}</span>
<q-icon
name="las la-calendar"
color="accent"
size="10em"
class="absolute"
style="opacity: 0.2;"
/>
</div>
<!-- Else show timesheets if found -->
<ShiftList />
</q-scroll-area>
<q-page-sticky
v-if="mode === 'normal'"
position="bottom-right"
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
class="z-top"
>
<transition
appear
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut"
>
<q-btn
v-if="scroll_y > 400"
fab
icon="las la-chevron-up"
color="white"
text-color="accent"
class="shadow-12"
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
/>
</transition>
</q-page-sticky>
</div>
</template>

View File

@ -5,39 +5,27 @@
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
import { date, useQuasar } from 'quasar';
import { computed, ref, watch } from 'vue';
import { ref, computed } 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, TouchSwipeValue } from 'quasar';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
import { date } from 'quasar';
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<{
mode: 'normal' | 'approval';
}>();
const mobile_animation_direction = ref('fadeInLeft');
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
const timesheet_page = ref<QScrollArea | null>(null);
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;
const new_shift = new Shift;
@ -59,72 +47,20 @@
return day.shifts.every(shift => shift.is_approved === true);
};
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="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="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 && !timesheet_store.is_loading"
class="col-auto column flex-center fit q-py-lg"
style="min-height: 20vh;"
>
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
}}</span>
<q-icon
name="las la-calendar"
color="accent"
size="10em"
class="absolute"
style="opacity: 0.2;"
/>
</div>
<!-- Else show timesheets if found -->
<div
v-else
ref="timesheet_container"
class="col fit"
class="fit"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
<div
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
:key="timesheet.timesheet_id"
class="col column fit flex-center"
class="col column fit items-center"
>
<transition
appear
@ -156,7 +92,7 @@
v-for="day, day_index in timesheet.days"
:key="day.date"
:ref="getMobileDayRef(day.date)"
class="col-auto row q-pa-sm fit"
class="col-auto row q-pa-sm full-width"
:style="`animation-delay: ${day_index / 15}s;`"
>
<!-- mobile version in portrait mode -->
@ -165,8 +101,8 @@
class="col column full-width q-px-md q-py-sm"
>
<q-card
class="mobile-rounded-10 shadow-12"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
class="shadow-12"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent rounded-10' : 'bg-dark mobile-rounded-10'"
>
<q-card-section
@ -275,32 +211,6 @@
</transition-group>
</div>
</div>
</q-scroll-area>
<q-page-sticky
v-if="mode === 'normal'"
position="bottom-right"
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
class="z-top"
>
<transition
appear
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut"
>
<q-btn
v-if="scroll_y > 400"
fab
icon="las la-chevron-up"
color="white"
text-color="accent"
class="shadow-12"
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
/>
</transition>
</q-page-sticky>
</div>
</template>
<style

View File

@ -4,6 +4,7 @@
>
/* eslint-disable */
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import ShiftListScrollable from 'src/modules/timesheets/components/shift-list-scrollable.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';
@ -28,6 +29,21 @@
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_hours.regular
+ timesheet.weekly_hours.evening
+ timesheet.weekly_hours.emergency
+ timesheet.weekly_hours.overtime,
0) //initial value
);
const total_expenses = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_expenses.expenses
+ timesheet.weekly_expenses.on_call
+ timesheet.weekly_expenses.per_diem,
0) //initial value
);
const { mode = 'normal' } = defineProps<{
mode?: 'approval' | 'normal';
}>();
@ -54,8 +70,15 @@
<!-- 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
v-if="!$q.platform.is.mobile"
class="col-xs-6 col-md-4 col-xl-3 q-pa-md"
>
<ShiftListWeeklyOverview
mode="total-hours"
:total-hours="total_hours"
:total-expenses="total_expenses"
/>
</div>
<PageHeaderTemplate
@ -66,10 +89,13 @@
class="col"
/>
<q-space v-else />
<q-space v-if="!$q.platform.is.mobile && mode === 'approval'" />
<!-- employee weekly overview -->
<div class="col-xs-6 col-md-4 col-xl-3 q-pa-md">
<div
v-if="!$q.platform.is.mobile"
class="col-xs-6 col-md-4 col-xl-3 q-pa-md"
>
<ShiftListWeeklyOverview mode="off-hours" />
</div>
</div>
@ -77,7 +103,7 @@
<!-- top menu -->
<div
class="col-auto row items-center full-width"
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-pb-sm q-px-md'"
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between q-px-md' : 'q-pb-sm q-px-xl'"
>
<!-- navigation btn -->
<PayPeriodNavigator
@ -133,12 +159,40 @@
<!-- mobile weekly overview widget -->
<ShiftListWeeklyOverviewMobile class="col-auto" />
<ShiftList
<!-- standard scrollable shift list for user input -->
<ShiftListScrollable
v-if="mode === 'normal'"
:mode="mode"
:class="mode === 'normal' ? 'col' : 'col-auto'"
:style="mode === 'normal' ? '' : 'min-height: 100vh'"
/>
<!-- full shift list for timesheet approval details dialog -->
<div
v-else
class="col-auto column full-width"
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
>
<!-- Show if no timesheets found (further than one month from present) -->
<div
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;"
>
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
}}</span>
<q-icon
name="las la-calendar"
color="accent"
size="10em"
class="absolute"
style="opacity: 0.2;"
/>
</div>
<!-- Else show timesheets if found -->
<ShiftList class="col" />
</div>
<q-btn
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
square

View File

@ -20,7 +20,7 @@
const headerComponent = ref<HTMLElement | null>(null);
const table_max_height = computed(() => {
const height = page_height.value - (headerComponent.value?.clientHeight ?? 0);
const height = page_height.value - Math.min(headerComponent.value?.clientHeight ?? 0, headerComponent.value?.offsetHeight ?? 0);
return height;
});