feat(employee-list): complete functional advanced search for employee list

enabling or disabling hide-inactive-employees will hide them or show them at the top. Also added more functionality to the search bar-- it can match many columns for terms separated by spaces in the search field. i.e. typing Bourdo and Solucom separated by commas will show all employees that have those words in any of the columns
This commit is contained in:
Nicolas Drolet 2025-12-05 16:07:12 -05:00
parent 5bdf1e5eaa
commit a0d87a0013
10 changed files with 88 additions and 51 deletions

View File

@ -63,14 +63,17 @@ export default defineConfigWithVueTs(
}
},
files: ['**/*.ts', '**/*.vue'],
// add your custom rules here
rules: {
'prefer-promise-reject-errors': 'off',
// warn about unused but underscored variables
"@typescript-eslint/no-unused-vars": "off",
'no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_' }
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
],
// allow debugger during development only

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { defineBoot } from '#q-app/wrappers';
import axios, { type AxiosInstance } from 'axios';

2
src/env.d.ts vendored
View File

@ -1,3 +1,5 @@
/* eslint-disable */
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;

View File

@ -10,6 +10,8 @@ export default {
supervisor: "Supervisor",
company: "Company",
is_supervisor: "is a supervisor",
active: "active",
inactive: "inactive",
},
},

View File

@ -10,6 +10,8 @@ export default {
supervisor: "superviseur",
company: "Compagnie",
is_supervisor: "est un superviseur",
active: "actif",
inactive: "inactif",
},
},

View File

@ -21,19 +21,21 @@
<template>
<transition
appear
enter-active-class="animated fadeInUp slow"
enter-active-class="animated fadeInUp fast"
leave-active-class="animated fadeOutDown fast"
mode="out-in"
>
<q-card
v-ripple
class="column col-xs-6 col-sm-4 col-md-3 col-lg-2 no-wrap rounded-15 cursor-pointer q-ma-sm"
style="max-width: 230px;"
:style="`animation-delay: ${index / 25}s;`"
:style="(`animation-delay: ${index / 25}s; `) + (row.last_work_day === null ? '' : 'opacity: 0.6;')"
@click="emit('onProfileClick', row.email)"
>
<q-card-section class="col-6 text-center">
<q-avatar
:color="row.last_work_day === undefined ? 'accent' : 'negative'"
:color="row.last_work_day === null ? 'accent' : 'negative'"
size="8em"
class="shadow-3 q-mb-md"
>
@ -51,12 +53,12 @@
>
<div
class="ellipsis"
:class="row.last_work_day === undefined ? 'text-accent' : 'text-negative'"
:class="row.last_work_day === null ? 'text-accent' : 'text-negative'"
>
{{ row.first_name }} {{ row.last_name }}
</div>
<q-separator
color="accent"
:color="row.last_work_day === null ? 'accent' : 'negative'"
class="q-mx-sm q-mt-xs"
/>
<div class=" ellipsis-2-lines text-caption">{{ row.job_title }}</div>

View File

@ -2,6 +2,7 @@
setup
lang="ts"
>
/* eslint-disable */
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
import { onMounted, ref } from 'vue';
@ -16,68 +17,59 @@
const employee_store = useEmployeeStore();
const timesheet_store = useTimesheetStore();
const ui_store = useUiStore();
const is_loading_list = ref<boolean>(true);
const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'company_name', 'supervisor_full_name', 'company_name', 'job_title', 'last_work_day']);
const filters = ref<EmployeeListFilters>({
search_bar_string: '',
hide_inactive_users: true,
});
const filterEmployeeRows = (
rows: readonly EmployeeProfile[],
terms: EmployeeListFilters,
_cols: readonly QTableColumn<EmployeeProfile>[],
_getCellValue: (col: QTableColumn<EmployeeProfile>, row: EmployeeProfile) => unknown
): EmployeeProfile[] => {
let active_rows: EmployeeProfile[] = Array.from(rows);
console.log('active rows at start: ', active_rows);
const filterEmployeeRows = (rows: readonly EmployeeProfile[], terms: EmployeeListFilters): EmployeeProfile[] => {
let result = [...rows];
if (terms.hide_inactive_users === true) {
active_rows = active_rows.filter(row =>
row.last_work_day === null
// {
// if (row.last_work_day === null) return true;
// const inactive_date = date.extractDate(row.last_work_day!, 'YYYY-MM-DD');
// const inactive_date_limit = new Date();
// inactive_date_limit.setDate(inactive_date.getDate() + 14)
// return inactive_date_limit > inactive_date
// }
);
if (terms.hide_inactive_users) {
const now = new Date();
result = result.filter(row => {
if (!row.last_work_day) return true;
const inactiveDate = date.extractDate(row.last_work_day, 'YYYY-MM-DD');
const limit = new Date(inactiveDate);
limit.setDate(limit.getDate() + 14);
return limit >= now;
});
}
let filtered_rows: EmployeeProfile[] = Array.from(active_rows);
if (terms.search_bar_string.trim().length > 0) {
const searchTerms = terms.search_bar_string.split(' ').map(s => s.trim().toLowerCase());
if (terms.search_bar_string.length > 0) {
console.log('more filtering!!')
const search_terms = terms.search_bar_string.split(',');
result = result.filter(row => {
const rowValues = Object.values(row).map(v => String(v ?? '').toLowerCase());
return searchTerms.every(term =>
rowValues.some(value => value.includes(term))
);
});
}
filtered_rows = active_rows.filter(row => Object.values(row).some(value => search_terms.includes(value)));
return result;
};
return filtered_rows;
}
onMounted(async () => {
is_loading_list.value = true;
await employee_list_api.getEmployeeList();
is_loading_list.value = false;
})
</script>
<template>
<div class="q-pa-lg">
<q-table
:key="filters.hide_inactive_users ? '1' : '0'"
dense
hide-pagination
virtual-scroll
title=" "
card-style="max-height: 70vh;"
:rows="employee_store.employee_list"
:columns="employee_list_columns"
row-key="name"
row-key="email"
:rows-per-page-options="[0]"
:pagination="{ sortBy: 'last_work_day', descending: true, }"
:filter="filters"
:filter-method="filterEmployeeRows"
class="bg-transparent no-shadow sticky-header-table"
@ -87,11 +79,11 @@
table-header-class="text-accent text-uppercase"
card-container-class="justify-center"
:grid="ui_store.user_preferences.is_employee_list_grid"
:loading="is_loading_list"
:loading="employee_store.is_loading"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
:visible-columns="['first_name', 'email', 'company', 'supervisor_full_name', 'company_name', 'job_title']"
:visible-columns="visible_columns"
>
<template #top>
<div class="row full-width q-mb-sm">
@ -127,6 +119,7 @@
color="accent"
bg-color="white"
label-color="accent"
debounce="300"
:label="$t('shared.label.search')"
>
<template v-slot:append>
@ -148,16 +141,13 @@
</template>
<template #header="props">
<q-tr
:props="props"
class="bg-primary"
>
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="text-uppercase text-weight-bolder text-white text-h6">
<span class="text-uppercase text-weight-bolder text-white" style="font-size: 1.2em;">
{{ $t(col.label) }}
</span>
</q-th>
@ -187,12 +177,32 @@
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5 cursor-pointer"
style="font-size: 1.2em;"
:style="`animation-delay: ${scope.rowIndex / 30}s;`"
:style="`animation-delay: ${scope.rowIndex / 30}s; ` + (scope.row.last_work_day === null ? '' : 'opacity: 0.5;')"
>
<div v-if="scope.col.name === 'first_name'">
<span class="text-h5 text-uppercase text-accent q-mr-xs">{{ scope.value }}</span>
<span
class="text-h5 text-uppercase q-mr-xs"
:class="scope.row.last_work_day === null ? 'text-accent' : 'text-negative'"
>{{ scope.value }}</span>
<span class="text-uppercase text-weight-light">{{ scope.row.last_name }}</span>
</div>
<div v-else-if="scope.col.name === 'last_work_day'">
<q-badge
:color="scope.row.last_work_day === null ? 'accent' : 'negative'"
class="row rounded-50 q-px-sm self-center"
>
<span class="text-bold text-uppercase q-mr-sm">
{{ scope.row.last_work_day === null ? $t('employee_list.table.active') :
$t('employee_list.table.inactive') }}
</span>
<q-icon
:name="scope.row.last_work_day === null ? 'check' : 'clear'"
size="xs"
/>
</q-badge>
</div>
<span v-else>{{ scope.value }}</span>
</div>
</transition>
@ -216,6 +226,15 @@
</template>
<style scoped>
:deep(.q-table__card .q-table__sort-icon) {
fill: white !important;
color: white !important;
}
:deep(.q-table--dense .q-table__sort-icon) {
font-size: 150%;
}
.sticky-header-table thead tr:first-child th {
background-color: var(--q-primary);
margin-top: none;

View File

@ -92,6 +92,11 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
field: 'last_work_day',
align: 'center',
sortable: true,
sort: (a: string | null, b: string | null) => {
if (a === null && b === null) return 0;
else if (a === null && b !== null) return 1;
else return -1;
},
},
];

View File

@ -113,7 +113,7 @@
dense
:icon="props.value ? 'lock' : 'lock_open'"
:color="props.value ? 'white' : 'grey-5'"
class="rounded-5 z-top"
class="rounded-5 "
:class="props.value ? 'bg-accent' : ''"
@click.stop="props.row.is_approved = !props.row.is_approved"
/>

View File

@ -1,3 +1,4 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import 'quasar/dist/types/feature-flag';