add field for extension in employee list, but will need to be manually entered from Facturation, current DB does not contain extensions.
376 lines
16 KiB
Vue
376 lines
16 KiB
Vue
<script
|
|
setup
|
|
lang="ts"
|
|
>
|
|
/* eslint-disable */
|
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
|
import OverviewListFilters from 'src/modules/timesheet-approval/components/overview-list-filters.vue';
|
|
|
|
import { computed, ref } from 'vue';
|
|
import { useAuthStore } from 'src/stores/auth-store';
|
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
|
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 { useUiStore } from 'src/stores/ui-store';
|
|
|
|
const WARNING_COLUMNS: OverviewColumns[] = ['EVENING', 'HOLIDAY', 'VACATION', 'SICK']
|
|
const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME', 'EMERGENCY']
|
|
|
|
const ui_store = useUiStore();
|
|
const auth_store = useAuthStore();
|
|
const timesheet_store = useTimesheetStore();
|
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
|
|
|
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION', 'SICK'];
|
|
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
|
|
'employee_first_name',
|
|
'REGULAR',
|
|
'EVENING',
|
|
'EMERGENCY',
|
|
'OVERTIME',
|
|
'HOLIDAY',
|
|
'VACATION',
|
|
'expenses',
|
|
'mileage',
|
|
'weekly_hours_1',
|
|
'weekly_hours_2',
|
|
'total_hours',
|
|
'is_approved',
|
|
]);
|
|
|
|
const { maxHeight } = defineProps<{
|
|
maxHeight: number;
|
|
}>();
|
|
|
|
const is_showing_filters = ref(false);
|
|
|
|
const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview));
|
|
const overview_filters = ref<PayPeriodOverviewFilters>({
|
|
is_showing_inactive: false,
|
|
is_showing_team_only: true,
|
|
supervisors: [],
|
|
name_search_string: '',
|
|
});
|
|
|
|
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
|
|
timesheet_store.current_pay_period_overview = row;
|
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
|
|
|
timesheet_store.is_details_dialog_open = true;
|
|
};
|
|
|
|
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
|
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
|
}
|
|
|
|
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
|
|
let result = [...rows];
|
|
|
|
if (!terms.is_showing_inactive) {
|
|
result = result.filter(row => row.is_active);
|
|
}
|
|
|
|
if (terms.is_showing_team_only) {
|
|
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (auth_store.user ? auth_store.user.email : ''));
|
|
}
|
|
|
|
if (terms.name_search_string.length > 0) {
|
|
const search_words = terms.name_search_string.trim().split(' ');
|
|
search_words.map(word => result = result.filter(row =>
|
|
row.employee_first_name.toLowerCase().includes(word.toLowerCase() ?? '') || row.employee_last_name.toLowerCase().includes(word ?? '')
|
|
));
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
const getListViewTimeCss = (column_name: OverviewColumns, value: number): { classes: string, style: string } => {
|
|
if (WARNING_COLUMNS.includes(column_name) && value > 0)
|
|
return { classes: 'bg-warning text-white rounded-5', style: '' };
|
|
|
|
if (NEGATIVE_COLUMNS.includes(column_name) && value > 0)
|
|
return { classes: 'bg-negative text-white text-bold rounded-5', style: '' };
|
|
|
|
return { classes: '', style: '' }
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="full-width">
|
|
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
|
<q-table
|
|
dense
|
|
row-key="email"
|
|
color="accent"
|
|
separator="none"
|
|
hide-pagination
|
|
:rows="overview_rows"
|
|
:columns="pay_period_overview_columns"
|
|
:table-colspan="pay_period_overview_columns.length"
|
|
:visible-columns="VISIBLE_COLUMNS"
|
|
:grid="ui_store.user_preferences.is_timesheet_approval_grid"
|
|
:pagination="{ sortBy: 'is_active' }"
|
|
:filter="overview_filters"
|
|
:filter-method="filterEmployeeRows"
|
|
:rows-per-page-options="[0]"
|
|
class="bg-transparent"
|
|
:class="ui_store.user_preferences.is_timesheet_approval_grid ? '' : 'sticky-header-table no-shadow'"
|
|
card-container-class="justify-center"
|
|
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
|
|
:no-data-label="$t('shared.error.no_data_found')"
|
|
:no-results-label="$t('shared.error.no_search_results')"
|
|
:loading-label="$t('shared.label.loading')"
|
|
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;` : ''"
|
|
:table-style="{ tableLayout: 'fixed' }"
|
|
@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
|
|
class="col-auto row no-wrap items-start"
|
|
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
|
>
|
|
<q-btn-toggle
|
|
v-model="ui_store.user_preferences.is_timesheet_approval_grid"
|
|
push
|
|
rounded
|
|
color="white"
|
|
text-color="accent"
|
|
toggle-color="accent"
|
|
class="col-auto"
|
|
:class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-sm'"
|
|
:options="[
|
|
{ icon: 'grid_view', value: true },
|
|
{ icon: 'view_list', value: false },
|
|
]"
|
|
style="height: 40px;"
|
|
/>
|
|
|
|
<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"
|
|
v-model:visible-columns="VISIBLE_COLUMNS"
|
|
class="q-mx-lg col-auto"
|
|
/>
|
|
</q-slide-transition>
|
|
|
|
<q-separator
|
|
color="primary"
|
|
size="5px"
|
|
class="q-mx-lg q-my-none q-pa-none"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<template #header="props">
|
|
<q-tr
|
|
:props="props"
|
|
class="bg-primary"
|
|
>
|
|
<q-th
|
|
v-for="col in props.cols"
|
|
:key="col.name"
|
|
:props="props"
|
|
>
|
|
<span class="text-uppercase text-weight-bolder text-white">
|
|
{{ $t(col.label) }}
|
|
</span>
|
|
|
|
<span
|
|
v-if="col.name.includes('weekly_hours')"
|
|
class="q-ml-sm text-uppercase text-weight-bolder text-white"
|
|
>
|
|
{{ col.name.slice(-1,) }}
|
|
</span>
|
|
</q-th>
|
|
</q-tr>
|
|
</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"
|
|
>
|
|
<q-btn
|
|
:key="props.row.is_approved"
|
|
flat
|
|
dense
|
|
:icon="props.value ? 'lock' : 'lock_open'"
|
|
:color="props.value ? 'white' : 'grey-5'"
|
|
class="rounded-5 "
|
|
:class="props.value ? (props.row.is_active ? 'bg-accent' : 'bg-negative') : ''"
|
|
@click.stop="onClickApproveAll(props.row.email, !props.row.is_approved)"
|
|
/>
|
|
</transition>
|
|
|
|
<!-- display full employee name with large first name and smaller last name -->
|
|
<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 weekly total hours (regular, vacation, holiday, emergency, evening) -->
|
|
<div
|
|
v-else-if="props.col.name.includes('weekly_hours')"
|
|
class="q-px-xs"
|
|
>
|
|
<span>
|
|
{{
|
|
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"
|
|
>
|
|
<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="getListViewTimeCss(props.col.name, props.value).classes"
|
|
>
|
|
{{ 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"
|
|
: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'"
|
|
/>
|
|
|
|
<span class="text-h6">
|
|
{{ message }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</q-table>
|
|
</div>
|
|
</template>
|
|
|
|
<style
|
|
lang="sass"
|
|
scoped
|
|
>
|
|
.sticky-header-table
|
|
thead tr:first-child th
|
|
background-color: var(--q-accent)
|
|
margin-top: none
|
|
|
|
thead tr th
|
|
position: sticky
|
|
z-index: 1
|
|
thead tr:first-child th
|
|
top: 0px
|
|
|
|
&.q-table--loading thead tr:last-child th
|
|
top: 48px
|
|
|
|
tbody
|
|
scroll-margin-top: 48px
|
|
|
|
.q-table__grid-content
|
|
overflow: auto
|
|
|
|
td
|
|
min-width: 80px
|
|
</style> |