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

View File

@ -13,6 +13,10 @@ export default {
},
employee_management: {
add_employee: "Ajouter employé",
modify_employee: "Modifier employé",
access_label: "accès",
details_label: "détails",
module_access: {
dashboard: "Accueil",
employee_list: "Répertoire du personnel",
@ -31,10 +35,10 @@ export default {
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",
},
add_employee: "Ajouter employé",
modify_employee: "Modifier employé",
access_label: "accès",
details_label: "détails",
filter: {
show_terminated: "Afficher les employés inactifs",
sort_by_tags: "filtrer par identifiants",
},
},
login: {

View File

@ -7,11 +7,13 @@
import { onMounted, ref } from 'vue';
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 } from 'src/modules/employee-list/models/employee-profile.models';
const employee_list_api = useEmployeeListApi();
const employee_store = useEmployeeStore();
const timesheet_store = useTimesheetStore();
const ui_store = useUiStore();
const is_loading_list = ref<boolean>(true);
@ -48,34 +50,10 @@
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
:visible-columns="['first_name', 'email', 'company', 'supervisor_full_name', 'company_name', 'job_title']"
@row-click="() => console.log('click!')"
>
<template #header="props">
<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>
<template #top>
<div class="row full-width q-mb-sm">
<q-btn
push
@ -121,17 +99,57 @@
</div>
</template>
<template #body-cell="scope">
<q-td
:props="scope"
class="text-weight-medium"
<template #header="props">
<q-tr
:props="props"
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>
</template>
<!-- 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">
<span class="text-h6 q-mt-xl">
{{ message }}
@ -146,21 +164,26 @@
</div>
</template>
<style lang="sass">
.sticky-header-table
thead tr:first-child th
background-color: var(--q-accent)
margin-top: none
<style scoped>
.sticky-header-table thead tr:first-child th {
background-color: var(--q-primary);
margin-top: none;
}
thead tr th
position: sticky
z-index: 1
thead tr:first-child th
top: 0px
thead tr th {
position: sticky;
z-index: 1;
}
&.q-table--loading thead tr:last-child th
top: 48px
thead tr:first-child th {
top: 0px;
}
tbody
scroll-margin-top: 48px
&.q-table--loading thead tr:last-child th {
top: 48px;
}
tbody {
scroll-margin-top: 48px;
}
</style>

View File

@ -5,13 +5,15 @@
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 { useEmployeeStore } from 'src/stores/employee-store';
import { ref } from 'vue';
import { useEmployeeStore } from 'src/stores/employee-store';
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const employee_store = useEmployeeStore();
const current_step = ref<'form' | 'access'>('form');
const transition_in_animation = ref('fadeInRight');
const transition_out_animation = ref('fadeOutLeft');
const initial_employee_profile = ref(new EmployeeProfile)
const getNextMenu = (animation_in: string, animation_out: string, next_step: 'form' | 'access') => {
transition_in_animation.value = animation_in;
@ -25,6 +27,7 @@
v-model="employee_store.is_add_modify_dialog_open"
full-width
@beforeShow="current_step = 'form'"
@show="Object.assign(initial_employee_profile, employee_store.employee)"
>
<div
class="column bg-secondary rounded-10"
@ -119,13 +122,13 @@
/>
</div> -->
<q-btn
square
color="accent"
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
class="col-auto q-py-sm shadow-up-5"
@click="employee_store.createOrUpdateEmployee(employee_store.employee)"
/>
<q-btn
square
color="accent"
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
class="col-auto q-py-sm shadow-up-5"
@click="employee_store.createOrUpdateEmployee(employee_store.employee)"
/>
<q-inner-loading :showing="employee_store.is_loading" />
</div>
</q-dialog>

View File

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

View File

@ -39,4 +39,9 @@
:deep(.q-field__control-container) {
padding-left: 16px;
}
:deep(.q-field__control::before) {
border: 1px solid var(--q-accent) !important;
background-color: transparent;
}
</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"
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">
{{ $t('timesheet.shift.errors.' + error.error_code) }}
{{ $t('timesheet.shift.errors.' + error) }}
</q-item-label>
</q-item>
</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 ShiftLegendItem = {
type: ShiftType;
color: string;
label_type: string;
text_color?: string;
};
export class Shift {
id: number;
timesheet_id: number;
@ -49,23 +42,4 @@ export interface ShiftOption {
value: ShiftType;
icon: 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 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 = {
deleteShiftById: async (shift_id: number) => {
@ -7,14 +8,14 @@ export const ShiftService = {
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);
return response.data;
},
updateShifts: async (existing_shifts: Shift[]) => {
updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
console.log('sent shifts: ', 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"
/>
</q-page>
</template>
<style scoped>
:deep(.q-field--outlined.q-field--readonly .q-field__control:before) {
border: 1px solid var(--q-accent);
background-color: transparent;
}
</style>
</template>

View File

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