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.
This commit is contained in:
Nicolas Drolet 2026-01-09 09:43:17 -05:00
parent c62350fde4
commit 35db8418a6
7 changed files with 90 additions and 46 deletions

View File

@ -4,7 +4,7 @@
> >
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { ref } from 'vue'; import { ref } from 'vue';
const q = useQuasar(); const q = useQuasar();
const is_mouseover = ref(false); const is_mouseover = ref(false);
@ -22,7 +22,7 @@ import { ref } from 'vue';
const getItemStyle = (): string => { const getItemStyle = (): string => {
const active_style = row.last_work_day === null ? '' : 'opacity: 0.6;'; const active_style = row.last_work_day === null ? '' : 'opacity: 0.6;';
const dark_style = q.dark.isActive ? 'border: 2px solid var(--q-accent);' : ''; const dark_style = q.dark.isActive ? 'border: 2px solid var(--q-accent);' : '';
const hover_style = isManagement ? (is_mouseover.value ? `transform: scale(1.1); z-index: 2;` :'transform: scale(1) skew(0)') : ''; const hover_style = isManagement ? (is_mouseover.value ? `transform: scale(1.1); z-index: 2;` : 'transform: scale(1) skew(0)') : '';
return `${active_style} ${dark_style} ${hover_style}`; return `${active_style} ${dark_style} ${hover_style}`;
} }
@ -36,7 +36,7 @@ import { ref } from 'vue';
<div <div
class="column col no-wrap bg-dark rounded-15 shadow-12" class="column col no-wrap bg-dark rounded-15 shadow-12"
:class="isManagement ? 'cursor-pointer item-mouse-hover' : ''" :class="isManagement ? 'cursor-pointer item-mouse-hover' : ''"
style="max-width: 230px; height: 275px;" style="height: 275px;"
:style="getItemStyle()" :style="getItemStyle()"
@click="$emit('onProfileClick', row.email)" @click="$emit('onProfileClick', row.email)"
@mouseenter="is_mouseover = true" @mouseenter="is_mouseover = true"
@ -57,7 +57,7 @@ import { ref } from 'vue';
</div> </div>
<div <div
class="col column items-center justify-start text-center text-weight-medium text-uppercase q-pa-sm no-wrap" class="col column items-center justify-start text-center text-weight-medium text-uppercase q-px-sm q-pt-sm no-wrap"
style="line-height: 1.2em; font-size: 1.3em;" style="line-height: 1.2em; font-size: 1.3em;"
> >
<div <div
@ -68,21 +68,40 @@ import { ref } from 'vue';
<q-separator class="q-mb-xs q-mx-md" /> <q-separator class="q-mb-xs q-mx-md" />
</div> </div>
<div class=" ellipsis-2-lines text-caption no-wrap">{{ row.job_title }}</div>
<div class="col-auto ellipsis-2-lines text-caption no-wrap">{{ row.job_title }}</div>
</div> </div>
<div <div
class="col-auto bg-primary text-white text-caption text-center text-weight-medium q-py-sm" class="col-auto column items-center bg-primary text-white text-caption text-center q-py-xs"
style="border-radius: 0 0 15px 15px;" style="border-radius: 0 0 15px 15px;"
> >
{{ row.email }} <div
class="col-auto row flex-center text-weight-light text-italic"
style="font-size: 1em;"
>
<q-icon
name="las la-phone"
size="xs"
color="accent"
class="col-auto"
/>
<span class="col-auto">{{ row.phone_number }}</span>
</div>
<span class="col-auto text-italic">extension: </span>
<span class="col-auto text-weight-medium">{{ row.email }}</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="css" scoped> <style
.item-mouse-hover { lang="css"
transition: all 0.2s ease-out; scoped
} >
.item-mouse-hover {
transition: all 0.2s ease-out;
}
</style> </style>

View File

@ -19,7 +19,7 @@
const is_management = auth_store.user?.user_module_access.includes('employee_management') ?? false; const is_management = auth_store.user?.user_module_access.includes('employee_management') ?? false;
const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'job_title', 'last_work_day']); const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'job_title', 'phone_number', 'last_work_day']);
const table_grid_container = ref<HTMLElement | null>(null); const table_grid_container = ref<HTMLElement | null>(null);
@ -28,6 +28,10 @@
hide_inactive_users: true, hide_inactive_users: true,
}); });
const { maxHeight } = defineProps<{
maxHeight: number;
}>();
const filterEmployeeRows = (rows: readonly EmployeeProfile[], terms: EmployeeListFilters, _cols: readonly QTableColumn<EmployeeProfile>[]): EmployeeProfile[] => { const filterEmployeeRows = (rows: readonly EmployeeProfile[], terms: EmployeeListFilters, _cols: readonly QTableColumn<EmployeeProfile>[]): EmployeeProfile[] => {
let result = [...rows]; let result = [...rows];
@ -63,7 +67,7 @@
</script> </script>
<template> <template>
<div class="q-pa-lg"> <div class="full-width">
<q-table <q-table
:key="filters.hide_inactive_users ? '1' : '0'" :key="filters.hide_inactive_users ? '1' : '0'"
dense dense
@ -77,10 +81,11 @@
:pagination="{ sortBy: 'first_name' }" :pagination="{ sortBy: 'first_name' }"
: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 full-width q-pt-lg"
:style="$q.screen.lt.md ? '' : 'width: 80vw;'" :style="employee_store.employee_list.length > 0 ? `max-height: ${maxHeight - (ui_store.user_preferences.is_employee_list_grid ? 0 : 20)}px;` : ''"
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10 hide-scrollbar' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10 hide-scrollbar'" :table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10 hide-scrollbar' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10 hide-scrollbar'"
color="accent" color="accent"
separator="none"
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"
@ -193,6 +198,7 @@
<q-td <q-td
:props="scope" :props="scope"
@click="is_management ? employee_store.openAddModifyDialog(scope.row.email) : ''" @click="is_management ? employee_store.openAddModifyDialog(scope.row.email) : ''"
:class="scope.rowIndex % 2 === 0 ? ($q.dark.isActive ? 'bg-primary' : 'bg-secondary') : ''"
> >
<transition <transition
appear appear
@ -287,4 +293,8 @@ tbody {
:deep(.q-table) { :deep(.q-table) {
transition: height 0.25s ease; transition: height 0.25s ease;
} }
:deep(.q-table__grid-content) {
overflow: auto
}
</style> </style>

View File

@ -80,6 +80,13 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
align: 'left', align: 'left',
sortable: true, sortable: true,
}, },
{
name: 'phone_number',
label: 'employee_list.table.phone_number',
field: 'phone_number',
align: 'left',
sortable: true,
},
{ {
name: 'job_title', name: 'job_title',
label: 'employee_list.table.role', label: 'employee_list.table.role',

View File

@ -7,9 +7,8 @@
const modelApproval = defineModel<boolean>(); const modelApproval = defineModel<boolean>();
const { row, index = 0 } = defineProps<{ const { row } = defineProps<{
row: TimesheetApprovalOverview; row: TimesheetApprovalOverview;
index?: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -26,7 +25,7 @@
> >
<q-card <q-card
class="rounded-10 shadow-5" class="rounded-10 shadow-5"
:style="`animation-delay: ${index / 15}s; opacity: ${row.is_active ? '1' : '0.75'}; transform: scale(${row.is_active ? '1' : '0.9'})`" :style="`opacity: ${row.is_active ? '1' : '0.75'}; transform: scale(${row.is_active ? '1' : '0.9'})`"
> >
<!-- Card header with employee name and details button--> <!-- Card header with employee name and details button-->
<q-card-section <q-card-section

View File

@ -15,10 +15,10 @@
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api'; 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 { 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 { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
const WARNING_COLUMNS: OverviewColumns[] = ['EMERGENCY', 'EVENING', 'HOLIDAY', 'VACATION', 'SICK'] const WARNING_COLUMNS: OverviewColumns[] = ['EVENING', 'HOLIDAY', 'VACATION', 'SICK']
const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME',] const NEGATIVE_COLUMNS: OverviewColumns[] = ['OVERTIME', 'EMERGENCY']
const ui_store = useUiStore(); const ui_store = useUiStore();
const auth_store = useAuthStore(); const auth_store = useAuthStore();
@ -51,7 +51,7 @@ import { useUiStore } from 'src/stores/ui-store';
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>({ const overview_filters = ref<PayPeriodOverviewFilters>({
is_showing_inactive: false, is_showing_inactive: false,
is_showing_team_only: false, is_showing_team_only: true,
supervisors: [], supervisors: [],
name_search_string: '', name_search_string: '',
}); });
@ -88,12 +88,14 @@ import { useUiStore } from 'src/stores/ui-store';
return result; return result;
}; };
const getListViewTimeClass = (column_name: OverviewColumns, value: number) => { const getListViewTimeCss = (column_name: OverviewColumns, value: number): { classes: string, style: string } => {
if(WARNING_COLUMNS.includes(column_name) && value > 0) if (WARNING_COLUMNS.includes(column_name) && value > 0)
return 'bg-warning text-white rounded-5'; return { classes: 'bg-warning text-white rounded-5', style: '' };
if(NEGATIVE_COLUMNS.includes(column_name) && value > 0) if (NEGATIVE_COLUMNS.includes(column_name) && value > 0)
return 'bg-negative text-white text-bold rounded-5'; return { classes: 'bg-negative text-white text-bold rounded-5', style: '' };
return { classes: '', style: '' }
} }
</script> </script>
@ -104,6 +106,7 @@ import { useUiStore } from 'src/stores/ui-store';
dense dense
row-key="email" row-key="email"
color="accent" color="accent"
separator="none"
hide-pagination hide-pagination
:rows="overview_rows" :rows="overview_rows"
:columns="pay_period_overview_columns" :columns="pay_period_overview_columns"
@ -123,7 +126,7 @@ import { useUiStore } from 'src/stores/ui-store';
:loading-label="$t('shared.label.loading')" :loading-label="$t('shared.label.loading')"
table-header-style="min-width: 80xp; max-width: 80px;" 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;` : ''" :style="overview_rows.length > 0 ? `max-height: ${maxHeight - (ui_store.user_preferences.is_timesheet_approval_grid ? 0 : 20)}px;` : ''"
:table-style="{ tableLayout: 'fixed'}" :table-style="{ tableLayout: 'fixed' }"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)" @row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
> >
<template #top> <template #top>
@ -278,17 +281,16 @@ import { useUiStore } from 'src/stores/ui-store';
{{ props.row.employee_last_name }} {{ props.row.employee_last_name }}
</span> </span>
</div> </div>
<!-- display weekly total hours (regular, vacation, holiday, emergency, evening) --> <!-- display weekly total hours (regular, vacation, holiday, emergency, evening) -->
<div <div
v-else-if="props.col.name.includes('weekly_hours')" v-else-if="props.col.name.includes('weekly_hours')"
class="q-px-xs" class="q-px-xs"
:class="props.value[Number(props.col.name.slice(-1,)) - 1] > 40 ? 'bg-negative text-white rounded-5' : ''"
> >
<span> <span>
{{ {{
getHoursMinutesStringFromHoursFloat((props.value[Number(props.col.name.slice(-1,)) - getHoursMinutesStringFromHoursFloat((props.value[Number(props.col.name.slice(-1,)) -
1]) ?? 0) }} 1]) ?? 0) }}
</span> </span>
</div> </div>
@ -296,7 +298,6 @@ import { useUiStore } from 'src/stores/ui-store';
<div <div
v-else-if="props.col.name === 'total_hours'" v-else-if="props.col.name === 'total_hours'"
class="q-px-xs" class="q-px-xs"
:class="props.value > 80 ? 'bg-negative rounded-5 text-white' : ''"
> >
<span>{{ getHoursMinutesStringFromHoursFloat(props.value) }}</span> <span>{{ getHoursMinutesStringFromHoursFloat(props.value) }}</span>
</div> </div>
@ -305,7 +306,7 @@ import { useUiStore } from 'src/stores/ui-store';
<div <div
v-else v-else
class="q-px-xs" class="q-px-xs"
:class="getListViewTimeClass(props.col.name, props.value)" :class="getListViewTimeCss(props.col.name, props.value).classes"
> >
{{ TIME_COLUMNS.includes(props.col.name) ? {{ TIME_COLUMNS.includes(props.col.name) ?
getHoursMinutesStringFromHoursFloat(props.value) : props.value }} getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
@ -320,7 +321,6 @@ import { useUiStore } from 'src/stores/ui-store';
<OverviewListItem <OverviewListItem
v-model="props.row.is_approved" v-model="props.row.is_approved"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no" :key="props.row.email + timesheet_store.pay_period?.pay_period_no"
:index="props.rowIndex"
:row="props.row" :row="props.row"
@click-details="onClickedDetails" @click-details="onClickedDetails"
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)" @click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
@ -347,7 +347,10 @@ import { useUiStore } from 'src/stores/ui-store';
</div> </div>
</template> </template>
<style lang="sass" scoped> <style
lang="sass"
scoped
>
.sticky-header-table .sticky-header-table
thead tr:first-child th thead tr:first-child th
background-color: var(--q-accent) background-color: var(--q-accent)

View File

@ -4,24 +4,33 @@
> >
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue'; import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
import AddModifyDialog from 'src/modules/employee-list/components/add-modify-dialog.vue'; import AddModifyDialog from 'src/modules/employee-list/components/add-modify-dialog.vue';
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import { onMounted, ref, computed } from 'vue';
import { onMounted } from 'vue';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api'; import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
const employee_list_api = useEmployeeListApi(); const employee_list_api = useEmployeeListApi();
const page_height = ref(0);
const table_max_height = computed(() => page_height.value);
const tableStyleFunction = (offset: number, height: number) => {
page_height.value = height - offset;
return { minHeight: height - offset + 'px' };
};
onMounted(async () => { onMounted(async () => {
await employee_list_api.getEmployeeList(); await employee_list_api.getEmployeeList();
}) })
</script> </script>
<template> <template>
<q-page class="column items-center bg-secondary"> <q-page
class="column items-center bg-secondary"
:style-fn="tableStyleFunction"
>
<AddModifyDialog /> <AddModifyDialog />
<PageHeaderTemplate title="employee_list.page_header" /> <EmployeeListTable :max-height="table_max_height" />
<EmployeeListTable />
</q-page> </q-page>
</template> </template>

View File

@ -48,10 +48,7 @@
class="column items-center scroll q-px-sm full-width" class="column items-center scroll q-px-sm full-width"
style="min-height: inherit;" style="min-height: inherit;"
> >
<div <div class="col-auto">
ref="headerComponent"
class="col-auto"
>
<PageHeaderTemplate <PageHeaderTemplate
title="timesheet_approvals.page_title" title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period?.period_start ?? ''" :start-date="timesheet_store.pay_period?.period_start ?? ''"