refactor(presets): set up work for schedule presets, minor cleanup of other modules

This commit is contained in:
Nicolas Drolet 2025-12-04 11:17:56 -05:00
parent 8852f5990b
commit 2affa8470b
13 changed files with 113 additions and 244 deletions

View File

@ -13,6 +13,10 @@ export default {
}, },
employee_management: { employee_management: {
add_employee: "Add employee",
modify_employee: "Modify employee",
access_label: "access",
details_label: "details",
module_access: { module_access: {
dashboard: "Dashboard", dashboard: "Dashboard",
employee_list: "employee list", employee_list: "employee list",
@ -31,10 +35,10 @@ export default {
none_description: "Uncheck all modules", none_description: "Uncheck all modules",
usage_description: "You can use roles to enable preset modules, add or remove modules individually, or both", usage_description: "You can use roles to enable preset modules, add or remove modules individually, or both",
}, },
add_employee: "Add employee", filter: {
modify_employee: "Modify employee", show_terminated: "Show inactive employees",
access_label: "access", sort_by_tags: "sort by tags",
details_label: "details", },
}, },
login: { login: {

View File

@ -13,6 +13,10 @@ export default {
}, },
employee_management: { employee_management: {
add_employee: "Ajouter employé",
modify_employee: "Modifier employé",
access_label: "accès",
details_label: "détails",
module_access: { module_access: {
dashboard: "Accueil", dashboard: "Accueil",
employee_list: "Répertoire du personnel", employee_list: "Répertoire du personnel",
@ -31,10 +35,10 @@ export default {
none_description: "Enlever tous les accès", none_description: "Enlever tous les accès",
usage_description: "Vous pouvez utiliser les rôles pour sélectionner des modules prédéfinis, enlever ou ajouter des modules individuellement, ou les deux", usage_description: "Vous pouvez utiliser les rôles pour sélectionner des modules prédéfinis, enlever ou ajouter des modules individuellement, ou les deux",
}, },
add_employee: "Ajouter employé", filter: {
modify_employee: "Modifier employé", show_terminated: "Afficher les employés inactifs",
access_label: "accès", sort_by_tags: "filtrer par identifiants",
details_label: "détails", },
}, },
login: { login: {

View File

@ -7,11 +7,13 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useEmployeeStore } from 'src/stores/employee-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 { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
import { employee_list_columns } from 'src/modules/employee-list/models/employee-profile.models'; import { employee_list_columns } from 'src/modules/employee-list/models/employee-profile.models';
const employee_list_api = useEmployeeListApi(); const employee_list_api = useEmployeeListApi();
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const timesheet_store = useTimesheetStore();
const ui_store = useUiStore(); const ui_store = useUiStore();
const is_loading_list = ref<boolean>(true); const is_loading_list = ref<boolean>(true);
@ -48,34 +50,10 @@
:no-data-label="$t('shared.error.no_data_found')" :no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')" :loading-label="$t('shared.label.loading')"
:visible-columns="['first_name', 'email', 'company', 'supervisor_full_name', 'company_name', 'job_title']"
@row-click="() => console.log('click!')" @row-click="() => console.log('click!')"
> >
<template #header="props"> <template #top>
<q-tr
:props="props"
class="bg-accent"
>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="text-uppercase text-weight-bolder text-white">
{{ $t(col.label) }}
</span>
</q-th>
</q-tr>
</template>
<template v-slot:item="props">
<EmployeeListTableItem
:row="props.row"
:index="props.rowIndex"
@on-profile-click="employee_store.openAddModifyDialog"
/>
</template>
<template v-slot:top>
<div class="row full-width q-mb-sm"> <div class="row full-width q-mb-sm">
<q-btn <q-btn
push push
@ -121,17 +99,57 @@
</div> </div>
</template> </template>
<template #body-cell="scope"> <template #header="props">
<q-td <q-tr
:props="scope" :props="props"
class="text-weight-medium" class="bg-primary"
> >
<span >{{ scope.value }}</span> <q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="text-uppercase text-weight-bolder text-white text-h6">
{{ $t(col.label) }}
</span>
</q-th>
</q-tr>
</template>
<template #item="props">
<EmployeeListTableItem
:row="props.row"
:index="props.rowIndex"
@on-profile-click="employee_store.openAddModifyDialog"
/>
</template>
<template #body-cell="scope">
<q-td :props="scope">
<transition
appear
enter-active-class="animated fadeInUp slow"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<div
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
style="font-size: 1.2em;"
:style="`animation-delay: ${scope.rowIndex / 30}s;`"
>
<div v-if="scope.col.name === 'first_name'">
<span class="text-h5 text-uppercase text-accent q-mr-xs">{{ scope.value }}</span>
<span class="text-uppercase text-weight-light">{{ scope.row.last_name }}</span>
</div>
<span v-else>{{ scope.value }}</span>
</div>
</transition>
</q-td> </q-td>
</template> </template>
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->
<template v-slot:no-data="{ message, filter }"> <template #no-data="{ message, filter }">
<div class="full-width column items-center text-accent q-gutter-sm"> <div class="full-width column items-center text-accent q-gutter-sm">
<span class="text-h6 q-mt-xl"> <span class="text-h6 q-mt-xl">
{{ message }} {{ message }}
@ -146,21 +164,26 @@
</div> </div>
</template> </template>
<style lang="sass"> <style scoped>
.sticky-header-table .sticky-header-table thead tr:first-child th {
thead tr:first-child th background-color: var(--q-primary);
background-color: var(--q-accent) margin-top: none;
margin-top: none }
thead tr th thead tr th {
position: sticky position: sticky;
z-index: 1 z-index: 1;
thead tr:first-child th }
top: 0px
&.q-table--loading thead tr:last-child th thead tr:first-child th {
top: 48px top: 0px;
}
tbody &.q-table--loading thead tr:last-child th {
scroll-margin-top: 48px top: 48px;
}
tbody {
scroll-margin-top: 48px;
}
</style> </style>

View File

@ -5,13 +5,15 @@
import AddModifyDialogForm from 'src/modules/employee-list/components/employee/add-modify-dialog-form.vue'; import AddModifyDialogForm from 'src/modules/employee-list/components/employee/add-modify-dialog-form.vue';
import AddModifyDialogAccess from 'src/modules/employee-list/components/employee/add-modify-dialog-access.vue'; import AddModifyDialogAccess from 'src/modules/employee-list/components/employee/add-modify-dialog-access.vue';
import { useEmployeeStore } from 'src/stores/employee-store';
import { ref } from 'vue'; import { ref } from 'vue';
import { useEmployeeStore } from 'src/stores/employee-store';
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const employee_store = useEmployeeStore(); const employee_store = useEmployeeStore();
const current_step = ref<'form' | 'access'>('form'); const current_step = ref<'form' | 'access'>('form');
const transition_in_animation = ref('fadeInRight'); const transition_in_animation = ref('fadeInRight');
const transition_out_animation = ref('fadeOutLeft'); const transition_out_animation = ref('fadeOutLeft');
const initial_employee_profile = ref(new EmployeeProfile)
const getNextMenu = (animation_in: string, animation_out: string, next_step: 'form' | 'access') => { const getNextMenu = (animation_in: string, animation_out: string, next_step: 'form' | 'access') => {
transition_in_animation.value = animation_in; transition_in_animation.value = animation_in;
@ -25,6 +27,7 @@
v-model="employee_store.is_add_modify_dialog_open" v-model="employee_store.is_add_modify_dialog_open"
full-width full-width
@beforeShow="current_step = 'form'" @beforeShow="current_step = 'form'"
@show="Object.assign(initial_employee_profile, employee_store.employee)"
> >
<div <div
class="column bg-secondary rounded-10" class="column bg-secondary rounded-10"
@ -119,13 +122,13 @@
/> />
</div> --> </div> -->
<q-btn <q-btn
square square
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="employee_store.createOrUpdateEmployee(employee_store.employee)"
/> />
<q-inner-loading :showing="employee_store.is_loading" /> <q-inner-loading :showing="employee_store.is_loading" />
</div> </div>
</q-dialog> </q-dialog>

View File

@ -39,7 +39,7 @@ export class EmployeeProfile {
export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
{ {
name: 'first_name', name: 'first_name',
label: 'employee_list.table.first_name', label: 'timesheet_approvals.table.full_name',
field: 'first_name', field: 'first_name',
align: 'left' align: 'left'
}, },

View File

@ -39,4 +39,9 @@
:deep(.q-field__control-container) { :deep(.q-field__control-container) {
padding-left: 16px; padding-left: 16px;
} }
:deep(.q-field__control::before) {
border: 1px solid var(--q-accent) !important;
background-color: transparent;
}
</style> </style>

View File

@ -1,43 +0,0 @@
export interface shiftColor {
type_label: string;
background_color: string;
font_color: string;
}
export const shift_type_legend: shiftColor[] = [
{
type_label: 'shared.shift_type.regular',
background_color: 'blue-grey-4',
font_color: 'blue-grey-8',
},
{
type_label: 'shared.shift_type.evening',
background_color: 'warning',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.emergency',
background_color: 'amber-10',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.overtime',
background_color: 'negative',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.vacation',
background_color: 'purple-10',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.holiday',
background_color: 'purple-8',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.sick',
background_color: 'grey-8',
font_color: 'blue-grey-2',
},
]

View File

@ -1,71 +0,0 @@
<script
setup
lang="ts"
>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { ShiftLegendItem } from 'src/modules/timesheets/models/shift.models';
const { t } = useI18n();
const is_showing_legend = ref(false);
const legend: ShiftLegendItem[] = [
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
]
const shift_type_legend = computed(() =>
legend.map(item => ({ ...item, label: t(item.label_type) }))
);
</script>
<template>
<div
class="items-center"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-btn
flat
dense
rounded
color="primary"
class="col-auto q-my-sm"
@click="is_showing_legend = !is_showing_legend"
>
<template #default>
<q-icon
:name="is_showing_legend ? 'close' : 'info_outline'"
size="md"
class="col-auto"
/>
</template>
</q-btn>
<transition
appear
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
class="col-auto"
>
<div
v-if="is_showing_legend"
class="q-py-xs bg-white rounded-5 shadow-2 text-center q-my-xs"
>
<q-badge
v-for="shift_type in shift_type_legend"
:key="shift_type.type"
:color="shift_type.color"
:label="shift_type.label"
:text-color="shift_type.text_color || 'white'"
class="q-pa-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
style="font-size: 0.8em;"
/>
</div>
</transition>
</div>
</template>

View File

@ -27,26 +27,8 @@
class="row items-center full-width bg-dark shadow-2 rounded-5 q-my-xs" class="row items-center full-width bg-dark shadow-2 rounded-5 q-my-xs"
style="border: 2px solid var(--q-negative)" style="border: 2px solid var(--q-negative)"
> >
<q-item-section class="col-auto">
<q-badge
outline
color="negative"
class="bg-dark text-weight-bolder"
>{{ error.conflicts.date }}</q-badge>
</q-item-section>
<q-item-section class="col-auto">
<q-badge
outline
color="negative"
class="bg-dark text-weight-bolder"
>
{{ error.conflicts.start_time }} - {{ error.conflicts.end_time }}
</q-badge>
</q-item-section>
<q-item-label class="text-weight-medium text-caption q-ml-md"> <q-item-label class="text-weight-medium text-caption q-ml-md">
{{ $t('timesheet.shift.errors.' + error.error_code) }} {{ $t('timesheet.shift.errors.' + error) }}
</q-item-label> </q-item-label>
</q-item> </q-item>
</q-list> </q-list>

View File

@ -11,13 +11,6 @@ export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACAT
export type ShiftErrorCode = 'SHIFT_OVERLAP' | 'MISSING_START_TIME' | 'MISSING_END_TIME' | 'COMMENT_LENGTH_EXCEEDED' | 'APPROVAL_LOCK' | 'INVALID_DATE' | 'INVALID TYPE' | 'INVALID_TIMESHEET'; export type ShiftErrorCode = 'SHIFT_OVERLAP' | 'MISSING_START_TIME' | 'MISSING_END_TIME' | 'COMMENT_LENGTH_EXCEEDED' | 'APPROVAL_LOCK' | 'INVALID_DATE' | 'INVALID TYPE' | 'INVALID_TIMESHEET';
export type ShiftLegendItem = {
type: ShiftType;
color: string;
label_type: string;
text_color?: string;
};
export class Shift { export class Shift {
id: number; id: number;
timesheet_id: number; timesheet_id: number;
@ -49,23 +42,4 @@ export interface ShiftOption {
value: ShiftType; value: ShiftType;
icon: string; icon: string;
icon_color: string; icon_color: string;
}
export interface ShiftAPIResponse {
ok: boolean;
data?: {
shift: Shift;
overtime: unknown;
}
error?: ShiftAPIError;
}
export interface ShiftAPIError {
error_code: ShiftErrorCode;
conflicts:
{
date: string;
start_time: string;
end_time: string;
}
} }

View File

@ -1,5 +1,6 @@
import { api } from "src/boot/axios"; import { api } from "src/boot/axios";
import type { Shift, ShiftAPIResponse } from "src/modules/timesheets/models/shift.models"; import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
import type { Shift } from "src/modules/timesheets/models/shift.models";
export const ShiftService = { export const ShiftService = {
deleteShiftById: async (shift_id: number) => { deleteShiftById: async (shift_id: number) => {
@ -7,14 +8,14 @@ export const ShiftService = {
return response.data; return response.data;
}, },
createNewShifts: async (new_shifts: Shift[]):Promise<ShiftAPIResponse[]> => { createNewShifts: async (new_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
const response = await api.post(`/shift/create`, new_shifts); const response = await api.post(`/shift/create`, new_shifts);
return response.data; return response.data;
}, },
updateShifts: async (existing_shifts: Shift[]) => { updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
console.log('sent shifts: ', existing_shifts) console.log('sent shifts: ', existing_shifts)
const response = await api.patch(`/shift/update`, existing_shifts); const response = await api.patch(`/shift/update`, existing_shifts);
return response; return response.data;
} }
}; };

View File

@ -27,11 +27,4 @@ import { onMounted } from 'vue';
class="col-sm-12 col-md-10 col-lg-7 col-xl-5" class="col-sm-12 col-md-10 col-lg-7 col-xl-5"
/> />
</q-page> </q-page>
</template> </template>
<style scoped>
:deep(.q-field--outlined.q-field--readonly .q-field__control:before) {
border: 1px solid var(--q-accent);
background-color: transparent;
}
</style>

View File

@ -4,11 +4,10 @@ import { Notify } from "quasar";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ShiftService } from "src/modules/timesheets/services/shift-service"; import { ShiftService } from "src/modules/timesheets/services/shift-service";
import { useTimesheetStore } from "src/stores/timesheet-store"; import { useTimesheetStore } from "src/stores/timesheet-store";
import type { ShiftAPIError } from "src/modules/timesheets/models/shift.models";
export const useShiftStore = defineStore('shift_store', () => { export const useShiftStore = defineStore('shift_store', () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const shift_errors = ref<ShiftAPIError[]>([]); const shift_errors = ref<string[]>([]);
const deleteShiftById = async (shift_id: number): Promise<boolean> => { const deleteShiftById = async (shift_id: number): Promise<boolean> => {
try { try {
@ -22,7 +21,6 @@ export const useShiftStore = defineStore('shift_store', () => {
const createNewShifts = async (): Promise<boolean> => { const createNewShifts = async (): Promise<boolean> => {
if (timesheet_store.timesheets === undefined) return false; if (timesheet_store.timesheets === undefined) return false;
const has_errors = false;
try { try {
const days = timesheet_store.timesheets.flatMap(week => week.days); const days = timesheet_store.timesheets.flatMap(week => week.days);
@ -30,14 +28,10 @@ export const useShiftStore = defineStore('shift_store', () => {
if (new_shifts?.length > 0) { if (new_shifts?.length > 0) {
const response = await ShiftService.createNewShifts(new_shifts); const response = await ShiftService.createNewShifts(new_shifts);
if (response.every(res => res.ok)) { if (response.success) {
return true; return true;
} }
else { else { shift_errors.value.push(response.error!) }
response.forEach(res => {
shift_errors.value.push(res.error!);
});
}
} }
return false; return false;
} catch (error) { } catch (error) {
@ -55,7 +49,7 @@ export const useShiftStore = defineStore('shift_store', () => {
if (existing_shifts?.length > 0) { if (existing_shifts?.length > 0) {
const response = await ShiftService.updateShifts(existing_shifts); const response = await ShiftService.updateShifts(existing_shifts);
if (response.status < 400) { if (response.success) {
return true; return true;
} }
} }