diff --git a/src/i18n/en-ca/index.ts b/src/i18n/en-ca/index.ts index 5f04d12..8f0bc2d 100644 --- a/src/i18n/en-ca/index.ts +++ b/src/i18n/en-ca/index.ts @@ -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", }, }, diff --git a/src/i18n/fr-ca/index.ts b/src/i18n/fr-ca/index.ts index cc77711..7da5978 100644 --- a/src/i18n/fr-ca/index.ts +++ b/src/i18n/fr-ca/index.ts @@ -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", }, }, diff --git a/src/modules/employee-list/components/employee/add-modify-dialog-access.vue b/src/modules/employee-list/components/add-modify-dialog-access.vue similarity index 90% rename from src/modules/employee-list/components/employee/add-modify-dialog-access.vue rename to src/modules/employee-list/components/add-modify-dialog-access.vue index 1b7aa49..8b4ba2f 100644 --- a/src/modules/employee-list/components/employee/add-modify-dialog-access.vue +++ b/src/modules/employee-list/components/add-modify-dialog-access.vue @@ -41,6 +41,20 @@ + + + + {{ $t('employee_management.module_access.usage_description') }} + + {{ $t('employee_management.module_access.by_role') }} diff --git a/src/modules/employee-list/components/employee/add-modify-dialog-form.vue b/src/modules/employee-list/components/add-modify-dialog-form.vue similarity index 92% rename from src/modules/employee-list/components/employee/add-modify-dialog-form.vue rename to src/modules/employee-list/components/add-modify-dialog-form.vue index 34a0543..d1db2c4 100644 --- a/src/modules/employee-list/components/employee/add-modify-dialog-form.vue +++ b/src/modules/employee-list/components/add-modify-dialog-form.vue @@ -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]" > + 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 }; + }); + + + + + + + + schedule_preset_store.setCurrentSchedulePreset(option.value)" + > + + + {{ current_preset.label === undefined ? $t('employee_management.schedule_presets.preset_list_placeholder') : current_preset.label }} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/employee-list/components/add-modify-dialog.vue b/src/modules/employee-list/components/add-modify-dialog.vue new file mode 100644 index 0000000..41d69f6 --- /dev/null +++ b/src/modules/employee-list/components/add-modify-dialog.vue @@ -0,0 +1,118 @@ + + + + + + + + {{ $t('employee_management.' + employee_store.management_mode) }} + + + + {{ `${employee_store.employee.first_name} ${employee_store.employee.last_name}` }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/employee-list/components/customer/customer-profile.vue b/src/modules/employee-list/components/customer/customer-profile.vue deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/employee-list/components/employee-list-table-item.vue b/src/modules/employee-list/components/employee-list-table-item.vue index 652eb07..7921f03 100644 --- a/src/modules/employee-list/components/employee-list-table-item.vue +++ b/src/modules/employee-list/components/employee-list-table-item.vue @@ -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 @@ - - - + - + - + > {{ row.first_name }} {{ row.last_name }} - - - {{ row.job_title }} - - - {{ row.email }} - - - + + + {{ row.job_title }} + + + + {{ row.email }} + + + \ No newline at end of file diff --git a/src/modules/employee-list/components/employee-list-table.vue b/src/modules/employee-list/components/employee-list-table.vue index 5606742..6dba076 100644 --- a/src/modules/employee-list/components/employee-list-table.vue +++ b/src/modules/employee-list/components/employee-list-table.vue @@ -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(null); + const filters = ref({ 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; }) @@ -146,7 +150,10 @@ :key="col.name" :props="props" > - + {{ $t(col.label) }} @@ -154,11 +161,19 @@ - + + + @@ -168,8 +183,8 @@ > \ No newline at end of file diff --git a/src/modules/employee-list/components/employee/add-modify-dialog.vue b/src/modules/employee-list/components/employee/add-modify-dialog.vue deleted file mode 100644 index e27f78f..0000000 --- a/src/modules/employee-list/components/employee/add-modify-dialog.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - {{ $t('employee_management.' + employee_store.management_mode) }} - - - - {{ `${employee_store.employee.first_name} ${employee_store.employee.last_name}` }} - - - - - - - {{ $t('employee_management.module_access.usage_description') }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/modules/employee-list/components/schedule_presets_dialog.vue b/src/modules/employee-list/components/schedule_presets_dialog.vue new file mode 100644 index 0000000..8eab988 --- /dev/null +++ b/src/modules/employee-list/components/schedule_presets_dialog.vue @@ -0,0 +1,99 @@ + + + + + + + {{ + schedule_preset_store.current_schedule_preset.id === -1 ? + $t('shared.label.add') : + $t('shared.label.modify') }} + + + + + + + + + + + + {{ + $t(`shared.weekday.${weekday.day.toLowerCase()}`) }} + + + + + + + + {{ $t('shared.misc.out') }} + + + + + + + + + + + + + + + + + + diff --git a/src/modules/employee-list/composables/use-employee-api.ts b/src/modules/employee-list/composables/use-employee-api.ts index 12ec52f..a2188cb 100644 --- a/src/modules/employee-list/composables/use-employee-api.ts +++ b/src/modules/employee-list/composables/use-employee-api.ts @@ -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 => { - return employeeListStore.getEmployeeList(); + const getEmployeeList = async (): Promise => { + 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 => { - return employeeListStore.getEmployeeDetails(email); + return employee_list_store.getEmployeeDetails(email); } return { diff --git a/src/modules/employee-list/models/employee-profile.models.ts b/src/modules/employee-list/models/employee-profile.models.ts index 24ccb9f..d801563 100644 --- a/src/modules/employee-list/models/employee-profile.models.ts +++ b/src/modules/employee-list/models/employee-profile.models.ts @@ -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[] = [ export const employee_access_options: QSelectOption[] = [ { 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' }, ] diff --git a/src/modules/employee-list/models/schedule-presets.models.ts b/src/modules/employee-list/models/schedule-presets.models.ts new file mode 100644 index 0000000..0cb1e6f --- /dev/null +++ b/src/modules/employee-list/models/schedule-presets.models.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/modules/employee-list/services/employee-list-service.ts b/src/modules/employee-list/services/employee-list-service.ts index 5291dea..19f1005 100644 --- a/src/modules/employee-list/services/employee-list-service.ts +++ b/src/modules/employee-list/services/employee-list-service.ts @@ -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 => { + getEmployeeList: async (): Promise> => { const response = await api.get>('/employees/employee-list') - if (response.data.data) return response.data.data; - return []; + return response.data; + }, getEmployeeDetails: async (): Promise => { diff --git a/src/modules/profile/services/schedule-presets-service.ts b/src/modules/employee-list/services/schedule-presets-service.ts similarity index 78% rename from src/modules/profile/services/schedule-presets-service.ts rename to src/modules/employee-list/services/schedule-presets-service.ts index 86c6a47..684f6da 100644 --- a/src/modules/profile/services/schedule-presets-service.ts +++ b/src/modules/employee-list/services/schedule-presets-service.ts @@ -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> => { const response = await api.get(`/schedule-presets/find-list`); return response.data; }, diff --git a/src/modules/profile/models/schedule-presets.models.ts b/src/modules/profile/models/schedule-presets.models.ts deleted file mode 100644 index 03ed36d..0000000 --- a/src/modules/profile/models/schedule-presets.models.ts +++ /dev/null @@ -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' | ''; \ No newline at end of file diff --git a/src/modules/shared/components/horizontal-slide-transition.vue b/src/modules/shared/components/horizontal-slide-transition.vue new file mode 100644 index 0000000..425020a --- /dev/null +++ b/src/modules/shared/components/horizontal-slide-transition.vue @@ -0,0 +1,78 @@ + + + + + + + + + + + + + diff --git a/src/modules/timesheets/components/timesheet-error-widget.vue b/src/modules/timesheets/components/timesheet-error-widget.vue index 1a914b9..c912bbd 100644 --- a/src/modules/timesheets/components/timesheet-error-widget.vue +++ b/src/modules/timesheets/components/timesheet-error-widget.vue @@ -28,7 +28,7 @@ style="border: 2px solid var(--q-negative)" > - {{ $t('timesheet.shift.errors.' + error) }} + {{ $t('timesheet.errors.' + error) }} diff --git a/src/pages/employee-list-page.vue b/src/pages/employee-list-page.vue index 60a6d75..972c358 100644 --- a/src/pages/employee-list-page.vue +++ b/src/pages/employee-list-page.vue @@ -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(); + }) - + - + \ No newline at end of file diff --git a/src/stores/employee-store.ts b/src/stores/employee-store.ts index fc77ef0..b175dc6 100644 --- a/src/stores/employee-store.ts +++ b/src/stores/employee-store.ts @@ -33,16 +33,16 @@ export const useEmployeeStore = defineStore('employee', () => { employee.value = new EmployeeProfile; }; - const getEmployeeList = async () => { - is_loading.value = true; + const getEmployeeList = async (): Promise => { 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) => { diff --git a/src/stores/schedule-presets.store.ts b/src/stores/schedule-presets.store.ts index ede62f9..bee40f4 100644 --- a/src/stores/schedule-presets.store.ts +++ b/src/stores/schedule-presets.store.ts @@ -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(); + const schedule_presets = ref([new SchedulePreset]); + const current_schedule_preset = ref(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 => { 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 => { try { @@ -27,7 +42,7 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', () return false; } - } + }; const deleteSchedulePreset = async (preset_id: number): Promise => { 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 => { 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 => { 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, diff --git a/src/utils/table-grid-FLIP.ts b/src/utils/table-grid-FLIP.ts new file mode 100644 index 0000000..a2a7f7f --- /dev/null +++ b/src/utils/table-grid-FLIP.ts @@ -0,0 +1,31 @@ +import { type Ref, nextTick } from 'vue'; + +export const animateFlip = (container: Ref) => { + 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 = ''; + }); + }); + }); +}