feat(filters): add possibility of hiding inactive users, filter structure set up for future filters

This commit is contained in:
Nicolas Drolet 2025-12-19 17:20:03 -05:00
parent e665cf87ab
commit 9a70875f78
10 changed files with 235 additions and 159 deletions

View File

@ -284,6 +284,7 @@ export default {
mileage: "mileage",
verified: "approved",
unverified: "pending",
inactive: "inactive",
},
tooltip: {
button_detailed_view: "detailed view",

View File

@ -285,6 +285,7 @@ export default {
mileage: "kilométrage",
verified: "approuvé",
unverified: "à vérifier",
inactive: "inactif",
},
tooltip: {
button_detailed_view: "vue détaillée",

View File

@ -9,11 +9,11 @@
dense
rounded
debounce="300"
class="right-rounded"
:label="$t('shared.label.search')"
label-color="accent"
bg-color="white"
color="accent"
bg-color="white"
label-color="accent"
class="text-primary"
>
<template #prepend>
<q-icon

View File

@ -2,9 +2,9 @@
setup
lang="ts"
>
import { ref } from 'vue';
import type { PayPeriodOverviewFilters } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
const boolean_filters = ref([])
const filters = defineModel<PayPeriodOverviewFilters>('filters', { required: true })
</script>
<template>
@ -17,15 +17,14 @@ import { ref } from 'vue';
<div class="col row">
<q-checkbox
v-model="boolean_filters"
v-model="filters.is_showing_inactive"
size="lg"
val="inactive"
color="accent"
label="show inactive"
class="col"
/>
<q-checkbox
v-model="boolean_filters"
v-model="filters.is_showing_team_only"
size="lg"
val="team"
color="accent"

View File

@ -24,16 +24,20 @@
>
<q-card
class="rounded-10 shadow-5"
:style="`animation-delay: ${index / 15}s;`"
:style="`animation-delay: ${index / 15}s; opacity: ${row.is_active ? '1' : '0.5'}; transform: scale(${row.is_active ? '1' : '0.9'})`"
>
<!-- Card header with employee name and details button-->
<q-card-section
horizontal
class="q-py-none q-px-sm q-ma-none justify-between items-center bg-primary text-white"
class="q-py-xs q-px-sm q-ma-none justify-between items-center bg-primary text-white"
>
<div>
<span class="text-h5 text-uppercase text-weight-medium text-accent q-mr-xs">{{ row.employee_name.split(' ')[0]
}}</span>
<span
class="text-h5 text-uppercase text-weight-medium q-mr-xs"
:class="row.is_active ? 'text-accent' : 'text-negative'"
>
{{ row.employee_name.split(' ')[0] }}
</span>
<span class="text-uppercase text-weight-light">{{ row.employee_name.split(' ')[1] }}</span>
</div>
@ -45,7 +49,7 @@
unelevated
class="col-auto q-pa-none q-ma-none"
color="accent"
icon="work_history"
icon="las la-chart-pie"
@click="emit('clickDetails', row)"
>
<q-tooltip
@ -55,6 +59,8 @@
>
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
</q-tooltip>
<q-icon name="las la-chart-bar" color="accent"/>
</q-btn>
</q-card-section>
@ -67,12 +73,12 @@
<div class="col column">
<span
class="text-weight-bold text-uppercase q-pa-none q-my-none"
:class="row.regular_hours > 80 ? 'text-negative' : 'text-accent'"
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : 'text-accent'"
> {{
$t('shared.shift_type.regular') }} </span>
<span
class="text-weight-bolder text-h3 q-py-none"
:class="row.regular_hours > 80 ? 'text-negative' : ''"
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''"
> {{ row.regular_hours }} </span>
<q-separator class="q-mr-sm" />
</div>
@ -133,30 +139,49 @@
<q-card-section
horizontal
class="justify-between items-center text-weight-bold q-pa-none"
:class="row.is_approved ? 'text-white bg-accent' : 'bg-dark text-accent'"
:class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark text-accent') : 'bg-transparent'"
>
<div class="col-auto">
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span>
<span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span>
<div
v-if="row.is_active"
class="col row full-width"
>
<div class="col">
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }}
</span>
<span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span>
</div>
<div
class="col-auto q-py-xs q-px-md"
>
<q-checkbox
v-model="modelApproval"
dense
left-label
keep-color
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="row.is_approved ? 'white' : 'accent'"
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="text-uppercase"
:class="row.is_approved ? '' : 'text-accent'"
/>
</div>
</div>
<div
class="col-auto q-py-xs q-px-md"
style="border: 1px solid var(--q-accent);"
v-else
class="col row flex-center q-px-sm full-width"
>
<q-checkbox
v-model="modelApproval"
dense
left-label
keep-color
<q-icon
name="block"
color="negative"
class="col-auto"
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="row.is_approved ? 'white' : 'accent'"
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="text-uppercase"
:class="row.is_approved ? '' : 'text-accent'"
/>
<span class="col q-pl-sm text-uppercase text-weight-bold text-h5">{{
$t('timesheet_approvals.table.inactive') }}</span>
</div>
</q-card-section>
</q-card>

View File

@ -1,33 +1,64 @@
<script setup lang="ts">
/* eslint-disable */
import { computed, ref } from 'vue';
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
<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';
const timesheet_store = useTimesheetStore();
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { overview_column_names, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
const visible_columns = ref<string[]>([
overview_column_names.REGULAR,
overview_column_names.EVENING,
overview_column_names.EMERGENCY,
overview_column_names.SICK,
overview_column_names.VACATION,
overview_column_names.HOLIDAY,
overview_column_names.OVERTIME,
overview_column_names.IS_APPROVED,
]);
const onClickedDetails = async (row: TimesheetOverview) => {
timesheet_store.current_pay_period_overview = row;
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi();
timesheet_store.is_details_dialog_open = true;
};
const visible_columns = ref<string[]>([
overview_column_names.REGULAR,
overview_column_names.EVENING,
overview_column_names.EMERGENCY,
overview_column_names.SICK,
overview_column_names.VACATION,
overview_column_names.HOLIDAY,
overview_column_names.OVERTIME,
overview_column_names.IS_APPROVED,
]);
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>({
is_showing_inactive: false,
is_showing_team_only: false,
supervisors: [],
name_search_string: search_string.value,
});
const onClickedDetails = async (row: TimesheetOverview) => {
timesheet_store.current_pay_period_overview = row;
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
timesheet_store.is_details_dialog_open = true;
};
const filterEmployeeRows = (rows: readonly TimesheetOverview[], terms: PayPeriodOverviewFilters): TimesheetOverview[] => {
let result = [...rows];
if (!terms.is_showing_inactive) {
result = result.filter(row => row.is_active);
}
// if (terms.name_search_string) {
// result = result.filter(row => row.employee_name.includes(terms.name_search_string ?? ''));
// }
return result;
};
</script>
<template>
@ -45,10 +76,12 @@ const onClickedDetails = async (row: TimesheetOverview) => {
:rows="overview_rows"
:columns="pay_period_overview_columns"
row-key="email"
:filter="timesheet_store.search_filter"
:grid="timesheet_store.is_approval_grid_mode"
:dense="timesheet_store.is_approval_grid_mode"
hide-pagination
:pagination="{ sortBy: 'is_active' }"
:filter="overview_filters"
:filter-method="filterEmployeeRows"
color="accent"
:rows-per-page-options="[0]"
card-container-class="justify-center"
@ -59,6 +92,87 @@ const onClickedDetails = async (row: TimesheetOverview) => {
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
>
<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') + (timesheet_store.is_approval_grid_mode ? '' : ' q-mb-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="timesheet_store.is_approval_grid_mode"
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="search_string"
class="col-auto q-mb-xs"
/>
<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-accent"
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
color="accent"
size="5px"
class="q-mx-lg"
/>
</div>
</template>
<template #header="props">
<q-tr
:props="props"
@ -115,7 +229,7 @@ const onClickedDetails = async (row: TimesheetOverview) => {
{{ props.value.split(' ')[0] }}
</span>
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1]
}}</span>
}}</span>
</div>
<span v-else>{{ props.value }}</span>
</div>

View File

@ -1,8 +1,9 @@
import type { QTableColumn } from "quasar";
export interface TimesheetOverview {
export class TimesheetOverview {
email: string;
employee_name: string;
is_active: boolean;
regular_hours: number;
other_hours: {
evening_hours: number;
@ -16,6 +17,25 @@ export interface TimesheetOverview {
expenses: number;
mileage: number;
is_approved: boolean;
constructor() {
this.email = '';
this.employee_name = 'John Doe';
this.is_active = true;
this.regular_hours = 0;
this.other_hours = {
evening_hours: 0,
emergency_hours: 0,
overtime_hours: 0,
sick_hours: 0,
holiday_hours: 0,
vacation_hours: 0,
}
this.total_hours = 0;
this.expenses = 0;
this.mileage = 0;
this.is_approved = false;
};
}
export interface PayPeriodOverviewResponse {
@ -28,22 +48,11 @@ export interface PayPeriodOverviewResponse {
employees_overview: TimesheetOverview[];
}
export const default_pay_period_overview: TimesheetOverview = {
email: '',
employee_name: '',
regular_hours: -1,
other_hours: {
evening_hours: -1,
emergency_hours: -1,
overtime_hours: -1,
sick_hours: -1,
holiday_hours: -1,
vacation_hours: -1,
},
total_hours: -1,
expenses: -1,
mileage: -1,
is_approved: false
export interface PayPeriodOverviewFilters {
is_showing_inactive: boolean;
is_showing_team_only: boolean;
supervisors: string[];
name_search_string: string | number | null;
}
export const overview_column_names = {
@ -58,7 +67,8 @@ export const overview_column_names = {
OVERTIME: 'OVERTIME',
EXPENSES: 'expenses',
MILEAGE: 'mileage',
IS_APPROVED: 'is_approved',
IS_APPROVED: 'is_approved',
IS_ACTIVE: 'is_active',
}
export const pay_period_overview_columns: QTableColumn[] = [
@ -145,5 +155,11 @@ export const pay_period_overview_columns: QTableColumn[] = [
label: 'timesheet_approvals.table.is_approved',
field: 'is_approved',
sortable: true,
},
{
name: overview_column_names.IS_ACTIVE,
label: 'timesheet_approvals.table.is_active',
field: 'is_active',
sortable: true,
}
]

View File

@ -31,7 +31,8 @@
}>();
onMounted(async () => {
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
if (mode === 'normal')
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
});
</script>

View File

@ -6,21 +6,16 @@
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.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 OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
import { date } from 'quasar';
import { onMounted, ref } from 'vue';
import { onMounted } from 'vue';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { useTimesheetStore } from 'src/stores/timesheet-store';
const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore();
const is_showing_filters = ref(false);
onMounted(async () => {
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
});
@ -44,80 +39,6 @@
:timesheets="timesheet_store.timesheets"
/>
<div
class="col-auto row items-start full-width q-px-lg"
:class="($q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md') + (timesheet_store.is_approval_grid_mode ? '' : ' q-mb-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="timesheet_store.is_approval_grid_mode"
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="timesheet_store.search_filter"
class="col-auto q-mb-xs"
/>
<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-accent"
style="border-radius: 5px 5px 0 0;"
@click="is_showing_filters = !is_showing_filters"
/>
</div>
</div>
<q-slide-transition class="col-auto">
<OverviewListFilters
v-if="is_showing_filters"
class="q-mx-lg col-auto"
/>
</q-slide-transition>
<q-separator
color="accent"
size="5px"
class="q-mx-lg"
/>
<OverviewReport />
<OverviewList class="col" />

View File

@ -23,7 +23,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const is_details_dialog_open = ref(false);
const selected_employee_name = ref<string>();
const current_pay_period_overview = ref<TimesheetOverview>();
const search_filter = ref<string | number | null>('');
const is_approval_grid_mode = ref<boolean>(true);
const pay_period_report = ref();
@ -147,7 +146,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
is_report_dialog_open,
is_approval_grid_mode,
is_details_dialog_open,
search_filter,
pay_period,
pay_period_overviews,
current_pay_period_overview,