refactor(employee-list): rework schedule preset selector, fix other ui issues, see notes

Add warning dialog when changes in an employee profile are unsaved.

Move schedule preset buttons from side of selector to side of each option in select menu.

optimize behavior of selector: will now switch to empty when deleting currently assigned preset, and will assign automatically any new or copied preset to current employee.
This commit is contained in:
Nic D 2026-02-03 13:09:46 -05:00
parent 1c6f7fd155
commit 6dc1804918
12 changed files with 365 additions and 165 deletions

View File

@ -109,10 +109,11 @@ export default {
banked_hours: "available banked hours", banked_hours: "available banked hours",
sick_hours: "available PTO hours", sick_hours: "available PTO hours",
vacation_hours: "available vacation hours", vacation_hours: "available vacation hours",
save_changes_notification: "save changes to employee profile?",
schedule_presets: { schedule_presets: {
preset_list_placeholder: "Select a schedule", preset_list_placeholder: "Select a schedule",
preset_name_placeholder: "schedule preset name", preset_name_placeholder: "schedule preset name",
delete_warning: "", delete_warning: "Are you certain you wish to delete this schedule?",
delete_warning_employee_1: "This schedule is used by", delete_warning_employee_1: "This schedule is used by",
delete_warning_employee_2: "Deleting this preset will not affect previous timesheets, but they will no longer be able to apply this preset to their timesheets going forward.", delete_warning_employee_2: "Deleting this preset will not affect previous timesheets, but they will no longer be able to apply this preset to their timesheets going forward.",
}, },
@ -237,11 +238,13 @@ export default {
cancel: "cancel", cancel: "cancel",
update: "update", update: "update",
modify: "modify", modify: "modify",
copy: "copy",
close: "close", close: "close",
download: "download", download: "download",
open: "open", open: "open",
day: "day", day: "day",
empty: "empty", empty: "empty",
name: "name",
}, },
misc: { misc: {
or: "or", or: "or",

View File

@ -109,6 +109,7 @@ export default {
banked_hours: "heures en banque disponibles", banked_hours: "heures en banque disponibles",
sick_hours: "heures d'absence payées disponibles", sick_hours: "heures d'absence payées disponibles",
vacation_hours: "heures de vacances disponibles", vacation_hours: "heures de vacances disponibles",
save_changes_notification: "Sauvegarder les modifications du profil?",
schedule_presets: { schedule_presets: {
preset_list_placeholder: "Sélectionner un horaire", preset_list_placeholder: "Sélectionner un horaire",
preset_name_placeholder: "nom de l'horaire", preset_name_placeholder: "nom de l'horaire",
@ -237,11 +238,13 @@ export default {
cancel: "annuler", cancel: "annuler",
update: "mettre à jour", update: "mettre à jour",
modify: "modifier", modify: "modifier",
copy: "copier",
close: "fermer", close: "fermer",
download: "télécharger", download: "télécharger",
open: "ouvrir", open: "ouvrir",
day: "jour", day: "jour",
empty: "vide", empty: "vide",
name: "nom",
}, },
misc: { misc: {
or: "ou", or: "ou",

View File

@ -2,43 +2,58 @@
setup setup
lang="ts" 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 SchedulePresetsDialog from 'src/modules/employee-list/components/schedule-presets-dialog.vue';
import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue'; import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue';
import { useI18n } from 'vue-i18n';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store'; import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
import { useEmployeeListApi } from '../composables/use-employee-api'; import { useEmployeeListApi } from '../composables/use-employee-api';
import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models'; import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models';
import type { QSelectOption } from 'quasar';
// ================= state ====================== // ========== state ========================================
const { t } = useI18n();
const schedule_preset_store = useSchedulePresetsStore(); const schedule_preset_store = useSchedulePresetsStore();
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const employee_list_api = useEmployeeListApi(); const employee_list_api = useEmployeeListApi();
const preset_options = ref<{ label: string, value: number }[]>([]); const preset_options = ref<QSelectOption<number>[]>([]);
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 }); const selected_preset = ref<QSelectOption<number>>({ label: '', value: -1 });
// ====================== methods ======================== // ========== methods ========================================
const getPresetOptions = (): QSelectOption<number>[] => {
const options: QSelectOption<number>[] = [{ label: t('shared.label.empty'), value: -1 }];
schedule_preset_store.schedule_presets.forEach(preset => {
options.push({ label: preset.name, value: preset.id })
});
options.push({ label: '', value: 0 });
const getPresetOptions = (): { label: string, value: number }[] => {
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
options.push({ label: 'Aucun', value: -1 });
return options; return options;
}; };
const onClickSchedulePresetManager = (mode: PresetManagerMode, preset_id?: number) => { const onClickSchedulePresetManager = (mode: PresetManagerMode, preset_id?: number) => {
schedule_preset_store.schedule_preset_dialog_mode = mode; schedule_preset_store.schedule_preset_dialog_mode = mode;
schedule_preset_store.openSchedulePresetManager(preset_id ?? current_preset.value.value); schedule_preset_store.openSchedulePresetManager(preset_id ?? selected_preset.value.value);
} }
const loadSelectedPresetOption = () => { const loadSelectedPresetOption = () => {
preset_options.value = getPresetOptions(); preset_options.value = getPresetOptions();
const current_option = preset_options.value.find(option => option.value === employee_store.employee.preset_id); const employee = employee_store.employee;
current_preset.value = current_option ?? { label: undefined, value: -1 };
schedule_preset_store.setCurrentSchedulePreset(current_preset.value.value); if (!employee.preset_id)
selected_preset.value = preset_options.value[0]!;
else
selected_preset.value = preset_options.value.find(opt =>
opt.value === employee.preset_id
)!;
schedule_preset_store.setCurrentSchedulePreset(selected_preset.value.value);
schedule_preset_store.schedule_preset_dialog_mode = undefined;
}; };
onMounted(() => { onMounted(() => {
@ -48,76 +63,111 @@
<template> <template>
<div <div
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'" :key="schedule_preset_store.isManagerOpen === false ? '0' : '1'"
class="column full-width flex-center items-start" class="column full-width flex-center items-start"
> >
<SchedulePresetsDialog @before-hide="loadSelectedPresetOption"/> <SchedulePresetsDialog @on-close="loadSelectedPresetOption" />
<div class="col row justify-center full-width no-wrap"> <div class="col row flex-center full-width no-wrap">
<q-select <q-select
v-model="current_preset" v-model="selected_preset"
standout="bg-accent" :options="getPresetOptions()"
standout="bg-primary"
dense dense
options-dense options-dense
rounded rounded
color="accent" 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-anchor="bottom middle"
menu-self="top middle" menu-self="top middle"
:menu-offset="[0, 10]" :menu-offset="[0, 5]"
class="col-xs-10 col-md-7"
popup-content-class="text-uppercase text-weight-medium rounded-20 shadow-24"
popup-content-style="border: 2px solid var(--q-primary)"
style="font-size: 1.4em;"
@update:modelValue="option => employee_list_api.setSchedulePreset(option.value)" @update:modelValue="option => employee_list_api.setSchedulePreset(option.value)"
> >
<template #selected> <template #selected>
<span <span
class="text-uppercase text-center text-weight-bold full-width" class="text-uppercase text-center text-weight-bold full-width"
:style="current_preset.label === undefined ? 'opacity: 0.5;' : ''" :style="selected_preset.label === undefined ? 'opacity: 0.5;' : ''"
> >
{{ current_preset.label === undefined ? {{ selected_preset.label === undefined ?
$t('employee_management.schedule_presets.preset_list_placeholder') : $t('employee_management.schedule_presets.preset_list_placeholder') :
current_preset.label }} selected_preset.label }}
</span> </span>
</template> </template>
<template #option="scope">
<q-item
v-if="scope.opt.value !== 0"
v-bind="scope.itemProps"
class="row flex-center"
style="font-size: 1.2em;"
>
<div class="col">
<span>{{ scope.label }}</span>
</div>
<div
v-if="scope.opt.value > 0"
class="row items-center no-wrap"
>
<!-- edit currently-selected preset -->
<div class="col-auto q-px-sm">
<q-btn
flat
dense
icon="edit"
color="accent"
@click.stop="onClickSchedulePresetManager('update', scope.opt.value)"
/>
</div>
<!-- copy currently-selected preset -->
<div class="col-auto">
<q-btn
flat
dense
icon="content_copy"
color="accent"
@click.stop="onClickSchedulePresetManager('copy', scope.opt.value)"
/>
</div>
<!-- delete currently-selected preset -->
<div class="col-auto q-px-xs">
<q-btn
flat
dense
color="negative"
icon="las la-trash"
class="q-py-none"
@click.stop="onClickSchedulePresetManager('delete', scope.opt.value)"
/>
</div>
</div>
</q-item>
<q-item
v-else
class="q-pa-none"
>
<q-btn
square
icon="add"
size="md"
color="accent"
class="full-width q-py-none"
:label="$t('shared.label.add')"
style="font-size: 1.2em;"
@click.stop="onClickSchedulePresetManager('create', -1)"
/>
</q-item>
</template>
</q-select> </q-select>
<q-btn
icon="add"
color="accent"
class="col-auto q-px-sm q-ml-sm rounded-50"
@click="onClickSchedulePresetManager('create', -1)"
/>
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
<div class="col-auto row no-wrap full-height">
<q-btn
icon="edit"
color="accent"
class="col-auto q-px-sm q-ml-sm rounded-50"
@click="onClickSchedulePresetManager('update')"
/>
<q-btn
icon="content_copy"
color="accent"
class="col-auto q-px-sm q-mx-sm rounded-50"
@click="onClickSchedulePresetManager('copy')"
/>
<q-btn
flat
dense
rounded
icon="clear"
color="negative"
class="col-auto q-px-sm full-height"
@click="onClickSchedulePresetManager('delete')"
/>
</div>
</HorizontalSlideTransition>
</div> </div>
<AddModifyDialogSchedulePreview :current-preset-id="current_preset.value" />
<AddModifyDialogSchedulePreview :current-preset-id="selected_preset.value" />
</div> </div>
</template> </template>

View File

@ -8,21 +8,52 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
// ========== state ========================================
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const current_step = ref<'form' | 'access' | 'schedule'>('form'); const current_step = ref<'form' | 'access' | 'schedule'>('form');
const initial_employee_profile = ref(new EmployeeProfile) const initial_employee_details = ref('');
const is_showing_close_confirm = ref(false);
// ========== methods ========================================
const onBeforeHide = () => {
const current_employee_details = JSON.stringify(employee_store.employee);
if (initial_employee_details.value !== current_employee_details)
is_showing_close_confirm.value = true;
else
employee_store.is_add_modify_dialog_open = false;
};
const onBeforeShow = () => {
current_step.value = 'form';
initial_employee_details.value = JSON.stringify(employee_store.employee);
};
const onClickSaveChanges = async () => {
const success = await employee_store.createOrUpdateEmployee(employee_store.employee);
if (success)
closeAllDialogs();
}
const closeAllDialogs = () => {
is_showing_close_confirm.value = false;
employee_store.is_add_modify_dialog_open = false;
}
</script> </script>
<template> <template>
<q-dialog <q-dialog
v-model="employee_store.is_add_modify_dialog_open" :model-value="employee_store.is_add_modify_dialog_open"
full-width full-width
full-height full-height
@beforeShow="current_step = 'form'" backdrop-filter="blur(4px)"
@show="Object.assign(initial_employee_profile, employee_store.employee)"
class="shadow-24" class="shadow-24"
@beforeShow="onBeforeShow"
@update:model-value="onBeforeHide"
> >
<div <div
class="column bg-secondary rounded-10 no-wrap" class="column bg-secondary rounded-10 no-wrap"
@ -112,8 +143,50 @@
color="accent" color="accent"
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')" :label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
class="col-auto q-py-sm shadow-up-5" class="col-auto q-py-sm shadow-up-5"
@click="employee_store.createOrUpdateEmployee(employee_store.employee)" @click="onClickSaveChanges"
/> />
</div> </div>
<!-- dialog to confirm if you want to save changes -->
<q-dialog
v-model="is_showing_close_confirm"
persistent
backdrop-filter="blur(4px)"
>
<q-card class="q-pa-md shadow-24">
<span class="text-uppercase text-weight-light text-h6">{{ $t('employee_management.save_changes_notification') }}</span>
<div class="row full-width">
<q-btn
flat
dense
size="lg"
:label="$t('shared.label.cancel')"
class="col"
@click="is_showing_close_confirm = false"
/>
<q-btn
flat
dense
size="lg"
color="negative"
:label="$t('shared.misc.no')"
class="col"
@click="closeAllDialogs"
/>
<q-btn
flat
dense
size="lg"
color="accent"
:label="$t('shared.misc.yes')"
class="col"
@click="onClickSaveChanges"
/>
</div>
</q-card>
</q-dialog>
</q-dialog> </q-dialog>
</template> </template>

View File

@ -147,7 +147,6 @@
color="accent" color="accent"
bg-color="white" bg-color="white"
label-color="accent" label-color="accent"
class="text-primary"
debounce="300" debounce="300"
:label="$t('shared.label.search')" :label="$t('shared.label.search')"
> >
@ -298,4 +297,8 @@ tbody {
:deep(.q-table__grid-content) { :deep(.q-table__grid-content) {
overflow: auto overflow: auto
} }
:deep(.q-field__native) {
color: var(--q-primary);
}
</style> </style>

View File

@ -6,17 +6,37 @@
import { useEmployeeStore } from 'src/stores/employee-store'; import { useEmployeeStore } from 'src/stores/employee-store';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api'; import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
const employee_store = useEmployeeStore(); // ========== state ===================================
const employee_list_api = useEmployeeListApi();
const { presetId } = defineProps<{ const { presetId } = defineProps<{
presetId: number; presetId: number;
}>(); }>();
const emit = defineEmits<{
'onConfirmDelete': [void];
}>();
const employee_store = useEmployeeStore();
const employee_list_api = useEmployeeListApi();
const employee_amount_using_preset = ref(0); const employee_amount_using_preset = ref(0);
const delete_input_string = ref(''); const delete_input_string = ref('');
// ========== computed ==================================
const is_approve_deletion = computed(() => ['SUPPRIMER', 'DELETE'].includes(delete_input_string.value)); const is_approve_deletion = computed(() => ['SUPPRIMER', 'DELETE'].includes(delete_input_string.value));
// ========== methods ===================================
const onClickDeleteConfirm = async () => {
const success = await employee_list_api.deleteSchedulePreset(presetId);
if (success && employee_store.employee.preset_id === presetId)
employee_store.employee.preset_id = null;
emit('onConfirmDelete');
};
onMounted(() => { onMounted(() => {
const employees_with_preset = employee_store.employee_list.filter(employee => employee.preset_id === presetId); const employees_with_preset = employee_store.employee_list.filter(employee => employee.preset_id === presetId);
employee_amount_using_preset.value = employees_with_preset.length; employee_amount_using_preset.value = employees_with_preset.length;
@ -68,7 +88,7 @@
:color="is_approve_deletion ? 'negative' : 'grey-6'" :color="is_approve_deletion ? 'negative' : 'grey-6'"
:label="$t('shared.label.remove')" :label="$t('shared.label.remove')"
class="q-px-md" class="q-px-md"
@click="employee_list_api.deleteSchedulePreset(presetId)" @click="onClickDeleteConfirm"
/> />
</div> </div>
</div> </div>

View File

@ -10,18 +10,35 @@
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store'; import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util'; import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
const schedule_preset_store = useSchedulePresetsStore(); const emit = defineEmits<{
'onClose': [void];
}>();
const schedulePresetStore = useSchedulePresetsStore();
const employee_list_api = useEmployeeListApi(); const employee_list_api = useEmployeeListApi();
const onClickSaveSchedulePreset = async () => {
const success = await employee_list_api.saveSchedulePreset();
if (success)
closePresetManager();
}
const closePresetManager = () => {
emit('onClose');
schedulePresetStore.isManagerOpen = false;
}
</script> </script>
<template> <template>
<q-dialog <q-dialog
v-model="schedule_preset_store.is_manager_open" v-model="schedulePresetStore.isManagerOpen"
full-width full-width
> >
<SchedulePresetsDialogDelete <SchedulePresetsDialogDelete
v-if="schedule_preset_store.schedule_preset_dialog_mode === 'delete'" v-if="schedulePresetStore.schedule_preset_dialog_mode === 'delete'"
:preset-id="schedule_preset_store.current_schedule_preset.id" :preset-id="schedulePresetStore.current_schedule_preset.id"
@on-confirm-delete="closePresetManager"
/> />
<div <div
@ -34,24 +51,23 @@
style="border-radius: 8px 8px 0 0;" style="border-radius: 8px 8px 0 0;"
> >
<span class="row col-auto text-uppercase text-weight-bold text-white q-py-sm">{{ <span class="row col-auto text-uppercase text-weight-bold text-white q-py-sm">{{
schedule_preset_store.current_schedule_preset.id === -1 ? schedulePresetStore.current_schedule_preset.id === -1 ?
$t('shared.label.add') : $t('shared.label.add') :
$t('shared.label.modify') }} $t('shared.label.modify') }}
</span> </span>
</div> </div>
<div class="row col-auto q-px-sm flex-center full-width q-py-sm"> <div class="row col-auto q-px-sm flex-center full-width q-pt-md">
<div class="col-8 bg-dark rounded-10 ellipsis"> <div class="col-8 bg-dark rounded-10 shadow-2 ellipsis">
<q-input <q-input
v-model="schedule_preset_store.current_schedule_preset.name" v-model="schedulePresetStore.current_schedule_preset.name"
standout standout
dense dense
hide-bottom-space hide-bottom-space
:placeholder="$t('employee_management.schedule_presets.preset_name_placeholder')" :placeholder="$t('employee_management.schedule_presets.preset_name_placeholder')"
class="text-uppercase"
input-class="text-weight-bold text-center" input-class="text-weight-bold text-center"
> >
<template #before> <template #prepend>
<q-icon <q-icon
name="edit" name="edit"
color="accent" color="accent"
@ -62,12 +78,9 @@
</div> </div>
</div> </div>
<div <div class="column col full-width q-py-sm q-px-lg no-wrap scroll">
v-if="schedule_preset_store.schedule_preset_dialog_mode !== 'copy'"
class="column col full-width q-py-sm q-px-lg no-wrap scroll"
>
<div <div
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays" v-for="weekday of schedulePresetStore.current_schedule_preset.weekdays"
:key="weekday.day" :key="weekday.day"
class="row col-auto items-center q-my-xs shadow-2 bg-dark rounded-10 ellipsis" class="row col-auto items-center q-my-xs shadow-2 bg-dark rounded-10 ellipsis"
style="min-height: 50px;" style="min-height: 50px;"
@ -106,14 +119,14 @@
<div class="col-auto row self-end q-px-lg q-mt-sm full-width"> <div class="col-auto row self-end q-px-lg q-mt-sm full-width">
<q-space /> <q-space />
<q-btn <q-btn
:disable="schedule_preset_store.current_schedule_preset.name === ''" :disable="schedulePresetStore.current_schedule_preset.name === ''"
push push
dense dense
:color="schedule_preset_store.current_schedule_preset.name === '' ? 'grey-7' : 'accent'" :color="schedulePresetStore.current_schedule_preset.name === '' ? 'grey-7' : 'accent'"
icon="download" icon="download"
:label="$t('shared.label.save')" :label="$t('shared.label.save')"
class="col-auto q-px-md q-mb-sm" class="col-auto q-px-md q-mb-sm"
@click="employee_list_api.saveSchedulePreset" @click="onClickSaveSchedulePreset"
/> />
</div> </div>
</div> </div>

View File

@ -4,76 +4,95 @@ import { SchedulePreset } from "../models/schedule-presets.models";
import { isShiftOverlap } from "src/modules/timesheets/utils/shift.util"; import { isShiftOverlap } from "src/modules/timesheets/utils/shift.util";
export const useEmployeeListApi = () => { export const useEmployeeListApi = () => {
const employee_store = useEmployeeStore(); const employeeStore = useEmployeeStore();
const schedule_preset_store = useSchedulePresetsStore(); const schedulePresetStore = useSchedulePresetsStore();
/**
* Populates the employee store with the exhaustive list of all employees past and present
* Also populates the schedule preset store with all schedule presets currently available.
*/
const getEmployeeList = async (): Promise<void> => { const getEmployeeList = async (): Promise<void> => {
employee_store.is_loading = true; employeeStore.is_loading = true;
const success = await employee_store.getEmployeeList(); const success = await employeeStore.getEmployeeList();
if (success) await schedule_preset_store.findSchedulePresetList(); if (success) await schedulePresetStore.getSchedulePresetList();
employee_store.is_loading = false; employeeStore.is_loading = false;
}; };
/**
* Assigns the details of a specific employee to the employee store. If the employee has a
* schedule preset assigned, it also assign that preset to the schedule preset store.
*
* @param email email associated to employee.
*/
const getEmployeeDetails = async (email: string): Promise<void> => { const getEmployeeDetails = async (email: string): Promise<void> => {
const success = await employee_store.getEmployeeDetails(email); const success = await employeeStore.getEmployeeDetails(email);
if (success && employee_store.employee.preset_id !== null) { if (success && employeeStore.employee.preset_id !== null) {
schedule_preset_store.setCurrentSchedulePreset(employee_store.employee.preset_id ?? -1); schedulePresetStore.setCurrentSchedulePreset(employeeStore.employee.preset_id ?? -1);
} }
} }
/**
* Assigns the specified preset as the current preset in the schedule preset store and also
* applies it to the current employee in the employee store. If a negative ID is provided, the
* employee's assigned preset is set to null.
*
* @param preset_id - the preset id currently selected
*/
const setSchedulePreset = (preset_id: number) => { const setSchedulePreset = (preset_id: number) => {
schedule_preset_store.setCurrentSchedulePreset(preset_id); schedulePresetStore.setCurrentSchedulePreset(preset_id);
employee_store.employee.preset_id = preset_id < 0 ? null : preset_id; employeeStore.employee.preset_id = preset_id < 0 ? null : preset_id;
} }
const saveSchedulePreset = async () => { /**
// Get the currently edited schedule preset from the store (frontend model) * Validates and converts the current schedule preset into a model that the backend
const preset = schedule_preset_store.current_schedule_preset; * can ingest and save, then sends a request to create or update preset.
*
// Check if there's any overlap between shifts. If there is, is_error property * @returns `true` if the preset is valid and was successfully saved, `false` otherwise.
// will be toggled to true and save process will stop */
for (const weekday of preset.weekdays) { const saveSchedulePreset = async (): Promise<boolean> => {
weekday.is_error = isShiftOverlap(weekday.shifts); const preset = schedulePresetStore.current_schedule_preset;
} preset.weekdays.forEach(weekday => weekday.is_error = isShiftOverlap(weekday.shifts));
if (preset.weekdays.some(weekday => weekday.is_error)) { if (preset.weekdays.some(weekday => weekday.is_error)) {
return; return false;
} }
// Flatten all weekday shifts into a single array
const preset_shifts = preset.weekdays.flatMap(weekday => weekday.shifts);
// Build a backend-compatible SchedulePreset instance const preset_shifts = preset.weekdays.flatMap(weekday => weekday.shifts);
const backend_preset = new SchedulePreset( const backend_preset = new SchedulePreset(
preset.id, preset.id,
preset.name, preset.name,
preset_shifts preset_shifts
); );
// Track whether the create/update operation succeeds
let success = false; let success = false;
// Create a new preset if it has no backend ID, otherwise update the existing one if (preset.id === -1)
if (preset.id === -1) success = await schedulePresetStore.createSchedulePreset(backend_preset);
success = await schedule_preset_store.createSchedulePreset(backend_preset); else
else success = await schedulePresetStore.updateSchedulePreset(backend_preset);
success = await schedule_preset_store.updateSchedulePreset(backend_preset);
// On success, refresh the preset list and close the preset manager UI
if (success) { if (success) {
await schedule_preset_store.findSchedulePresetList(); await schedulePresetStore.getSchedulePresetList();
schedule_preset_store.is_manager_open = false; employeeStore.employee.preset_id = schedulePresetStore.current_schedule_preset.id;
} }
return success;
} }
const deleteSchedulePreset = async (preset_id: number) => { /**
const success = await schedule_preset_store.deleteSchedulePreset(preset_id); * Sends request to delete the preset associated to the provided ID.
if (success) { *
await schedule_preset_store.findSchedulePresetList(); * @param preset_id Backend ID of preset to delete
schedule_preset_store.is_manager_open = false; * @return `true` if successfully deleted, `false` otherwise.
} */
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
const success = await schedulePresetStore.deleteSchedulePreset(preset_id);
if (success)
await schedulePresetStore.getSchedulePresetList();
return success;
} }
return { return {

View File

@ -11,7 +11,4 @@ export const useEmployeeProfileRules = () => {
} }
} }
export const company_options = [ export const company_options = ['Targo', 'Solucom'];
{ label: 'Targo', value: 'Targo' },
{ label: 'Solucom', value: 'Solucom' },
]

View File

@ -3,7 +3,7 @@ import type { SchedulePreset } from "src/modules/employee-list/models/schedule-p
import type { BackendResponse } from "src/modules/shared/models/backend-response.models"; import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
export const SchedulePresetsService = { export const SchedulePresetsService = {
createSchedulePresets: async (preset: SchedulePreset) => { createSchedulePresets: async (preset: SchedulePreset): Promise<BackendResponse<SchedulePreset>> => {
const response = await api.post(`/schedule-presets/create/`, preset); const response = await api.post(`/schedule-presets/create/`, preset);
return response.data; return response.data;
}, },

View File

@ -13,14 +13,14 @@ export const useEmployeeStore = defineStore('employee', () => {
const is_loading = ref(false); const is_loading = ref(false);
const openAddModifyDialog = async (employee_email?: string) => { const openAddModifyDialog = async (employee_email?: string) => {
if (employee_email === undefined) { if (employee_email === undefined) {
management_mode.value = 'add_employee' management_mode.value = 'add_employee'
employee.value = new EmployeeProfile(); employee.value = new EmployeeProfile();
is_add_modify_dialog_open.value = true; is_add_modify_dialog_open.value = true;
return; return;
} }
is_loading.value = true; is_loading.value = true;
management_mode.value = 'modify_employee'; management_mode.value = 'modify_employee';
await getEmployeeDetails(employee_email); await getEmployeeDetails(employee_email);
@ -65,25 +65,29 @@ export const useEmployeeStore = defineStore('employee', () => {
return false; return false;
}; };
const createOrUpdateEmployee = async (profile: EmployeeProfile) => { const createOrUpdateEmployee = async (profile: EmployeeProfile): Promise<boolean> => {
let response; let response;
if (management_mode.value === 'add_employee') { if (management_mode.value === 'add_employee') {
const { birth_date, last_work_day, ...create_payload} = profile; const { birth_date, last_work_day, ...create_payload } = profile;
response = await EmployeeListService.createNewEmployee(create_payload); response = await EmployeeListService.createNewEmployee(create_payload);
} else { } else {
response = await EmployeeListService.updateEmployee(profile); response = await EmployeeListService.updateEmployee(profile);
} }
closeAddModifyDialog(); closeAddModifyDialog();
if (response.success) await getEmployeeList(); if (response.success)
else { await getEmployeeList();
else
Notify.create({ Notify.create({
message: 'failed to update or create employee', message: 'failed to update or create employee',
color: 'negative', color: 'negative',
})} });
return response.success;
}; };
return { return {

View File

@ -7,39 +7,49 @@ import { type PresetManagerMode, SchedulePreset, SchedulePresetFrontend } from "
export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => { export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
const schedule_presets = ref<SchedulePreset[]>([new SchedulePreset]); const schedule_presets = ref<SchedulePreset[]>([new SchedulePreset]);
const current_schedule_preset = ref<SchedulePresetFrontend>(new SchedulePresetFrontend); const current_schedule_preset = ref<SchedulePresetFrontend>(new SchedulePresetFrontend);
const schedule_preset_dialog_mode = ref<PresetManagerMode>('create'); const schedule_preset_dialog_mode = ref<PresetManagerMode | undefined>();
const is_manager_open = ref(false); const isManagerOpen = ref(false);
/**
* Opens the schedule preset manager with the preset associated with provided ID. If mode is
* set to `copy`, a clone of the preset will be created with a blank name instead.
* @param preset_id
*/
const openSchedulePresetManager = (preset_id: number) => { const openSchedulePresetManager = (preset_id: number) => {
if (preset_id === -1) if (preset_id === -1) {
current_schedule_preset.value = new SchedulePresetFrontend; current_schedule_preset.value = new SchedulePresetFrontend;
else if (schedule_preset_dialog_mode.value === 'copy') { } else if (schedule_preset_dialog_mode.value === 'copy') {
const preset = schedule_presets.value.find(preset => preset.id === preset_id)!; const preset = schedule_presets.value.find(preset => preset.id === preset_id)!;
const copied_preset = new SchedulePresetFrontend(preset); const copied_preset = new SchedulePresetFrontend(preset);
copied_preset.id = -1; copied_preset.id = -1;
copied_preset.name = ""; copied_preset.name = "";
current_schedule_preset.value = copied_preset; current_schedule_preset.value = copied_preset;
} } else
else
setCurrentSchedulePreset(preset_id); setCurrentSchedulePreset(preset_id);
is_manager_open.value = true; isManagerOpen.value = true;
}; };
const setCurrentSchedulePreset = (preset_id: number) => { const setCurrentSchedulePreset = (preset_id: number) => {
if (preset_id === -1) { if (preset_id === -1)
current_schedule_preset.value = new SchedulePresetFrontend; current_schedule_preset.value = new SchedulePresetFrontend;
return; else
} current_schedule_preset.value = new SchedulePresetFrontend(
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id)) schedule_presets.value.find(preset => preset.id === preset_id)
);
}; };
const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => { const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
try { try {
const response = await SchedulePresetsService.createSchedulePresets(preset); const response = await SchedulePresetsService.createSchedulePresets(preset);
if (response.success && response.data)
current_schedule_preset.value = new SchedulePresetFrontend(response.data);
return response.success; return response.success;
} catch (error) { } catch (error) {
console.error('DEV ERROR || error while creating schedule preset: ', error); console.error('DEV ERROR || error while creating schedule preset: ', error);
return false; return false;
} }
}; };
@ -47,9 +57,11 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
const updateSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => { const updateSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
try { try {
const response = await SchedulePresetsService.updateSchedulePresets(preset); const response = await SchedulePresetsService.updateSchedulePresets(preset);
return response.success; return response.success;
} catch (error) { } catch (error) {
console.error('DEV ERROR || error while updating schedule preset: ', error); console.error('DEV ERROR || error while updating schedule preset: ', error);
return false; return false;
} }
@ -58,17 +70,20 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => { const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
try { try {
const response = await SchedulePresetsService.deleteSchedulePresets(preset_id); const response = await SchedulePresetsService.deleteSchedulePresets(preset_id);
return response.success; return response.success;
} catch (error) { } catch (error) {
console.error('DEV ERROR || error while deleting schedule preset: ', error); console.error('DEV ERROR || error while deleting schedule preset: ', error);
return false; return false;
} }
}; };
const findSchedulePresetList = async (): Promise<boolean> => { const getSchedulePresetList = async (): Promise<boolean> => {
try { try {
const response = await SchedulePresetsService.getSchedulePresetsList(); const response = await SchedulePresetsService.getSchedulePresetsList();
if (response.success && response.data) schedule_presets.value = response.data; if (response.success && response.data)
schedule_presets.value = response.data;
return response.success; return response.success;
} catch (error) { } catch (error) {
@ -82,12 +97,12 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
schedule_presets, schedule_presets,
current_schedule_preset, current_schedule_preset,
schedule_preset_dialog_mode, schedule_preset_dialog_mode,
is_manager_open, isManagerOpen,
setCurrentSchedulePreset, setCurrentSchedulePreset,
openSchedulePresetManager, openSchedulePresetManager,
createSchedulePreset, createSchedulePreset,
updateSchedulePreset, updateSchedulePreset,
deleteSchedulePreset, deleteSchedulePreset,
findSchedulePresetList, getSchedulePresetList,
} }
}) })