targo-frontend/src/modules/timesheet-approval/components/overview-list.vue
Nicolas Drolet 35db8418a6 fix(many): ui adjustments to employee-list and timesheet-approvals, add phone number to employee-list
add field for extension in employee list, but will need to be manually entered from Facturation, current DB does not contain extensions.
2026-01-09 09:43:17 -05:00

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>