Merge pull request 'dev/nicolas/employee-management' (#32) from dev/nicolas/employee-management into main
Reviewed-on: Targo/targo_frontend#32
This commit is contained in:
commit
35500eccda
|
|
@ -21,9 +21,13 @@ export default {
|
|||
access_label: "access",
|
||||
details_label: "details",
|
||||
schedule_label: "schedule",
|
||||
enter_delete_input: "type 'DELETE' to remove",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Select a schedule",
|
||||
preset_name_placeholder: "schedule preset name",
|
||||
delete_warning: "",
|
||||
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.",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Dashboard",
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ export default {
|
|||
access_label: "accès",
|
||||
details_label: "détails",
|
||||
schedule_label: "horaire",
|
||||
enter_delete_input: "tappez 'SUPPRIMER' pour confirmer",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Sélectionner un horaire",
|
||||
preset_name_placeholder: "nom de l'horaire",
|
||||
delete_warning: "Êtes-vous certain de vouloir supprimer cet horaire?",
|
||||
delete_warning_employee_1: "Cet horaire est présentement utilisé par",
|
||||
delete_warning_employee_2: "La suppression n'affectera pas leurs feuilles de temps antérieures, mais ils ne pourront plus appliquer cet horaire à leurs feuilles de temps à partir de maintenant.",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Accueil",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
// import { date } from 'quasar';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
|
|
@ -27,7 +27,7 @@ import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
|||
<span class="col-2 text-weight-bolder text-accent text-uppercase text-overline" style="font-size: 1.3em;">{{
|
||||
$t(`shared.weekday.${weekday.day.toLowerCase()}`) }}</span>
|
||||
<div
|
||||
v-for="shift, index in weekday.shifts.sort((a, b) => date.extractDate(a.start_time, 'HH:mm').getTime() - date.extractDate(b.start_time, 'HH:mm').getTime())"
|
||||
v-for="shift, index in weekday.shifts"
|
||||
:key="index"
|
||||
class="col q-px-md q-py-xs"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
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 { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.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';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
|
|
@ -17,6 +18,7 @@ import { useEmployeeListApi } from '../composables/use-employee-api';
|
|||
|
||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
||||
const manager_watcher = ref(schedule_preset_store.is_manager_open);
|
||||
|
||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||
|
|
@ -24,15 +26,30 @@ import { useEmployeeListApi } from '../composables/use-employee-api';
|
|||
return options;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const onClickSchedulePresetManager = (mode: PresetManagerMode, preset_id?: number) => {
|
||||
schedule_preset_store.schedule_preset_dialog_mode = mode;
|
||||
schedule_preset_store.openSchedulePresetManager(preset_id ?? current_preset.value.value);
|
||||
}
|
||||
|
||||
const loadSelectedPresetOption = () => {
|
||||
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 };
|
||||
schedule_preset_store.setCurrentSchedulePreset(current_preset.value.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadSelectedPresetOption();
|
||||
});
|
||||
|
||||
watch(manager_watcher, loadSelectedPresetOption)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :key="schedule_preset_store.is_manager_open === false ? '0' : '1'" class="column full-width flex-center items-start">
|
||||
<div
|
||||
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'"
|
||||
class="column full-width flex-center items-start"
|
||||
>
|
||||
<SchedulePresetsDialog />
|
||||
|
||||
<div class="col row justify-center full-width no-wrap">
|
||||
|
|
@ -65,32 +82,38 @@ import { useEmployeeListApi } from '../composables/use-employee-api';
|
|||
</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)"
|
||||
class="col-auto q-px-sm q-ml-sm rounded-50"
|
||||
@click="onClickSchedulePresetManager('create', -1)"
|
||||
/>
|
||||
|
||||
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
|
||||
<transition
|
||||
enter-active-class="animated zoomIn"
|
||||
leave-active-class="animated zoomOut"
|
||||
mode="out-in"
|
||||
>
|
||||
<div class="col-auto row no-wrap full-height">
|
||||
<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)"
|
||||
class="col-auto q-px-sm q-ml-sm rounded-50"
|
||||
@click="onClickSchedulePresetManager('update')"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
<q-dialog
|
||||
v-model="employee_store.is_add_modify_dialog_open"
|
||||
full-width
|
||||
full-height
|
||||
@beforeShow="current_step = 'form'"
|
||||
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
||||
class="shadow-24"
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
<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') +
|
||||
:style="($q.screen.lt.md ? ' ' : 'max-width: 60vw !important; max-height: 80vh !important;') +
|
||||
($q.dark.isActive ? 'border: 2px solid var(--q-accent)' : '')"
|
||||
>
|
||||
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
||||
|
|
@ -79,8 +80,8 @@
|
|||
<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'"
|
||||
:transition-prev="$q.screen.lt.sm ? 'jump-down' : 'jump-right'"
|
||||
:transition-next="$q.screen.lt.sm ? 'jump-up' : 'jump-left'"
|
||||
class="bg-transparent full-height"
|
||||
>
|
||||
<q-tab-panel
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@
|
|||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
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_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 visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'job_title', 'last_work_day']);
|
||||
|
||||
const table_grid_container = ref<HTMLElement | null>(null);
|
||||
|
||||
|
|
@ -50,8 +49,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
animateFlip(table_grid_container);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
|
@ -72,12 +69,12 @@
|
|||
:columns="employee_list_columns"
|
||||
row-key="email"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ sortBy: 'last_work_day', descending: true, }"
|
||||
:pagination="{ sortBy: 'first_name' }"
|
||||
:filter="filters"
|
||||
:filter-method="filterEmployeeRows"
|
||||
class="bg-transparent no-shadow sticky-header-table"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
|
||||
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10'"
|
||||
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10 hide-scrollbar' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10 hide-scrollbar'"
|
||||
color="accent"
|
||||
table-header-class="text-accent text-uppercase"
|
||||
card-container-class="justify-center"
|
||||
|
|
@ -114,9 +111,10 @@
|
|||
{ icon: 'view_list', value: false },
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="filters.search_bar_string"
|
||||
outlined
|
||||
standout
|
||||
dense
|
||||
rounded
|
||||
color="accent"
|
||||
|
|
@ -133,6 +131,7 @@
|
|||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<q-space />
|
||||
<q-checkbox
|
||||
|
|
@ -183,15 +182,15 @@
|
|||
>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp fast"
|
||||
leave-active-class="animated fadeOutDown fast"
|
||||
enter-active-class="animated fadeInUp slow"
|
||||
leave-active-class="animated fadeOutDown faster"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
||||
class="rounded-5 cursor-pointer"
|
||||
style="font-size: 1.2em;"
|
||||
:style="`animation-delay: ${scope.rowIndex / 30}s; ` + (scope.row.last_work_day === null ? '' : 'opacity: 0.5;')"
|
||||
:style="scope.row.last_work_day === null ? '' : 'opacity: 0.5;'"
|
||||
>
|
||||
<div v-if="scope.col.name === 'first_name'">
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
const { presetId } = defineProps<{
|
||||
presetId: number;
|
||||
}>();
|
||||
|
||||
const employee_amount_using_preset = ref(0);
|
||||
const delete_input_string = ref('');
|
||||
const is_approve_deletion = computed(() => ['SUPPRIMER', 'DELETE'].includes(delete_input_string.value));
|
||||
|
||||
onMounted(() => {
|
||||
const employees_with_preset = employee_store.employee_list.filter(employee => employee.preset_id === presetId);
|
||||
employee_amount_using_preset.value = employees_with_preset.length;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="column flex-center bg-secondary q-pa-md rounded-10 shadow-24"
|
||||
style="border: 2px solid var(--q-negative); width: 40vw !important;"
|
||||
>
|
||||
<span class="col-auto text-weight-bold text-uppercase text-center text-negative text-h5 q-pb-lg">
|
||||
{{ $t('shared.label.remove') }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="employee_amount_using_preset > 0"
|
||||
class="col row flex-center text-weight-medium text-center q-mb-lg"
|
||||
>
|
||||
<span class="col-auto">{{ $t('employee_management.schedule_presets.delete_warning_employee_1') }}</span>
|
||||
<span class="col-auto q-px-sm text-h6 text-weight-bolder text-negative">{{ employee_amount_using_preset
|
||||
}}</span>
|
||||
<span class="col-auto">{{ $t('employee_management.module_access.preset_employee') +
|
||||
(employee_amount_using_preset > 1 ? 's' : '') }}</span>
|
||||
<span>{{ $t('employee_management.schedule_presets.delete_warning_employee_2') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<span class="text-weight-bold text-uppercase">{{ $t('employee_management.schedule_presets.delete_warning')
|
||||
}}</span>
|
||||
<q-input
|
||||
v-model="delete_input_string"
|
||||
standout
|
||||
dense
|
||||
rounded
|
||||
:placeholder="$t('employee_management.enter_delete_input')"
|
||||
input-class="text-center"
|
||||
:input-style="delete_input_string.length > 0 ? '' : 'opacity: 0.6;'"
|
||||
class="q-my-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row">
|
||||
<q-space />
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
:disable="!is_approve_deletion"
|
||||
:color="is_approve_deletion ? 'negative' : 'grey-6'"
|
||||
:label="$t('shared.label.remove')"
|
||||
class="q-px-md"
|
||||
@click="employee_list_api.deleteSchedulePreset(presetId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -21,8 +21,13 @@
|
|||
const shift = defineModel<SchedulePresetShift>('shift', { required: true });
|
||||
const shift_type_selected = ref(SHIFT_OPTIONS[0]);
|
||||
|
||||
defineProps<{
|
||||
error: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'click-delete': [void];
|
||||
'clickDelete': [void];
|
||||
'blurTimeField': [void];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
@ -72,6 +77,8 @@
|
|||
hide-bottom-space
|
||||
type="time"
|
||||
class="text-uppercase weekday-field"
|
||||
:error="error"
|
||||
@blur="$emit('blurTimeField')"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
|
|
@ -92,6 +99,8 @@
|
|||
hide-bottom-space
|
||||
type="time"
|
||||
class="text-uppercase weekday-field"
|
||||
:error="error"
|
||||
@blur="$emit('blurTimeField')"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
|
|
@ -112,7 +121,7 @@
|
|||
icon="clear"
|
||||
size="sm"
|
||||
tabindex="-1"
|
||||
@click="$emit('click-delete')"
|
||||
@click="$emit('clickDelete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -128,6 +137,11 @@
|
|||
min-height: 25px;
|
||||
}
|
||||
|
||||
.weekday-field :deep(.q-field__marginal) {
|
||||
height: 25px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
:deep(.q-field--auto-height.q-field--dense .q-field__native) {
|
||||
min-height: 25px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
lang="ts"
|
||||
>
|
||||
import SchedulePresetsDialogRow from './schedule-presets-dialog-row.vue';
|
||||
import SchedulePresetsDialogDelete from 'src/modules/employee-list/components/schedule-presets-dialog-delete.vue';
|
||||
|
||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||
import { SchedulePresetShift } from '../models/schedule-presets.models';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
|
@ -17,8 +19,14 @@
|
|||
v-model="schedule_preset_store.is_manager_open"
|
||||
full-width
|
||||
>
|
||||
<SchedulePresetsDialogDelete
|
||||
v-if="schedule_preset_store.schedule_preset_dialog_mode === 'delete'"
|
||||
:preset-id="schedule_preset_store.current_schedule_preset.id"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="column flex-center bg-secondary rounded-10 shadow-24"
|
||||
v-else
|
||||
class="column flex-center bg-secondary rounded-10 shadow-24 no-wrap"
|
||||
style="border: 2px solid var(--q-accent); width: 50vw !important;"
|
||||
>
|
||||
<div
|
||||
|
|
@ -54,7 +62,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column col full-width q-py-sm q-px-lg">
|
||||
<div
|
||||
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
|
||||
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays"
|
||||
:key="weekday.day"
|
||||
|
|
@ -72,7 +83,9 @@
|
|||
>
|
||||
<SchedulePresetsDialogRow
|
||||
v-model:shift="weekday.shifts[index]!"
|
||||
:error="weekday.is_error"
|
||||
@click-delete="weekday.shifts.splice(index, 1)"
|
||||
@blur-time-field="weekday.is_error = isShiftOverlap(weekday.shifts)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,7 +103,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row self-end q-px-lg full-width">
|
||||
<div class="col-auto row self-end q-px-lg q-mt-sm full-width">
|
||||
<q-space />
|
||||
<q-btn
|
||||
:disable="schedule_preset_store.current_schedule_preset.name === ''"
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEmployeeStore } from "src/stores/employee-store";
|
||||
import { useSchedulePresetsStore } from "src/stores/schedule-presets.store";
|
||||
import { SchedulePreset } from "../models/schedule-presets.models";
|
||||
import { isShiftOverlap } from "src/modules/timesheets/utils/shift.util";
|
||||
|
||||
export const useEmployeeListApi = () => {
|
||||
const employee_store = useEmployeeStore();
|
||||
|
|
@ -15,7 +16,7 @@ export const useEmployeeListApi = () => {
|
|||
employee_store.is_loading = false;
|
||||
};
|
||||
|
||||
const getEmployeeDetails = async(email: string): Promise<void> => {
|
||||
const getEmployeeDetails = async (email: string): Promise<void> => {
|
||||
const success = await employee_store.getEmployeeDetails(email);
|
||||
if (success && employee_store.employee.preset_id !== null) {
|
||||
schedule_preset_store.setCurrentSchedulePreset(employee_store.employee.preset_id ?? -1);
|
||||
|
|
@ -27,18 +28,55 @@ export const useEmployeeListApi = () => {
|
|||
employee_store.employee.preset_id = preset_id < 0 ? null : preset_id;
|
||||
}
|
||||
|
||||
const saveSchedulePreset = async() => {
|
||||
const saveSchedulePreset = async () => {
|
||||
// Get the currently edited schedule preset from the store (frontend model)
|
||||
const preset = schedule_preset_store.current_schedule_preset;
|
||||
|
||||
// Check if there's any overlap between shifts. If there is, is_error property
|
||||
// will be toggled to true and save process will stop
|
||||
for (const weekday of preset.weekdays) {
|
||||
weekday.is_error = isShiftOverlap(weekday.shifts);
|
||||
}
|
||||
|
||||
console.log('current preset: ', preset);
|
||||
|
||||
if (preset.weekdays.some(weekday => weekday.is_error)) {
|
||||
console.log('overlap!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Flatten all weekday shifts into a single array
|
||||
const preset_shifts = preset.weekdays.flatMap(weekday => weekday.shifts);
|
||||
const backend_preset = new SchedulePreset(preset.id, preset.name, preset.is_default, preset_shifts);
|
||||
|
||||
// Build a backend-compatible SchedulePreset instance
|
||||
const backend_preset = new SchedulePreset(
|
||||
preset.id,
|
||||
preset.name,
|
||||
preset.is_default,
|
||||
preset_shifts
|
||||
);
|
||||
|
||||
// Track whether the create/update operation succeeds
|
||||
let success = false;
|
||||
|
||||
if (preset.id === -1) success = await schedule_preset_store.createSchedulePreset(backend_preset);
|
||||
else success = await schedule_preset_store.updateSchedulePreset(backend_preset);
|
||||
// Create a new preset if it has no backend ID, otherwise update the existing one
|
||||
if (preset.id === -1)
|
||||
success = await schedule_preset_store.createSchedulePreset(backend_preset);
|
||||
else
|
||||
success = await schedule_preset_store.updateSchedulePreset(backend_preset);
|
||||
|
||||
// On success, refresh the preset list and close the preset manager UI
|
||||
if (success) {
|
||||
schedule_preset_store.is_manager_open = false;
|
||||
await schedule_preset_store.findSchedulePresetList();
|
||||
schedule_preset_store.is_manager_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSchedulePreset = async (preset_id: number) => {
|
||||
const success = await schedule_preset_store.deleteSchedulePreset(preset_id);
|
||||
if (success) {
|
||||
await schedule_preset_store.findSchedulePresetList();
|
||||
schedule_preset_store.is_manager_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,5 +85,6 @@ export const useEmployeeListApi = () => {
|
|||
getEmployeeDetails,
|
||||
setSchedulePreset,
|
||||
saveSchedulePreset,
|
||||
deleteSchedulePreset,
|
||||
};
|
||||
};
|
||||
|
|
@ -34,7 +34,7 @@ export class EmployeeProfile {
|
|||
this.residence = '';
|
||||
this.birth_date = '';
|
||||
this.is_supervisor = false;
|
||||
this.external_payroll_id = -1;
|
||||
this.external_payroll_id = 999;
|
||||
this.user_module_access = ['dashboard',];
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +85,6 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
|
|||
label: 'employee_list.table.role',
|
||||
field: 'job_title',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'last_work_day',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import type { ShiftType } from "src/modules/timesheets/models/shift.models";
|
|||
|
||||
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT';
|
||||
|
||||
export const WEEKDAYS: Weekday[] = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
|
||||
export const WEEKDAYS: Weekday[] = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
||||
|
||||
export type PresetManagerMode = 'create' | 'update' | 'copy' | 'delete';
|
||||
|
||||
export class SchedulePreset {
|
||||
id: number;
|
||||
|
|
@ -48,6 +50,7 @@ export class SchedulePresetFrontend {
|
|||
this.is_default = schedule_preset?.is_default ?? false;
|
||||
this.weekdays = WEEKDAYS.map(day => ({
|
||||
day,
|
||||
is_error: false,
|
||||
shifts: schedule_preset !== undefined ? schedule_preset?.shifts.filter(shift => shift.week_day === day) : [],
|
||||
}))
|
||||
}
|
||||
|
|
@ -55,5 +58,6 @@ export class SchedulePresetFrontend {
|
|||
|
||||
export interface WeekdayPresetShifts {
|
||||
day: Weekday;
|
||||
is_error: boolean;
|
||||
shifts: SchedulePresetShift[];
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export const EmployeeListService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
createNewEmployee: async (profile: Omit<EmployeeProfile, 'last_work_day' | 'birth_date' | 'external_payroll_id'>): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
createNewEmployee: async (profile: Omit<EmployeeProfile, 'last_work_day' | 'birth_date'>): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
const response = await api.post('employees/create', profile);
|
||||
return response.data;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const SchedulePresetsService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
deleteSchedulePresets: async (preset_id: number) => {
|
||||
deleteSchedulePresets: async (preset_id: number): Promise<BackendResponse<boolean>> => {
|
||||
const response = await api.delete(`/schedule-presets/delete/${preset_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto row justify-center content-center q-mb-sm q-pa-sm rounded-5"
|
||||
class="col-auto justify-center content-center q-mb-sm q-pa-sm rounded-5"
|
||||
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
<q-item
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
class="col-auto"
|
||||
/>
|
||||
|
||||
<div class="row col full-width">
|
||||
<div class="col full-width" :class="$q.screen.lt.sm ? 'column' : 'row'">
|
||||
<div
|
||||
class="col-auto q-pa-xs bg-dark rounded-5 shadow-2"
|
||||
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<q-card
|
||||
class="col"
|
||||
:class="$q.screen.lt.md ? 'full-width' : 'q-ml-sm'"
|
||||
:class="$q.screen.lt.sm ? 'full-width' : 'q-ml-sm'"
|
||||
>
|
||||
<q-tab-panels
|
||||
v-model="current_menu"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
<template>
|
||||
<q-list
|
||||
dense
|
||||
class="row full-width"
|
||||
class="full-width"
|
||||
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
||||
>
|
||||
<q-item
|
||||
v-for="locale in $i18n.availableLocales"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
import { date, patterns, type ValidationRule } from "quasar";
|
||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const isShiftOverlap = (shifts: Shift[]): boolean => {
|
||||
export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean => {
|
||||
if (shifts.length < 2) return false;
|
||||
|
||||
const parsed_shifts = shifts.map(shift => ({
|
||||
start: date.extractDate(`${shift.date} ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
end: date.extractDate(`${shift.date} ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
start: date.extractDate(`2000-01-01 ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||
}));
|
||||
|
||||
console.log('parsed_shifts: ', parsed_shifts);
|
||||
|
||||
for (let i = 0; i < parsed_shifts.length; i++) {
|
||||
for (let j = i + 1; j < parsed_shifts.length; j++) {
|
||||
const parsed_shift_a = parsed_shifts[i];
|
||||
const parsed_shift_b = parsed_shifts[j];
|
||||
|
||||
if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue;
|
||||
console.log('times(a start, b start, a end, b end): ', parsed_shift_a.start, parsed_shift_b.start, parsed_shift_a.end, parsed_shift_b.end);
|
||||
console.log('result: ', Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end))
|
||||
|
||||
if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) {
|
||||
return true; // overlap found
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
import { ref } from 'vue';
|
||||
import { Notify } from 'quasar';
|
||||
|
||||
const click_number = ref(1);
|
||||
const icon = ref('las la-hand-peace');
|
||||
const color = ref('accent');
|
||||
|
||||
const LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et \
|
||||
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip \
|
||||
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu \
|
||||
|
|
@ -14,10 +18,22 @@
|
|||
const slide = ref<string>('welcome');
|
||||
|
||||
const clickNotify = () => {
|
||||
if (click_number.value % 7 === 0) {
|
||||
icon.value = 'las la-hand-middle-finger';
|
||||
color.value = 'negative';
|
||||
}
|
||||
else {
|
||||
icon.value = 'las la-hand-peace';
|
||||
color.value = 'accent';
|
||||
}
|
||||
Notify.create({
|
||||
message: 'You clicked the little click button!',
|
||||
color: 'info'
|
||||
})
|
||||
color: color.value,
|
||||
icon: icon.value,
|
||||
iconSize: '5em',
|
||||
iconColor: 'white',
|
||||
});
|
||||
|
||||
click_number.value += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { onMounted } from 'vue';
|
|||
<q-page class="bg-secondary row items-center justify-center">
|
||||
<MenuEmployee
|
||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||
class="col-sm-12 col-md-10 col-lg-7 col-xl-5"
|
||||
class="col-xs-12 col-md-10 col-lg-7 col-xl-5"
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -13,11 +13,11 @@ export const useEmployeeStore = defineStore('employee', () => {
|
|||
const is_loading = ref(false);
|
||||
|
||||
const openAddModifyDialog = async (employee_email?: string) => {
|
||||
is_add_modify_dialog_open.value = true;
|
||||
|
||||
if (employee_email === undefined) {
|
||||
management_mode.value = 'add_employee'
|
||||
employee.value = new EmployeeProfile();
|
||||
is_add_modify_dialog_open.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ export const useEmployeeStore = defineStore('employee', () => {
|
|||
management_mode.value = 'modify_employee';
|
||||
await getEmployeeDetails(employee_email);
|
||||
is_loading.value = false;
|
||||
is_add_modify_dialog_open.value = true;
|
||||
}
|
||||
|
||||
const closeAddModifyDialog = () => {
|
||||
|
|
@ -68,7 +69,7 @@ export const useEmployeeStore = defineStore('employee', () => {
|
|||
let response;
|
||||
|
||||
if (management_mode.value === 'add_employee') {
|
||||
const { birth_date, external_payroll_id, last_work_day, ...create_payload} = profile;
|
||||
const { birth_date, last_work_day, ...create_payload} = profile;
|
||||
response = await EmployeeListService.createNewEmployee(create_payload);
|
||||
} else {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,37 @@
|
|||
/* eslint-disable */
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
|
||||
import { SchedulePreset, SchedulePresetFrontend } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import { type PresetManagerMode, SchedulePreset, SchedulePresetFrontend } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
|
||||
|
||||
export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
|
||||
const schedule_presets = ref<SchedulePreset[]>([new SchedulePreset]);
|
||||
const current_schedule_preset = ref<SchedulePresetFrontend>(new SchedulePresetFrontend);
|
||||
const schedule_preset_dialog_mode = ref<PresetManagerMode>('create');
|
||||
const is_manager_open = ref(false);
|
||||
|
||||
const openSchedulePresetManager = (preset_id: number) => {
|
||||
if (preset_id === -1)
|
||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||
else if (schedule_preset_dialog_mode.value === 'copy') {
|
||||
const preset = schedule_presets.value.find(preset => preset.id === preset_id)!;
|
||||
const copied_preset = new SchedulePresetFrontend(preset);
|
||||
copied_preset.id = -1;
|
||||
copied_preset.name = "";
|
||||
current_schedule_preset.value = copied_preset;
|
||||
}
|
||||
else
|
||||
setCurrentSchedulePreset(preset_id);
|
||||
|
||||
is_manager_open.value = true;
|
||||
};
|
||||
|
||||
const setCurrentSchedulePreset = (preset_id: number) => {
|
||||
if (preset_id === -1) {
|
||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||
return;
|
||||
}
|
||||
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;
|
||||
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id))
|
||||
};
|
||||
|
||||
const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
|
||||
|
|
@ -51,11 +57,11 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
|
||||
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
|
||||
try {
|
||||
await SchedulePresetsService.deleteSchedulePresets(preset_id);
|
||||
return true;
|
||||
const response = await SchedulePresetsService.deleteSchedulePresets(preset_id);
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while deleting schedule preset: ', error);
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -72,18 +78,10 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
}
|
||||
};
|
||||
|
||||
const applySchedulePreset = async (): Promise<boolean> => {
|
||||
try {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while building schedule: ', error);
|
||||
return false
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
schedule_presets,
|
||||
current_schedule_preset,
|
||||
schedule_preset_dialog_mode,
|
||||
is_manager_open,
|
||||
setCurrentSchedulePreset,
|
||||
openSchedulePresetManager,
|
||||
|
|
@ -91,6 +89,5 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
updateSchedulePreset,
|
||||
deleteSchedulePreset,
|
||||
findSchedulePresetList,
|
||||
applySchedulePreset,
|
||||
}
|
||||
})
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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