Merge pull request 'fix(approvals): add more functionality and ui fixes to list view, add weekly breakdown hours, ui adjustments to card view' (#43) from dev/nicolas/staging-prep into main

Reviewed-on: Targo/targo_frontend#43
This commit is contained in:
Nicolas 2026-01-09 07:38:26 -05:00
commit 78d70f8fe4
21 changed files with 513 additions and 264 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -353,8 +353,20 @@ export default {
verified: "approved", verified: "approved",
unverified: "pending", unverified: "pending",
inactive: "inactive", inactive: "inactive",
regular: "regular",
evening: "evening",
emergency: "emergency",
overtime: "overtime",
holiday: "holiday",
vacation: "vacation",
sick: "sick",
remote: "remote work",
weekly_hours_1: "1st week hours",
weekly_hours_2: "2nd week hours",
total_hours: "total hours",
filter_active: "show only active employees", filter_active: "show only active employees",
filter_team: "show my team only", filter_team: "show my team only",
filter_columns: "Information displayed",
}, },
tooltip: { tooltip: {
button_detailed_view: "detailed view", button_detailed_view: "detailed view",

View File

@ -354,8 +354,20 @@ export default {
verified: "approuvé", verified: "approuvé",
unverified: "à vérifier", unverified: "à vérifier",
inactive: "inactif", inactive: "inactif",
regular: "régulier",
evening: "soir",
emergency: "urgence",
overtime: "supplémentaire",
holiday: "férié",
vacation: "vacances",
sick: "maladie",
remote: "télétravail",
weekly_hours_1: "heures semaine 1",
weekly_hours_2: "heures semaine 2",
total_hours: "heures totales",
filter_active: "montrer les employés inactifs", filter_active: "montrer les employés inactifs",
filter_team: "montrer mon équipe seulement", filter_team: "montrer mon équipe seulement",
filter_columns: "informations affichés",
}, },
tooltip: { tooltip: {
button_detailed_view: "vue détaillée", button_detailed_view: "vue détaillée",

View File

@ -1,7 +1,20 @@
<script
setup
lang="ts"
>
const CREATE_YEAR = 2025;
const today = new Date();
</script>
<template> <template>
<q-footer elevated class="bg-primary text-white"> <q-footer
<q-toolbar> elevated
<q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title> class="bg-primary text-white"
</q-toolbar> >
</q-footer> <div class="q-px-md q-py-xs full-width text-right">
<span class="text-weight-light text-caption text-uppercase">
© {{ CREATE_YEAR }} - {{ today.getFullYear() }} Targo Communications inc.
</span>
</div>
</q-footer>
</template> </template>

View File

@ -76,7 +76,7 @@
rounded rounded
disabled disabled
type="submit" type="submit"
color="accent" color="grey-5"
:label="$t('login.button.connect')" :label="$t('login.button.connect')"
class="full-width q-mt-lg" class="full-width q-mt-lg"
/> />
@ -108,7 +108,7 @@
rounded rounded
push push
disabled disabled
color="fb-blue" color="blue-grey-7"
icon="img:src/assets/Facebook-f_Logo-White-Logo.wine.svg" icon="img:src/assets/Facebook-f_Logo-White-Logo.wine.svg"
:label="$t('login.button.facebook')" :label="$t('login.button.facebook')"
class="full-width row q-mb-sm" class="full-width row q-mb-sm"

View File

@ -2,8 +2,9 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { date, useQuasar } from 'quasar'; import { date, useQuasar } from 'quasar';
import { computed } from 'vue'; import { computed, onMounted, onUpdated, ref } from 'vue';
const { title, startDate = "", endDate = "" } = defineProps<{ const { title, startDate = "", endDate = "" } = defineProps<{
title: string; title: string;
@ -13,17 +14,36 @@
const q = useQuasar(); const q = useQuasar();
const emit = defineEmits<{ 'onGetComponentHeight': [value: number] }>();
const selfRef = ref<HTMLElement | null>(null);
const date_format_options = computed(() => q.platform.is.mobile ? { day: 'numeric', month: 'short', year: 'numeric' } : { day: 'numeric', month: 'long', year: 'numeric', }); const date_format_options = computed(() => q.platform.is.mobile ? { day: 'numeric', month: 'short', year: 'numeric' } : { day: 'numeric', month: 'long', year: 'numeric', });
onUpdated(() => {
if (selfRef.value) {
emit('onGetComponentHeight', selfRef.value.offsetHeight);
}
});
onMounted(() => {
if (selfRef.value) {
emit('onGetComponentHeight', selfRef.value.offsetHeight);
}
})
</script> </script>
<template> <template>
<div class="column text-uppercase text-center text-weight-bolder text-h4"> <div
<span ref="selfRef"
class="column text-uppercase text-center text-weight-bolder text-h4 q-pt-md"
>
<!-- <span
v-if="!$q.platform.is.mobile" v-if="!$q.platform.is.mobile"
class="col q-pt-lg" class="col q-pt-lg"
> >
{{ $t(title) }} {{ $t(title) }}
</span> </span> -->
<transition <transition
enter-active-class="animated fadeInDown" enter-active-class="animated fadeInDown"

View File

@ -5,17 +5,17 @@
<template> <template>
<q-input <q-input
v-model="search_model" v-model="search_model"
outlined :dark="false"
dense dense
outlined
rounded rounded
debounce="300" debounce="300"
:label="$t('shared.label.search')" :label="$t('shared.label.search')"
color="accent" color="accent"
bg-color="white" bg-color="white"
label-color="accent" label-color="accent"
class="text-primary"
> >
<template #prepend> <template #append>
<q-icon <q-icon
name="search" name="search"
color="accent" color="accent"

View File

@ -2,34 +2,62 @@
setup setup
lang="ts" lang="ts"
> >
import type { PayPeriodOverviewFilters } from 'src/modules/timesheet-approval/models/timesheet-overview.models'; import { overview_column_names, type OverviewColumns, type PayPeriodOverviewFilters } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { onMounted, ref } from 'vue';
const filters = defineModel<PayPeriodOverviewFilters>('filters', { required: true }) const filters = defineModel<PayPeriodOverviewFilters>('filters', { required: true });
const visible_columns = defineModel<OverviewColumns[]>('visibleColumns', {required: true});
const column_options = ref<{ label: string, value: OverviewColumns }[]>([]);
const EXCLUDED_COLUMNS: OverviewColumns[] = ['employee_last_name', 'employee_first_name', 'is_active']
onMounted(() => {
Object.values(overview_column_names).map(column => column_options.value.push({ label: `timesheet_approvals.table.${column}`, value: column as OverviewColumns }))
column_options.value = column_options.value.filter(column => !EXCLUDED_COLUMNS.includes(column.value));
console.log('filter column values: ', column_options.value )
})
</script> </script>
<template> <template>
<div class="column bg-primary text-uppercase"> <div class="column bg-primary text-uppercase q-px-sm text-white">
<div class="col row"> <div class="col row">
<q-checkbox <q-checkbox
v-model="filters.is_showing_inactive" v-model="filters.is_showing_inactive"
keep-color keep-color
size="lg"
color="accent" color="accent"
:label="$t('timesheet_approvals.table.filter_active')" :label="$t('timesheet_approvals.table.filter_active')"
class="col" class="col-auto"
: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"
val="team" val="team"
color="accent" color="accent"
:label="$t('timesheet_approvals.table.filter_team')" :label="$t('timesheet_approvals.table.filter_team')"
class="col" class="col-auto q-px-sm"
: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'"
/> />
</div> </div>
<span class="col-auto q-px-md q-pt-sm text-h6 text-bold">
{{ $t('timesheet_approvals.table.filter_columns') }}
</span>
<div class="col row">
<q-option-group
v-model="visible_columns"
:options="column_options"
inline
keep-color
color="accent"
type="checkbox"
>
<template #label="scope">
<span class="text-caption text-uppercase text-weight-light">{{ $t(scope.label.toLowerCase()) }}</span>
</template>
</q-option-group>
</div>
</div> </div>
</template> </template>

View File

@ -3,7 +3,7 @@
lang="ts" lang="ts"
> >
import type { TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models'; import type { TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-and-time-utils'; import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
const modelApproval = defineModel<boolean>(); const modelApproval = defineModel<boolean>();
@ -82,7 +82,7 @@
> {{ > {{
$t('shared.shift_type.regular') }} </span> $t('shared.shift_type.regular') }} </span>
<span <span
class="text-weight-bolder text-h3 q-py-none" class="text-weight-bolder text-h4 q-py-none"
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''" :class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''"
> {{ getHoursMinutesStringFromHoursFloat(row.regular_hours) }} </span> > {{ getHoursMinutesStringFromHoursFloat(row.regular_hours) }} </span>
<q-separator class="q-mr-sm" /> <q-separator class="q-mr-sm" />
@ -94,7 +94,7 @@
v-for="hour_type, index in row.other_hours" v-for="hour_type, index in row.other_hours"
:key="index" :key="index"
class="col-4 column ellipsis" class="col-4 column ellipsis"
:class="hour_type === 0 ? 'invisible' : ''" :class="hour_type === 0 ? 'invisible order-last' : 'order-first'"
> >
<span <span
class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none" class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
@ -102,10 +102,13 @@
> >
{{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }}
</span> </span>
<span <span
class="text-weight-bolder q-pa-none q-mb-xs" class="text-weight-bolder q-pa-none q-mb-xs"
style="font-size: 1.2em; line-height: 1em;" style="font-size: 1.2em; line-height: 1em;"
> {{ getHoursMinutesStringFromHoursFloat(hour_type) }} </span> >
{{ getHoursMinutesStringFromHoursFloat(hour_type) }}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -144,20 +147,47 @@
<!-- Validate Pay Period section --> <!-- Validate Pay Period section -->
<q-card-section <q-card-section
horizontal class="justify-between items-center q-pa-none"
class="justify-between items-center text-weight-bold q-pa-none"
:class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark') : 'bg-transparent'" :class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark') : 'bg-transparent'"
> >
<!-- weekly totals -->
<q-separator />
<div
v-if="row.is_active"
class="col row full-width justify-between"
>
<div
v-for="weekly_hours, index in row.weekly_hours"
:key="index"
class="col q-px-sm text-uppercase"
>
<span class="text-weight-bold">
{{ $t('timesheet.week') }} {{ index + 1 }}
</span>
<span
class="q-ml-sm"
:class="row.total_hours > 80 ? 'bg-negative q-px-sm rounded-5 text-white' : ''"
>
{{ getHoursMinutesStringFromHoursFloat(weekly_hours) }}
</span>
</div>
</div>
<q-separator />
<!-- overall totals and approve all button -->
<div <div
v-if="row.is_active" v-if="row.is_active"
class="col row full-width" class="col row full-width"
> >
<div class="col text-uppercase"> <div class="col text-uppercase">
<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</span>
}}</span> <span
<span class="text-uppercase text-weight-medium text-caption">H</span> class="q-ml-sm text-h6"
<span class="text-h6 q-ml-sm text-weight-bolder">{{ getMinutes(row.total_hours) }}</span> :class="row.total_hours > 80 ? 'bg-negative q-px-sm rounded-5 text-white' : 'text-weight-light'"
<span class="text-uppercase text-weight-medium text-caption">M</span> >
{{ getHoursMinutesStringFromHoursFloat(row.total_hours) }}
</span>
</div> </div>
<div class="col-auto q-py-xs q-px-md"> <div class="col-auto q-py-xs q-px-md">

View File

@ -13,15 +13,19 @@
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
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 { overview_column_names, OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models'; import { type OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils'; import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
import { useUiStore } from 'src/stores/ui-store';
const WARNING_COLUMNS: OverviewColumns[] = ['EMERGENCY', 'EVENING', 'HOLIDAY', 'VACATION', 'SICK']
const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME',]
const ui_store = useUiStore();
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION']; const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION', 'SICK'];
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([ const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
'employee_first_name', 'employee_first_name',
'REGULAR', 'REGULAR',
@ -32,6 +36,9 @@
'VACATION', 'VACATION',
'expenses', 'expenses',
'mileage', 'mileage',
'weekly_hours_1',
'weekly_hours_2',
'total_hours',
'is_approved', 'is_approved',
]); ]);
@ -74,229 +81,276 @@
if (terms.name_search_string.length > 0) { if (terms.name_search_string.length > 0) {
const search_words = terms.name_search_string.trim().split(' '); const search_words = terms.name_search_string.trim().split(' ');
search_words.map(word => result = result.filter(row => search_words.map(word => result = result.filter(row =>
row.employee_first_name.includes(word ?? '') || row.employee_last_name.includes(word ?? '') row.employee_first_name.toLowerCase().includes(word.toLowerCase() ?? '') || row.employee_last_name.toLowerCase().includes(word ?? '')
)); ));
} }
return result; return result;
}; };
const getListViewTimeClass = (column_name: OverviewColumns, value: number) => {
if(WARNING_COLUMNS.includes(column_name) && value > 0)
return 'bg-warning text-white rounded-5';
if(NEGATIVE_COLUMNS.includes(column_name) && value > 0)
return 'bg-negative text-white text-bold rounded-5';
}
</script> </script>
<template> <template>
<div class="full-width"> <div class="full-width">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<q-table <q-table
dense dense
row-key="email" row-key="email"
color="accent" color="accent"
hide-pagination hide-pagination
:rows="overview_rows" :rows="overview_rows"
:columns="pay_period_overview_columns" :columns="pay_period_overview_columns"
:visible-columns="VISIBLE_COLUMNS" :table-colspan="pay_period_overview_columns.length"
:grid="timesheet_store.is_approval_grid_mode" :visible-columns="VISIBLE_COLUMNS"
:pagination="{ sortBy: 'is_active' }" :grid="ui_store.user_preferences.is_timesheet_approval_grid"
:filter="overview_filters" :pagination="{ sortBy: 'is_active' }"
:filter-method="filterEmployeeRows" :filter="overview_filters"
:rows-per-page-options="[0]" :filter-method="filterEmployeeRows"
class="bg-transparent" :rows-per-page-options="[0]"
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'" class="bg-transparent"
card-container-class="justify-center" :class="ui_store.user_preferences.is_timesheet_approval_grid ? '' : 'sticky-header-table no-shadow'"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar" card-container-class="justify-center"
:no-data-label="$t('shared.error.no_data_found')" table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
:no-results-label="$t('shared.error.no_search_results')" :no-data-label="$t('shared.error.no_data_found')"
:loading-label="$t('shared.label.loading')" :no-results-label="$t('shared.error.no_search_results')"
:style="overview_rows.length > 0 ? `max-height: ${maxHeight - (timesheet_store.is_approval_grid_mode ? 0 : 20)}px;` : ''" :loading-label="$t('shared.label.loading')"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)" table-header-style="min-width: 80xp; max-width: 80px;"
> :style="overview_rows.length > 0 ? `max-height: ${maxHeight - (ui_store.user_preferences.is_timesheet_approval_grid ? 0 : 20)}px;` : ''"
<template #top> :table-style="{ tableLayout: 'fixed'}"
<div class="column full-width"> @row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
>
<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 />
<div <div
class="col-auto row items-start full-width q-px-lg" class="col-auto row no-wrap items-start"
:class="$q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md'" :class="$q.platform.is.mobile ? 'q-mb-md' : ''"
> >
<PayPeriodNavigator <q-btn-toggle
@date-selected="timesheet_approval_api.getTimesheetOverviews" v-model="ui_store.user_preferences.is_timesheet_approval_grid"
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews" push
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews" rounded
:class="$q.platform.is.mobile ? 'q-mb-sm' : ''" 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;" style="height: 40px;"
/> />
<q-space /> <q-btn
push
<div rounded
class="col-auto row no-wrap items-start" icon="download"
:class="$q.platform.is.mobile ? 'q-mb-md' : ''" color="accent"
> :label="$q.screen.lt.md ? '' : $t('shared.label.download')"
<q-btn-toggle class="col-auto q-mr-sm"
v-model="timesheet_store.is_approval_grid_mode" style="height: 40px;"
push @click="timesheet_store.is_report_dialog_open = true"
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;"
/>
<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 <QTableFilters
color="primary" v-model:search="overview_filters.name_search_string"
size="5px" class="col-auto q-mb-sm"
class="q-mx-lg q-my-none q-pa-none" />
/>
<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> </div>
</template>
<template #header="props"> <q-slide-transition>
<q-tr <OverviewListFilters
:props="props" v-if="is_showing_filters"
class="bg-primary" v-model:filters="overview_filters"
> v-model:visible-columns="VISIBLE_COLUMNS"
<q-th class="q-mx-lg col-auto"
v-for="col in props.cols" />
:key="col.name" </q-slide-transition>
: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-separator
<q-td 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" :props="props"
class="text-weight-medium"
> >
<transition <span class="text-uppercase text-weight-bolder text-white">
appear {{ $t(col.label) }}
enter-active-class="animated fadeInUp slow" </span>
leave-active-class="animated fadeOutDown"
mode="out-in" <span
v-if="col.name.includes('weekly_hours')"
class="q-ml-sm text-uppercase text-weight-bolder text-white"
> >
<div {{ col.name.slice(-1,) }}
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)" </span>
class="rounded-5" </q-th>
style="font-size: 1.2em;" </q-tr>
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`" </template>
<template #body-cell="props">
<q-td
:props="props"
:class="props.rowIndex % 2 === 0 ? ($q.dark.isActive ? 'bg-primary' : 'bg-secondary') : ''"
>
<transition
appear
enter-active-class="animated fadeInUp"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<div
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
>
<!-- button to toggle approval status -->
<transition
v-if="props.col.name === 'is_approved'"
enter-active-class="animated swing"
mode="out-in"
> >
<transition <q-btn
v-if="props.col.name === 'is_approved'" :key="props.row.is_approved"
enter-active-class="animated swing" flat
mode="out-in" dense
> :icon="props.value ? 'lock' : 'lock_open'"
<q-btn :color="props.value ? 'white' : 'grey-5'"
:key="props.row.is_approved" class="rounded-5 "
flat :class="props.value ? (props.row.is_active ? 'bg-accent' : 'bg-negative') : ''"
dense @click.stop="onClickApproveAll(props.row.email, !props.row.is_approved)"
:icon="props.value ? 'lock' : 'lock_open'" />
:color="props.value ? 'white' : 'grey-5'" </transition>
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>
<!-- display full employee name with large first name and smaller last name -->
<div v-else-if="props.col.name === 'employee_first_name'">
<span <span
v-else class="text-h5 text-uppercase q-mr-xs"
:class="props.col.name === overview_column_names.REGULAR && props.row.overtime > 0 ? 'text-negative text-weight-bolder' : 'text-weight-regular'" :class="props.row.is_active ? 'text-accent' : 'text-negative'"
> >
{{ TIME_COLUMNS.includes(props.col.name) ? {{ props.value }}
getHoursMinutesStringFromHoursFloat(props.value) : props.value }} </span>
<span class="text-uppercase text-weight-light">
{{ props.row.employee_last_name }}
</span> </span>
</div> </div>
</transition>
</q-td>
</template>
<!-- Template for individual employee cards --> <!-- display weekly total hours (regular, vacation, holiday, emergency, evening) -->
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }"> <div
<OverviewListItem v-else-if="props.col.name.includes('weekly_hours')"
v-model="props.row.is_approved" class="q-px-xs"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no" :class="props.value[Number(props.col.name.slice(-1,)) - 1] > 40 ? 'bg-negative text-white rounded-5' : ''"
:index="props.rowIndex" >
:row="props.row" <span>
@click-details="onClickedDetails" {{
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)" getHoursMinutesStringFromHoursFloat((props.value[Number(props.col.name.slice(-1,)) -
1]) ?? 0) }}
</span>
</div>
<!-- display total worked hours (regular, vacation, holiday, emergency, evening) -->
<div
v-else-if="props.col.name === 'total_hours'"
class="q-px-xs"
:class="props.value > 80 ? 'bg-negative rounded-5 text-white' : ''"
>
<span>{{ getHoursMinutesStringFromHoursFloat(props.value) }}</span>
</div>
<!-- any other fields, though time fields will have their own conditional class to highlight abnormalities -->
<div
v-else
class="q-px-xs"
:class="getListViewTimeClass(props.col.name, props.value)"
>
{{ TIME_COLUMNS.includes(props.col.name) ?
getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
</div>
</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
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'"
/> />
</template>
<!-- Template for custome failed-to-load state --> <span class="text-h6">
<template #no-data="{ message, filter }"> {{ message }}
<div v-if="!timesheet_store.is_loading" class="full-width column items-center text-accent"> </span>
<q-icon </div>
size="4em" </template>
:name="filter ? 'filter_alt_off' : 'error_outline'" </q-table>
/>
<span class="text-h6">
{{ message }}
</span>
</div>
</template>
</q-table>
</div> </div>
</template> </template>
<style lang="sass"> <style lang="sass" scoped>
.sticky-header-table .sticky-header-table
thead tr:first-child th thead tr:first-child th
background-color: var(--q-primary) background-color: var(--q-accent)
margin-top: none margin-top: none
thead tr th thead tr th
@ -313,4 +367,7 @@
.q-table__grid-content .q-table__grid-content
overflow: auto overflow: auto
td
min-width: 80px
</style> </style>

View File

@ -19,6 +19,7 @@ export class TimesheetApprovalOverview {
holiday_hours: number; holiday_hours: number;
vacation_hours: number; vacation_hours: number;
}; };
weekly_hours: number[];
total_hours: number; total_hours: number;
expenses: number; expenses: number;
mileage: number; mileage: number;
@ -39,6 +40,7 @@ export class TimesheetApprovalOverview {
holiday_hours: 0, holiday_hours: 0,
vacation_hours: 0, vacation_hours: 0,
} }
this.weekly_hours = [0];
this.total_hours = 0; this.total_hours = 0;
this.expenses = 0; this.expenses = 0;
this.mileage = 0; this.mileage = 0;
@ -63,7 +65,7 @@ export interface PayPeriodOverviewFilters {
name_search_string: string; name_search_string: string;
} }
export type OverviewColumns = 'employee_first_name' | 'employee_last_name' | 'email' | 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'SICK' | 'HOLIDAY' | 'VACATION' | 'OVERTIME' | 'expenses' | 'mileage' | 'is_approved' | 'is_active' export type OverviewColumns = 'employee_first_name' | 'employee_last_name' | 'email' | 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'SICK' | 'HOLIDAY' | 'VACATION' | 'OVERTIME' | 'expenses' | 'mileage' | 'total_hours' | 'weekly_hours_1' | 'weekly_hours_2' | 'is_approved' | 'is_active'
export const overview_column_names = { export const overview_column_names = {
FIRST_NAME: 'employee_first_name', FIRST_NAME: 'employee_first_name',
@ -78,6 +80,9 @@ export const overview_column_names = {
OVERTIME: 'OVERTIME', OVERTIME: 'OVERTIME',
EXPENSES: 'expenses', EXPENSES: 'expenses',
MILEAGE: 'mileage', MILEAGE: 'mileage',
WEEKLY_HOURS_1: 'weekly_hours_1',
WEEKLY_HOURS_2: 'weekly_hours_2',
TOTAL_HOURS: 'total_hours',
IS_APPROVED: 'is_approved', IS_APPROVED: 'is_approved',
IS_ACTIVE: 'is_active', IS_ACTIVE: 'is_active',
} }
@ -90,12 +95,13 @@ export const pay_period_overview_columns: QTableColumn[] = [
field: overview_column_names.FIRST_NAME, field: overview_column_names.FIRST_NAME,
sortable: true, sortable: true,
required: true, required: true,
style: 'width: 10vw;'
}, },
{ {
name: overview_column_names.LAST_NAME, name: overview_column_names.LAST_NAME,
label: 'timesheet_approvals.table.full_name', label: 'timesheet_approvals.table.full_name',
align: 'left', align: 'left',
field: 'employee_last_name', field: '',
sortable: true, sortable: true,
}, },
{ {
@ -104,6 +110,7 @@ export const pay_period_overview_columns: QTableColumn[] = [
align: 'left', align: 'left',
field: overview_column_names.EMAIL, field: overview_column_names.EMAIL,
sortable: true, sortable: true,
style: 'width: 10vw;'
}, },
{ {
name: overview_column_names.REGULAR, name: overview_column_names.REGULAR,
@ -168,6 +175,27 @@ export const pay_period_overview_columns: QTableColumn[] = [
field: 'mileage', field: 'mileage',
sortable: true, sortable: true,
}, },
{
name: overview_column_names.WEEKLY_HOURS_1,
label: 'timesheet.week',
align: 'left',
field: 'weekly_hours',
sortable: true,
},
{
name: overview_column_names.WEEKLY_HOURS_2,
label: 'timesheet.week',
align: 'left',
field: 'weekly_hours',
sortable: true,
},
{
name: overview_column_names.TOTAL_HOURS,
label: 'timesheet_approvals.table.total_hours',
align: 'left',
field: 'total_hours',
sortable: true,
},
{ {
name: overview_column_names.IS_APPROVED, name: overview_column_names.IS_APPROVED,
label: 'timesheet_approvals.table.is_approved', label: 'timesheet_approvals.table.is_approved',

View File

@ -4,7 +4,7 @@
> >
import { date } from 'quasar'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
@ -40,7 +40,7 @@
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') }, { label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
] ]
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type)); const expense_selected = ref<ExpenseOption | undefined>();
const openDatePicker = () => { const openDatePicker = () => {
is_navigator_open.value = true; is_navigator_open.value = true;
@ -61,6 +61,10 @@
expenses_store.mode = 'create'; expenses_store.mode = 'create';
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')); expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
}; };
onMounted(() => {
expense_selected.value = expense_options.find(expense => expense.value === expenses_store.current_expense.type);
})
</script> </script>
<template> <template>

View File

@ -73,10 +73,19 @@
<div class="col-auto"> <div class="col-auto">
<q-icon <q-icon
:name="getExpenseIcon(expense.type)" :name="getExpenseIcon(expense.type)"
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')" :color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'blue-grey-8')"
size="lg" size="lg"
class="q-pr-md" class="q-pr-md"
/> />
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ $t(`timesheet.expense.types.${expense.type}`) }}
</q-tooltip>
</div> </div>
<!-- amount or mileage section --> <!-- amount or mileage section -->
@ -118,18 +127,27 @@
</div> </div>
<!-- comment section --> <!-- comment section -->
<div class="col column"> <div class="col column no-wrap">
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption"> <span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.employee_comment') }} {{ $t('timesheet.expense.employee_comment') }}
</span> </span>
<span <span
class="col" class="col ellipsis"
:class="expense.is_approved ? ' bg-accent text-white' : ''" :class="expense.is_approved ? ' bg-accent text-white' : ''"
style="font-size: 1.3em;" style="font-size: 1em;"
> >
{{ expense.comment }} {{ expense.comment }}
</span> </span>
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ expense.comment }}
</q-tooltip>
</div> </div>
<!-- supervisor comment section --> <!-- supervisor comment section -->
@ -167,7 +185,8 @@
:offset="[0, 20]" :offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold" class="bg-accent text-uppercase text-weight-bold"
> >
{{ expense.is_approved ? $t('timesheet_approvals.tooltip.unapprove') : $t('timesheet_approvals.tooltip.approve') }} {{ expense.is_approved ? $t('timesheet_approvals.tooltip.unapprove') :
$t('timesheet_approvals.tooltip.approve') }}
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>

View File

@ -39,6 +39,14 @@
} }
} }
const onBlurShiftTypeSelect = () => {
if (shift_type_selected.value === undefined) {
shift.value.type = 'REGULAR';
shift.value.id = 0;
emit('requestDelete');
}
};
const getCommentCounterColor = (comment_length: number) => { const getCommentCounterColor = (comment_length: number) => {
if (comment_length < 200) return 'primary'; if (comment_length < 200) return 'primary';
if (comment_length < 250) return 'warning'; if (comment_length < 250) return 'warning';
@ -66,7 +74,7 @@
> >
<!-- shift type --> <!-- shift type -->
<q-select <q-select
ref="select" ref="select_ref"
v-model="shift_type_selected" v-model="shift_type_selected"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense dense
@ -83,6 +91,7 @@
popup-content-class="text-uppercase text-weight-bold text-center rounded-5" popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect"
@update:model-value="option => shift.type = option.value" @update:model-value="option => shift.type = option.value"
> >
<template #selected-item="scope"> <template #selected-item="scope">

View File

@ -4,14 +4,11 @@
> >
import ShiftList from 'src/modules/timesheets/components/shift-list.vue'; import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import { useQuasar } from 'quasar'; import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { QScrollArea, TouchSwipeValue } from 'quasar'; import type { QScrollArea, TouchSwipeValue } from 'quasar';
const q = useQuasar();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
@ -22,8 +19,6 @@
const mobile_animation_direction = ref('fadeInLeft'); const mobile_animation_direction = ref('fadeInLeft');
const timesheet_page = ref<QScrollArea | null>(null); 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 scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0);
@ -34,12 +29,10 @@
} }
}; };
watch(currentDayComponentWatcher, () => { const onTodayComponentFound = (today_component: HTMLElement | undefined) => {
if (currentDayComponent.value && timesheet_page.value && q.platform.is.mobile) { if (timesheet_page.value && today_component)
timesheet_page.value.setScrollPosition('vertical', currentDayComponent.value[0]!.offsetTop, 800); timesheet_page.value.setScrollPosition('vertical', today_component.offsetTop, 800);
return; }
}
})
</script> </script>
<template> <template>
@ -73,7 +66,7 @@
</div> </div>
<!-- Else show timesheets if found --> <!-- Else show timesheets if found -->
<ShiftList /> <ShiftList @on-current-day-component-found="onTodayComponentFound" />
</q-scroll-area> </q-scroll-area>
<q-page-sticky <q-page-sticky

View File

@ -5,27 +5,34 @@
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 { ref, computed } from 'vue'; import { date, useQuasar } from 'quasar';
import { ref, computed, watch } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { Shift } from 'src/modules/timesheets/models/shift.models'; import { Shift } from 'src/modules/timesheets/models/shift.models';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models'; 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 CURRENT_DATE_STRING = new Date().toISOString().slice(0, 10);
const { extractDate } = date; const { extractDate } = date;
const q = useQuasar();
const ui_store = useUiStore(); const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
const mobile_animation_direction = ref('fadeInLeft'); const mobile_animation_direction = ref('fadeInLeft');
const currentDayComponent = ref<HTMLElement[] | null>(null);
const currentDayComponentWatcher = ref(currentDayComponent);
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown'); const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
const emit = defineEmits<{
'onCurrentDayComponentFound': [component: HTMLElement | undefined];
}>();
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => { const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true; ui_store.focus_next_component = true;
const new_shift = new Shift; const new_shift = new Shift;
@ -50,6 +57,12 @@
const getMobileDayRef = (iso_date_string: string): string => { const getMobileDayRef = (iso_date_string: string): string => {
return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : ''; return iso_date_string === CURRENT_DATE_STRING ? 'currentDayComponent' : '';
}; };
watch(currentDayComponentWatcher, () => {
if (currentDayComponent.value && q.platform.is.mobile) {
emit('onCurrentDayComponentFound', currentDayComponent.value[0])
}
})
</script> </script>
<template> <template>

View File

@ -3,11 +3,27 @@
lang="ts" lang="ts"
> >
import { useShiftStore } from 'src/stores/shift-store'; import { useShiftStore } from 'src/stores/shift-store';
import { computed } from 'vue'; import { computed, ref } from 'vue';
const shift_store = useShiftStore(); const shift_store = useShiftStore();
const is_counting_error_display_time = ref(false);
const is_showing_errors = computed(() => shift_store.shift_errors.length > 0); const is_showing_errors = computed(() => {
if (shift_store.shift_errors.length > 0 && !is_counting_error_display_time.value) {
onShowingErrorMessage();
}
return shift_store.shift_errors.length > 0;
});
const onShowingErrorMessage = () => {
is_counting_error_display_time.value = true;
setTimeout(() => {
is_counting_error_display_time.value = false
shift_store.shift_errors = [];
}, 5000);
};
</script> </script>
<template> <template>

View File

@ -12,7 +12,7 @@ import { useRouter } from 'vue-router';
<div class=" column justify-center" :class="$q.platform.is.mobile ? 'col-11' : 'col-8'"> <div class=" column justify-center" :class="$q.platform.is.mobile ? 'col-11' : 'col-8'">
<div class="column rounded-20 q-pa-xs bg-accent" :class="$q.platform.is.mobile ? 'col-5' : 'col-4'"> <div class="column rounded-20 q-pa-xs bg-accent" :class="$q.platform.is.mobile ? 'col-5' : 'col-4'">
<div class="col"> <div class="col">
<q-img src="src/assets/line-truck-1.jpg" fit="cover" class="relative-position fit border-radius-inherit"> <q-img src="src/assets/line-truck-1.jpg" fit="cover" class="relative-position fit" style="border-radius: 18px 18px 0 0;">
<div class="absolute-bottom text-center column flex-center"> <div class="absolute-bottom text-center column flex-center">
<div class="q-pr-md text-white text-h2 text-weight-bolder">404</div> <div class="q-pr-md text-white text-h2 text-weight-bolder">404</div>
<div class="q-pr-md text-white text-h5 text-weight-bold">{{ <div class="q-pr-md text-white text-h5 text-weight-bold">{{
@ -22,7 +22,7 @@ import { useRouter } from 'vue-router';
</q-img> </q-img>
</div> </div>
<div class="col-auto text-center text-h6 text-weight-light bg-dark q-pa-md"> <div class="col-auto text-center text-h6 text-weight-light bg-dark q-pa-md" style="border-radius: 0 0 18px 18px;">
<div>{{ $t('error.not_found_description') }}</div> <div>{{ $t('error.not_found_description') }}</div>
</div> </div>
</div> </div>

View File

@ -17,12 +17,8 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const page_height = ref(0); const page_height = ref(0);
const headerComponent = ref<HTMLElement | null>(null); const header_height = ref(0);
const table_max_height = computed(() => page_height.value - header_height.value);
const table_max_height = computed(() => {
const height = page_height.value - Math.min(headerComponent.value?.clientHeight ?? 0, headerComponent.value?.offsetHeight ?? 0);
return height;
});
const tableStyleFunction = (offset: number, height: number) => { const tableStyleFunction = (offset: number, height: number) => {
page_height.value = height - offset; page_height.value = height - offset;
@ -60,6 +56,7 @@
title="timesheet_approvals.page_title" title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period?.period_start ?? ''" :start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''" :end-date="timesheet_store.pay_period?.period_end ?? ''"
@on-get-component-height="value => header_height = value"
/> />
</div> </div>

View File

@ -24,7 +24,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const selected_employee_name = ref<string>(); const selected_employee_name = ref<string>();
const has_timesheet_preset = ref(false); 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 pay_period_report = ref(); const pay_period_report = ref();
const getNextOrPreviousPayPeriod = (direction: number) => { const getNextOrPreviousPayPeriod = (direction: number) => {
@ -169,7 +168,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return { return {
is_loading, is_loading,
is_report_dialog_open, is_report_dialog_open,
is_approval_grid_mode,
is_details_dialog_open, is_details_dialog_open,
pay_period, pay_period,
pay_period_overviews, pay_period_overviews,

View File

@ -23,5 +23,5 @@ export const getMinutes = (hours: number) => {
export const getHoursMinutesStringFromHoursFloat = (hours: number): string => { export const getHoursMinutesStringFromHoursFloat = (hours: number): string => {
const flat_hours = Math.floor(hours); const flat_hours = Math.floor(hours);
const minutes = Math.round((hours - flat_hours) * 60); const minutes = Math.round((hours - flat_hours) * 60);
return `${flat_hours}h ${minutes > 1 ? minutes : ''}` return `${flat_hours}h${minutes > 1 ? ' ' + minutes : ''}`
} }