feat(preset): add preset function, select preset for employee, revamp employee management dialog UI/UX
Also change display of employee list to better wrap around card content
This commit is contained in:
parent
c7fadbcaf1
commit
f6e9415369
|
|
@ -20,6 +20,11 @@ export default {
|
|||
modify_employee: "Modify employee",
|
||||
access_label: "access",
|
||||
details_label: "details",
|
||||
schedule_label: "schedules",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Select a schedule",
|
||||
preset_name_placeholder: "schedule preset name",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Dashboard",
|
||||
employee_list: "employee list",
|
||||
|
|
@ -154,13 +159,13 @@ export default {
|
|||
remote: "remote work",
|
||||
},
|
||||
weekday: {
|
||||
sunday: "dimanche",
|
||||
monday: "lundi",
|
||||
tuesday: "mardi",
|
||||
wednesday: "mercredi",
|
||||
thursday: "jeudi",
|
||||
friday: "vendredi",
|
||||
saturday: "samedi",
|
||||
sun: "dimanche",
|
||||
mon: "lundi",
|
||||
tue: "mardi",
|
||||
wed: "mercredi",
|
||||
thu: "jeudi",
|
||||
fri: "vendredi",
|
||||
sat: "samedi",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ export default {
|
|||
modify_employee: "Modifier employé",
|
||||
access_label: "accès",
|
||||
details_label: "détails",
|
||||
schedule_label: "horaires",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Sélectionner un horaire",
|
||||
preset_name_placeholder: "nom de l'horaire",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Accueil",
|
||||
employee_list: "Répertoire du personnel",
|
||||
|
|
@ -155,13 +160,13 @@ export default {
|
|||
remote: "télétravail",
|
||||
},
|
||||
weekday: {
|
||||
sunday: "dimanche",
|
||||
monday: "lundi",
|
||||
tuesday: "mardi",
|
||||
wednesday: "mercredi",
|
||||
thursday: "jeudi",
|
||||
friday: "vendredi",
|
||||
saturday: "samedi",
|
||||
sun: "dimanche",
|
||||
mon: "lundi",
|
||||
tue: "mardi",
|
||||
wed: "mercredi",
|
||||
thu: "jeudi",
|
||||
fri: "vendredi",
|
||||
sat: "samedi",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,20 @@
|
|||
|
||||
<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">
|
||||
<q-icon
|
||||
name="info_outline"
|
||||
size="sm"
|
||||
color="accent"
|
||||
class="col-auto q-mr-sm"
|
||||
/>
|
||||
|
||||
<q-item-label
|
||||
caption
|
||||
class="col-auto text-weight-medium"
|
||||
>{{ $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') }}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
]
|
||||
|
||||
const supervisor_options = computed(() => {
|
||||
const supervisors = employee_store.employee_list.filter(employee => employee.is_supervisor === true);
|
||||
const supervisors = employee_store.employee_list.filter(employee => employee.is_supervisor === true && employee.last_work_day === null);
|
||||
return supervisors.map(supervisor => supervisor.first_name + ' ' + supervisor.last_name);
|
||||
})
|
||||
|
||||
|
|
@ -158,6 +158,11 @@
|
|||
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
|
||||
|
|
@ -182,6 +187,11 @@
|
|||
:options="supervisor_options"
|
||||
options-selected-class="text-white text-bold bg-accent"
|
||||
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
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import HorizontalSlideTransition from 'src/modules/shared/components/horizontal-slide-transition.vue';
|
||||
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule_presets_dialog.vue';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
|
||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
||||
|
||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||
return schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
preset_options.value = getPresetOptions();
|
||||
const current_option = preset_options.value.find(option => option.value === employee_store.employee.preset_id);
|
||||
current_preset.value = current_option ?? { label: undefined, value: -1 };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column full-width flex-center items-start">
|
||||
<SchedulePresetsDialog />
|
||||
|
||||
<div class="col row justify-center full-width no-wrap">
|
||||
<q-select
|
||||
v-model="current_preset"
|
||||
standout="bg-accent"
|
||||
dense
|
||||
rounded
|
||||
color="accent"
|
||||
:options="getPresetOptions()"
|
||||
class="col-xs-10 col-md-7"
|
||||
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]"
|
||||
@update:modelValue="option => schedule_preset_store.setCurrentSchedulePreset(option.value)"
|
||||
>
|
||||
<template #selected>
|
||||
<span class="text-uppercase" :style="current_preset.label === undefined ? 'opacity: 0.5;' : ''">
|
||||
{{ current_preset.label === undefined ? $t('employee_management.schedule_presets.preset_list_placeholder') : current_preset.label }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
rounded
|
||||
icon="add"
|
||||
color="accent"
|
||||
class="col-auto q-px-sm q-ml-sm"
|
||||
@click="schedule_preset_store.openSchedulePresetManager(-1)"
|
||||
/>
|
||||
|
||||
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
|
||||
<transition
|
||||
enter-active-class="animated zoomIn"
|
||||
leave-active-class="animated zoomOut"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-btn
|
||||
v-if="current_preset !== undefined && current_preset?.value !== -1"
|
||||
push
|
||||
dense
|
||||
rounded
|
||||
icon="edit"
|
||||
color="accent"
|
||||
class="col-auto q-px-sm q-ml-sm full-height"
|
||||
@click="schedule_preset_store.openSchedulePresetManager(current_preset.value)"
|
||||
/>
|
||||
</transition>
|
||||
</HorizontalSlideTransition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
118
src/modules/employee-list/components/add-modify-dialog.vue
Normal file
118
src/modules/employee-list/components/add-modify-dialog.vue
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import AddModifyDialogForm from 'src/modules/employee-list/components/add-modify-dialog-form.vue';
|
||||
import AddModifyDialogAccess from 'src/modules/employee-list/components/add-modify-dialog-access.vue';
|
||||
import AddModifyDialogSchedule from 'src/modules/employee-list/components/add-modify-dialog-schedule.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const current_step = ref<'form' | 'access' | 'schedule'>('form');
|
||||
const initial_employee_profile = ref(new EmployeeProfile)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="employee_store.is_add_modify_dialog_open"
|
||||
full-width
|
||||
@beforeShow="current_step = 'form'"
|
||||
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
||||
class="shadow-24"
|
||||
>
|
||||
<div
|
||||
class="column bg-secondary rounded-10 no-wrap"
|
||||
:class="$q.dark.isActive ? 'shadow-24' : 'shadow-10'"
|
||||
:style="($q.screen.lt.md ? ' ' : 'max-width: 60vw !important; height: 60vh') +
|
||||
($q.dark.isActive ? 'border: 2px solid var(--q-accent)' : '')"
|
||||
>
|
||||
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
||||
<div class="q-py-sm text-uppercase text-weight-bolder text-h5 ">
|
||||
{{ $t('employee_management.' + employee_store.management_mode) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="employee_store.employee.first_name.length > 0"
|
||||
class="text-uppercase text-weight-light text-h6 q-ml-sm"
|
||||
>
|
||||
{{ `${employee_store.employee.first_name} ${employee_store.employee.last_name}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col column q-pa-md no-wrap scroll">
|
||||
<q-tabs
|
||||
v-model="current_step"
|
||||
dense
|
||||
inline-label
|
||||
align="justify"
|
||||
indicator-color="transparent"
|
||||
active-class="text-white bg-accent"
|
||||
class="q-mb-sm"
|
||||
>
|
||||
<q-tab
|
||||
name="form"
|
||||
icon="badge"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.details_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
<q-tab
|
||||
name="access"
|
||||
icon="key"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.access_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
<q-tab
|
||||
name="schedule"
|
||||
icon="calendar_month"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.schedule_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
</q-tabs>
|
||||
|
||||
<q-tab-panels
|
||||
v-model="current_step"
|
||||
animated
|
||||
:transition-prev="$q.screen.lt.sm ? 'jump-down' : 'jump-left'"
|
||||
:transition-next="$q.screen.lt.sm ? 'jump-up' : 'jump-right'"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<q-tab-panel
|
||||
name="form"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogForm />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
name="access"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogAccess />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
name="schedule"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogSchedule />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
square
|
||||
color="accent"
|
||||
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
|
||||
class="col-auto q-py-sm shadow-up-5"
|
||||
@click="employee_store.createOrUpdateEmployee(employee_store.employee)"
|
||||
/>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
// return first_name.charAt(0) + last_name.charAt(0);
|
||||
// };
|
||||
|
||||
const { row, index = 0 } = defineProps<{
|
||||
const { row, index = -1 } = defineProps<{
|
||||
row: EmployeeProfile
|
||||
index?: number
|
||||
}>()
|
||||
|
|
@ -19,25 +19,21 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp fast"
|
||||
leave-active-class="animated fadeOutDown fast"
|
||||
mode="out-in"
|
||||
<div
|
||||
class="col-xs-6 col-sm-4 col-md-3 col-lg-3 col-xl-2 q-pa-sm row flex-center"
|
||||
:style="`animation-delay: ${index / 25}s;`"
|
||||
>
|
||||
<q-card
|
||||
|
||||
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"
|
||||
style="max-width: 230px;"
|
||||
:style="(`animation-delay: ${index / 25}s; `) + (row.last_work_day === null ? '' : 'opacity: 0.6;')"
|
||||
<div
|
||||
class="column col no-wrap cursor-pointer bg-dark rounded-15 shadow-12"
|
||||
style="max-width: 230px; height: 275px;"
|
||||
:style="(row.last_work_day === null ? ' ' : 'opacity: 0.6; ') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||
@click="emit('onProfileClick', row.email)"
|
||||
>
|
||||
<q-card-section class="col-6 text-center">
|
||||
<div class="col-auto column flex-center q-pt-md">
|
||||
<q-avatar
|
||||
:color="row.last_work_day === null ? 'accent' : 'negative'"
|
||||
size="8em"
|
||||
class="shadow-3 q-mb-md"
|
||||
class="col-auto shadow-3"
|
||||
>
|
||||
<img
|
||||
src="src/assets/targo-default-avatar.png"
|
||||
|
|
@ -45,28 +41,29 @@
|
|||
class="q-pa-xs"
|
||||
>
|
||||
</q-avatar>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-section
|
||||
class="col-grow text-center text-h6 text-weight-medium text-uppercase q-pb-none"
|
||||
style="line-height: 0.8em;"
|
||||
<div
|
||||
class="col column items-center justify-start text-center text-weight-medium text-uppercase q-pa-sm no-wrap"
|
||||
style="line-height: 1.2em; font-size: 1.3em;"
|
||||
>
|
||||
<div
|
||||
class="ellipsis"
|
||||
class="ellipsis-2-lines"
|
||||
:class="row.last_work_day === null ? 'text-accent' : 'text-negative'"
|
||||
>
|
||||
>
|
||||
{{ row.first_name }} {{ row.last_name }}
|
||||
</div>
|
||||
<q-separator
|
||||
:color="row.last_work_day === null ? 'accent' : 'negative'"
|
||||
class="q-mx-sm q-mt-xs"
|
||||
/>
|
||||
<div class=" ellipsis-2-lines text-caption">{{ row.job_title }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center">
|
||||
<div> {{ row.email }} </div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</transition>
|
||||
<q-separator class="q-mb-xs q-mx-md" />
|
||||
</div>
|
||||
<div class=" ellipsis-2-lines text-caption no-wrap">{{ row.job_title }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto bg-primary text-white text-caption text-center text-weight-medium q-py-sm"
|
||||
style="border-radius: 0 0 15px 15px;"
|
||||
>
|
||||
{{ row.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -9,15 +9,16 @@
|
|||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
import { employee_list_columns, type EmployeeProfile, type EmployeeListFilters } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import { animateFlip } from 'src/utils/table-grid-FLIP';
|
||||
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
const employee_store = useEmployeeStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const ui_store = useUiStore();
|
||||
const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'company_name', 'supervisor_full_name', 'company_name', 'job_title', 'last_work_day']);
|
||||
|
||||
const table_grid_container = ref<HTMLElement | null>(null);
|
||||
|
||||
const filters = ref<EmployeeListFilters>({
|
||||
search_bar_string: '',
|
||||
hide_inactive_users: true,
|
||||
|
|
@ -41,18 +42,21 @@
|
|||
const searchTerms = terms.search_bar_string.split(' ').map(s => s.trim().toLowerCase());
|
||||
|
||||
result = result.filter(row => {
|
||||
const rowValues = Object.values(row).map(v => String(v ?? '').toLowerCase());
|
||||
const row_values = Object.values(row).map(v => String(v ?? '').toLowerCase());
|
||||
const row_values_without_emails = row_values.filter(value => !value.includes('@'));
|
||||
return searchTerms.every(term =>
|
||||
rowValues.some(value => value.includes(term))
|
||||
row_values_without_emails.some(value => value.includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
animateFlip(table_grid_container);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await employee_list_api.getEmployeeList();
|
||||
onMounted(() => {
|
||||
table_grid_container.value = document.querySelector(".q-table__grid-content") as HTMLElement;
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -146,7 +150,10 @@
|
|||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<span class="text-uppercase text-weight-bolder text-white" style="font-size: 1.2em;">
|
||||
<span
|
||||
class="text-uppercase text-weight-bolder text-white"
|
||||
style="font-size: 1.2em;"
|
||||
>
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
</q-th>
|
||||
|
|
@ -154,11 +161,19 @@
|
|||
</template>
|
||||
|
||||
<template #item="props">
|
||||
<EmployeeListTableItem
|
||||
:row="props.row"
|
||||
:index="props.rowIndex"
|
||||
@on-profile-click="employee_store.openAddModifyDialog"
|
||||
/>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn fast"
|
||||
leave-active-class="animated zoomOut fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<EmployeeListTableItem
|
||||
:key="props.rowIndex"
|
||||
:row="props.row"
|
||||
:index="props.rowIndex"
|
||||
@on-profile-click="employee_store.openAddModifyDialog"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<template #body-cell="scope">
|
||||
|
|
@ -168,8 +183,8 @@
|
|||
>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp slow"
|
||||
leave-active-class="animated fadeOutDown"
|
||||
enter-active-class="animated fadeInUp fast"
|
||||
leave-active-class="animated fadeOutDown fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
|
|
@ -255,4 +270,8 @@ thead tr:first-child th {
|
|||
tbody {
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
|
||||
:deep(.q-table) {
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import AddModifyDialogForm from 'src/modules/employee-list/components/employee/add-modify-dialog-form.vue';
|
||||
import AddModifyDialogAccess from 'src/modules/employee-list/components/employee/add-modify-dialog-access.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const current_step = ref<'form' | 'access'>('form');
|
||||
const transition_in_animation = ref('fadeInRight');
|
||||
const transition_out_animation = ref('fadeOutLeft');
|
||||
const initial_employee_profile = ref(new EmployeeProfile)
|
||||
|
||||
const getNextMenu = (animation_in: string, animation_out: string, next_step: 'form' | 'access') => {
|
||||
transition_in_animation.value = animation_in;
|
||||
transition_out_animation.value = animation_out;
|
||||
current_step.value = next_step;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="employee_store.is_add_modify_dialog_open"
|
||||
full-width
|
||||
@beforeShow="current_step = 'form'"
|
||||
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
||||
>
|
||||
<div
|
||||
class="column bg-secondary rounded-10 no-wrap"
|
||||
:class="$q.dark.isActive ? 'shadow-24' : 'shadow-10'"
|
||||
:style="($q.screen.lt.md ? ' ' : 'max-width: 60vw !important; height: 60vh') +
|
||||
($q.dark.isActive ? 'border: 2px solid var(--q-accent)' : '')"
|
||||
>
|
||||
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
||||
<div class="q-py-sm text-uppercase text-weight-bolder text-h5 ">
|
||||
{{ $t('employee_management.' + employee_store.management_mode) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="employee_store.employee.first_name.length > 0"
|
||||
class="text-uppercase text-weight-light text-h6 q-ml-sm"
|
||||
>
|
||||
{{ `${employee_store.employee.first_name} ${employee_store.employee.last_name}` }}
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<div
|
||||
v-if="current_step === 'access'"
|
||||
class="col-12 row flex-center q-px-sm q-py-xs bg-accent no-wrap"
|
||||
>
|
||||
<q-icon
|
||||
name="info_outline"
|
||||
color="white"
|
||||
size="sm"
|
||||
class="col-auto q-mr-sm"
|
||||
/>
|
||||
|
||||
<q-item-label
|
||||
caption
|
||||
class="col text-white"
|
||||
>{{ $t('employee_management.module_access.usage_description') }}</q-item-label>
|
||||
</div>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col column q-pa-md no-wrap scroll">
|
||||
<div class="col">
|
||||
<transition
|
||||
:enter-active-class="'animated ' + transition_in_animation"
|
||||
:leave-active-class="'animated ' + transition_out_animation"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="current_step === 'form'"
|
||||
class="rounded-5 q-pb-sm bg-dark shadow-10"
|
||||
>
|
||||
<AddModifyDialogForm />
|
||||
</div>
|
||||
|
||||
<AddModifyDialogAccess v-else />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row col-auto">
|
||||
<q-btn
|
||||
v-if="current_step === 'access'"
|
||||
flat
|
||||
size="lg"
|
||||
color="accent"
|
||||
icon="arrow_back"
|
||||
:label="$t('employee_management.details_label')"
|
||||
@click="getNextMenu('fadeInLeft', 'fadeOutRight', 'form')"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
|
||||
<q-btn
|
||||
v-if="current_step === 'form'"
|
||||
flat
|
||||
size="lg"
|
||||
color="accent"
|
||||
icon-right="arrow_forward"
|
||||
:label="$t('employee_management.access_label')"
|
||||
@click="getNextMenu('fadeInRight', 'fadeOutLeft', 'access')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
square
|
||||
color="accent"
|
||||
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
|
||||
class="col-auto q-py-sm shadow-up-5"
|
||||
@click="employee_store.createOrUpdateEmployee(employee_store.employee)"
|
||||
/>
|
||||
<!-- <q-inner-loading :showing="employee_store.is_loading" /> -->
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog v-model="schedule_preset_store.is_manager_open">
|
||||
<div
|
||||
class="column flex-center bg-secondary rounded-10 shadow-24 full-width"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
>
|
||||
<div class="row col-auto flex-center bg-primary full-width">
|
||||
<span class="row col-auto text-uppercase text-weight-bold text-white q-py-sm">{{
|
||||
schedule_preset_store.current_schedule_preset.id === -1 ?
|
||||
$t('shared.label.add') :
|
||||
$t('shared.label.modify') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row col-auto q-px-sm flex-center full-width q-py-sm">
|
||||
<div class="col-8 bg-dark rounded-10 ellipsis">
|
||||
<q-input
|
||||
v-model="schedule_preset_store.current_schedule_preset.name"
|
||||
standout
|
||||
dense
|
||||
hide-bottom-space
|
||||
:placeholder="$t('employee_management.schedule_presets.preset_name_placeholder')"
|
||||
class="text-uppercase"
|
||||
input-class="text-weight-bold text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column col full-width q-py-sm q-px-lg">
|
||||
<div
|
||||
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays"
|
||||
:key="weekday.day"
|
||||
class="row col-auto items-center q-my-xs full-width shadow-2 q-px-sm bg-dark rounded-10"
|
||||
style="min-height: 50px;"
|
||||
>
|
||||
<span class="col-2 text-uppercase text-weight-bold ellipsis">{{
|
||||
$t(`shared.weekday.${weekday.day.toLowerCase()}`) }}
|
||||
</span>
|
||||
|
||||
<div class="col column">
|
||||
<div
|
||||
v-for="shift in weekday.shifts"
|
||||
:key="shift.week_day"
|
||||
class="row col-auto"
|
||||
>
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
standout
|
||||
dense
|
||||
hide-bottom-space
|
||||
class="text-uppercase weekday-field"
|
||||
input-class="text-weight-bold"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="column text-uppercase text-accent text-weight-bold full-height" style="font-size: 0.5em; transform: translateY(4px);">
|
||||
{{ $t('shared.misc.out') }}
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row self-end q-px-lg full-width">
|
||||
<q-space />
|
||||
<q-btn
|
||||
:disable="schedule_preset_store.current_schedule_preset.name === ''"
|
||||
push
|
||||
dense
|
||||
:color="schedule_preset_store.current_schedule_preset.name === '' ? 'grey-7' : 'accent'"
|
||||
icon="save"
|
||||
label="save"
|
||||
class="col-3 q-px-sm q-mb-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field__native) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.weekday-field :deep(.q-field__control) {
|
||||
height: 25px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
import { useEmployeeStore } from "src/stores/employee-store";
|
||||
import { useSchedulePresetsStore } from "src/stores/schedule-presets.store";
|
||||
|
||||
export const useEmployeeListApi = () => {
|
||||
const employeeListStore = useEmployeeStore();
|
||||
const employee_list_store = useEmployeeStore();
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
|
||||
const getEmployeeList = (): Promise<void> => {
|
||||
return employeeListStore.getEmployeeList();
|
||||
const getEmployeeList = async (): Promise<void> => {
|
||||
employee_list_store.is_loading = true;
|
||||
|
||||
const success = await employee_list_store.getEmployeeList();
|
||||
if (success) await schedule_preset_store.findSchedulePresetList();
|
||||
|
||||
employee_list_store.is_loading = false;
|
||||
};
|
||||
|
||||
const getEmployeeDetails = (email: string): Promise<void> => {
|
||||
return employeeListStore.getEmployeeDetails(email);
|
||||
return employee_list_store.getEmployeeDetails(email);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class EmployeeProfile {
|
|||
birth_date: string;
|
||||
is_supervisor: boolean;
|
||||
user_module_access: ModuleAccessName[];
|
||||
preset_id?: number;
|
||||
|
||||
constructor() {
|
||||
this.first_name = '';
|
||||
|
|
@ -103,9 +104,9 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
|
|||
export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
|
||||
{ label: 'dashboard', value: 'dashboard' },
|
||||
{ label: 'employee_list', value: 'employee_list' },
|
||||
{ label: 'employee_management', value: 'employee_management' },
|
||||
{ label: 'personal_profile', value: 'personal_profile' },
|
||||
{ label: 'timesheets', value: 'timesheets' },
|
||||
{ label: 'employee_management', value: 'employee_management' },
|
||||
{ label: 'timesheets_approval', value: 'timesheets_approval' },
|
||||
]
|
||||
|
||||
|
|
|
|||
57
src/modules/employee-list/models/schedule-presets.models.ts
Normal file
57
src/modules/employee-list/models/schedule-presets.models.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT';
|
||||
|
||||
export const WEEKDAYS: Weekday[] = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
|
||||
|
||||
export class SchedulePreset {
|
||||
id: number;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
shifts: SchedulePresetShift[];
|
||||
|
||||
constructor() {
|
||||
this.id = -1;
|
||||
this.name = 'default';
|
||||
this.is_default = true;
|
||||
this.shifts = [];
|
||||
}
|
||||
}
|
||||
|
||||
class SchedulePresetShift {
|
||||
preset_id: number;
|
||||
week_day: Weekday;
|
||||
type: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
is_remote: boolean;
|
||||
|
||||
constructor(weekday: Weekday) {
|
||||
this.preset_id = -1;
|
||||
this.week_day = weekday;
|
||||
this.type = '';
|
||||
this.start_time = '00:00';
|
||||
this.end_time = '00:00';
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class SchedulePresetFrontend {
|
||||
id: number;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
weekdays: WeekdayPresetShifts[];
|
||||
|
||||
constructor(schedule_preset?: SchedulePreset) {
|
||||
this.id = schedule_preset?.id ?? -1;
|
||||
this.name = schedule_preset?.name ?? '';
|
||||
this.is_default = schedule_preset?.is_default ?? false;
|
||||
this.weekdays = WEEKDAYS.map(day => ({
|
||||
day,
|
||||
shifts: schedule_preset !== undefined ? schedule_preset?.shifts.filter(shift => shift.week_day === day) : [],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export interface WeekdayPresetShifts {
|
||||
day: Weekday;
|
||||
shifts: SchedulePresetShift[];
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profi
|
|||
import type { BackendResponse } from 'src/modules/shared/models/backend-response.models';
|
||||
|
||||
export const EmployeeListService = {
|
||||
getEmployeeList: async (): Promise<EmployeeProfile[]> => {
|
||||
getEmployeeList: async (): Promise<BackendResponse<EmployeeProfile[]>> => {
|
||||
const response = await api.get<BackendResponse<EmployeeProfile[]>>('/employees/employee-list')
|
||||
if (response.data.data) return response.data.data;
|
||||
return [];
|
||||
return response.data;
|
||||
|
||||
},
|
||||
|
||||
getEmployeeDetails: async (): Promise<EmployeeProfile> => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||
import type { SchedulePreset } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
|
||||
export const SchedulePresetsService = {
|
||||
createSchedulePresets: async (new_schedule: SchedulePreset) => {
|
||||
|
|
@ -17,7 +18,7 @@ export const SchedulePresetsService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
findListOfSchedulePresets: async () => {
|
||||
getSchedulePresetsList: async (): Promise<BackendResponse<SchedulePreset[]>> => {
|
||||
const response = await api.get(`/schedule-presets/find-list`);
|
||||
return response.data;
|
||||
},
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
export interface SchedulePreset {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
presets_shifts: ShiftPreset[];
|
||||
}
|
||||
|
||||
class ShiftPreset {
|
||||
week_day: Weekday;
|
||||
preset_id: number;
|
||||
sort_order: number;
|
||||
type: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
is_remote: boolean;
|
||||
|
||||
constructor() {
|
||||
this.week_day = '';
|
||||
this.preset_id = -1;
|
||||
this.sort_order = -1;
|
||||
this.type = '';
|
||||
this.start_time = '';
|
||||
this.end_time = '';
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT' | '';
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
defineProps({
|
||||
show: { type: Boolean, required: true }
|
||||
})
|
||||
|
||||
const beforeEnter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
html_element.style.width = '0'
|
||||
html_element.style.opacity = '0'
|
||||
}
|
||||
|
||||
const enter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
const w = html_element.scrollWidth
|
||||
html_element.style.transition = 'width 0.25s ease, opacity 0.25s ease'
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
html_element.style.width = w + 'px'
|
||||
html_element.style.opacity = '1'
|
||||
})
|
||||
}
|
||||
|
||||
const afterEnter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
html_element.style.width = 'auto' // restore natural width
|
||||
}
|
||||
|
||||
const beforeLeave = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
html_element.style.width = element.scrollWidth + 'px'
|
||||
html_element.style.opacity = '1'
|
||||
}
|
||||
|
||||
const leave = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
// force reflow
|
||||
void html_element.offsetWidth
|
||||
|
||||
html_element.style.transition = 'width 0.25s ease, opacity 0.25s ease'
|
||||
html_element.style.width = '0'
|
||||
html_element.style.opacity = '0'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@before-leave="beforeLeave"
|
||||
@leave="leave"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="h-slide"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.h-slide {
|
||||
overflow: hidden;
|
||||
/* required for width transitions */
|
||||
display: inline-block;
|
||||
/* ensures width applies correctly */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
style="border: 2px solid var(--q-negative)"
|
||||
>
|
||||
<q-item-label class="text-weight-medium text-caption q-ml-md">
|
||||
{{ $t('timesheet.shift.errors.' + error) }}
|
||||
{{ $t('timesheet.errors.' + error) }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,25 @@
|
|||
lang="ts"
|
||||
>
|
||||
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
|
||||
import AddModifyDialog from 'src/modules/employee-list/components/employee/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 } from 'vue';
|
||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
onMounted(async () => {
|
||||
await employee_list_api.getEmployeeList();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="column flex-center">
|
||||
<q-page class="column items-center justify-start">
|
||||
<AddModifyDialog />
|
||||
|
||||
<PageHeaderTemplate title="employee_list.page_header" />
|
||||
|
||||
|
||||
<EmployeeListTable />
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -33,16 +33,16 @@ export const useEmployeeStore = defineStore('employee', () => {
|
|||
employee.value = new EmployeeProfile;
|
||||
};
|
||||
|
||||
const getEmployeeList = async () => {
|
||||
is_loading.value = true;
|
||||
const getEmployeeList = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await EmployeeListService.getEmployeeList();
|
||||
employee_list.value = response;
|
||||
if (response.success && response.data) employee_list.value = response.data;
|
||||
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error("Ran into an error fetching employee list: ", error);
|
||||
//TODO: trigger an alert window with an error message here!
|
||||
return false;
|
||||
}
|
||||
is_loading.value = false;
|
||||
};
|
||||
|
||||
const getEmployeeDetails = async (email?: string) => {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,38 @@
|
|||
/* eslint-disable */
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { SchedulePresetsService } from "src/modules/profile/services/schedule-presets-service";
|
||||
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||
import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
|
||||
import { SchedulePreset, SchedulePresetFrontend } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
|
||||
|
||||
export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
|
||||
const schedule_presets = ref<SchedulePreset>();
|
||||
const schedule_presets = ref<SchedulePreset[]>([new SchedulePreset]);
|
||||
const current_schedule_preset = ref<SchedulePresetFrontend>(new SchedulePresetFrontend);
|
||||
const is_manager_open = ref(false);
|
||||
|
||||
const setCurrentSchedulePreset = (preset_id: number) => {
|
||||
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id)!)
|
||||
};
|
||||
|
||||
const openSchedulePresetManager = (preset_id: number) => {
|
||||
if (preset_id === -1) {
|
||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||
} else {
|
||||
setCurrentSchedulePreset(preset_id);
|
||||
}
|
||||
|
||||
is_manager_open.value = true;
|
||||
};
|
||||
|
||||
const createSchedulePreset = async (): Promise<boolean> => {
|
||||
try {
|
||||
// const new_preset: SchedulePreset = ??
|
||||
// await SchedulePresetsService.createSchedulePresets(new_preset);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while creating schedule preset: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateSchedulePreset = async (): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -27,7 +42,7 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -37,16 +52,20 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
console.error('DEV ERROR || error while deleting schedule preset: ', error);
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findSchedulePresetList = async (): Promise<boolean> => {
|
||||
try {
|
||||
return true;
|
||||
const response = await SchedulePresetsService.getSchedulePresetsList();
|
||||
if (response.success && response.data) schedule_presets.value = response.data;
|
||||
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while searching for schedule presets: ', error);
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applySchedulePreset = async (): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -55,10 +74,14 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
console.error('DEV ERROR || error while building schedule: ', error);
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
schedule_presets,
|
||||
current_schedule_preset,
|
||||
is_manager_open,
|
||||
setCurrentSchedulePreset,
|
||||
openSchedulePresetManager,
|
||||
createSchedulePreset,
|
||||
updateSchedulePreset,
|
||||
deleteSchedulePreset,
|
||||
|
|
|
|||
31
src/utils/table-grid-FLIP.ts
Normal file
31
src/utils/table-grid-FLIP.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { type Ref, nextTick } from 'vue';
|
||||
|
||||
export const animateFlip = (container: Ref<HTMLElement | null>) => {
|
||||
const el = container.value;
|
||||
if (!el) return;
|
||||
|
||||
const children = Array.from(el.children) as HTMLElement[];
|
||||
|
||||
// FIRST: record initial positions
|
||||
const firstRects = children.map(c => c.getBoundingClientRect());
|
||||
|
||||
// Do LAST → INVERT → PLAY after DOM update
|
||||
void nextTick(() => {
|
||||
const lastRects = children.map(c => c.getBoundingClientRect());
|
||||
|
||||
children.forEach((child, i) => {
|
||||
const dx = firstRects[i]!.left - lastRects[i]!.left;
|
||||
const dy = firstRects[i]!.top - lastRects[i]!.top;
|
||||
|
||||
if (!dx && !dy) return;
|
||||
|
||||
child.style.transition = 'none';
|
||||
child.style.transform = `translate(${dx}px, ${dy}px)`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
child.style.transition = 'transform 250ms ease';
|
||||
child.style.transform = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user