Merge pull request 'dev/nicolas/staging-prep' (#45) from dev/nicolas/staging-prep into main

Reviewed-on: Targo/targo_frontend#45
This commit is contained in:
Nicolas 2026-01-09 16:01:40 -05:00
commit 10e0939e4e
10 changed files with 207 additions and 99 deletions

View File

@ -64,4 +64,13 @@ input::-webkit-inner-spin-button {
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
.q-field--dark .q-field__control::before {
border-color: #fff3;
}
.q-field--dark .q-field__control:hover::before, .q-field--outlined .q-field__control:hover::before {
border-color: var(--q-accent);
border-width: 2px;
}

View File

@ -74,6 +74,15 @@ export default {
active: "active",
inactive: "inactive",
},
errors: {
first_name_required: "Employee's first name is required",
last_name_required: "Employee's last name is required",
company_required: "Employee must be assigned to a company",
phone_number_required: "Employee's phone number required",
hire_date_required: "Employee's first work day must be entered",
daily_hours_required: "Provide employee's expected daily hours worked",
no_modules_warning: "All modules disabled. This will lock the user out.",
}
},
employee_management: {

View File

@ -74,6 +74,15 @@ export default {
active: "actif",
inactive: "inactif",
},
errors: {
first_name_required: "Vous devez spécifier le prénom",
last_name_required: "Vous devez spécifier le nom de famille",
company_required: "Vous devez assignerl'employé à une compagnie",
phone_number_required: "Vous devez entrer un numéro de téléphone",
hire_date_required: "Vous devez entrer une date d'embauche",
daily_hours_required: "Spécifiez le nombre d'heures quotidiennes travaillé",
no_modules_warning: "Tout les modules sont désactivés. L'utilisateur sera verrouillé hors de l'application.",
}
},
employee_management: {

View File

@ -42,8 +42,34 @@
</script>
<template>
<div class="row full-width items-start content-start overflow-hidden-y">
<div class="col-12 row flex-center q-px-sm q-py-xs no-wrap">
<div class="column full-width items-start content-start overflow-hidden-y">
<!-- warning when all modules are disabled -->
<div class="col-auto row flex-center q-px-lg q-py-xs full-width">
<q-slide-transition>
<div
v-if="employee_store.employee.user_module_access.length === 0"
class="row flex-center q-px-md q-py-xs bg-dark"
style="border: 2px solid var(--q-warning);"
>
<q-icon
name="las la-exclamation-triangle"
color="warning"
size="sm"
/>
<span class="text-warning text-weight-medium q-px-sm">{{ $t('employee_list.errors.no_modules_warning') }}</span>
<q-icon
name="las la-exclamation-triangle"
color="warning"
size="sm"
/>
</div>
</q-slide-transition>
</div>
<!-- info line explaining how to customize access -->
<div class="col-auto row flex-center q-px-sm q-py-xs no-wrap">
<q-icon
name="info_outline"
size="sm"
@ -57,74 +83,14 @@
>{{ $t('employee_management.module_access.usage_description') }}</q-item-label>
</div>
<div class="column col-3 overflow-hidden-y">
<span class="text-uppercase text-weight-medium q-mx-sm">
{{ $t('employee_management.module_access.by_role') }}
</span>
<q-separator
size="2px"
color="accent"
class="q-mx-sm"
style="transform: translateY(-4px);"
/>
<q-item
clickable
class="shadow-2 rounded-5 q-ma-sm bg-dark"
@click="applyAccessPreset('admin')"
@mouseover="preset_preview = 'admin'"
@mouseleave="preset_preview = undefined"
>
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">
{{ $t('employee_management.module_access.preset_admin') }}
</q-item-label>
<q-item-label caption>
{{ $t('employee_management.module_access.admin_description') }}
</q-item-label>
</q-item-section>
</q-item>
<q-item
clickable
class="shadow-2 rounded-5 q-ma-sm bg-dark"
@click="applyAccessPreset('employee')"
@mouseover="preset_preview = 'employee'"
@mouseleave="preset_preview = undefined"
>
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">
{{ $t('employee_management.module_access.preset_employee') }}
</q-item-label>
<q-item-label caption>
{{ $t('employee_management.module_access.employee_description') }}
</q-item-label>
</q-item-section>
</q-item>
<q-btn
flat
color="negative"
icon="clear"
size="md"
align="left"
:label="$t('employee_management.module_access.uncheck_all')"
class="q-ma-sm q-px-xs rounded-5"
@click="applyAccessPreset('none')"
@mouseover="preset_preview = 'none'"
@mouseleave="preset_preview = undefined"
/>
</div>
<div class="col-xs-0 col-sm-1"></div>
<div class="row col items-start content-start">
<div class="col-12 q-mb-xs">
<div
class="col"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
<!-- column to attribute access by roles -->
<div class="column col-4 overflow-hidden-y q-pr-sm">
<span class="text-uppercase text-weight-medium q-mx-sm">
{{ $t('employee_management.module_access.by_module') }}
{{ $t('employee_management.module_access.by_role') }}
</span>
<q-separator
@ -133,31 +99,95 @@
class="q-mx-sm"
style="transform: translateY(-4px);"
/>
</div>
<div
v-for="option in employee_access_options"
:key="option.label"
class="col-lg-6 col-sm-12 col-xs-12 q-pa-xs"
>
<div
class="row full-width cursor-pointer flex-center q-pa-sm rounded-5 no-wrap shadow-5"
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)"
@click="toggleInSelected(option.value)"
<q-item
clickable
class="shadow-2 rounded-5 q-ma-sm bg-dark"
@click="applyAccessPreset('admin')"
@mouseover="preset_preview = 'admin'"
@mouseleave="preset_preview = undefined"
>
<q-icon
:name="getEmployeeAccessOptionIcon(option.value)"
size="sm"
class="q-mr-sm"
/>
<span class="text-uppercase text-weight-bold non-selectable">
{{ $t('employee_management.module_access.' + option.value) }}
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">
{{ $t('employee_management.module_access.preset_admin') }}
</q-item-label>
<q-item-label caption>
{{ $t('employee_management.module_access.admin_description') }}
</q-item-label>
</q-item-section>
</q-item>
<q-item
clickable
class="shadow-2 rounded-5 q-ma-sm bg-dark"
@click="applyAccessPreset('employee')"
@mouseover="preset_preview = 'employee'"
@mouseleave="preset_preview = undefined"
>
<q-item-section>
<q-item-label class="text-uppercase text-weight-bold">
{{ $t('employee_management.module_access.preset_employee') }}
</q-item-label>
<q-item-label caption>
{{ $t('employee_management.module_access.employee_description') }}
</q-item-label>
</q-item-section>
</q-item>
<q-btn
flat
color="negative"
icon="clear"
size="md"
align="left"
:label="$t('employee_management.module_access.uncheck_all')"
class="q-ma-sm q-px-xs rounded-5"
@click="applyAccessPreset('none')"
@mouseover="preset_preview = 'none'"
@mouseleave="preset_preview = undefined"
/>
</div>
<div class="row col items-start content-start q-pl-sm">
<div class="col-12 q-mb-xs">
<span class="text-uppercase text-weight-medium q-mx-sm">
{{ $t('employee_management.module_access.by_module') }}
</span>
<q-space />
<q-icon
:name="employee_store.employee.user_module_access.includes(option.value) ? 'check' : ''"
size="sm"
<q-separator
size="2px"
color="accent"
class="q-mx-sm"
style="transform: translateY(-4px);"
/>
</div>
<div
v-for="option in employee_access_options"
:key="option.label"
class="col-lg-6 col-sm-12 col-xs-12 q-pa-xs"
>
<div
class="row full-width cursor-pointer flex-center q-pa-sm rounded-5 no-wrap shadow-5"
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)"
@click="toggleInSelected(option.value)"
>
<q-icon
:name="getEmployeeAccessOptionIcon(option.value)"
size="sm"
class="q-mr-sm"
/>
<span class="text-uppercase text-weight-bold non-selectable">
{{ $t('employee_management.module_access.' + option.value) }}
</span>
<q-space />
<q-icon
:name="employee_store.employee.user_module_access.includes(option.value) ? 'check' : ''"
size="sm"
/>
</div>
</div>
</div>
</div>
</div>

View File

@ -19,7 +19,9 @@
color="accent"
stack-label
label-slot
class="col q-px-sm q-py-xs"
no-error-icon
hide-bottom-space
class="col q-mx-sm q-my-xs rounded-5 shadow-12"
>
<template #label>
<span

View File

@ -17,8 +17,11 @@
color="accent"
stack-label
label-slot
lazy-rules
no-error-icon
hide-bottom-space
options-selected-class="text-white text-bold bg-accent"
class="col q-px-sm q-py-xs"
class="col q-mx-sm q-my-xs bg-dark rounded-5 shadow-12"
popup-content-class="text-uppercase text-weight-medium rounded-5"
popup-content-style="border: 2px solid var(--q-accent)"
menu-anchor="bottom middle"

View File

@ -7,16 +7,14 @@
import { ref, computed } from 'vue';
import { useEmployeeStore } from 'src/stores/employee-store';
import { type QuasarRules, useEmployeeProfileRules, company_options } from 'src/modules/employee-list/employee-list-utils';
const employee_store = useEmployeeStore();
const last_work_day = computed(() => employee_store.employee.last_work_day ?? '---');
const is_first_day_picker_open = ref(false);
const is_last_day_picker_open = ref(false);
const company_options = [
{ label: 'Targo', value: 'Targo' },
{ label: 'Solucom', value: 'Solucom' },
]
const form_rules = useEmployeeProfileRules();
const supervisor_options = computed(() => {
const supervisors = employee_store.employee_list.filter(employee => employee.is_supervisor === true && employee.last_work_day === null);
@ -60,11 +58,13 @@
<AddModifyDialogFormInput
v-model="employee_store.employee.first_name"
:label="$t('profile.personal.first_name')"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.first_name_required'))]"
/>
<AddModifyDialogFormInput
v-model="employee_store.employee.last_name"
:label="$t('profile.personal.last_name')"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.last_name_required'))]"
/>
</div>
@ -80,6 +80,8 @@
<AddModifyDialogFormInput
v-model="employee_store.employee.phone_number"
:label="$t('profile.personal.phone_number')"
mask="(###) ### - ####"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.phone_number_required'))]"
/>
</div>
@ -96,6 +98,7 @@
v-model="employee_store.employee.company_name"
:options="company_options"
:label="$t('profile.employee.company')"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.company_required'))]"
/>
</div>
@ -123,6 +126,7 @@
<AddModifyDialogFormInput
v-model="employee_store.employee.daily_expected_hours"
:label="$t('employee_list.table.expected_daily_hours')"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.daily_hours_required'))]"
type="number"
/>
@ -133,7 +137,10 @@
type="number"
/>
<div v-else class="col q-px-sm"></div>
<div
v-else
class="col q-px-sm"
></div>
</div>
<div
@ -145,6 +152,7 @@
v-model="employee_store.employee.paid_time_off.sick_hours"
:label="$t('employee_management.sick_hours')"
type="number"
@update:model-value="employee_store.employee.paid_time_off.last_updated = new Date().toISOString().slice(0, 10)"
/>
<AddModifyDialogFormInput
@ -164,6 +172,7 @@
v-model:is-date-picker-open="is_first_day_picker_open"
reqires-date-picker
:label="$t('profile.employee.hired_date')"
:rules="[(value: string, rules: QuasarRules) => form_rules.isNotEmpty(value, rules, $t('employee_list.errors.hire_date_required'))]"
mask="####-##-##"
/>
@ -178,4 +187,24 @@
</div>
</q-form>
</div>
</template>
</template>
<style scoped>
:deep(.q-field--error .q-field__bottom) {
color: white;
font-weight: 500;
text-transform: uppercase;
border-radius: 0 0 5px 5px;
padding-top: 0;
align-items: center;
background-color: var(--q-negative);
}
:deep(.row > .col) {
height: fit-content;
}
:deep(.q-field--outlined.q-field--highlighted .q-field__control::after) {
border-radius: 5px 5px 0 0;
}
</style>

View File

@ -0,0 +1,17 @@
import type { EmbeddedValidationRule, EmbeddedValidationRuleFn } from "quasar";
export type QuasarRules = Record<EmbeddedValidationRule, EmbeddedValidationRuleFn>;
type EmployeeProfileValidationRule<T> = EmbeddedValidationRule | ((value: T, rules: QuasarRules, error_message: string) => boolean | string | Promise<boolean | string>);
export const useEmployeeProfileRules = () => {
const isNotEmpty: EmployeeProfileValidationRule<unknown> = (value, _rules, error_message) => (value !== undefined && value !== null && value !== '') || error_message;
return {
isNotEmpty,
}
}
export const company_options = [
{ label: 'Targo', value: 'Targo' },
{ label: 'Solucom', value: 'Solucom' },
]

View File

@ -8,7 +8,7 @@ export interface PaidTimeOff {
sick_hours: number;
vacation_hours: number;
banked_hours: number;
last_updated: string;
last_updated?: string | null;
}
export class EmployeeProfile {