feat(profile): Profile module nearly complete, technically modular and could be used in employee list. Mobile friendly. Implement Dark Mode.

This commit is contained in:
Nicolas Drolet 2025-09-18 14:40:06 -04:00
parent b9a549b9f9
commit e9a8350b09
31 changed files with 620 additions and 549 deletions

View File

@ -105,7 +105,8 @@ export default defineConfig((ctx) => {
notify: { notify: {
color: 'primary', color: 'primary',
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png', avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
} },
dark: 'auto',
}, },
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set

View File

@ -1,5 +1,5 @@
// app global css in SCSS form // app global css in SCSS form
@each $size in (5, 10, 15, 20, 25, 50, 75, 100) { @each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) {
.rounded-#{$size} { .rounded-#{$size} {
border-radius: #{$size}px !important; border-radius: #{$size}px !important;
} }
@ -22,4 +22,14 @@
.q-table tbody tr:hover { .q-table tbody tr:hover {
background: #00ff260c; background: #00ff260c;
}
.body--dark {
--q-secondary: #0f1114;
color: $grey-2;
}
.body--light {
--q-dark: #FFF;
color: $grey-8;
} }

View File

@ -20,8 +20,8 @@ $verdigris: #6EBAB0;
$mint: #56B586; $mint: #56B586;
$dark-font: #1f3a1f; $dark-font: #1f3a1f;
$dark: #000; $dark: #333;
$dark-page: #323232; $dark-page: #343434;
$positive: #21ba45; $positive: #21ba45;
$negative: #e6364b; $negative: #e6364b;

View File

@ -115,31 +115,28 @@ export default {
}, },
profilePage: { profilePage: {
personalInfo: { personalInfo: {
title: 'Profile', title: 'PERSONAL',
firstName: 'First name', firstName: 'FIRST NAME',
lastName: 'Last name', lastName: 'LAST NAME',
gender: 'Gender', phoneNumber: 'PHONE NUMBER',
genderMale: 'Man', address: 'ADDRESS',
genderFemale: 'Woman',
genderNonBinary: 'Non-binary',
genderUnspecified: 'Unspecified',
phoneNumber: 'Phone number',
jobTitle: 'Job title',
company: 'Company',
supervisor: 'Supervisor',
role: 'Role',
address: 'Address',
addressPlaceholder: '# address, city, region, country', addressPlaceholder: '# address, city, region, country',
birthDate: 'Date of birth', birthDate: 'DATE OF BIRTH',
submitInfo: 'Update Profile',
}, },
employeeInfo: { employeeInfo: {
title: 'Employee info', title: 'CAREER',
workEmail: 'Work e-mail', workEmail: 'E-MAIL',
jobTitle: 'Job Title', jobTitle: 'JOB TITLE',
companyName: 'Company', companyName: 'COMPANY',
supervisorName: 'Supervisor', supervisorName: 'SUPERVISOR',
hiredDate: 'Hiring date', hiredDate: 'HIRING DATE',
},
preferences: {
title: 'PREFERENCES',
display_options: "display options",
language_options: "language options",
dark_mode: 'dark',
light_mode: 'light',
}, },
errors: { errors: {
mustEnterBirthdate: 'You must enter a valid birthdate', mustEnterBirthdate: 'You must enter a valid birthdate',

View File

@ -191,31 +191,28 @@ export default {
}, },
profilePage: { profilePage: {
personalInfo: { personalInfo: {
title: 'Info personnel', title: 'PERSONNELLE',
firstName: 'Prénom', firstName: 'PRENOM',
lastName: 'Nom de famille', lastName: 'NOM DE FAMILLE',
gender: 'Genre', phoneNumber: 'NUMERO DE TELEPHONE',
genderMale: 'Homme', address: 'ADRESSE',
genderFemale: 'Femme',
genderNonBinary: 'Non-binaire',
genderUnspecified: 'Non-spécifié',
phoneNumber: 'Numéro de téléphone',
jobTitle: 'Titre du poste',
company: 'Entreprise',
supervisor: 'Superviseur',
role: 'Role',
address: 'Adresse',
addressPlaceholder: '# addresse, ville, région, pays', addressPlaceholder: '# addresse, ville, région, pays',
birthDate: 'Date de naissance', birthDate: 'DATE DE NAISSANCE',
submitInfo: 'Modifier Profil',
}, },
employeeInfo: { employeeInfo: {
title: 'Info Employé', title: 'CARRIÈRE',
workEmail: 'Courriel employé', workEmail: 'COURRIEL',
jobTitle: 'Poste', jobTitle: 'POSTE',
companyName: 'Compagnie', companyName: 'COMPAGNIE',
supervisorName: 'Nom du superviseur', supervisorName: 'NOM DU SUPERVISEUR',
hiredDate: 'Date embauché', hiredDate: 'DATE EMBAUCHE',
},
preferences: {
title: 'PRÉFÉRENCES',
display_options: "Options d'affichage",
language_options: "Options de langue",
dark_mode: 'sombre',
light_mode: 'clair',
}, },
errors: { errors: {
mustEnterBirthdate: 'Vous devez entrer une date de naissance valide', mustEnterBirthdate: 'Vous devez entrer une date de naissance valide',

View File

@ -35,7 +35,7 @@
<div class="q-pt-sm q-px-xl q-pb-lg"> <div class="q-pt-sm q-px-xl q-pb-lg">
<q-card-section class="text-center"> <q-card-section class="text-center">
<div class="text-h6 text-grey-9 text-weight-bold"> <div class="text-h6 text-weight-bold">
{{ $t('loginPage.title') }} {{ $t('loginPage.title') }}
</div> </div>
</q-card-section> </q-card-section>

View File

@ -18,7 +18,7 @@
<template> <template>
<q-card <q-card
v-ripple v-ripple
class="rounded-15 bg-white col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer q-ma-sm" class="rounded-15 col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer q-ma-sm"
style="max-width: 230px;" style="max-width: 230px;"
@click="emit('onProfileClick', props.row.email)" @click="emit('onProfileClick', props.row.email)"
> >
@ -33,7 +33,7 @@
</div> </div>
</q-card-section> </q-card-section>
<q-separator color="primary" class="q-mx-sm q-mt-xs" /> <q-separator color="primary" class="q-mx-sm q-mt-xs" />
<q-card-section class="text-caption text-grey-8 text-body2 text-uppercase q-pt-none text-center col content-start" style="min-height: 5em;"> <q-card-section class="text-caption text-body2 text-uppercase q-pt-none text-center col content-start" style="min-height: 5em;">
<div class=" ellipsis-2-lines"> <div class=" ellipsis-2-lines">
{{ props.row.job_title }} {{ props.row.job_title }}
</div> </div>

View File

@ -49,6 +49,8 @@
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
:filter="filter" :filter="filter"
class="q-pa-md bg-transparent" class="q-pa-md bg-transparent"
:class="isGridMode ? '': 'my-sticky-header-table'"
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
color="primary" color="primary"
table-header-class="text-primary text-uppercase" table-header-class="text-primary text-uppercase"
card-container-class="justify-center" card-container-class="justify-center"
@ -57,8 +59,6 @@
:no-data-label="$t('shared.failedToLoad')" :no-data-label="$t('shared.failedToLoad')"
:no-results-label="$t('shared.failedToSearch')" :no-results-label="$t('shared.failedToSearch')"
:loading-label="$t('shared.loading')" :loading-label="$t('shared.loading')"
table-class="bg-white q-pa-md q-mx-md rounded-10 shadow-12"
table-style=""
@row-click="() => console.log('click!')" @row-click="() => console.log('click!')"
> >
<template v-slot:item="props"> <template v-slot:item="props">
@ -102,4 +102,23 @@
</template> </template>
</q-table> </q-table>
</div> </div>
</template> </template>
<style lang="sass">
.my-sticky-header-table
thead tr:first-child th
background-color: var(--q-dark)
margin-top: none
thead tr th
position: sticky
z-index: 1
thead tr:first-child th
top: 0
&.q-table--loading thead tr:last-child th
top: 48px
tbody
scroll-margin-top: 48px
</style>

View File

@ -9,4 +9,19 @@ export interface EmployeeProfile {
first_work_day: string; first_work_day: string;
last_work_day: string; last_work_day: string;
residence: string; residence: string;
birth_date: string;
}
export const default_employee_profile: EmployeeProfile = {
first_name: '',
last_name: '',
supervisor_full_name: '',
company_name: -1,
job_title: '',
email: '',
phone_number: '',
first_work_day: '',
last_work_day: '',
residence: '',
birth_date: '',
} }

View File

@ -1,77 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
import { default_employee_job_info, type EmployeeJobInfo } from 'src/modules/profile/types/profile-employee-interface';
import ProfileInputField from 'src/modules/profile/components/shared/profile-input-field.vue';
import { deepEqual } from 'src/utils/deep-equal';
// import ProfileSelectField from 'src/modules/profile/components/shared/profile-select-field.vue';
const props = withDefaults( defineProps<{
employeeInfo?: EmployeeJobInfo;
}>(), {
employeeInfo: () => default_employee_job_info,
});
const initial_info = props.employeeInfo;
const job_info = ref<EmployeeJobInfo>(props.employeeInfo);
const is_editing = ref<boolean>(false);
const onSubmit = () => {
if (!deepEqual(job_info.value, initial_info)) {
// saving profile logic here
console.log('Changes saved!');
}
console.log('Nothing was changed...');
};
const onReset = () => {
job_info.value = initial_info;
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
class="col"
:model-reference="job_info.company"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.firstName')"
/>
<ProfileInputField
class="col"
:model-reference="job_info.email"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.lastName')"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('timesheet.saveButton') : $t('shiftsTemplate.updateButton')"
@click="is_editing = !is_editing"
/>
</div>
</q-form>
</template>

View File

@ -1,147 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
import { default_employee_personal_info, type EmployeePersonalInfo } from 'src/modules/profile/types/profile-employee-interface';
import ProfileInputField from 'src/modules/profile/components/shared/profile-input-field.vue';
import ProfileSelectField from 'src/modules/profile/components/shared/profile-select-field.vue';
import { deepEqual } from 'src/utils/deep-equal';
const props = withDefaults( defineProps<{
personalInfo?: EmployeePersonalInfo;
}>(), {
personalInfo: () => default_employee_personal_info,
});
const initial_info = { ...props.personalInfo };
const personal_info = ref<EmployeePersonalInfo>(props.personalInfo);
const is_editing = ref<boolean>(false);
const gender = props.personalInfo.gender === '' ?
'profilePage.personalInfo.genderUnspecified' :
personal_info.value.gender;
const gender_options = [
'profilePage.personalInfo.genderMale',
'profilePage.personalInfo.genderFemale',
'profilePage.personalInfo.genderUnspecified',
// 'profilePage.personalInfo.genderNonBinary',
];
const changeFieldValue = (value: string | number | null, property: keyof EmployeePersonalInfo) => {
if ( typeof value === 'string' ) personal_info.value[property] = value;
else if ( typeof value === 'number' ) personal_info.value[property] = value.toString();
return;
}
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
return;
}
is_editing.value = false;
if (!deepEqual(personal_info.value, initial_info)) {
// saving profile logic here
console.log('Changes saved! ', initial_info, personal_info.value);
return;
}
console.log('Nothing was changed...', initial_info, personal_info.value);
};
const onReset = () => {
personal_info.value = initial_info;
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
class="col"
type="text"
:model-reference="personal_info.first_name"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.firstName')"
@is-value-changed="value => changeFieldValue( value, 'first_name')"
/>
<ProfileInputField
class="col"
type="text"
:model-reference="personal_info.last_name"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.lastName')"
@is-value-changed="value => changeFieldValue( value, 'last_name' )"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
class="col"
type="text"
:model-reference="personal_info.phone_number"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.phoneNumber')"
@is-value-changed="value => changeFieldValue( value, 'phone_number' )"
/>
<ProfileSelectField
class="col"
:model-reference="gender"
:options="gender_options"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.gender')"
@is-value-changed="value => changeFieldValue( value, 'gender' )"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
class="col"
:model-reference="personal_info.address"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.address')"
:hint="$t('profilePage.personalInfo.addressPlaceholder')"
@is-value-changed="value => changeFieldValue( value, 'address' )"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
class="col"
mask="#### / ## / ##"
hint="ex: 1970 / 01 / 01"
:rules="[ val => val.length === 14 || $t('profilePage.errors.mustEnterBirthdate') ]"
:model-reference="personal_info.birth_date"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.birthDate')"
@is-value-changed="value => changeFieldValue( value, 'birth_date' )"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('timesheet.saveButton') : $t('shiftsTemplate.updateButton')"
/>
</div>
</q-form>
</template>

View File

@ -0,0 +1,109 @@
<script setup lang="ts">
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue';
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const { employeeProfile } = defineProps<{
employeeProfile: EmployeeProfile;
}>();
let initial_info: EmployeeProfile = employeeProfile;
let employee_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const is_editing = ref<boolean>(false);
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
return;
}
is_editing.value = false;
initial_info = { ...employee_form_data.value }; // update initial value for future possible resets
if (!deepEqual(employee_form_data, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
employee_form_data = ref<EmployeeProfile>(initial_info);
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="employee_form_data.job_title"
class="col"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.jobTitle')"
/>
<ProfileInputField
v-model="employee_form_data.company_name"
class="col"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.companyName')"
/>
</div>
<div class="q-mx-xs">
<ProfileSelectField
v-model="employee_form_data.supervisor_full_name"
:options="supervisor_options"
:label-string="$t('profilePage.employeeInfo.supervisorName')"
:is-editing="is_editing"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="employee_form_data.email"
class="col"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.workEmail')"
/>
<ProfileInputField
v-model="employee_form_data.first_work_day"
readonly
class="col"
type="date"
:is-editing="is_editing"
:label-string="$t('profilePage.employeeInfo.hiredDate')"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('timesheet.saveButton') : $t('shiftsTemplate.updateButton')"
/>
</div>
</q-form>
</template>

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const { employeeProfile } = defineProps<{
employeeProfile: EmployeeProfile;
}>();
const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = employeeProfile;
const personal_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
return;
}
is_editing.value = false;
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets
if (!deepEqual(personal_form_data.value, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
personal_form_data.value= { ...initial_info };
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.first_name"
type="text"
class="col"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.firstName')"
/>
<ProfileInputField
v-model="personal_form_data.last_name"
class="col"
type="text"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.lastName')"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.phone_number"
class="col"
type="text"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.phoneNumber')"
/>
<ProfileInputField
v-model="personal_form_data.birth_date"
class="col"
mask="#### / ## / ##"
hint="ex: 1970 / 01 / 01"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.birthDate')"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.residence"
class="col"
:is-editing="is_editing"
:label-string="$t('profilePage.personalInfo.address')"
:hint="$t('profilePage.personalInfo.addressPlaceholder')"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('timesheet.saveButton') : $t('shiftsTemplate.updateButton')"
/>
</div>
</q-form>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
const { userFirstName = '', userLastName = '' } = defineProps<{
userFirstName: string;
userLastName: string;
}>();
</script>
<template>
<q-img
src="src/assets/profile_header_default.png"
height="15vh"
:width="$q.screen.lt.md ? '80vw' : '40vw'"
class="rounded-5 q-mb-md shadow-2 col-auto"
fit="cover"
>
<div class="absolute-bottom text-h5 text-uppercase text-weight-bolder" style="line-height: 0.8em;">{{ userFirstName }} {{ userLastName }}</div>
</q-img>
</template>

View File

@ -1,46 +0,0 @@
<script setup lang="ts">
import type { ValidationRule } from 'quasar';
import { ref } from 'vue';
const props = withDefaults( defineProps<{
modelReference: string;
labelString: string;
isEditing: boolean;
readonly?: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
hint?: string;
mask?: string;
rules?: ValidationRule[];
}>(), {
readonly: false,
});
const emit = defineEmits<{
isValueChanged: [value: string | number | null];
}>();
const input_ref = ref<string>(props.modelReference);
</script>
<template>
<q-input
v-model="input_ref"
dense
stack-label
autogrow
hide-bottom-space
debounce="500"
label-color="primary"
class="q-ma-xs"
input-class="text-weight-bolder text-h6 text-grey-8"
:hint="props.isEditing ? props.hint : ''"
:mask="props.mask"
:readonly="props.readonly || !props.isEditing"
:outlined="props.isEditing"
:borderless="!props.isEditing"
:type="props.type"
:label="props.labelString"
:rules="props.rules"
@update:model-value="value => emit('isValueChanged', value)"
/>
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { ValidationRule } from 'quasar';
const model = defineModel<string | number>({ required: true });
const { readonly = false, hint = '' } = defineProps<{
labelString: string;
isEditing: boolean;
readonly?: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
hint?: string;
mask?: string;
rules?: ValidationRule[];
}>();
</script>
<template>
<q-input
v-model="model"
dense
:stack-label="!isEditing"
autogrow
filled
debounce="500"
label-color="primary"
class="q-ma-xs"
input-class="text-weight-medium text-h6"
:hide-hint="hint === ''"
:hint="isEditing ? hint : ''"
:mask="mask"
:readonly="readonly || !isEditing"
:type="type"
:label="labelString"
:rules="rules"
/>
</template>

View File

@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Dark } from 'quasar';
import LanguageSwitch from 'src/modules/shared/components/language-switch.vue';
const initial_dark_mode_value = Dark.isActive;
const is_dark_mode = ref<boolean>(initial_dark_mode_value);
const toggle_dark_mode = (value: boolean) => {
is_dark_mode.value = value;
Dark.set(value);
}
</script>
<template>
<q-form class="q-pa-md column fit">
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profilePage.preferences.display_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
>
<div class="row col">
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #DAE0E7;"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: white;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profilePage.preferences.light_mode')}}</span>
</q-card>
<q-toggle
v-model="is_dark_mode"
@update:model-value="value => toggle_dark_mode(value)"
size="xl"
class="col-auto"
checked-icon="dark_mode"
unchecked-icon="light_mode"
/>
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #0f1114;"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: #333;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profilePage.preferences.dark_mode')}}</span>
</q-card>
</div>
</q-card>
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profilePage.preferences.language_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
>
<LanguageSwitch class="q-mr-xs col-auto" />
</q-card>
</q-form>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
const model = defineModel<string>();
const { readonly = false, localizeOptions = false } = defineProps<{
options: { label: string, value: string }[];
labelString: string;
isEditing: boolean;
readonly?: boolean;
localizeOptions?: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
}>();
</script>
<template>
<q-select
v-model="model"
dense
:stack-label="!isEditing"
filled
label-color="primary"
class="q-ma-xs text-h6"
popup-content-class="text-weight-medium text-h6"
input-class="text-weight-medium"
:options="options"
:readonly="readonly || !isEditing"
:hide-dropdown-icon="!isEditing"
:label="labelString"
:option-label="opt => localizeOptions ? $t(opt) : opt"
hint=''
/>
</template>

View File

@ -1,40 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
const props = withDefaults( defineProps<{
modelReference: string;
options: string[];
readonly?: boolean;
labelString: string;
isEditing: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
}>(), {
readonly: false,
});
const emit = defineEmits<{
isValueChanged: [value: string];
}>();
const select_ref = ref(props.modelReference);
</script>
<template>
<q-select
v-model="select_ref"
dense
options-dense
stack-label
color="primary"
label-color="primary"
class="q-ma-xs"
:options="props.options"
:outlined="props.isEditing"
:borderless="!props.isEditing"
:readonly="props.readonly || !props.isEditing"
:hide-dropdown-icon="!props.isEditing"
:label="props.labelString"
:option-label="opt => $t(opt)"
@update:model-value="value => emit('isValueChanged', value.toString())"
/>
</template>

View File

@ -1,49 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { type Component, ref } from 'vue'; import { ref } from 'vue';
import ProfileHeader from 'src/modules/profile/components/shared/profile-header.vue';
interface MenuTab { const { firstName, lastName, initialMenu } = defineProps<{
name: string; firstName: string;
icon: string; lastName: string;
label: string; initialMenu: string;
};
const props = defineProps<{
menuTabs: MenuTab[];
tabContents: Component[];
currentMenu?: string | undefined;
}>(); }>();
const current_menu = ref<string>(props.currentMenu || ''); const current_menu = ref<string>(initialMenu);
</script> </script>
<template> <template>
<div class="row" :class="$q.screen.lt.md ? 'full-width' : ''"> <div
<div class="col-auto column"> :class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
<q-card class="col-auto q-mr-sm q-pa-xs"> :style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
>
<ProfileHeader
:user-first-name="firstName"
:user-last-name="lastName"
/>
<div
class="col-3 no-wrap"
:class="$q.screen.lt.md ? '' : 'column'"
>
<q-card
class="col-auto q-pa-xs"
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
>
<q-tabs <q-tabs
v-model="current_menu" v-model="current_menu"
vertical :vertical="$q.screen.gt.sm"
dense dense
active-color="primary" active-color="primary"
indicator-color="primary" indicator-color="primary"
content-class=""
inline-label
align="left"
> >
<q-tab <slot name="tabs"></slot>
v-for="tab, index in props.menuTabs"
:key="index"
:name="tab.name"
:icon="tab.icon"
:label="$q.screen.lt.md ? '' : tab.label"
content-class="items-start"
/>
</q-tabs> </q-tabs>
</q-card> </q-card>
<div class="col"></div> <div class="col"></div>
</div> </div>
<q-card class="col q-ml-sm"> <q-card
class="col"
:class="$q.screen.lt.md ? '' : 'q-ml-sm'"
>
<q-tab-panels <q-tab-panels
v-model="current_menu" v-model="current_menu"
animated animated
@ -51,16 +53,9 @@
transition-prev="jump-up" transition-prev="jump-up"
transition-next="jump-up" transition-next="jump-up"
class="rounded-5" class="rounded-5"
:style="$q.screen.lt.md ? 'height: 70vh;' : 'height: 50vh; width: 40vw;'" style="height: 50vh;"
> >
<q-tab-panel <slot name="panels"></slot>
v-for="content, index in props.tabContents"
:key="index"
:name="props.menuTabs[index]?.name"
class="q-pa-none"
>
<component :is="content" />
</q-tab-panel>
</q-tab-panels> </q-tab-panels>
</q-card> </q-card>

View File

@ -1,32 +1,60 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import PanelInfoPersonal from 'src/modules/profile/components/employee/profile-panel-info-personal.vue';
import PanelInfoPersonal from 'src/modules/profile/components/employee/panel-info-personal.vue'; import PanelInfoEmployee from 'src/modules/profile/components/employee/profile-panel-info-employee.vue';
import PanelPreferences from 'src/modules/profile/components/shared/profile-panel-preferences.vue';
import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue'; import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue';
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const { t } = useI18n(); const PanelNames = {
PERSONAL_INFO: 'personal_info',
EMPLOYEE_INFO: 'employee_info',
PREFERENCES: 'references',
};
const tabs = [ const { employeeProfile = default_employee_profile } = defineProps<{
{ employeeProfile?: EmployeeProfile | undefined;
name: 'personal_info', }>();
icon: 'badge',
label: t('profilePage.personalInfo.title'),
},
{
name: 'employee_info',
icon: 'business',
label: t('profilePage.employeeInfo.title'),
},
];
const components = [ PanelInfoPersonal, ];
</script> </script>
<template> <template>
<q-card flat class="rounded-5 bg-transparent q-pa-none"> <q-card flat class="rounded-5 bg-transparent q-pa-none">
<ProfileTabMenuTemplate <ProfileTabMenuTemplate
:menu-tabs="tabs" :first-name="employeeProfile.first_name"
:tab-contents="components" :last-name="employeeProfile.last_name"
:current-menu="tabs[0]?.name" :initial-menu="PanelNames.PERSONAL_INFO"
/> >
<template #tabs>
<q-tab
:name='PanelNames.PERSONAL_INFO'
icon='person_outline'
:label="$q.screen.lt.md ? '' : $t('profilePage.personalInfo.title')"
/>
<q-tab
:name="PanelNames.EMPLOYEE_INFO"
icon="work_outline"
:label="$q.screen.lt.md ? '' : $t('profilePage.employeeInfo.title')"
/>
<q-tab
:name="PanelNames.PREFERENCES"
icon="display_settings"
:label="$q.screen.lt.md ? '' : $t('profilePage.preferences.title')"
/>
</template>
<template #panels>
<q-tab-panel :name="PanelNames.PERSONAL_INFO" class="q-pa-none">
<PanelInfoPersonal :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.EMPLOYEE_INFO" class="q-pa-none">
<PanelInfoEmployee :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.PREFERENCES" class="q-pa-none">
<PanelPreferences />
</q-tab-panel>
</template>
</ProfileTabMenuTemplate>
</q-card> </q-card>
</template> </template>

View File

@ -1,31 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from 'src/stores/auth-store';
import { type Component, h } from 'vue';
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue'; import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
import { useAuthStore } from 'src/stores/auth-store';
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
const getUserProfileComponent = (): Component => { const { employeeProfile } = defineProps<{
console.log('user role: ', auth_store.user.role); employeeProfile?: EmployeeProfile | undefined;
}>();
switch (auth_store.user.role.toUpperCase()) {
case 'SUPERVISOR': return ProfileEmployee;
default: return h('div', { class: 'empty' }, '');
}
}
</script> </script>
<template> <template>
<q-page class="bg-secondary column items-center justify-center"> <q-page class="bg-secondary column items-center justify-center">
<q-img <ProfileEmployee
src="src/assets/profile_header_default.png" v-if="employee_roles.includes( auth_store.user.role.toUpperCase() )"
height="15vh" class="col-auto"
width="50vw" :employee-profile="employeeProfile"
class="rounded-5 q-mb-md shadow-5" />
fit="cover"
>
<div class="absolute-bottom text-h5 text-uppercase text-weight-bolder" style="line-height: 0.8em;">{{ auth_store.user.firstName }} {{ auth_store.user.lastName }}</div>
</q-img>
<component :is="getUserProfileComponent()" class="col-auto"/>
</q-page> </q-page>
</template> </template>

View File

@ -1,33 +0,0 @@
export interface EmployeePersonalInfo {
first_name: string;
last_name: string;
gender: string;
phone_number: string;
address: string;
birth_date: string;
}
export interface EmployeeJobInfo {
email: string;
job_title: string;
company: string;
supervisor: string;
hired_date: string;
}
export const default_employee_personal_info: EmployeePersonalInfo = {
first_name: '',
last_name: '',
gender: '',
phone_number: '',
address: '',
birth_date: '1970 / 01 / 01'
}
export const default_employee_job_info: EmployeeJobInfo = {
email: '',
job_title: '',
company: '',
supervisor: '',
hired_date: '',
}

View File

@ -9,26 +9,28 @@
</script> </script>
<template> <template>
<q-btn-dropdown flat :label="$t('shared.languageLabel')" class="rounded-borders" icon="language"> <div>
<q-list dense class="row">
<q-item v-for="option in localeOptions"
:key="option.value"
tag="label"
v-ripple
>
<q-item-section avatar>
<q-radio v-model="locale" :val="option.value" />
</q-item-section>
<q-item-section>
<q-item-label>{{ option.label }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- <q-btn-dropdown push color="primary" :label="$t('shared.languageLabel')" icon="language">
<q-list> <q-list>
<q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="locale = option.value"> <q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="locale = option.value">
<q-item-section>{{ option.label }}</q-item-section> <q-item-section>{{ option.label }}</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown> -->
<!-- <q-select
v-model="locale"
:options="localeOptions"
dense
borderless
emit-value
map-options
hide-dropdown-icon
class="text-white"
>
<template v-slot:prepend>
<q-icon name="language" color="white"/>
</template>
</q-select> -->
</template> </template>

View File

@ -1,12 +1,7 @@
<script setup lang="ts">
import LanguageSwitch from '../language-switch.vue';
</script>
<template> <template>
<q-footer elevated class="bg-primary text-white"> <q-footer elevated class="bg-primary text-white">
<q-toolbar> <q-toolbar>
<q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title> <q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title>
<LanguageSwitch class="q-mr-xs text-white" />
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
</template> </template>

View File

@ -36,7 +36,7 @@
<q-icon name="home" color="primary" /> <q-icon name="home" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHome') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuHome') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -47,7 +47,7 @@
<q-icon name="event_available" color="primary" /> <q-icon name="event_available" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuShiftValidation') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuShiftValidation') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -58,7 +58,7 @@
<q-icon name="view_list" color="primary" /> <q-icon name="view_list" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuEmployeeList') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuEmployeeList') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -69,7 +69,7 @@
<q-icon name="punch_clock" color="primary" /> <q-icon name="punch_clock" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuTimesheetTemp') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuTimesheetTemp') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -79,7 +79,7 @@
<q-icon name="account_box" color="primary" /> <q-icon name="account_box" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuProfile') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuProfile') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -89,7 +89,7 @@
<q-icon name="contact_support" color="primary" /> <q-icon name="contact_support" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHelp') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuHelp') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -100,7 +100,7 @@
<q-icon name="exit_to_app" color="primary" /> <q-icon name="exit_to_app" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuLogout') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('navBar.userMenuLogout') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-scroll-area> </q-scroll-area>

View File

@ -20,25 +20,9 @@ import { ref } from 'vue';
default : return 'transparent'; default : return 'transparent';
} }
}; };
const getTextColor = (type: string): string => {
switch(type) {
case 'REGULAR': return 'grey-8';
case '': return 'transparent';
default: return 'white';
}
}
</script> </script>
<template> <template>
<q-dialog
v-model="is_showing_time_popup"
>
<q-card class="q-pa-xl">
LOL
</q-card>
</q-dialog>
<q-card-section <q-card-section
horizontal horizontal
class="q-pa-none text-uppercase text-center items-center cursor-pointer rounded-10" class="q-pa-none text-uppercase text-center items-center cursor-pointer rounded-10"
@ -49,7 +33,7 @@ import { ref } from 'vue';
<q-card-section class="q-pa-none col"> <q-card-section class="q-pa-none col">
<q-item-label <q-item-label
class="text-weight-bolder q-pa-xs rounded-5" class="text-weight-bolder q-pa-xs rounded-5"
:class="'bg-' + getShiftColor(props.shift.type) + ' text-' + getTextColor(props.shift.type)" :class="'bg-' + getShiftColor(props.shift.type) + (!$q.dark.isActive && props.shift.type === 'REGULAR' ? '' : ' text-white')"
style="font-size: 1.5em; line-height: 80% !important;" style="font-size: 1.5em; line-height: 80% !important;"
> >
{{ props.shift.start_time }} {{ props.shift.start_time }}
@ -80,8 +64,8 @@ import { ref } from 'vue';
<!-- punch-out timestamps --> <!-- punch-out timestamps -->
<q-card-section class="q-pa-none col"> <q-card-section class="q-pa-none col">
<q-item-label <q-item-label
class="text-weight-bolder text-white q-pa-xs rounded-5" class="text-weight-bolder q-pa-xs rounded-5"
:class="'bg-' + getShiftColor(props.shift.type) + ' text-' + getTextColor(props.shift.type)" :class="'bg-' + getShiftColor(props.shift.type) + (!$q.dark.isActive && props.shift.type === 'REGULAR' ? '' : ' text-white')"
style="font-size: 1.5em; line-height: 80% !important;" style="font-size: 1.5em; line-height: 80% !important;"
> >
{{ props.shift.end_time }} {{ props.shift.end_time }}
@ -97,7 +81,6 @@ import { ref } from 'vue';
v-if="props.shift.type !== ''" v-if="props.shift.type !== ''"
flat flat
dense dense
color='grey-8'
icon="chat_bubble_outline" icon="chat_bubble_outline"
class="q-pa-none" class="q-pa-none"
/> />
@ -107,7 +90,6 @@ import { ref } from 'vue';
v-if="props.shift.type !== ''" v-if="props.shift.type !== ''"
flat flat
dense dense
color='grey-8'
icon="attach_money" icon="attach_money"
class="q-pa-none q-mx-xs" class="q-pa-none q-mx-xs"
/> />

View File

@ -67,10 +67,7 @@
</q-btn> </q-btn>
</q-card-section> </q-card-section>
<q-separator <q-separator size="2px" />
color="accent"
style="height: 2px;"
/>
<!-- Main body of pay period card --> <!-- Main body of pay period card -->
<q-card-section class="q-pa-none q-mt-xs q-mb-sm"> <q-card-section class="q-pa-none q-mt-xs q-mb-sm">
@ -89,15 +86,12 @@
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption"> <q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption">
{{ props.cols.find(c => c.name === 'regular_hours')?.label }} {{ props.cols.find(c => c.name === 'regular_hours')?.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder text-h3 text-grey-8 q-py-none"> <q-item-label class="text-weight-bolder text-h3 q-py-none">
{{ props.cols.find(c => c.name === 'regular_hours')?.value }} {{ props.cols.find(c => c.name === 'regular_hours')?.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
<q-separator <q-separator class="q-mx-sm" />
color="accent"
class="q-mx-sm"
/>
<!-- Other hour types segment --> <!-- Other hour types segment -->
<div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'"> <div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'">
@ -113,16 +107,15 @@
> >
{{ col.label }} {{ col.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8"> <q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }} {{ col.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
</div> </div>
</div> </div>
<q-separator <q-separator
vertical vertical
color="accent"
class="q-mt-xs q-mb-none" class="q-mt-xs q-mb-none"
/> />
@ -140,7 +133,7 @@
> >
{{ col.label }} {{ col.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8"> <q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }} {{ col.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
@ -157,7 +150,7 @@
<q-card-section <q-card-section
horizontal horizontal
class="q-pa-sm text-weight-bold" class="q-pa-sm text-weight-bold"
:class="props.initialState ? 'text-white bg-primary' : 'text-primary bg-white'" :class="props.initialState ? 'text-white bg-primary' : 'bg-dark'"
> >
<q-item-label class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ props.row.total_hours + 'h' }} </q-item-label> <q-item-label class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ props.row.total_hours + 'h' }} </q-item-label>
<q-item-label class="text-uppercase text-weight-bold q-ml-xs"> total </q-item-label> <q-item-label class="text-uppercase text-weight-bold q-ml-xs"> total </q-item-label>

View File

@ -169,8 +169,8 @@
:no-results-label="$t('shared.failedToSearch')" :no-results-label="$t('shared.failedToSearch')"
:loading-label="$t('shared.loading')" :loading-label="$t('shared.loading')"
> >
<!-- Top Bar that contains Search, Title, Filters --> <!-- Top Bar that contains Date Picker, Search, Filters, Print Report, etc -->
<template v-slot:top> <template #top>
<div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'"> <div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
<!-- Date Picker --> <!-- Date Picker -->
<TimesheetApprovalPeriodPicker <TimesheetApprovalPeriodPicker
@ -232,7 +232,7 @@
</template> </template>
<!-- Template for individual employee cards --> <!-- Template for individual employee cards -->
<template v-slot:item="props: { <template #item="props: {
cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[], cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[],
row: PayPeriodOverviewEmployee, row: PayPeriodOverviewEmployee,
key: string, key: string,
@ -247,7 +247,7 @@
</template> </template>
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->
<template v-slot:no-data="{ message, filter }"> <template #no-data="{ message, filter }">
<div class="full-width column items-center text-primary q-gutter-sm"> <div class="full-width column items-center text-primary q-gutter-sm">
<span class="text-h6 q-mt-xl"> <span class="text-h6 q-mt-xl">
{{ message }} {{ message }}

View File

@ -63,7 +63,7 @@
<template> <template>
<q-card <q-card
class="q-pa-sm bg-white shadow-12 rounded-15 column no-wrap relative" class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !important;' : '' " :style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !important;' : '' "
> >
<!-- loader --> <!-- loader -->
@ -88,7 +88,7 @@
> >
{{ props.employeeName }} {{ props.employeeName }}
<q-separator class="q-mb-sm" color="accent" size="2px" /> <q-separator class="q-mb-sm" size="2px" />
<q-card-actions align="center" class="q-pa-none"> <q-card-actions align="center" class="q-pa-none">
<q-card flat class="bg-secondary rounded-5 q-pa-xs"> <q-card flat class="bg-secondary rounded-5 q-pa-xs">
<q-btn-toggle <q-btn-toggle

View File

@ -1,11 +1,11 @@
import { ref } from "vue"; import { ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list"; import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list";
import type { EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface"; import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface"; import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface";
export const useEmployeeStore = defineStore('employee', () => { export const useEmployeeStore = defineStore('employee', () => {
const employee = ref<EmployeeProfile>(); const employee = ref<EmployeeProfile>( default_employee_profile );
const employeeList = ref<EmployeeListTableItem[]>([]); const employeeList = ref<EmployeeListTableItem[]>([]);
const isShowingEmployeeAddModifyWindow = ref<boolean>(false); const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
const isLoadingEmployeeProfile = ref(false); const isLoadingEmployeeProfile = ref(false);