fix(all): More changes to UI:

Timesheet: fix UI spaces with scrolling, change ui to not show preset apply if no preset set to employee. Layout Drawer: fix display of options according to user permissions, fix highlight of menu item to match current route name. Employee list: add functionality to prevent users without user management permissions to see or edit user info and prevent seeing inactive users, add remote to shifts for preset editor, add hover effect to employee items when management mode to visually hint at clickable item.
This commit is contained in:
Nicolas Drolet 2026-01-02 17:26:20 -05:00
parent 20fcc0206c
commit f738a5872a
20 changed files with 141 additions and 146 deletions

View File

@ -13,7 +13,7 @@
const DRAWER_BUTTONS: { i18n_key: string, icon: string, route: RouteNames, required_module?: UserModuleAccess }[] = [ 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.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.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.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.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.profile', icon: "account_box", route: RouteNames.PROFILE, required_module: ModuleNames.PERSONAL_PROFILE },
{ i18n_key: 'nav_bar.help', icon: "contact_support", route: RouteNames.HELP }, { i18n_key: 'nav_bar.help', icon: "contact_support", route: RouteNames.HELP },
@ -26,7 +26,6 @@
const is_mini = ref(true); const is_mini = ref(true);
const onClickDrawerPage = (page_name: RouteNames) => { const onClickDrawerPage = (page_name: RouteNames) => {
ui_store.current_page = page_name;
is_mini.value = true; is_mini.value = true;
router.push({ name: page_name }).catch(error => { router.push({ name: page_name }).catch(error => {
@ -43,7 +42,7 @@
}; };
onMounted(() => { onMounted(() => {
if(q.platform.is.mobile) { if (q.platform.is.mobile) {
ui_store.is_left_drawer_open = false; ui_store.is_left_drawer_open = false;
} }
}) })
@ -66,9 +65,12 @@
v-for="button, index in DRAWER_BUTTONS" v-for="button, index in DRAWER_BUTTONS"
:key="index" :key="index"
v-show="button.required_module ?? true" 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)" @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 <q-icon
:name="button.icon" :name="button.icon"
@ -84,6 +86,7 @@
{{ $t(button.i18n_key) }} {{ $t(button.i18n_key) }}
</div> </div>
</div> </div>
</div>
<q-separator spaced /> <q-separator spaced />

View File

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

View File

@ -19,6 +19,7 @@ import { ref } from 'vue';
arrows arrows
:autoplay="9001" :autoplay="9001"
control-color="accent" control-color="accent"
control-type="outline"
class="bg-dark full-width rounded-15 shadow-18" class="bg-dark full-width rounded-15 shadow-18"
> >
<!-- welcome slide --> <!-- welcome slide -->

View File

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

View File

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

View File

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

View File

@ -40,14 +40,13 @@
}>(); }>();
const is_showing_filters = ref(false); const is_showing_filters = ref(false);
const search_string = ref('');
const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview)); const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview));
const overview_filters = ref<PayPeriodOverviewFilters>({ const overview_filters = ref<PayPeriodOverviewFilters>({
is_showing_inactive: false, is_showing_inactive: false,
is_showing_team_only: false, is_showing_team_only: false,
supervisors: [], supervisors: [],
name_search_string: search_string.value, name_search_string: '',
}); });
const onClickedDetails = async (row: TimesheetApprovalOverview) => { const onClickedDetails = async (row: TimesheetApprovalOverview) => {
@ -106,7 +105,7 @@
: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="`max-height: ${maxHeight}px;`" :style="overview_rows.length > 0 ? `max-height: ${maxHeight - (timesheet_store.is_approval_grid_mode ? 0 : 20)}px;` : ''"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)" @row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
> >
<template #top> <template #top>
@ -158,7 +157,7 @@
/> />
<QTableFilters <QTableFilters
v-model:search="search_string" v-model:search="overview_filters.name_search_string"
class="col-auto q-mb-sm" class="col-auto q-mb-sm"
/> />
@ -279,7 +278,7 @@
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }"> <template #no-data="{ message, filter }">
<div class="full-width column items-center text-accent q-gutter-sm"> <div v-if="!timesheet_store.is_loading" class="full-width column items-center text-accent">
<q-icon <q-icon
size="4em" size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'" :name="filter ? 'filter_alt_off' : 'error_outline'"

View File

@ -12,7 +12,7 @@
<template> <template>
<div <div
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height" v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
class="col-auto row items-start q-px-sm q-pt-sm full-width" class="row items-start q-px-sm q-pt-sm full-width"
> >
<!-- per timesheet --> <!-- per timesheet -->
<div <div

View File

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

View File

@ -5,7 +5,7 @@
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue'; import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue'; import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
import { date } from 'quasar'; import { date, useQuasar } from 'quasar';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
@ -17,6 +17,7 @@
const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10); const CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
const { extractDate } = date; const { extractDate } = date;
const q = useQuasar();
const ui_store = useUiStore(); const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
@ -68,7 +69,7 @@
}; };
watch(currentDayComponentWatcher, () => { watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && timesheet_page.value) { if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) {
console.log('setting scroll position to offsetTop of currentDayComponent: ', currentDayComponent.value[0]!.offsetTop); console.log('setting scroll position to offsetTop of currentDayComponent: ', currentDayComponent.value[0]!.offsetTop);
timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800); timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800);
return; return;
@ -78,7 +79,7 @@
<template> <template>
<div <div
class="col column fit relative-position" class="column fit relative-position"
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''" :style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })" v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })"
> >
@ -91,7 +92,7 @@
> >
<!-- Show if no timesheets found (further than one month from present) --> <!-- Show if no timesheets found (further than one month from present) -->
<div <div
v-if="timesheet_store.timesheets.length < 1" v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
class="col-auto column flex-center fit q-py-lg" class="col-auto column flex-center fit q-py-lg"
style="min-height: 20vh;" style="min-height: 20vh;"
> >
@ -123,7 +124,7 @@
leave-active-class="animated fadeOutUp" leave-active-class="animated fadeOutUp"
> >
<q-btn <q-btn
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1)" v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1) && timesheet_store.has_timesheet_preset"
:disable="!timesheet.days.every(day => day.shifts.length < 1)" :disable="!timesheet.days.every(day => day.shifts.length < 1)"
flat flat
dense dense

View File

@ -8,7 +8,7 @@
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue'; import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue'; import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue'; import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview.vue'; import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
@ -67,7 +67,10 @@
/> />
<!-- label for approval mode to delimit that this is the timesheet --> <!-- 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> <span
v-if="mode === 'approval'"
class="col-auto text-uppercase text-bold text-h5"
> {{ $t('timesheet.page_header') }}</span>
<q-space v-if="$q.screen.width > $q.screen.height" /> <q-space v-if="$q.screen.width > $q.screen.height" />
@ -100,9 +103,12 @@
<TimesheetErrorWidget class="col-auto" /> <TimesheetErrorWidget class="col-auto" />
<!-- mobile weekly overview widget --> <!-- mobile weekly overview widget -->
<ShiftListWeeklyOverview /> <ShiftListWeeklyOverviewMobile class="col-auto" />
<ShiftList :mode="mode" /> <ShiftList
:mode="mode"
class="col"
/>
<q-btn <q-btn
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height" v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@
<iframe <iframe
title="Environment Canada Weather" title="Environment Canada Weather"
height="400px" height="400px"
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=e" src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=f"
allowtransparency="true" allowtransparency="true"
style="border: 0;" style="border: 0;"
class="col-auto" class="col-auto"

View File

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

View File

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

View File

@ -20,14 +20,14 @@
const headerComponent = ref<HTMLElement | null>(null); const headerComponent = ref<HTMLElement | null>(null);
const table_max_height = computed(() => { const table_max_height = computed(() => {
const height = page_height.value - (headerComponent.value?.clientHeight ?? 0) - 20; const height = page_height.value - (headerComponent.value?.offsetHeight ?? 0);
console.log('offset height of header: ', headerComponent.value?.clientHeight);
console.log('height calculated: ', height);
return height; return height;
}); });
const tableStyleFunction = (offset: number, height: number) => { const tableStyleFunction = (offset: number, height: number) => {
page_height.value = height - offset; page_height.value = height - offset;
return { minHeight: height - offset + 'px' };
}; };
onMounted(async () => { onMounted(async () => {
@ -49,7 +49,7 @@
<OverviewReport /> <OverviewReport />
<div <div
class="column items-center scroll q-pa-sm" class="column items-center scroll q-px-sm full-width"
style="min-height: inherit;" style="min-height: inherit;"
> >
<div <div
@ -63,7 +63,7 @@
/> />
</div> </div>
<div class="col"> <div class="col-grow full-width">
<OverviewList :max-height="table_max_height" /> <OverviewList :max-height="table_max_height" />
</div> </div>
</div> </div>

View File

@ -15,7 +15,6 @@
<template> <template>
<q-page <q-page
padding
class="column bg-secondary items-center" class="column bg-secondary items-center"
> >
<PageHeaderTemplate <PageHeaderTemplate

View File

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