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 // add your custom rules here
rules: { rules: {
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
// warn about unused but underscored variables // warn about unused but underscored variables
"@typescript-eslint/no-unused-vars": "off",
'no-unused-vars': [ 'no-unused-vars': [
'warn', 'warn',
{ argsIgnorePattern: '^_' } { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
], ],
// allow debugger during development only // allow debugger during development only

View File

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

2
src/env.d.ts vendored
View File

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

View File

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

View File

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

View File

@ -21,19 +21,21 @@
<template> <template>
<transition <transition
appear appear
enter-active-class="animated fadeInUp slow" enter-active-class="animated fadeInUp fast"
leave-active-class="animated fadeOutDown fast"
mode="out-in" mode="out-in"
> >
<q-card <q-card
v-ripple 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" 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="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)" @click="emit('onProfileClick', row.email)"
> >
<q-card-section class="col-6 text-center"> <q-card-section class="col-6 text-center">
<q-avatar <q-avatar
:color="row.last_work_day === undefined ? 'accent' : 'negative'" :color="row.last_work_day === null ? 'accent' : 'negative'"
size="8em" size="8em"
class="shadow-3 q-mb-md" class="shadow-3 q-mb-md"
> >
@ -51,12 +53,12 @@
> >
<div <div
class="ellipsis" 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 }} {{ row.first_name }} {{ row.last_name }}
</div> </div>
<q-separator <q-separator
color="accent" :color="row.last_work_day === null ? 'accent' : 'negative'"
class="q-mx-sm q-mt-xs" class="q-mx-sm q-mt-xs"
/> />
<div class=" ellipsis-2-lines text-caption">{{ row.job_title }}</div> <div class=" ellipsis-2-lines text-caption">{{ row.job_title }}</div>

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue'; import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
@ -16,68 +17,59 @@
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const ui_store = useUiStore(); 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>({ const filters = ref<EmployeeListFilters>({
search_bar_string: '', search_bar_string: '',
hide_inactive_users: true, hide_inactive_users: true,
}); });
const filterEmployeeRows = ( const filterEmployeeRows = (rows: readonly EmployeeProfile[], terms: EmployeeListFilters): EmployeeProfile[] => {
rows: readonly EmployeeProfile[], let result = [...rows];
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);
if (terms.hide_inactive_users === true) { if (terms.hide_inactive_users) {
active_rows = active_rows.filter(row => const now = new Date();
row.last_work_day === null result = result.filter(row => {
// { if (!row.last_work_day) return true;
// if (row.last_work_day === null) return true; const inactiveDate = date.extractDate(row.last_work_day, 'YYYY-MM-DD');
const limit = new Date(inactiveDate);
// const inactive_date = date.extractDate(row.last_work_day!, 'YYYY-MM-DD'); limit.setDate(limit.getDate() + 14);
// const inactive_date_limit = new Date(); return limit >= now;
// inactive_date_limit.setDate(inactive_date.getDate() + 14) });
// return inactive_date_limit > inactive_date
// }
);
} }
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) { result = result.filter(row => {
console.log('more filtering!!') const rowValues = Object.values(row).map(v => String(v ?? '').toLowerCase());
const search_terms = terms.search_bar_string.split(','); 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 () => { onMounted(async () => {
is_loading_list.value = true;
await employee_list_api.getEmployeeList(); await employee_list_api.getEmployeeList();
is_loading_list.value = false;
}) })
</script> </script>
<template> <template>
<div class="q-pa-lg"> <div class="q-pa-lg">
<q-table <q-table
:key="filters.hide_inactive_users ? '1' : '0'"
dense dense
hide-pagination hide-pagination
virtual-scroll
title=" " title=" "
card-style="max-height: 70vh;" card-style="max-height: 70vh;"
:rows="employee_store.employee_list" :rows="employee_store.employee_list"
:columns="employee_list_columns" :columns="employee_list_columns"
row-key="name" row-key="email"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
:pagination="{ sortBy: 'last_work_day', descending: true, }"
:filter="filters" :filter="filters"
:filter-method="filterEmployeeRows" :filter-method="filterEmployeeRows"
class="bg-transparent no-shadow sticky-header-table" class="bg-transparent no-shadow sticky-header-table"
@ -87,11 +79,11 @@
table-header-class="text-accent text-uppercase" table-header-class="text-accent text-uppercase"
card-container-class="justify-center" card-container-class="justify-center"
:grid="ui_store.user_preferences.is_employee_list_grid" :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-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')" :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> <template #top>
<div class="row full-width q-mb-sm"> <div class="row full-width q-mb-sm">
@ -127,6 +119,7 @@
color="accent" color="accent"
bg-color="white" bg-color="white"
label-color="accent" label-color="accent"
debounce="300"
:label="$t('shared.label.search')" :label="$t('shared.label.search')"
> >
<template v-slot:append> <template v-slot:append>
@ -148,16 +141,13 @@
</template> </template>
<template #header="props"> <template #header="props">
<q-tr <q-tr :props="props">
:props="props"
class="bg-primary"
>
<q-th <q-th
v-for="col in props.cols" v-for="col in props.cols"
:key="col.name" :key="col.name"
:props="props" :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) }} {{ $t(col.label) }}
</span> </span>
</q-th> </q-th>
@ -187,12 +177,32 @@
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)" :key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5 cursor-pointer" class="rounded-5 cursor-pointer"
style="font-size: 1.2em;" 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'"> <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> <span class="text-uppercase text-weight-light">{{ scope.row.last_name }}</span>
</div> </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> <span v-else>{{ scope.value }}</span>
</div> </div>
</transition> </transition>
@ -216,6 +226,15 @@
</template> </template>
<style scoped> <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 { .sticky-header-table thead tr:first-child th {
background-color: var(--q-primary); background-color: var(--q-primary);
margin-top: none; margin-top: none;

View File

@ -92,6 +92,11 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
field: 'last_work_day', field: 'last_work_day',
align: 'center', align: 'center',
sortable: true, 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 dense
:icon="props.value ? 'lock' : 'lock_open'" :icon="props.value ? 'lock' : 'lock_open'"
:color="props.value ? 'white' : 'grey-5'" :color="props.value ? 'white' : 'grey-5'"
class="rounded-5 z-top" class="rounded-5 "
:class="props.value ? 'bg-accent' : ''" :class="props.value ? 'bg-accent' : ''"
@click.stop="props.row.is_approved = !props.row.is_approved" @click.stop="props.row.is_approved = !props.row.is_approved"
/> />

View File

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