feat(employees-list): add paid time off fields to employee details, DRY details dialog code

This commit is contained in:
Nicolas Drolet 2026-01-09 11:40:21 -05:00
parent 35db8418a6
commit 6ec05a00b8
6 changed files with 197 additions and 213 deletions

View File

@ -70,6 +70,7 @@ export default {
supervisor: "Supervisor", supervisor: "Supervisor",
company: "Company", company: "Company",
is_supervisor: "is a supervisor", is_supervisor: "is a supervisor",
expected_daily_hours: "Expected Daily Hours",
active: "active", active: "active",
inactive: "inactive", inactive: "inactive",
}, },
@ -81,7 +82,11 @@ export default {
access_label: "access", access_label: "access",
details_label: "details", details_label: "details",
schedule_label: "schedule", schedule_label: "schedule",
can_be_entered_later: "OPTIONAL: can be entered later",
enter_delete_input: "type 'DELETE' to remove", enter_delete_input: "type 'DELETE' to remove",
banked_hours: "available banked hours",
sick_hours: "available PTO hours",
vacation_hours: "available vacation hours",
schedule_presets: { schedule_presets: {
preset_list_placeholder: "Select a schedule", preset_list_placeholder: "Select a schedule",
preset_name_placeholder: "schedule preset name", preset_name_placeholder: "schedule preset name",

View File

@ -70,6 +70,7 @@ export default {
supervisor: "superviseur", supervisor: "superviseur",
company: "Compagnie", company: "Compagnie",
is_supervisor: "est un superviseur", is_supervisor: "est un superviseur",
expected_daily_hours: "Heures quotidiennes attendues",
active: "actif", active: "actif",
inactive: "inactif", inactive: "inactif",
}, },
@ -81,7 +82,11 @@ export default {
access_label: "accès", access_label: "accès",
details_label: "détails", details_label: "détails",
schedule_label: "horaire", schedule_label: "horaire",
can_be_entered_later: "FACULTATIF: peut être entré plus tard",
enter_delete_input: "tappez 'SUPPRIMER' pour confirmer", enter_delete_input: "tappez 'SUPPRIMER' pour confirmer",
banked_hours: "heures en banque disponibles",
sick_hours: "heures d'absence payées disponibles",
vacation_hours: "heures de vacances disponibles",
schedule_presets: { schedule_presets: {
preset_list_placeholder: "Sélectionner un horaire", preset_list_placeholder: "Sélectionner un horaire",
preset_name_placeholder: "nom de l'horaire", preset_name_placeholder: "nom de l'horaire",

View File

@ -0,0 +1,56 @@
<script
setup
lang="ts"
>
const model = defineModel<string | number | null | undefined>({ required: true });
const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', {default: false});
defineProps<{
label?: string | undefined;
requiresDatePicker?: boolean | undefined;
}>();
</script>
<template>
<q-input
v-model="model"
dense
outlined
color="accent"
stack-label
label-slot
class="col q-px-sm q-py-xs"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ label }}
</span>
</template>
<template #append v-if="requiresDatePicker">
<q-btn
flat
dense
size="lg"
icon="calendar_month"
color="accent"
@click="is_date_picker_open = true"
>
<q-dialog
v-model="is_date_picker_open"
backdrop-filter="none"
>
<q-date
v-model="model"
mask="YYYY-MM-DD"
color="accent"
@update:model-value="is_date_picker_open = false"
/>
</q-dialog>
</q-btn>
</template>
</q-input>
</template>

View File

@ -0,0 +1,37 @@
<script
setup
lang="ts"
>
const model = defineModel<string>({ required: true });
defineProps<{
label?: string | undefined;
}>();
</script>
<template>
<q-select
v-model="model"
dense
outlined
color="accent"
stack-label
label-slot
options-selected-class="text-white text-bold bg-accent"
class="col q-px-sm q-py-xs"
popup-content-class="text-uppercase text-weight-medium rounded-5"
popup-content-style="border: 2px solid var(--q-accent)"
menu-anchor="bottom middle"
menu-self="top middle"
:menu-offset="[0, 5]"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ label }}
</span>
</template>
</q-select>
</template>

View File

@ -2,6 +2,9 @@
setup setup
lang="ts" lang="ts"
> >
import AddModifyDialogFormInput from 'src/modules/employee-list/components/add-modify-dialog-form-input.vue';
import AddModifyDialogFormSelect from 'src/modules/employee-list/components/add-modify-dialog-form-select.vue';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
@ -20,7 +23,7 @@
return supervisors.map(supervisor => supervisor.first_name + ' ' + supervisor.last_name); return supervisors.map(supervisor => supervisor.first_name + ' ' + supervisor.last_name);
}) })
const setLastWorkDay = (date: string | number | null) => { const setLastWorkDay = (date: string | number | null | undefined) => {
if (typeof date === 'string' && date.length > 0) { if (typeof date === 'string' && date.length > 0) {
employee_store.employee.last_work_day = date; employee_store.employee.last_work_day = date;
} }
@ -54,260 +57,124 @@
class="q-ma-xs" class="q-ma-xs"
:class="$q.screen.lt.sm ? 'column' : 'row'" :class="$q.screen.lt.sm ? 'column' : 'row'"
> >
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.first_name" v-model="employee_store.employee.first_name"
color="accent" :label="$t('profile.personal.first_name')"
stack-label />
label-slot
class="col q-mx-md"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.personal.first_name') }}
</span>
</template>
</q-input>
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.last_name" v-model="employee_store.employee.last_name"
color="accent" :label="$t('profile.personal.last_name')"
stack-label />
label-slot
class="col q-mx-md"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.personal.last_name') }}
</span>
</template>
</q-input>
</div> </div>
<div <div
class="q-ma-xs" class="q-ma-xs"
:class="$q.screen.lt.sm ? 'column' : 'row'" :class="$q.screen.lt.sm ? 'column' : 'row'"
> >
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.email" v-model="employee_store.employee.email"
color="accent" :label="$t('profile.employee.email')"
stack-label />
label-slot
class="col q-mx-md"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.email') }}
</span>
</template>
</q-input>
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.phone_number" v-model="employee_store.employee.phone_number"
color="accent" :label="$t('profile.personal.phone_number')"
stack-label />
label-slot
mask="(###) ### - ####"
class="col q-mx-md"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.personal.phone_number') }}
</span>
</template>
</q-input>
</div> </div>
<div <div
class="q-ma-xs" class="q-ma-xs"
:class="$q.screen.lt.sm ? 'column' : 'row'" :class="$q.screen.lt.sm ? 'column' : 'row'"
> >
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.job_title" v-model="employee_store.employee.job_title"
color="accent" :label="$t('profile.employee.job_title')"
stack-label />
label-slot
class="col q-mx-md"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.job_title') }}
</span>
</template>
</q-input>
<q-select <AddModifyDialogFormSelect
v-model="employee_store.employee.company_name" v-model="employee_store.employee.company_name"
color="accent"
:options="company_options" :options="company_options"
stack-label :label="$t('profile.employee.company')"
emit-value />
label-slot
class="col q-mx-md"
popup-content-class="text-uppercase text-weight-medium rounded-20"
popup-content-style="border: 2px solid var(--q-accent)"
menu-anchor="bottom middle"
menu-self="top middle"
:menu-offset="[0, 10]"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.company') }}
</span>
</template>
</q-select>
</div> </div>
<div <div
class="q-ma-xs" class="q-ma-xs"
:class="$q.screen.lt.sm ? 'column' : 'row'" :class="$q.screen.lt.sm ? 'column' : 'row'"
> >
<q-select <AddModifyDialogFormSelect
v-model="employee_store.employee.supervisor_full_name" v-model="employee_store.employee.supervisor_full_name"
color="accent"
stack-label
label-slot
:options="supervisor_options" :options="supervisor_options"
options-selected-class="text-white text-bold bg-accent" :label="$t('profile.employee.supervisor')"
class="col q-mx-md" />
popup-content-class="text-uppercase text-weight-medium rounded-20"
popup-content-style="border: 2px solid var(--q-accent)"
menu-anchor="bottom middle"
menu-self="top middle"
:menu-offset="[0, 10]"
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.supervisor') }}
</span>
</template>
</q-select>
<q-input <AddModifyDialogFormInput
v-if="employee_store.management_mode === 'modify_employee'"
v-model="employee_store.employee.external_payroll_id" v-model="employee_store.employee.external_payroll_id"
color="accent" :label="$t('profile.employee.bankroll_id')"
stack-label :placeholder="$t('employee_management.can_be_entered_later')"
label-slot />
class="col q-mx-md" </div>
>
<template #label> <div
<span class="q-ma-xs"
class="text-weight-bolder text-uppercase" :class="$q.screen.lt.sm ? 'column' : 'row'"
style="font-size: 0.85em;" >
> <AddModifyDialogFormInput
{{ $t('profile.employee.bankroll_id') }} v-model="employee_store.employee.daily_expected_hours"
</span> :label="$t('employee_list.table.expected_daily_hours')"
</template> type="number"
</q-input> />
<AddModifyDialogFormInput
v-if="employee_store.employee.paid_time_off"
v-model="employee_store.employee.paid_time_off.banked_hours"
:label="$t('employee_management.banked_hours')"
type="number"
/>
<div v-else class="col q-px-sm"></div>
</div>
<div
class="q-ma-xs"
:class="$q.screen.lt.sm ? 'column' : 'row'"
>
<AddModifyDialogFormInput
v-if="employee_store.employee.paid_time_off"
v-model="employee_store.employee.paid_time_off.sick_hours"
:label="$t('employee_management.sick_hours')"
type="number"
/>
<AddModifyDialogFormInput
v-if="employee_store.employee.paid_time_off"
v-model="employee_store.employee.paid_time_off.vacation_hours"
:label="$t('employee_management.vacation_hours')"
type="number"
/>
</div> </div>
<div <div
class="q-ma-xs" class="q-ma-xs"
:class="$q.screen.lt.md ? 'column' : 'row'" :class="$q.screen.lt.md ? 'column' : 'row'"
> >
<q-input <AddModifyDialogFormInput
v-model="employee_store.employee.first_work_day" v-model="employee_store.employee.first_work_day"
color="accent" v-model:is-date-picker-open="is_first_day_picker_open"
stack-label reqires-date-picker
label-slot :label="$t('profile.employee.hired_date')"
mask="####-##-##" mask="####-##-##"
class="col q-mx-md" />
>
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.hired_date') }}
</span>
</template>
<template #append> <AddModifyDialogFormInput
<q-btn
flat
dense
size="lg"
icon="calendar_month"
color="accent"
@click="is_first_day_picker_open = true"
>
<q-dialog
v-model="is_first_day_picker_open"
backdrop-filter="none"
>
<q-date
v-model="employee_store.employee.first_work_day"
mask="YYYY-MM-DD"
color="accent"
@update:model-value="is_first_day_picker_open = false"
/>
</q-dialog>
</q-btn>
</template>
</q-input>
<q-input
v-model="last_work_day" v-model="last_work_day"
color="accent" v-model:is-date-picker-open="is_last_day_picker_open"
stack-label reqires-date-picker
label-slot :label="$t('profile.employee.fired_date')"
mask="####-##-##" mask="####-##-##"
class="col q-mx-md"
@update:model-value="setLastWorkDay" @update:model-value="setLastWorkDay"
> />
<template #label>
<span
class="text-weight-bolder text-uppercase"
style="font-size: 0.85em;"
>
{{ $t('profile.employee.fired_date') }}
</span>
</template>
<template #append>
<q-btn
flat
dense
size="lg"
icon="calendar_month"
color="accent"
@click="is_last_day_picker_open = true"
>
<q-dialog
v-model="is_last_day_picker_open"
backdrop-filter="none"
>
<q-date
v-model="employee_store.employee.last_work_day"
mask="YYYY-MM-DD"
color="accent"
@update:model-value="is_last_day_picker_open = false"
/>
</q-dialog>
</q-btn>
</template>
</q-input>
</div> </div>
</q-form> </q-form>
</div> </div>

View File

@ -4,6 +4,13 @@ import type { UserModuleAccess } from "src/modules/shared/models/user.models";
export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none'; export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none';
export type CompanyNames = 'Targo' | 'Solucom'; export type CompanyNames = 'Targo' | 'Solucom';
export interface PaidTimeOff {
sick_hours: number;
vacation_hours: number;
banked_hours: number;
last_updated: string;
}
export class EmployeeProfile { export class EmployeeProfile {
first_name: string; first_name: string;
last_name: string; last_name: string;
@ -14,7 +21,9 @@ export class EmployeeProfile {
phone_number: string; phone_number: string;
first_work_day: string; first_work_day: string;
last_work_day?: string | null; last_work_day?: string | null;
external_payroll_id: number; external_payroll_id?: number;
daily_expected_hours?: number;
paid_time_off?: PaidTimeOff;
residence: string; residence: string;
birth_date: string; birth_date: string;
is_supervisor: boolean; is_supervisor: boolean;
@ -34,7 +43,6 @@ export class EmployeeProfile {
this.residence = ''; this.residence = '';
this.birth_date = ''; this.birth_date = '';
this.is_supervisor = false; this.is_supervisor = false;
this.external_payroll_id = 999;
this.user_module_access = ['dashboard',]; this.user_module_access = ['dashboard',];
} }
} }
@ -92,6 +100,12 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
label: 'employee_list.table.role', label: 'employee_list.table.role',
field: 'job_title', field: 'job_title',
align: 'left', align: 'left',
},
{
name: 'expected_daily_hours',
label: 'employee_list.table.expected_daily_hours',
field: 'daily_expected_hours',
align: 'left',
}, },
{ {
name: 'last_work_day', name: 'last_work_day',