Merge branch 'main' of git.targo.ca:Targo/targo_frontend into dev/matthieu/help-page
This commit is contained in:
commit
f059682dd9
|
|
@ -52,4 +52,16 @@ body.body--dark {
|
||||||
|
|
||||||
.q-btn--push:active::before {
|
.q-btn--push:active::before {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
tab_title: "preferences",
|
tab_title: "preferences",
|
||||||
display_options: "Color mode",
|
display_options: "Color mode",
|
||||||
language_options: "language options",
|
language_options: "language options",
|
||||||
'fr-FR': "French",
|
'fr-FR': "Français",
|
||||||
'en-CA': "English",
|
'en-CA': "English",
|
||||||
dark_mode: "dark",
|
dark_mode: "dark",
|
||||||
light_mode: "light",
|
light_mode: "light",
|
||||||
|
|
@ -194,6 +194,7 @@ export default {
|
||||||
update: "update",
|
update: "update",
|
||||||
modify: "modify",
|
modify: "modify",
|
||||||
close: "close",
|
close: "close",
|
||||||
|
download: "download",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "or",
|
or: "or",
|
||||||
|
|
@ -228,16 +229,14 @@ export default {
|
||||||
|
|
||||||
timesheet: {
|
timesheet: {
|
||||||
page_header: "Timesheet",
|
page_header: "Timesheet",
|
||||||
|
apply_preset_day: "Apply schedule to day",
|
||||||
|
apply_preset_week: "Apply schedule to week",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker: "Calendar",
|
calendar_date_picker: "Calendar",
|
||||||
current_week: "This week",
|
current_week: "This week",
|
||||||
next_week: "Next period",
|
next_week: "Next period",
|
||||||
previous_week: "Previous period",
|
previous_week: "Previous period",
|
||||||
},
|
},
|
||||||
save_button: "Save",
|
|
||||||
cancel_button: "Cancel",
|
|
||||||
remote_button: "Remote work",
|
|
||||||
delete_button: "Delete",
|
|
||||||
shift: {
|
shift: {
|
||||||
actions: {
|
actions: {
|
||||||
add: "Add Shift",
|
add: "Add Shift",
|
||||||
|
|
@ -254,7 +253,8 @@ export default {
|
||||||
REGULAR: "Regular",
|
REGULAR: "Regular",
|
||||||
SICK: "Sick Leave",
|
SICK: "Sick Leave",
|
||||||
VACATION: "Vacation",
|
VACATION: "Vacation",
|
||||||
REMOTE: "Remote work",
|
REMOTE: "Remote",
|
||||||
|
OFFICE: "Office",
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start: "Start (HH:mm)",
|
start: "Start (HH:mm)",
|
||||||
|
|
@ -291,8 +291,9 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
||||||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||||
|
SHIFT_OVERLAP_SHORT: "Overlap",
|
||||||
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
||||||
SHIFT_TIME_REQUIRED: "Valid time required",
|
SHIFT_TIME_REQUIRED: "Time required",
|
||||||
SHIFT_TYPE_REQUIRED: "Shift type required",
|
SHIFT_TYPE_REQUIRED: "Shift type required",
|
||||||
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
||||||
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||||
|
|
@ -320,10 +321,12 @@ export default {
|
||||||
expenses_title: "expenses accrued",
|
expenses_title: "expenses accrued",
|
||||||
},
|
},
|
||||||
print_report: {
|
print_report: {
|
||||||
company: "company",
|
title: "Download options",
|
||||||
|
company: "companies",
|
||||||
type: "type",
|
type: "type",
|
||||||
shifts: "shifts",
|
shifts: "shifts",
|
||||||
expenses: "expenses",
|
expenses: "expenses",
|
||||||
|
options: "options",
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
button_detailed_view: "detailed view",
|
button_detailed_view: "detailed view",
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export default {
|
||||||
display_options: "Mode d'affichage",
|
display_options: "Mode d'affichage",
|
||||||
language_options: "Options de langue",
|
language_options: "Options de langue",
|
||||||
'fr-FR': "Français",
|
'fr-FR': "Français",
|
||||||
'en-CA': "Anglais",
|
'en-CA': "English",
|
||||||
dark_mode: "sombre",
|
dark_mode: "sombre",
|
||||||
light_mode: "clair",
|
light_mode: "clair",
|
||||||
auto_mode: "automatique",
|
auto_mode: "automatique",
|
||||||
|
|
@ -195,6 +195,7 @@ export default {
|
||||||
update: "mettre à jour",
|
update: "mettre à jour",
|
||||||
modify: "modifier",
|
modify: "modifier",
|
||||||
close: "fermer",
|
close: "fermer",
|
||||||
|
download: "téléchargement",
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
or: "ou",
|
or: "ou",
|
||||||
|
|
@ -229,16 +230,14 @@ export default {
|
||||||
|
|
||||||
timesheet: {
|
timesheet: {
|
||||||
page_header: "Carte de temps",
|
page_header: "Carte de temps",
|
||||||
|
apply_preset_day: "Appliquer horaire pour la journée",
|
||||||
|
apply_preset_week: "Appliquer horaire pour la semaine",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker: "Calendrier",
|
calendar_date_picker: "Calendrier",
|
||||||
current_week: "Semaine actuelle",
|
current_week: "Semaine actuelle",
|
||||||
next_week: "Prochaine période",
|
next_week: "Prochaine période",
|
||||||
previous_week: "Période précédente",
|
previous_week: "Période précédente",
|
||||||
},
|
},
|
||||||
save_button: "Enregistrer",
|
|
||||||
cancel_button: "Annuler",
|
|
||||||
remote_button: "Télétravail",
|
|
||||||
delete_button: "Supprimer",
|
|
||||||
shift: {
|
shift: {
|
||||||
actions: {
|
actions: {
|
||||||
add: "Ajouter un Quart",
|
add: "Ajouter un Quart",
|
||||||
|
|
@ -256,6 +255,7 @@ export default {
|
||||||
SICK: "Maladie",
|
SICK: "Maladie",
|
||||||
VACATION: "Vacance",
|
VACATION: "Vacance",
|
||||||
REMOTE: "Télétravail",
|
REMOTE: "Télétravail",
|
||||||
|
OFFICE: "Bureau",
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start: "Début (HH:mm)",
|
start: "Début (HH:mm)",
|
||||||
|
|
@ -292,6 +292,7 @@ export default {
|
||||||
errors: {
|
errors: {
|
||||||
INVALID_SHIFT_TIME: "Les heures d'entrée et de sortie sont inversées",
|
INVALID_SHIFT_TIME: "Les heures d'entrée et de sortie sont inversées",
|
||||||
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||||
|
SHIFT_OVERLAP_SHORT: "Chevauchement",
|
||||||
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
||||||
SHIFT_TIME_REQUIRED: "Heure requise",
|
SHIFT_TIME_REQUIRED: "Heure requise",
|
||||||
SHIFT_TYPE_REQUIRED: "Type requis",
|
SHIFT_TYPE_REQUIRED: "Type requis",
|
||||||
|
|
@ -321,10 +322,12 @@ export default {
|
||||||
expenses_title: "dépenses encourues"
|
expenses_title: "dépenses encourues"
|
||||||
},
|
},
|
||||||
print_report: {
|
print_report: {
|
||||||
company: "compagnie",
|
title: "options de téléchargement",
|
||||||
|
company: "compagnies",
|
||||||
type: "types de données",
|
type: "types de données",
|
||||||
shifts: "quarts de travail",
|
shifts: "quarts de travail",
|
||||||
expenses: "dépenses",
|
expenses: "dépenses",
|
||||||
|
options: "options",
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
button_detailed_view: "vue détaillée",
|
button_detailed_view: "vue détaillée",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
auth_store.logout();
|
auth_store.logout();
|
||||||
|
|
||||||
router.push({ name: 'login' }).catch(err => {
|
router.push({ name: 'login' }).catch(err => {
|
||||||
console.log('could not log you out: ', err);
|
console.error('could not log you out: ', err);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
|
|
||||||
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;
|
||||||
console.log('preset id: ', preset_id);
|
|
||||||
schedule_preset_store.openSchedulePresetManager(preset_id ?? current_preset.value.value);
|
schedule_preset_store.openSchedulePresetManager(preset_id ?? current_preset.value.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,27 +82,28 @@
|
||||||
</q-select>
|
</q-select>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
|
||||||
dense
|
|
||||||
rounded
|
|
||||||
icon="add"
|
icon="add"
|
||||||
color="accent"
|
color="accent"
|
||||||
class="col-auto q-px-sm q-ml-sm"
|
class="col-auto q-px-sm q-ml-sm rounded-50"
|
||||||
@click="onClickSchedulePresetManager('create', -1)"
|
@click="onClickSchedulePresetManager('create', -1)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
|
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
|
||||||
<div class="col-auto row no-wrap full-height">
|
<div class="col-auto row no-wrap full-height">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
|
||||||
dense
|
|
||||||
rounded
|
|
||||||
icon="edit"
|
icon="edit"
|
||||||
color="accent"
|
color="accent"
|
||||||
class="col-auto q-px-sm q-mx-sm full-height"
|
class="col-auto q-px-sm q-ml-sm rounded-50"
|
||||||
@click="onClickSchedulePresetManager('update')"
|
@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
|
<q-btn
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="employee_store.is_add_modify_dialog_open"
|
v-model="employee_store.is_add_modify_dialog_open"
|
||||||
full-width
|
full-width
|
||||||
|
full-height
|
||||||
@beforeShow="current_step = 'form'"
|
@beforeShow="current_step = 'form'"
|
||||||
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
||||||
class="shadow-24"
|
class="shadow-24"
|
||||||
|
|
@ -26,7 +27,7 @@
|
||||||
<div
|
<div
|
||||||
class="column bg-secondary rounded-10 no-wrap"
|
class="column bg-secondary rounded-10 no-wrap"
|
||||||
:class="$q.dark.isActive ? 'shadow-24' : 'shadow-10'"
|
: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)' : '')"
|
($q.dark.isActive ? 'border: 2px solid var(--q-accent)' : '')"
|
||||||
>
|
>
|
||||||
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-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 { 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 employee_store = useEmployeeStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -50,8 +49,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
animateFlip(table_grid_container);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -114,6 +111,7 @@
|
||||||
{ icon: 'view_list', value: false },
|
{ icon: 'view_list', value: false },
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="filters.search_bar_string"
|
v-model="filters.search_bar_string"
|
||||||
outlined
|
outlined
|
||||||
|
|
@ -133,6 +131,7 @@
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
|
|
@ -191,7 +190,7 @@
|
||||||
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
||||||
class="rounded-5 cursor-pointer"
|
class="rounded-5 cursor-pointer"
|
||||||
style="font-size: 1.2em;"
|
style="font-size: 1.2em;"
|
||||||
:style="`animation-delay: ${scope.pageIndex + scope.rowIndex * 5}ms; ` + (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'">
|
<div v-if="scope.col.name === 'first_name'">
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,14 @@
|
||||||
|
|
||||||
const shift = defineModel<SchedulePresetShift>('shift', { required: true });
|
const shift = defineModel<SchedulePresetShift>('shift', { required: true });
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS[0]);
|
const shift_type_selected = ref(SHIFT_OPTIONS[0]);
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
error: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
'click-delete': [void];
|
'clickDelete': [void];
|
||||||
|
'blurTimeField': [void];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -72,6 +77,8 @@
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
type="time"
|
type="time"
|
||||||
class="text-uppercase weekday-field"
|
class="text-uppercase weekday-field"
|
||||||
|
:error="error"
|
||||||
|
@blur="$emit('blurTimeField')"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<div
|
<div
|
||||||
|
|
@ -92,6 +99,8 @@
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
type="time"
|
type="time"
|
||||||
class="text-uppercase weekday-field"
|
class="text-uppercase weekday-field"
|
||||||
|
:error="error"
|
||||||
|
@blur="$emit('blurTimeField')"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<div
|
<div
|
||||||
|
|
@ -112,7 +121,7 @@
|
||||||
icon="clear"
|
icon="clear"
|
||||||
size="sm"
|
size="sm"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click="$emit('click-delete')"
|
@click="$emit('clickDelete')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -128,6 +137,11 @@
|
||||||
min-height: 25px;
|
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) {
|
:deep(.q-field--auto-height.q-field--dense .q-field__native) {
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||||
import { SchedulePresetShift } from '../models/schedule-presets.models';
|
import { SchedulePresetShift } from '../models/schedule-presets.models';
|
||||||
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';
|
||||||
|
|
||||||
const schedule_preset_store = useSchedulePresetsStore();
|
const schedule_preset_store = useSchedulePresetsStore();
|
||||||
const employee_list_api = useEmployeeListApi();
|
const employee_list_api = useEmployeeListApi();
|
||||||
|
|
@ -18,14 +19,14 @@
|
||||||
v-model="schedule_preset_store.is_manager_open"
|
v-model="schedule_preset_store.is_manager_open"
|
||||||
full-width
|
full-width
|
||||||
>
|
>
|
||||||
<SchedulePresetsDialogDelete
|
<SchedulePresetsDialogDelete
|
||||||
v-if="schedule_preset_store.schedule_preset_dialog_mode === 'delete'"
|
v-if="schedule_preset_store.schedule_preset_dialog_mode === 'delete'"
|
||||||
:preset-id="schedule_preset_store.current_schedule_preset.id"
|
:preset-id="schedule_preset_store.current_schedule_preset.id"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="column flex-center bg-secondary rounded-10 shadow-24"
|
class="column flex-center bg-secondary rounded-10 shadow-24 no-wrap"
|
||||||
style="border: 2px solid var(--q-accent); width: 50vw !important;"
|
style="border: 2px solid var(--q-accent); width: 50vw !important;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -61,7 +62,10 @@
|
||||||
</div>
|
</div>
|
||||||
</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
|
<div
|
||||||
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays"
|
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays"
|
||||||
:key="weekday.day"
|
:key="weekday.day"
|
||||||
|
|
@ -79,7 +83,9 @@
|
||||||
>
|
>
|
||||||
<SchedulePresetsDialogRow
|
<SchedulePresetsDialogRow
|
||||||
v-model:shift="weekday.shifts[index]!"
|
v-model:shift="weekday.shifts[index]!"
|
||||||
|
:error="weekday.is_error"
|
||||||
@click-delete="weekday.shifts.splice(index, 1)"
|
@click-delete="weekday.shifts.splice(index, 1)"
|
||||||
|
@blur-time-field="weekday.is_error = isShiftOverlap(weekday.shifts)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -97,7 +103,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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-space />
|
||||||
<q-btn
|
<q-btn
|
||||||
:disable="schedule_preset_store.current_schedule_preset.name === ''"
|
:disable="schedule_preset_store.current_schedule_preset.name === ''"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEmployeeStore } from "src/stores/employee-store";
|
import { useEmployeeStore } from "src/stores/employee-store";
|
||||||
import { useSchedulePresetsStore } from "src/stores/schedule-presets.store";
|
import { useSchedulePresetsStore } from "src/stores/schedule-presets.store";
|
||||||
import { SchedulePreset } from "../models/schedule-presets.models";
|
import { SchedulePreset } from "../models/schedule-presets.models";
|
||||||
|
import { isShiftOverlap } from "src/modules/timesheets/utils/shift.util";
|
||||||
|
|
||||||
export const useEmployeeListApi = () => {
|
export const useEmployeeListApi = () => {
|
||||||
const employee_store = useEmployeeStore();
|
const employee_store = useEmployeeStore();
|
||||||
|
|
@ -15,34 +16,60 @@ export const useEmployeeListApi = () => {
|
||||||
employee_store.is_loading = false;
|
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);
|
const success = await employee_store.getEmployeeDetails(email);
|
||||||
if (success && employee_store.employee.preset_id !== null) {
|
if (success && employee_store.employee.preset_id !== null) {
|
||||||
schedule_preset_store.setCurrentSchedulePreset(employee_store.employee.preset_id ?? -1);
|
schedule_preset_store.setCurrentSchedulePreset(employee_store.employee.preset_id ?? -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSchedulePreset = (preset_id: number) => {
|
const setSchedulePreset = (preset_id: number) => {
|
||||||
schedule_preset_store.setCurrentSchedulePreset(preset_id);
|
schedule_preset_store.setCurrentSchedulePreset(preset_id);
|
||||||
employee_store.employee.preset_id = preset_id < 0 ? null : preset_id;
|
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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.weekdays.some(weekday => weekday.is_error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten all weekday shifts into a single array
|
||||||
const preset_shifts = preset.weekdays.flatMap(weekday => weekday.shifts);
|
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;
|
let success = false;
|
||||||
|
|
||||||
if (preset.id === -1) success = await schedule_preset_store.createSchedulePreset(backend_preset);
|
// Create a new preset if it has no backend ID, otherwise update the existing one
|
||||||
else success = await schedule_preset_store.updateSchedulePreset(backend_preset);
|
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) {
|
if (success) {
|
||||||
await schedule_preset_store.findSchedulePresetList();
|
await schedule_preset_store.findSchedulePresetList();
|
||||||
schedule_preset_store.is_manager_open = false;
|
schedule_preset_store.is_manager_open = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSchedulePreset = async(preset_id: number) => {
|
const deleteSchedulePreset = async (preset_id: number) => {
|
||||||
const success = await schedule_preset_store.deleteSchedulePreset(preset_id);
|
const success = await schedule_preset_store.deleteSchedulePreset(preset_id);
|
||||||
if (success) {
|
if (success) {
|
||||||
await schedule_preset_store.findSchedulePresetList();
|
await schedule_preset_store.findSchedulePresetList();
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export class SchedulePresetFrontend {
|
||||||
this.is_default = schedule_preset?.is_default ?? false;
|
this.is_default = schedule_preset?.is_default ?? false;
|
||||||
this.weekdays = WEEKDAYS.map(day => ({
|
this.weekdays = WEEKDAYS.map(day => ({
|
||||||
day,
|
day,
|
||||||
|
is_error: false,
|
||||||
shifts: schedule_preset !== undefined ? schedule_preset?.shifts.filter(shift => shift.week_day === day) : [],
|
shifts: schedule_preset !== undefined ? schedule_preset?.shifts.filter(shift => shift.week_day === day) : [],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -57,5 +58,6 @@ export class SchedulePresetFrontend {
|
||||||
|
|
||||||
export interface WeekdayPresetShifts {
|
export interface WeekdayPresetShifts {
|
||||||
day: Weekday;
|
day: Weekday;
|
||||||
|
is_error: boolean;
|
||||||
shifts: SchedulePresetShift[];
|
shifts: SchedulePresetShift[];
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
const setDisplayLanguage = (locale: MessageLanguages) => {
|
const setDisplayLanguage = (locale: MessageLanguages) => {
|
||||||
if (ui_store.user_preferences !== undefined) {
|
if (ui_store.user_preferences !== undefined) {
|
||||||
ui_store.user_preferences.display_language = locale;
|
ui_store.user_preferences.display_language = locale;
|
||||||
console.log('triggered language change: ', ui_store.user_preferences.display_language);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -10,13 +11,17 @@
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', };
|
const q = useQuasar();
|
||||||
|
|
||||||
|
const date_format_options = computed(() => q.platform.is.mobile ? { day: 'numeric', month: 'short', year: 'numeric' } : { day: 'numeric', month: 'long', year: 'numeric', });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
<div class="column text-uppercase text-center text-weight-bolder text-h4">
|
||||||
<span class="col">{{ $t(title) }}</span>
|
<span
|
||||||
|
v-if="!$q.platform.is.mobile"
|
||||||
|
class="col q-mt-lg"
|
||||||
|
>{{ $t(title) }}</span>
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="animated fadeInDown"
|
enter-active-class="animated fadeInDown"
|
||||||
|
|
@ -27,6 +32,7 @@
|
||||||
:key="startDate"
|
:key="startDate"
|
||||||
v-if="startDate.length > 0"
|
v-if="startDate.length > 0"
|
||||||
class="col row flex-center full-width q-py-none q-my-none"
|
class="col row flex-center full-width q-py-none q-my-none"
|
||||||
|
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||||
>
|
>
|
||||||
<div class="text-accent text-weight-bold text-h6">
|
<div class="text-accent text-weight-bold text-h6">
|
||||||
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
|
|
|
||||||
|
|
@ -33,30 +33,13 @@
|
||||||
emit('date-selected', value);
|
emit('date-selected', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNextOrPreviousPayPeriod = (direction: number) => {
|
|
||||||
const pay_period = timesheet_store.pay_period;
|
|
||||||
if (!pay_period) return;
|
|
||||||
|
|
||||||
pay_period.pay_period_no += direction;
|
|
||||||
|
|
||||||
if (pay_period.pay_period_no > 26) {
|
|
||||||
pay_period.pay_period_no = 1;
|
|
||||||
pay_period.pay_year += direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pay_period.pay_period_no < 1) {
|
|
||||||
pay_period.pay_period_no = 26;
|
|
||||||
pay_period.pay_year += direction;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextPayPeriod = () => {
|
const getNextPayPeriod = () => {
|
||||||
getNextOrPreviousPayPeriod(NEXT);
|
timesheet_store.getNextOrPreviousPayPeriod(NEXT);
|
||||||
emit('pressed-next-button');
|
emit('pressed-next-button');
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreviousPayPeriod = () => {
|
const getPreviousPayPeriod = () => {
|
||||||
getNextOrPreviousPayPeriod(PREVIOUS);
|
timesheet_store.getNextOrPreviousPayPeriod(PREVIOUS);
|
||||||
emit('pressed-previous-button');
|
emit('pressed-previous-button');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,37 @@
|
||||||
<script
|
<script setup lang="ts">
|
||||||
setup
|
/* eslint-disable */
|
||||||
lang="ts"
|
import { computed, ref } from 'vue';
|
||||||
>
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||||
/* eslint-disable */
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
||||||
import { computed, ref } from 'vue';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
|
||||||
import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
|
||||||
|
|
||||||
const visible_columns = ref<string[]>([
|
const visible_columns = ref<string[]>([
|
||||||
overview_column_names.REGULAR,
|
overview_column_names.REGULAR,
|
||||||
overview_column_names.EVENING,
|
overview_column_names.EVENING,
|
||||||
overview_column_names.EMERGENCY,
|
overview_column_names.EMERGENCY,
|
||||||
overview_column_names.SICK,
|
overview_column_names.SICK,
|
||||||
overview_column_names.VACATION,
|
overview_column_names.VACATION,
|
||||||
overview_column_names.HOLIDAY,
|
overview_column_names.HOLIDAY,
|
||||||
overview_column_names.OVERTIME,
|
overview_column_names.OVERTIME,
|
||||||
overview_column_names.IS_APPROVED,
|
overview_column_names.IS_APPROVED,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ?
|
||||||
|
[] :
|
||||||
|
timesheet_store.pay_period_overviews
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
|
timesheet_store.current_pay_period_overview = row;
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
||||||
|
|
||||||
|
timesheet_store.is_details_dialog_open = true;
|
||||||
|
};
|
||||||
|
|
||||||
const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ?
|
|
||||||
[] :
|
|
||||||
timesheet_store.pay_period_overviews
|
|
||||||
)
|
|
||||||
|
|
||||||
const onClickedDetails = async (row: TimesheetOverview) => {
|
|
||||||
timesheet_store.current_pay_period_overview = row;
|
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
|
||||||
|
|
||||||
timesheet_store.is_details_dialog_open = true;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,143 @@
|
||||||
<script
|
<script setup lang="ts">
|
||||||
setup
|
import { computed, ref, watch } from 'vue';
|
||||||
lang="ts"
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
>
|
import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
import { default_timesheet_approval_cvs_report_filters, type TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(default_timesheet_approval_cvs_report_filters);
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const report_filter_options = ref<TimesheetApprovalCSVReportFilters>(new TimesheetApprovalCSVReportFilters);
|
||||||
|
|
||||||
const company_options = [
|
const selected_report_filters = ref<(keyof TimesheetApprovalCSVReportFilters)[]>(
|
||||||
{ label: 'Targo', value: report_filter_options.value.companies.targo },
|
Object.entries(report_filter_options.value).filter(([_key, value]) => value).map(([key]) => key as keyof TimesheetApprovalCSVReportFilters)
|
||||||
{ label: 'Solucom', value: report_filter_options.value.companies.solucom },
|
);
|
||||||
];
|
|
||||||
|
|
||||||
const type_options = [
|
interface ReportOptions {
|
||||||
{ label: 'timesheet_approvals.print_report.shifts', value: report_filter_options.value.types.shifts },
|
label: string;
|
||||||
{ label: 'timesheet_approvals.print_report.expenses', value: report_filter_options.value.types.expenses },
|
value: keyof TimesheetApprovalCSVReportFilters;
|
||||||
{ label: 'shared.shift_type.holiday', value: report_filter_options.value.types.holiday },
|
};
|
||||||
{ label: 'shared.shift_type.vacation', value: report_filter_options.value.types.vacation },
|
|
||||||
];
|
|
||||||
|
|
||||||
const is_download_button_disabled = computed(() => {
|
const company_options: ReportOptions[] = [
|
||||||
return company_options.map(option => option.value).filter(value => value === true).length > 0 ||
|
{ label: 'Targo', value: 'targo' },
|
||||||
type_options.map(option => option.value).filter(value => value === true).length > 0;
|
{ label: 'Solucom', value: 'solucom' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const type_options: ReportOptions[] = [
|
||||||
|
{ label: 'timesheet_approvals.print_report.shifts', value: 'shifts' },
|
||||||
|
{ label: 'timesheet_approvals.print_report.expenses', value: 'expenses' },
|
||||||
|
{ label: 'shared.shift_type.holiday', value: 'holiday' },
|
||||||
|
{ label: 'shared.shift_type.vacation', value: 'vacation' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const is_download_button_enable = computed(() =>
|
||||||
|
company_options.map(option => option.value).some(option => selected_report_filters.value.includes(option)) &&
|
||||||
|
type_options.map(option => option.value).some(option => selected_report_filters.value.includes(option))
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickedDownload = async () => {
|
||||||
|
try {
|
||||||
|
const data = await timesheet_store.getPayPeriodReport(report_filter_options.value);
|
||||||
|
|
||||||
|
const companies = Object.entries(report_filter_options.value)
|
||||||
|
.filter(([key, value]) => value && (key === 'targo' || key === 'solucom')).map(([key]) => key).join('-');
|
||||||
|
|
||||||
|
const types = Object.entries(report_filter_options.value)
|
||||||
|
.filter(([key, value]) => value && ['shifts', 'expenses', 'holiday', 'vacation'].includes(key)).map(([key]) => key).join('-');
|
||||||
|
|
||||||
|
const file_name = `Desjardins_${companies}_${types}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
|
||||||
|
const blob = new Blob([data], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', file_name);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`An error occured during the CSV download: `, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selected_report_filters, (new_values) => {
|
||||||
|
Object.keys(report_filter_options.value).forEach(key => {
|
||||||
|
const typed_key = key as keyof TimesheetApprovalCSVReportFilters;
|
||||||
|
report_filter_options.value[typed_key] = new_values.includes(key as keyof TimesheetApprovalCSVReportFilters);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-btn-group
|
<q-dialog v-model="timesheet_store.is_report_dialog_open">
|
||||||
rounded
|
<div class="bg-secondary full-width shadow-24 rounded-10 column">
|
||||||
push
|
<div class="shadow-1 bg-primary text-accent text-weight-bold text-center text-uppercase">
|
||||||
>
|
<span> {{ $t('timesheet_approvals.print_report.title') }}</span>
|
||||||
<q-btn
|
</div>
|
||||||
rounded
|
<div class="row q-py-md q-px-lg">
|
||||||
push
|
<div class="col-auto full-width shadow-1 row bg-dark q-py-xs q-px-lg rounded-10">
|
||||||
color="primary"
|
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3">
|
||||||
icon="print"
|
|
||||||
:disable="is_download_button_disabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn-dropdown
|
|
||||||
rounded
|
|
||||||
push
|
|
||||||
color="white"
|
|
||||||
text-color="primary"
|
|
||||||
icon="checklist"
|
|
||||||
>
|
|
||||||
<q-list class="row">
|
|
||||||
<q-item class="col">
|
|
||||||
<q-item-label class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
|
|
||||||
{{ $t('timesheet_approvals.print_report.company') }}
|
{{ $t('timesheet_approvals.print_report.company') }}
|
||||||
</q-item-label>
|
</span>
|
||||||
|
<div class="row bordered-primary col-auto full-width">
|
||||||
<q-item-section
|
<div
|
||||||
row
|
v-for="company, index in company_options"
|
||||||
no-wrap
|
|
||||||
>
|
|
||||||
<q-checkbox
|
|
||||||
v-for="option, index in company_options"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
v-model="option.value"
|
class="q-pa-xs col-6"
|
||||||
:val="option.label"
|
>
|
||||||
:label="option.label"
|
<q-checkbox
|
||||||
/>
|
v-model="selected_report_filters"
|
||||||
</q-item-section>
|
left-label
|
||||||
</q-item>
|
color="white"
|
||||||
|
class="q-px-md shadow-4 rounded-25 full-width"
|
||||||
<q-separator
|
dense
|
||||||
spaced
|
:class="selected_report_filters.includes(company.value) ? 'bg-accent text-white' : 'bg-dark'"
|
||||||
vertical
|
:label="$t(company.label)"
|
||||||
color="primary"
|
:val="company.value"
|
||||||
|
checked-icon="download"
|
||||||
|
unchecked-icon="highlight_off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-py-md">
|
||||||
|
<div class="col-auto full-width shadow-1 row bg-dark q-px-lg rounded-10 q-pb-sm">
|
||||||
|
<span class="col q-px-sm q-pt-xs text-weight-bolder text-accent text-uppercase col-3">
|
||||||
|
{{ $t('timesheet_approvals.print_report.options') }}
|
||||||
|
</span>
|
||||||
|
<div class=" row bordered-primary col-auto full-width">
|
||||||
|
<div
|
||||||
|
v-for="type, index in type_options"
|
||||||
|
:key="index"
|
||||||
|
class="q-pa-xs col-6"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="selected_report_filters"
|
||||||
|
left-label
|
||||||
|
color="white"
|
||||||
|
class="q-px-md shadow-4 rounded-25 full-width"
|
||||||
|
dense
|
||||||
|
:class="selected_report_filters.includes(type.value) ? 'bg-accent text-white' : 'bg-dark'"
|
||||||
|
:label="$t(type.label)"
|
||||||
|
:val="type.value"
|
||||||
|
checked-icon="download"
|
||||||
|
unchecked-icon="highlight_off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<q-btn
|
||||||
|
:disable="!is_download_button_enable"
|
||||||
|
square
|
||||||
|
size="md"
|
||||||
|
icon="download"
|
||||||
|
:color="is_download_button_enable ? 'accent' : 'grey-5'"
|
||||||
|
:label="$t('shared.label.download')"
|
||||||
|
@click="onClickedDownload()"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<q-item class="col">
|
</div>
|
||||||
<q-item-section
|
</q-dialog>
|
||||||
row
|
</template>
|
||||||
no-wrap
|
|
||||||
>
|
|
||||||
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
|
|
||||||
{{ $t('timesheet_approvals.print_report.type') }}</p>
|
|
||||||
<q-checkbox
|
|
||||||
v-for="option, index in type_options"
|
|
||||||
:key="index"
|
|
||||||
v-model="option.value"
|
|
||||||
:val="option.label"
|
|
||||||
:label="option.label"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-btn-dropdown>
|
|
||||||
</q-btn-group>
|
|
||||||
</template>
|
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,16 @@ export const useTimesheetApprovalApi = () => {
|
||||||
timesheet_store.is_loading = false;
|
timesheet_store.is_loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number) => {
|
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[]) => {
|
||||||
if (timesheet_store.pay_period === undefined) return;
|
if (timesheet_store.pay_period === undefined) return;
|
||||||
|
|
||||||
const [targo, solucom] = report_filter_company;
|
const [targo, solucom] = report_filter_company;
|
||||||
const [shifts, expenses, holiday, vacation] = report_filter_type;
|
const [shifts, expenses, holiday, vacation] = report_filter_type;
|
||||||
const options = {
|
const options = {
|
||||||
types: { shifts, expenses, holiday, vacation },
|
shifts, expenses, holiday, vacation, targo, solucom
|
||||||
companies: { targo, solucom },
|
|
||||||
} as TimesheetApprovalCSVReportFilters;
|
} as TimesheetApprovalCSVReportFilters;
|
||||||
|
|
||||||
await timesheet_store.getPayPeriodReportByYearAndPeriodNumber(
|
await timesheet_store.getPayPeriodReport(options);
|
||||||
year ?? timesheet_store.pay_period.pay_year,
|
|
||||||
period_number ?? timesheet_store.pay_period.pay_period_no,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
export interface TimesheetApprovalCSVReportFilters {
|
export class TimesheetApprovalCSVReportFilters {
|
||||||
types: {
|
shifts: boolean;
|
||||||
shifts: boolean;
|
expenses: boolean;
|
||||||
expenses: boolean;
|
holiday: boolean;
|
||||||
holiday: boolean;
|
vacation: boolean;
|
||||||
vacation: boolean;
|
targo: boolean;
|
||||||
};
|
solucom: boolean;
|
||||||
companies: {
|
|
||||||
targo: boolean;
|
constructor() {
|
||||||
solucom: boolean;
|
this.shifts = true;
|
||||||
|
this.expenses = true;
|
||||||
|
this.holiday = false;
|
||||||
|
this.vacation = false;
|
||||||
|
this.targo = true;
|
||||||
|
this.solucom = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const default_timesheet_approval_cvs_report_filters: TimesheetApprovalCSVReportFilters = {
|
|
||||||
types: {
|
|
||||||
shifts: true,
|
|
||||||
expenses: true,
|
|
||||||
holiday: true,
|
|
||||||
vacation: true,
|
|
||||||
},
|
|
||||||
companies: {
|
|
||||||
targo: true,
|
|
||||||
solucom: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -4,12 +4,12 @@ import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/m
|
||||||
|
|
||||||
export const timesheetApprovalService = {
|
export const timesheetApprovalService = {
|
||||||
getPayPeriodOverviews: async (year: number, period_number: number): Promise<PayPeriodOverviewResponse> => {
|
getPayPeriodOverviews: async (year: number, period_number: number): Promise<PayPeriodOverviewResponse> => {
|
||||||
const response = await api.get<{success: boolean, data: PayPeriodOverviewResponse, error? : string}>(`pay-periods/overview/${year}/${period_number}`);
|
const response = await api.get<{ success: boolean, data: PayPeriodOverviewResponse, error?: string }>(`pay-periods/overview/${year}/${period_number}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, filters?: TimesheetApprovalCSVReportFilters) => {
|
||||||
const response = await api.get(`csv/${year}/${period_number}`, { params: { report_filters, }});
|
const response = await api.get(`exports/csv/${year}/${period_number}`, { params: filters, responseType: 'arraybuffer' });
|
||||||
return response.data;
|
return response;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import { date } from 'quasar';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
interface ExpenseOption {
|
interface ExpenseOption {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
|
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
const is_initial_expense = ref(true);
|
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
const rules = useExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
@ -35,17 +34,13 @@
|
||||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||||
|
|
||||||
const expense_options: ExpenseOption[] = [
|
const expense_options: ExpenseOption[] = [
|
||||||
{label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM')},
|
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
|
||||||
{label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES')},
|
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
|
||||||
{label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE')},
|
{ label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
|
||||||
{label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL')},
|
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
|
||||||
]
|
]
|
||||||
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
||||||
'onClickUpdateCancel': [void];
|
|
||||||
'onClickSaveUpdates': [void];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const openDatePicker = () => {
|
const openDatePicker = () => {
|
||||||
is_navigator_open.value = true;
|
is_navigator_open.value = true;
|
||||||
|
|
@ -61,144 +56,132 @@
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||||
|
|
||||||
if (expenses_store.current_expense.id) {
|
|
||||||
emit('onClickSaveUpdates');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
expenses_store.is_showing_create_form = true;
|
||||||
|
expenses_store.mode = 'create';
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(expenses_store.current_expense, () => {
|
|
||||||
is_initial_expense.value = deepEqual(expenses_store.current_expense, expenses_store.initial_expense);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||||
:key="expenses_store.current_expense.id"
|
|
||||||
flat
|
flat
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
class="full-width"
|
class="full-width q-mt-md q-px-md"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="text-uppercase text-weight-medium q-pt-sm q-ma-sm"
|
|
||||||
:class="expenses_store.mode === 'create' ? 'q-px-lg' : 'invisible'"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="row justify-between items-start rounded-5 q-pb-sm"
|
class="row justify-between items-start rounded-5 q-pb-sm"
|
||||||
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
|
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
|
||||||
>
|
>
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
<q-input
|
<div class="col q-px-xs">
|
||||||
v-model="expenses_store.current_expense.date"
|
<q-input
|
||||||
dense
|
v-model="expenses_store.current_expense.date"
|
||||||
type="date"
|
dense
|
||||||
borderless
|
standout
|
||||||
readonly
|
readonly
|
||||||
stack-label
|
stack-label
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col-auto q-px-xs"
|
input-class="text-weight-medium"
|
||||||
input-class="text-weight-medium"
|
input-style="font-size: 1em;"
|
||||||
input-style="font-size: 1em;"
|
:label="$t('timesheet.expense.date')"
|
||||||
:label="$t('timesheet.expense.date')"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<template #prepend>
|
<q-btn
|
||||||
<q-btn
|
push
|
||||||
push
|
dense
|
||||||
dense
|
icon="event"
|
||||||
icon="event"
|
color="accent"
|
||||||
color="accent"
|
class="q-mr-sm"
|
||||||
class="q-mr-sm"
|
@click="openDatePicker"
|
||||||
@click="openDatePicker"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-dialog
|
|
||||||
v-model="is_navigator_open"
|
|
||||||
transition-show="jump-right"
|
|
||||||
transition-hide="jump-right"
|
|
||||||
>
|
|
||||||
<q-date
|
|
||||||
v-model="expenses_store.current_expense.date"
|
|
||||||
mask="YYYY-MM-DD"
|
|
||||||
event-color="accent"
|
|
||||||
:options="date => date >= period_start_date && date <= period_end_date"
|
|
||||||
@update:model-value="closeDatePicker"
|
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #label>
|
<q-dialog
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
v-model="is_navigator_open"
|
||||||
{{ $t('timesheet.expense.date') }}
|
transition-show="jump-right"
|
||||||
</span>
|
transition-hide="jump-right"
|
||||||
</template>
|
>
|
||||||
</q-input>
|
<q-date
|
||||||
|
v-model="expenses_store.current_expense.date"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
event-color="accent"
|
||||||
|
:options="date => date >= period_start_date && date <= period_end_date"
|
||||||
|
@update:model-value="closeDatePicker"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.date') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
<q-select
|
<div class="col q-px-xs">
|
||||||
v-model="expense_selected"
|
<q-select
|
||||||
standout="bg-blue-grey-9"
|
v-model="expense_selected"
|
||||||
dense
|
standout
|
||||||
:options="expense_options"
|
dense
|
||||||
hide-dropdown-icon
|
:options="expense_options"
|
||||||
stack-label
|
hide-dropdown-icon
|
||||||
label-slot
|
stack-label
|
||||||
class="col q-px-xs"
|
label-slot
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
:menu-offset="[0, 10]"
|
:menu-offset="[0, 10]"
|
||||||
menu-anchor="bottom middle"
|
menu-anchor="bottom middle"
|
||||||
menu-self="top middle"
|
menu-self="top middle"
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
{{ $t('timesheet.expense.type') }}
|
{{ $t('timesheet.expense.type') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
:tabindex="scope.tabindex"
|
:tabindex="scope.tabindex"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="scope.opt.icon"
|
:name="scope.opt.icon"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="col-auto q-mx-xs"
|
class="col-auto q-mx-xs"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style="line-height: 1em;"
|
style="line-height: 1em;"
|
||||||
class="col-auto ellipsis text-uppercase"
|
class="col-auto ellipsis text-uppercase"
|
||||||
>{{ scope.opt.label }}</span>
|
>{{ scope.opt.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- amount input -->
|
<!-- amount input -->
|
||||||
<div v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')">
|
<div class="col q-px-xs">
|
||||||
<q-input
|
<q-input
|
||||||
key="amount"
|
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')"
|
||||||
v-model.number="expenses_store.current_expense.amount"
|
v-model="expenses_store.current_expense.amount"
|
||||||
standout="bg-blue-grey-9"
|
standout
|
||||||
dense
|
dense
|
||||||
label-slot
|
label-slot
|
||||||
stack-label
|
stack-label
|
||||||
suffix="$"
|
suffix="$"
|
||||||
|
type="number"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col-auto q-px-xs"
|
input-class="text-right text-weight-medium"
|
||||||
input-class="text-right text-weight-bold"
|
input-style="font-size: 1.3em;"
|
||||||
:input-style="'font-size: 1.2em;'"
|
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.amountRequired]"
|
:rules="[rules.amountRequired]"
|
||||||
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
|
@ -206,22 +189,19 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- mileage input -->
|
|
||||||
<div v-else>
|
|
||||||
<q-input
|
<q-input
|
||||||
key="mileage"
|
v-else
|
||||||
v-model.number="expenses_store.current_expense.mileage"
|
v-model="expenses_store.current_expense.mileage"
|
||||||
standout="bg-blue-grey-9"
|
standout
|
||||||
input-class="text-right"
|
|
||||||
dense
|
dense
|
||||||
stack-label
|
|
||||||
clearable
|
|
||||||
color="primary"
|
|
||||||
class="col q-px-xs"
|
|
||||||
label-slot
|
label-slot
|
||||||
|
stack-label
|
||||||
suffix="km"
|
suffix="km"
|
||||||
|
type="number"
|
||||||
|
input-class="text-right text-weight-medium"
|
||||||
|
input-style="font-size: 1.3em;"
|
||||||
|
color="primary"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.mileageRequired]"
|
:rules="[rules.mileageRequired]"
|
||||||
>
|
>
|
||||||
|
|
@ -234,71 +214,63 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- employee comment input -->
|
<!-- employee comment input -->
|
||||||
<q-input
|
<div class="col q-px-xs">
|
||||||
v-model="expenses_store.current_expense.comment"
|
<q-input
|
||||||
standout="bg-blue-grey-9"
|
v-model="expenses_store.current_expense.comment"
|
||||||
dense
|
standout
|
||||||
stack-label
|
dense
|
||||||
label-slot
|
stack-label
|
||||||
color="primary"
|
label-slot
|
||||||
type="text"
|
color="primary"
|
||||||
class="col q-px-sm"
|
input-class="text-weight-medium"
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
input-style="font-size: 1.3em;"
|
||||||
lazy-rules="ondemand"
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
:rules="[rules.commentRequired]"
|
lazy-rules="ondemand"
|
||||||
>
|
:rules="[rules.commentRequired]"
|
||||||
<template #label>
|
>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<template #label>
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
</span>
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
</template>
|
</span>
|
||||||
</q-input>
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- import attach file section -->
|
<!-- import attach file section -->
|
||||||
<q-file
|
<div class="col q-px-xs">
|
||||||
v-model="files"
|
<q-file
|
||||||
standout="bg-blue-grey-9"
|
v-model="files"
|
||||||
dense
|
standout
|
||||||
use-chips
|
dense
|
||||||
multiple
|
use-chips
|
||||||
stack-label
|
multiple
|
||||||
:label="$t('timesheet.expense.hints.attach_file')"
|
stack-label
|
||||||
class="col"
|
label-slot
|
||||||
style="max-width: 300px;"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<template #prepend>
|
<q-icon
|
||||||
<q-icon
|
name="attach_file"
|
||||||
name="attach_file"
|
size="sm"
|
||||||
size="sm"
|
color="accent"
|
||||||
color="accent"
|
/>
|
||||||
/>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
{{ $t('timesheet.expense.hints.attach_file') }}
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-file>
|
</q-file>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col row full-width items-center">
|
<div class="col row full-width items-center">
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-if="expenses_store.mode === 'update'"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
class="col-auto q-ml-sm"
|
|
||||||
icon="clear"
|
|
||||||
color="negative"
|
|
||||||
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
|
||||||
@click="$emit('onClickUpdateCancel')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
:disable="is_initial_expense"
|
:disable="expenses_store.is_save_disabled"
|
||||||
:color="is_initial_expense ? 'grey-5' : 'accent'"
|
:color="expenses_store.is_save_disabled ? 'grey-5' : 'accent'"
|
||||||
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||||
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
class="q-px-sm "
|
class="q-px-sm "
|
||||||
|
|
@ -307,4 +279,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||||
|
border: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
let expenses = 0;
|
let expenses = 0;
|
||||||
let mileage = 0;
|
let mileage = 0;
|
||||||
timesheet_store.timesheets.forEach(timesheet => {
|
timesheet_store.timesheets.forEach(timesheet => {
|
||||||
expenses += timesheet.weekly_expenses.expenses ?? 0;
|
expenses += timesheet.weekly_expenses.expenses;
|
||||||
mileage += timesheet.weekly_expenses.mileage ?? 0;
|
expenses += timesheet.weekly_expenses.on_call;
|
||||||
|
expenses += timesheet.weekly_expenses.per_diem;
|
||||||
|
mileage += timesheet.weekly_expenses.mileage;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { expenses, mileage };
|
return { expenses, mileage };
|
||||||
|
|
@ -22,8 +24,8 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column items-center q-pa-none">
|
<div class="column items-center q-pa-none">
|
||||||
<div class="col row full-width">
|
<div class="col row full-width bg-primary">
|
||||||
<q-item-label class="col text-h6 text-weight-bolder text-uppercase q-py-sm q-px-md">
|
<q-item-label class="col text-h6 text-weight-bolder text-uppercase text-white q-py-sm q-px-md">
|
||||||
{{ $t('timesheet.expense.title') }}
|
{{ $t('timesheet.expense.title') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
|
|
@ -39,8 +41,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column items-end full-width q-pt-sm q-px-md">
|
<div class="col row flex-center full-width q-pt-sm q-px-md">
|
||||||
<div class="col-auto row items-center q-px-sm">
|
<div class="col-auto row items-center q-px-md">
|
||||||
<span
|
<span
|
||||||
v-if="$q.screen.gt.sm"
|
v-if="$q.screen.gt.sm"
|
||||||
class="col-auto text-uppercase text-weight-light text-accent q-mr-xs"
|
class="col-auto text-uppercase text-weight-light text-accent q-mr-xs"
|
||||||
|
|
@ -50,7 +52,7 @@
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-weight-light"
|
class="col-auto text-weight-light"
|
||||||
style="font-size: 2.5em; line-height: 1em;"
|
style="font-size: 2em; line-height: 1em;"
|
||||||
>
|
>
|
||||||
{{ weekly_totals.expenses.toFixed(2) }}
|
{{ weekly_totals.expenses.toFixed(2) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -64,7 +66,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto row items-center q-px-sm">
|
<div class="col-auto row items-center q-px-md">
|
||||||
<span
|
<span
|
||||||
v-if="$q.screen.gt.sm"
|
v-if="$q.screen.gt.sm"
|
||||||
class="col text-uppercase text-weight-light text-accent q-mr-xs"
|
class="col text-uppercase text-weight-light text-accent q-mr-xs"
|
||||||
|
|
@ -74,7 +76,7 @@
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-weight-light"
|
class="col-auto text-weight-light"
|
||||||
style="font-size: 2.5em; line-height: 1em;"
|
style="font-size: 2em; line-height: 1em;"
|
||||||
>
|
>
|
||||||
{{ weekly_totals.mileage.toFixed(1) }}
|
{{ weekly_totals.mileage.toFixed(1) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -2,219 +2,152 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
|
||||||
import { computed, ref, toRaw } from 'vue';
|
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
|
||||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
|
||||||
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
|
|
||||||
const { expense, horizontal = false } = defineProps<{
|
import { date } from 'quasar';
|
||||||
expense: Expense;
|
import { ref } from 'vue';
|
||||||
index: number;
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
horizontal?: boolean;
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
}>();
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
const is_approved = defineModel<boolean>({ required: true });
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
|
const expense = defineModel<Expense>({ required: true });
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const auth_store = useAuthStore();
|
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
const refresh_key = ref(1);
|
|
||||||
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
|
||||||
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
|
||||||
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
|
||||||
const is_showing_update_form = ref(false);
|
const is_showing_update_form = ref(false);
|
||||||
const is_current_expense = computed(() => expense.id === expenses_store.current_expense.id);
|
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
await expenses_api.deleteExpenseById(expense.id);
|
await expenses_api.deleteExpenseById(expense.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onExpenseClicked = () => {
|
const onClickExpenseUpdate = () => {
|
||||||
if (is_authorized_to_approve.value) {
|
if (expense.value.is_approved) return;
|
||||||
is_approved.value = !is_approved.value;
|
|
||||||
refresh_key.value += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onUpdateClicked = () => {
|
|
||||||
if (deepEqual(expense, expenses_store.current_expense)) {
|
|
||||||
expenses_store.mode = 'create';
|
|
||||||
Object.assign(expense, toRaw(expenses_store.initial_expense))
|
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
is_showing_update_form.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
expenses_store.mode = 'update';
|
expenses_store.mode = 'update';
|
||||||
expenses_store.current_expense = expense;
|
expenses_store.current_expense = expense.value;
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
expenses_store.initial_expense = unwrapAndClone(expense.value);
|
||||||
is_showing_update_form.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSaveUpdatesClicked = () => {
|
|
||||||
is_showing_update_form.value = false;
|
|
||||||
expenses_store.mode = 'create';
|
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-item
|
<q-expansion-item
|
||||||
:key="refresh_key"
|
v-model="is_showing_update_form"
|
||||||
:clickable="horizontal"
|
hide-expand-icon
|
||||||
class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
|
dense
|
||||||
:class="background_class + approved_class"
|
group="expenses"
|
||||||
@click="onExpenseClicked"
|
class="shadow-3 rounded-5 bg-dark q-my-sm"
|
||||||
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
|
@before-show="onClickExpenseUpdate()"
|
||||||
>
|
>
|
||||||
<div class="col row fit items-center">
|
<template #header>
|
||||||
<!-- avatar type icon section -->
|
<div class="col row items-center full-width">
|
||||||
<q-item-section avatar>
|
<!-- avatar type icon section -->
|
||||||
<q-icon
|
<div class="col-auto">
|
||||||
:name="getExpenseIcon(expense.type)"
|
<q-icon
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
:name="getExpenseIcon(expense.type)"
|
||||||
size="lg"
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
||||||
/>
|
size="lg"
|
||||||
</q-item-section>
|
class="q-pr-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<q-item-section class="col col-md-2 text-weight-bold">
|
<div class="col column">
|
||||||
<q-item-label v-if="expense.type === 'MILEAGE'">
|
<span
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
class="text-weight-bolder"
|
||||||
</q-item-label>
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
<q-item-label v-else>
|
style="font-size: 1.3em;"
|
||||||
$ {{ expense.amount.toFixed(2) }}
|
>
|
||||||
</q-item-label>
|
{{ expense.type === 'MILEAGE' ? `${Number(expense.mileage).toFixed(1)} km` : `$
|
||||||
|
${Number(expense.amount).toFixed(2)}` }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- date label -->
|
<!-- date label -->
|
||||||
<q-item-label
|
<span
|
||||||
caption
|
class="text-uppercase text-weight-light text-caption"
|
||||||
lines="1"
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
class="text-uppercase text-weight-light"
|
>
|
||||||
:class="approved_class"
|
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
||||||
>
|
month: 'short', day: 'numeric', weekday:
|
||||||
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
'long'
|
||||||
month: 'short', day: 'numeric', weekday:
|
}) }}
|
||||||
'long'
|
</span>
|
||||||
}) }}
|
</div>
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-space v-if="horizontal" />
|
<!-- attachment file icon -->
|
||||||
|
<div class="col row items-center justify-start">
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
:color="expense.is_approved ? 'white' : 'accent'"
|
||||||
|
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||||
|
class="col-auto q-px-sm q-mr-sm"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<q-item-label class="col">
|
||||||
<q-item-section avatar>
|
attachment_name.jpg
|
||||||
<q-btn
|
</q-item-label>
|
||||||
push
|
</div>
|
||||||
:color="expense.is_approved ? 'white' : 'accent'"
|
|
||||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
|
||||||
class="col-auto q-mx-sm q-px-sm q-pb-sm"
|
|
||||||
icon="attach_file"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-label class="col text-weight-light text-caption">
|
<!-- comment section -->
|
||||||
<span>attachment_goes_here.jpg</span>
|
<div class="col column">
|
||||||
</q-item-label>
|
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- comment section -->
|
<span
|
||||||
<q-item-section
|
class="col"
|
||||||
v-if="!horizontal"
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
top
|
style="font-size: 1.3em;"
|
||||||
>
|
>
|
||||||
<q-item-label
|
{{ expense.comment }}
|
||||||
lines="1"
|
</span>
|
||||||
class="text-weight-medium text-uppercase"
|
</div>
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
lines="1"
|
|
||||||
:class="approved_class"
|
|
||||||
>
|
|
||||||
{{ expense.comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
<!-- supervisor comment section -->
|
||||||
<q-item-section
|
<div
|
||||||
v-if="is_authorized_to_approve"
|
|
||||||
top
|
|
||||||
>
|
|
||||||
<q-item-label
|
|
||||||
lines="1"
|
|
||||||
class="text-weight-medium text-uppercase"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
v-if="expense.supervisor_comment"
|
v-if="expense.supervisor_comment"
|
||||||
caption
|
class="col column"
|
||||||
lines="2"
|
|
||||||
>
|
>
|
||||||
{{ expense.supervisor_comment }}
|
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
|
||||||
</q-item-label>
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
</q-item-section>
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="col"
|
||||||
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
|
style="font-size: 1.3em;"
|
||||||
|
>
|
||||||
|
{{ expense.supervisor_comment }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<q-item-section
|
<div class="col-auto">
|
||||||
:key="refresh_key"
|
<q-icon
|
||||||
side
|
v-if="expense.is_approved"
|
||||||
:class="is_current_expense ? 'invisible' : ''"
|
name="verified"
|
||||||
>
|
color="white"
|
||||||
<q-btn
|
size="lg"
|
||||||
flat
|
/>
|
||||||
dense
|
|
||||||
size="lg"
|
|
||||||
icon="edit"
|
|
||||||
color="accent"
|
|
||||||
:disable="expense.is_approved"
|
|
||||||
class="q-py-none z-top"
|
|
||||||
:class="expense.is_approved ? 'invisible no-pointer' : ''"
|
|
||||||
@click.stop="onUpdateClicked"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section
|
<q-btn
|
||||||
side
|
v-else
|
||||||
:class="is_current_expense ? 'invisible' : ''"
|
flat
|
||||||
>
|
dense
|
||||||
<q-icon
|
size="lg"
|
||||||
v-if="expense.is_approved"
|
icon="close"
|
||||||
name="verified"
|
color="negative"
|
||||||
color="white"
|
class="q-py-none q-my-xs"
|
||||||
size="lg"
|
@click.stop="requestExpenseDeletion"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<q-btn
|
<ExpenseDialogForm />
|
||||||
v-else
|
</q-expansion-item>
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="lg"
|
|
||||||
icon="close"
|
|
||||||
color="negative"
|
|
||||||
class="q-py-none z-top q-my-xs"
|
|
||||||
@click.stop="requestExpenseDeletion"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-slide-transition
|
|
||||||
@hide="expenses_store.mode === 'update' ? null : expenses_store.is_hiding_create_form = false"
|
|
||||||
:duration="200"
|
|
||||||
>
|
|
||||||
<ExpenseDialogForm
|
|
||||||
v-if="is_current_expense && expenses_store.is_hiding_create_form"
|
|
||||||
@on-click-update-cancel="onUpdateClicked"
|
|
||||||
@on-click-save-updates="onSaveUpdatesClicked"
|
|
||||||
/>
|
|
||||||
</q-slide-transition>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -42,19 +42,14 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<ExpenseDialogListItemMobile
|
<ExpenseDialogListItemMobile
|
||||||
v-if="$q.screen.lt.md"
|
v-if="$q.platform.is.mobile"
|
||||||
v-model="expense.is_approved"
|
v-model="expenses_list[index]!"
|
||||||
:index="index"
|
|
||||||
:expense="expense"
|
|
||||||
:horizontal="horizontal"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExpenseDialogListItem
|
<ExpenseDialogListItem
|
||||||
v-else
|
v-else
|
||||||
v-model="expense.is_approved"
|
v-model="expenses_list[index]!"
|
||||||
:index="index"
|
:index="index"
|
||||||
:expense="expense"
|
|
||||||
:horizontal="horizontal"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,37 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
|
||||||
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||||
|
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||||
|
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
const expense_store = useExpensesStore();
|
||||||
|
|
||||||
|
const { isApproved = false} = defineProps<{
|
||||||
|
isApproved?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onClickExpenseCreate = () => {
|
||||||
|
expense_store.mode = 'create';
|
||||||
|
expense_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="expense_store.is_open"
|
v-model="expense_store.is_open"
|
||||||
persistent
|
persistent
|
||||||
|
:full-width="$q.platform.is.mobile"
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa-none rounded-10 shadow-10"
|
class="q-pa-none rounded-10 shadow-24 bg-secondary"
|
||||||
:class="$q.screen.lt.md ? ' bg-primary' : 'bg-secondary'"
|
|
||||||
style=" min-width: 70vw;"
|
style=" min-width: 70vw;"
|
||||||
:style="$q.dark.isActive ? 'border: solid 2px var(--q-accent);' : ''"
|
:style="$q.dark.isActive ? 'border: solid 2px var(--q-accent);' : ''"
|
||||||
>
|
>
|
||||||
|
|
@ -29,26 +41,37 @@
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<!-- <q-banner
|
|
||||||
v-if="expenses_error"
|
|
||||||
dense
|
|
||||||
class="bg-red-2 col-auto text-negative q-mt-sm"
|
|
||||||
>
|
|
||||||
{{ expenses_error }}
|
|
||||||
</q-banner> -->
|
|
||||||
|
|
||||||
<ExpenseDialogHeader />
|
<ExpenseDialogHeader />
|
||||||
|
|
||||||
<ExpenseDialogList />
|
<ExpenseDialogList />
|
||||||
|
|
||||||
<q-separator v-if="$q.screen.lt.md" spaced color="accent" size="2px" class="q-mx-md" />
|
<q-expansion-item
|
||||||
|
v-if="!isApproved"
|
||||||
|
v-model="expense_store.is_showing_create_form"
|
||||||
|
hide-expand-icon
|
||||||
|
:dense="!$q.platform.is.mobile"
|
||||||
|
group="expenses"
|
||||||
|
@show="onClickExpenseCreate()"
|
||||||
|
header-class="bg-accent text-white"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="row items-center">
|
||||||
|
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-lg q-mr-sm">
|
||||||
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
|
</span>
|
||||||
|
<q-icon
|
||||||
|
v-if="expense_store.mode !== 'create'"
|
||||||
|
name="las la-plus-square"
|
||||||
|
size="md"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<q-slide-transition @hide="expense_store.is_hiding_create_form = true" :duration="200">
|
<ExpenseDialogFormMobile v-if="$q.platform.is.mobile" />
|
||||||
<div v-if="!expense_store.current_expense.is_approved && expense_store.mode !== 'update' && expense_store.is_hiding_create_form === false">
|
|
||||||
<ExpenseDialogFormMobile v-if="$q.screen.lt.md" />
|
<ExpenseDialogForm v-else />
|
||||||
<ExpenseDialogForm v-else/>
|
</q-expansion-item>
|
||||||
</div>
|
|
||||||
</q-slide-transition>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
interface ExpenseOption {
|
interface ExpenseOption {
|
||||||
|
|
@ -51,31 +51,18 @@
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||||
};
|
};
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
'onClickUpdateCancel': [void];
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||||
:key="expenses_store.current_expense.id"
|
|
||||||
flat
|
flat
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
class="column full-width"
|
class="column full-width"
|
||||||
>
|
>
|
||||||
<!-- header -->
|
|
||||||
<div
|
|
||||||
class="col text-uppercase text-weight-medium text-h6 q-ma-xs"
|
|
||||||
:class="expenses_store.mode === 'create' ? 'q-px-md' : 'invisible'"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col column items-start rounded-5 q-pb-sm"
|
class="col column items-start rounded-5 q-pb-sm"
|
||||||
:class="expenses_store.mode === 'create' ? 'q-px-md' : ''"
|
:class="expenses_store.is_showing_create_form ? 'q-px-md' : 'q-px-sm'"
|
||||||
>
|
>
|
||||||
<!-- date and type row -->
|
<!-- date and type row -->
|
||||||
<div class="col row q-my-xs full-width">
|
<div class="col row q-my-xs full-width">
|
||||||
|
|
@ -83,7 +70,6 @@
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
type="date"
|
|
||||||
outlined
|
outlined
|
||||||
readonly
|
readonly
|
||||||
stack-label
|
stack-label
|
||||||
|
|
@ -189,7 +175,6 @@
|
||||||
:input-style="'font-size: 1.2em;'"
|
:input-style="'font-size: 1.2em;'"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.amountRequired]"
|
:rules="[rules.amountRequired]"
|
||||||
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
|
@ -287,27 +272,15 @@
|
||||||
</template>
|
</template>
|
||||||
</q-file>
|
</q-file>
|
||||||
</div>
|
</div>
|
||||||
<div class="col row full-width items-center">
|
|
||||||
<q-space />
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-if="expenses_store.mode === 'update'"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
class="col-auto"
|
|
||||||
icon="clear"
|
|
||||||
color="negative"
|
|
||||||
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
|
||||||
@click="$emit('onClickUpdateCancel')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<div class="col row full-width items-center" :class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
color="accent"
|
color="accent"
|
||||||
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||||
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
class="q-px-sm"
|
class="q-px-sm full-width"
|
||||||
:class="expenses_store.mode === 'create' ? 'q-mr-md q-mb-md' : 'q-mb-sm q-ml-lg'"
|
:class="expenses_store.mode === 'create' ? '' : 'q-mb-sm'"
|
||||||
type="submit"
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,72 +2,48 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||||
|
|
||||||
const { expense, horizontal = false } = defineProps<{
|
const expense = defineModel<Expense>({ required: true })
|
||||||
expense: Expense;
|
|
||||||
index: number;
|
|
||||||
horizontal?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
const refresh_key = ref(1);
|
const approved_class = computed(() => expense.value.is_approved ? ' bg-accent text-white' : '')
|
||||||
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
|
||||||
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
|
||||||
const is_showing_update_form = ref(false);
|
const is_showing_update_form = ref(false);
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
await expenses_api.deleteExpenseById(expense.id);
|
await expenses_api.deleteExpenseById(expense.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateClicked = () => {
|
const onUpdateClicked = () => {
|
||||||
if (expense.is_approved) return;
|
if (expense.value.is_approved) return;
|
||||||
|
|
||||||
if (deepEqual(expense, expenses_store.current_expense)) {
|
|
||||||
expenses_store.mode = 'create';
|
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
is_showing_update_form.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
expenses_store.mode = 'update';
|
expenses_store.mode = 'update';
|
||||||
expenses_store.current_expense = expense;
|
expenses_store.current_expense = expense.value;
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
expenses_store.initial_expense = unwrapAndClone(expense.value);
|
||||||
is_showing_update_form.value = true;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column bg-dark rounded-5 q-my-sm full-width">
|
<div class="column bg-dark shadow-5 rounded-5 q-my-sm full-width">
|
||||||
<q-slide-item
|
<q-expansion-item
|
||||||
right-color="negative"
|
v-model="is_showing_update_form"
|
||||||
class="rounded-5 bg-dark full-width"
|
hide-expand-icon
|
||||||
@right="requestExpenseDeletion"
|
dense
|
||||||
|
group="expenses"
|
||||||
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
|
@before-show="onUpdateClicked()"
|
||||||
>
|
>
|
||||||
<template
|
<template #header>
|
||||||
#right
|
|
||||||
v-if="$q.screen.lt.md && !expenses_store.is_hiding_create_form && !expense.is_approved"
|
|
||||||
>
|
|
||||||
<q-icon name="delete" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-item
|
|
||||||
:key="refresh_key"
|
|
||||||
clickable
|
|
||||||
class="row q-py-none q-pa-xs rounded-5 full-width"
|
|
||||||
:class="background_class + approved_class"
|
|
||||||
@click="onUpdateClicked"
|
|
||||||
>
|
|
||||||
<div class="column col">
|
<div class="column col">
|
||||||
<!-- date label -->
|
<!-- date label -->
|
||||||
<div class="col-auto row items-center q-pl-xs">
|
<div class="col-auto row items-center q-pl-xs">
|
||||||
|
|
@ -94,22 +70,21 @@
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<q-item-section class="col text-weight-bold text-h6">
|
<div class="col text-weight-bold text-h6">
|
||||||
<q-item-label v-if="expense.type === 'MILEAGE'">
|
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label v-else>
|
<q-item-label v-else>
|
||||||
$ {{ expense.amount.toFixed(2) }}
|
$ {{ expense.amount.toFixed(2) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</div>
|
||||||
|
|
||||||
<q-space v-if="horizontal" />
|
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<!-- attachment file icon -->
|
||||||
<q-item-section avatar>
|
<div class="col-auto q-px-xs">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
:color="expense.is_approved ? 'white' : 'accent'"
|
:color="expense.is_approved ? 'white' : 'accent'"
|
||||||
|
|
@ -117,34 +92,24 @@
|
||||||
class="col-auto q-mx-sm q-px-sm q-pb-sm"
|
class="col-auto q-mx-sm q-px-sm q-pb-sm"
|
||||||
icon="attach_file"
|
icon="attach_file"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-icon
|
||||||
|
v-if="expense.is_approved"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
|
size="lg"
|
||||||
|
class="full-height"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<div class="q-px-sm">
|
||||||
class="col-auto q-px-sm"
|
<ExpenseDialogFormMobile />
|
||||||
:class="expense.is_approved ? '' : 'invisible'"
|
</div>
|
||||||
>
|
</q-expansion-item>
|
||||||
<q-icon
|
|
||||||
v-if="expense.is_approved"
|
|
||||||
name="verified"
|
|
||||||
color="white"
|
|
||||||
size="lg"
|
|
||||||
class="full-height"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item>
|
|
||||||
</q-slide-item>
|
|
||||||
|
|
||||||
<q-slide-transition
|
|
||||||
@hide="expenses_store.is_hiding_create_form = false"
|
|
||||||
:duration="200"
|
|
||||||
>
|
|
||||||
<ExpenseDialogFormMobile
|
|
||||||
v-if="is_showing_update_form && expenses_store.is_hiding_create_form"
|
|
||||||
class="q-mt-sm q-pa-sm"
|
|
||||||
@on-click-update-cancel="onUpdateClicked"
|
|
||||||
/>
|
|
||||||
</q-slide-transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -8,23 +8,13 @@
|
||||||
import { QSelect, QInput } from 'quasar';
|
import { QSelect, QInput } from 'quasar';
|
||||||
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useShiftRules } from 'src/modules/timesheets/utils/shift.util';
|
import { useShiftRules, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'),);
|
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
const SHIFT_OPTIONS: ShiftOption[] = [
|
|
||||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
|
|
||||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
|
||||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
|
||||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
|
||||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
|
||||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
|
|
@ -32,15 +22,19 @@
|
||||||
const start_time_ref = useTemplateRef<QInput>('start_time');
|
const start_time_ref = useTemplateRef<QInput>('start_time');
|
||||||
const end_time_ref = useTemplateRef<QInput>('end_time');
|
const end_time_ref = useTemplateRef<QInput>('end_time');
|
||||||
|
|
||||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
const { dayShifts = [], dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||||
|
dayShifts: Shift[];
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
hasShiftAfter?: boolean;
|
hasShiftAfter?: boolean;
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'), t('timesheet.errors.SHIFT_OVERLAP_SHORT'), dayShifts);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'saveComment': [comment: string, shift_id: number];
|
'saveComment': [comment: string, shift_id: number];
|
||||||
'requestDelete': [void];
|
'requestDelete': [void];
|
||||||
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const onBlurShiftTypeSelect = () => {
|
const onBlurShiftTypeSelect = () => {
|
||||||
|
|
@ -98,7 +92,7 @@
|
||||||
|
|
||||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
||||||
<div
|
<div
|
||||||
class="row items-start text-uppercase rounded-5"
|
class="row items-center text-uppercase rounded-5"
|
||||||
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
||||||
>
|
>
|
||||||
<!-- mobile comment button -->
|
<!-- mobile comment button -->
|
||||||
|
|
@ -168,7 +162,7 @@
|
||||||
menu-anchor="bottom middle"
|
menu-anchor="bottom middle"
|
||||||
menu-self="top middle"
|
menu-self="top middle"
|
||||||
:options="SHIFT_OPTIONS"
|
:options="SHIFT_OPTIONS"
|
||||||
class="col rounded-5 q-mx-xs bg-dark q-pt-xs"
|
class="col rounded-5 q-mx-xs bg-dark"
|
||||||
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
|
@ -178,23 +172,68 @@
|
||||||
>
|
>
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
:tabindex="scope.tabindex"
|
:tabindex="scope.tabindex"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="scope.opt.icon"
|
:name="scope.opt.icon"
|
||||||
:color="scope.opt.icon_color"
|
:color="scope.opt.icon_color"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="col-auto q-mx-xs"
|
class="col-auto"
|
||||||
|
:class="shift.is_approved ? 'q-mx-xs': 'q-mr-xs'"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
style="line-height: 0.9em;"
|
style="line-height: 1.2em;"
|
||||||
class="col-auto ellipsis"
|
class="col-auto ellipsis"
|
||||||
:class="!shift.is_approved ? '' : 'text-white'"
|
:class="!shift.is_approved ? '' : 'text-white'"
|
||||||
>{{ scope.opt.label }}</span>
|
>
|
||||||
|
{{ $t(scope.opt.label) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #after>
|
||||||
|
<q-icon
|
||||||
|
v-if="shift.is_approved"
|
||||||
|
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||||
|
size="1.2em"
|
||||||
|
color="white"
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
v-else
|
||||||
|
v-model="shift.is_remote"
|
||||||
|
:disable="shift.is_approved"
|
||||||
|
dense
|
||||||
|
keep-color
|
||||||
|
size="3em"
|
||||||
|
color="accent"
|
||||||
|
icon="las la-building"
|
||||||
|
checked-icon="las la-laptop"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-toggle>
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -212,13 +251,14 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
no-error-icon
|
no-error-icon
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
:rules="[shift_rules.isTimeRequired]"
|
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
class="col rounded-5 bg-dark"
|
class="col rounded-5 bg-dark"
|
||||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
input-style="font-size: 1.2em;"
|
input-style="font-size: 1.2em;"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="emit('onTimeFieldBlur')"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
|
|
@ -242,13 +282,14 @@
|
||||||
lazy-rules
|
lazy-rules
|
||||||
no-error-icon
|
no-error-icon
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
:rules="[shift_rules.isTimeRequired]"
|
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
input-style="font-size: 1.2em;"
|
input-style="font-size: 1.2em;"
|
||||||
class="col rounded-5 bg-dark"
|
class="col rounded-5 bg-dark"
|
||||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="emit('onTimeFieldBlur')"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span
|
<span
|
||||||
|
|
@ -260,26 +301,24 @@
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<!-- comment and delete buttons -->
|
<!-- comment and delete buttons -->
|
||||||
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto self-start'">
|
<div class="row full-height" :class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'">
|
||||||
<q-icon
|
|
||||||
v-if="shift.type && dense"
|
|
||||||
:name="shift.comment ? 'comment' : ''"
|
|
||||||
color="primary"
|
|
||||||
:size="dense ? 'xs' : 'sm'"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- desktop comment button -->
|
<!-- desktop comment button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else-if="!ui_store.is_mobile_mode"
|
v-if="!ui_store.is_mobile_mode"
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
:color="shift.is_approved ? 'accent' : 'dark'"
|
:color="shift.is_approved ? 'white' : 'accent'"
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
:text-color="shift.is_approved ? '' : (shift.comment ? 'accent' : 'grey-5')"
|
:text-color="shift.is_approved ? 'accent' : 'white'"
|
||||||
class="col"
|
class="col"
|
||||||
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
||||||
>
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="shift.comment"
|
||||||
|
floating
|
||||||
|
rounded
|
||||||
|
color="negative"
|
||||||
|
/>
|
||||||
<q-popup-edit
|
<q-popup-edit
|
||||||
v-model="shift.comment"
|
v-model="shift.comment"
|
||||||
:title="$t('timesheet.shift.fields.header_comment')"
|
:title="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
|
@ -327,23 +366,18 @@
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!ui_store.is_mobile_mode"
|
v-if="!ui_store.is_mobile_mode && !shift.is_approved"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
:disable="shift.is_approved"
|
:disable="shift.is_approved"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
icon="cancel"
|
icon="las la-trash"
|
||||||
text-color="negative"
|
text-color="negative"
|
||||||
class="col"
|
class="col"
|
||||||
|
size="1.2em"
|
||||||
:class="shift.is_approved ? 'invisible' : ''"
|
:class="shift.is_approved ? 'invisible' : ''"
|
||||||
@click="$emit('requestDelete')"
|
@click="$emit('requestDelete')"
|
||||||
>
|
>
|
||||||
<q-badge
|
|
||||||
v-if="!shift.is_approved"
|
|
||||||
color="white"
|
|
||||||
class="absolute"
|
|
||||||
style="z-index: -1;"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,26 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
|
||||||
const shift_api = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
const { day, dense = false, approved = false } = defineProps<{
|
const { day, dense = false, approved = false } = defineProps<{
|
||||||
|
timesheetId: number;
|
||||||
|
weekDayIndex: number;
|
||||||
day: TimesheetDay;
|
day: TimesheetDay;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
approved?: boolean;
|
approved?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const preset_mouseover = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'deleteUnsavedShift': [void];
|
'deleteUnsavedShift': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -30,15 +38,46 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column justify-center q-py-xs" :class="approved ? '' : ''">
|
<div
|
||||||
<ShiftListDayRow
|
class="column justify-center q-py-xs"
|
||||||
v-for="shift, shift_index in day.shifts"
|
:class="approved ? '' : ''"
|
||||||
:key="shift_index"
|
@mouseenter="preset_mouseover = true"
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
@mouseleave="preset_mouseover = false"
|
||||||
:is-timesheet-approved="approved"
|
>
|
||||||
:dense="dense"
|
<!-- Button to apply preset to day -->
|
||||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
<transition
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
appear
|
||||||
/>
|
enter-active-class="animated zoomIn fast"
|
||||||
|
leave-active-class="animated zoomOut fast"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="!$q.platform.is.mobile && day.shifts.length < 1 && preset_mouseover"
|
||||||
|
:disable="day.shifts.length > 0"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
:label="$t('timesheet.apply_preset_day')"
|
||||||
|
class="text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||||
|
style="opacity: 0.6;"
|
||||||
|
@click.stop="timesheet_api.applyPreset(timesheetId, weekDayIndex, day.date)"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="las la-calendar-day"
|
||||||
|
color="accent"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<ShiftListDayRow
|
||||||
|
v-for="shift, shift_index in day.shifts"
|
||||||
|
:key="shift_index"
|
||||||
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
|
:day-shifts="day.shifts"
|
||||||
|
:is-timesheet-approved="approved"
|
||||||
|
:dense="dense"
|
||||||
|
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||||
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,21 +2,29 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { computed } from 'vue';
|
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
||||||
|
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
||||||
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
import type { QScrollArea } from 'quasar';
|
||||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
|
||||||
const { extractDate } = date;
|
const { extractDate } = date;
|
||||||
|
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
const animation_style = computed(() => ui_store.is_mobile_mode ? 'fadeInLeft' : 'fadeInDown' );
|
const mobile_animation_direction = ref('fadeInLeft');
|
||||||
|
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
||||||
|
|
||||||
|
const timesheet_page = ref<QScrollArea | null>(null);
|
||||||
|
const scroll_y = computed(() => timesheet_page.value?.getScrollPosition().top ?? 0)
|
||||||
|
|
||||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||||
ui_store.focus_next_component = true;
|
ui_store.focus_next_component = true;
|
||||||
|
|
@ -38,141 +46,233 @@
|
||||||
if (day.shifts.length < 1) return false;
|
if (day.shifts.length < 1) return false;
|
||||||
return day.shifts.every(shift => shift.is_approved === true);
|
return day.shifts.every(shift => shift.is_approved === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: {x?: number, y?: number}) => {
|
||||||
|
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||||
|
if (distance.x && Math.abs(distance.x) > 10 ) {
|
||||||
|
await timesheet_api.getTimesheetsBySwiping( direction === 'left' ? 1 : -1 )
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="col column fit relative-position" v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? {x: 0, y: 0})">
|
||||||
:class="$q.screen.lt.md ? 'column full-width' : 'row'"
|
<q-scroll-area
|
||||||
>
|
ref="timesheet_page"
|
||||||
<div
|
:horizontal-offset="[0, 3]"
|
||||||
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
class="absolute-full hide-scrollbar q-mt-sm"
|
||||||
:key="timesheet.timesheet_id"
|
:thumb-style="{ opacity: '0' }"
|
||||||
class="col column"
|
:bar-style="{ opacity: '0' }"
|
||||||
|
style="min-height: 50vh;"
|
||||||
>
|
>
|
||||||
<transition-group
|
<!-- Show if no timesheets found (further than one month from present) -->
|
||||||
appear
|
<div
|
||||||
:enter-active-class="`animated ${animation_style}`"
|
v-if="timesheet_store.timesheets.length < 1"
|
||||||
|
class="col-auto column flex-center fit q-py-lg"
|
||||||
|
style="min-height: 20vh;"
|
||||||
|
>
|
||||||
|
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
|
||||||
|
}}</span>
|
||||||
|
<q-icon
|
||||||
|
name="las la-calendar"
|
||||||
|
color="accent"
|
||||||
|
size="10em"
|
||||||
|
class="absolute"
|
||||||
|
style="opacity: 0.2;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Else show timesheets if found -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col fit"
|
||||||
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="day, day_index in timesheet.days"
|
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
|
||||||
:key="day.date"
|
:key="timesheet.timesheet_id"
|
||||||
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
class="col fit"
|
||||||
:style="`animation-delay: ${day_index / 15}s;`"
|
|
||||||
>
|
>
|
||||||
<div
|
<transition
|
||||||
v-if="ui_store.is_mobile_mode"
|
appear
|
||||||
class="col column full-width"
|
enter-active-class="animated fadeInDown"
|
||||||
|
leave-active-class="animated fadeOutUp"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-btn
|
||||||
class="rounded-10"
|
v-if="!$q.platform.is.mobile"
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
flat
|
||||||
|
dense
|
||||||
|
:label="$t('timesheet.apply_preset_week')"
|
||||||
|
class="text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||||
|
:class="timesheet.days.every(day => day.shifts.length < 1) ? '' : 'invisible'"
|
||||||
|
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
|
||||||
style="line-height: 1em;"
|
|
||||||
>
|
|
||||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
|
||||||
weekday: 'long', day: 'numeric', month:
|
|
||||||
'long'
|
|
||||||
}) }}</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
|
||||||
class="q-pa-none transparent"
|
|
||||||
>
|
|
||||||
<ShiftListDay
|
|
||||||
outlined
|
|
||||||
:animation-delay-multiplier="day_index"
|
|
||||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
:day="day"
|
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions class="q-pa-none">
|
|
||||||
<q-btn
|
|
||||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
square
|
|
||||||
color="accent"
|
|
||||||
icon="more_time"
|
|
||||||
class="full-width"
|
|
||||||
style="border-radius: 0 0 5px 5px;"
|
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
|
|
||||||
<q-badge
|
|
||||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
floating
|
|
||||||
class="transparent q-pa-none rounded-50"
|
|
||||||
style="transform: translate(15px, -5px);"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="verified"
|
|
||||||
size="5em"
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
</q-badge>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="col row full-width"
|
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
|
||||||
>
|
|
||||||
<!-- List of shifts -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="col row bg-dark"
|
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
|
||||||
style="border-radius: 10px 0 0 10px;"
|
|
||||||
>
|
|
||||||
<!-- Date block -->
|
|
||||||
<ShiftListDateWidget
|
|
||||||
:display-date="day.date"
|
|
||||||
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<ShiftListDay
|
|
||||||
:day="day"
|
|
||||||
:approved="getDayApproval(day) || timesheet.is_approved"
|
|
||||||
class="col"
|
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col-auto self-stretch">
|
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
name="las la-calendar-week"
|
||||||
name="verified"
|
|
||||||
color="white"
|
|
||||||
size="xl"
|
|
||||||
class="full-height"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
:dense="!ui_store.is_mobile_mode"
|
|
||||||
icon="more_time"
|
|
||||||
size="lg"
|
|
||||||
color="accent"
|
color="accent"
|
||||||
text-color="white"
|
size="md"
|
||||||
class="full-height"
|
|
||||||
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
|
||||||
style="border-radius: 0 10px 10px 0;"
|
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
|
||||||
/>
|
/>
|
||||||
|
</q-btn>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<transition-group
|
||||||
|
appear
|
||||||
|
:enter-active-class="`animated ${animation_style}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="day, day_index in timesheet.days"
|
||||||
|
:key="day.date"
|
||||||
|
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
||||||
|
:style="`animation-delay: ${day_index / 15}s;`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||||
|
class="col column full-width"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="rounded-10"
|
||||||
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||||
|
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
||||||
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
||||||
|
style="line-height: 1em;"
|
||||||
|
>
|
||||||
|
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||||
|
weekday: 'long', day: 'numeric', month:
|
||||||
|
'long'
|
||||||
|
}) }}</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||||
|
class="q-pa-none transparent"
|
||||||
|
>
|
||||||
|
<ShiftListDay
|
||||||
|
outlined
|
||||||
|
:timesheet-id="timesheet.timesheet_id"
|
||||||
|
:week-day-index="day_index"
|
||||||
|
:animation-delay-multiplier="day_index"
|
||||||
|
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
:day="day"
|
||||||
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions class="q-pa-none">
|
||||||
|
<q-btn
|
||||||
|
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
square
|
||||||
|
color="accent"
|
||||||
|
icon="more_time"
|
||||||
|
class="full-width"
|
||||||
|
style="border-radius: 0 0 5px 5px;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
floating
|
||||||
|
class="transparent q-pa-none rounded-50"
|
||||||
|
style="transform: translate(15px, -5px);"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="verified"
|
||||||
|
size="5em"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col row full-width"
|
||||||
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||||
|
>
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated fadeInDown"
|
||||||
|
leave-active-class="animated fadeOutUp"
|
||||||
|
>
|
||||||
|
</transition>
|
||||||
|
<!-- List of shifts -->
|
||||||
|
<div
|
||||||
|
class="col row bg-dark"
|
||||||
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
||||||
|
style="border-radius: 10px 0 0 10px;"
|
||||||
|
>
|
||||||
|
<!-- Date block -->
|
||||||
|
<ShiftListDateWidget
|
||||||
|
:display-date="day.date"
|
||||||
|
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShiftListDay
|
||||||
|
:timesheet-id="timesheet.timesheet_id"
|
||||||
|
:week-day-index="day_index"
|
||||||
|
:day="day"
|
||||||
|
:approved="getDayApproval(day) || timesheet.is_approved"
|
||||||
|
class="col"
|
||||||
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-auto self-stretch">
|
||||||
|
<q-icon
|
||||||
|
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
|
size="xl"
|
||||||
|
class="full-height"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
:dense="!ui_store.is_mobile_mode"
|
||||||
|
icon="more_time"
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
text-color="white"
|
||||||
|
class="full-height"
|
||||||
|
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
||||||
|
style="border-radius: 0 10px 10px 0;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</div>
|
||||||
</div>
|
|
||||||
|
<q-page-sticky
|
||||||
|
position="bottom-right"
|
||||||
|
:offset="[0, -35]"
|
||||||
|
class="z-top"
|
||||||
|
>
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated zoomIn"
|
||||||
|
leave-active-class="animated zoomOut"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="scroll_y > 400"
|
||||||
|
fab
|
||||||
|
icon="las la-chevron-up"
|
||||||
|
color="white"
|
||||||
|
text-color="accent"
|
||||||
|
class="shadow-12"
|
||||||
|
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</q-page-sticky>
|
||||||
|
</q-scroll-area>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -9,11 +9,12 @@
|
||||||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
|
|
@ -22,93 +23,90 @@
|
||||||
const shift_api = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
const has_shift_errors = computed(() => timesheet_store.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
const has_shift_errors = computed(() => timesheet_store.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
|
||||||
|
|
||||||
|
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
|
||||||
|
|
||||||
const { mode = 'normal' } = defineProps<{
|
const { mode = 'normal' } = defineProps<{
|
||||||
mode?: 'approval' | 'normal';
|
mode?: 'approval' | 'normal';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column flex-center full-width">
|
<div class="column items-center full-height">
|
||||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||||
|
|
||||||
<q-card
|
<div
|
||||||
flat
|
class="col-auto row items-center full-width"
|
||||||
class="transparent full-width"
|
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-mt-md q-px-md'"
|
||||||
>
|
>
|
||||||
<q-card-section
|
<!-- navigation btn -->
|
||||||
:horizontal="$q.screen.gt.sm"
|
<PayPeriodNavigator
|
||||||
class="q-px-md items-center q-mb-md"
|
v-if="mode === 'normal'"
|
||||||
:class="$q.screen.lt.md ? 'column' : ''"
|
class="col-auto"
|
||||||
>
|
@date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value)"
|
||||||
<!-- navigation btn -->
|
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||||
<PayPeriodNavigator
|
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
||||||
v-if="mode === 'normal'"
|
/>
|
||||||
@date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value)"
|
|
||||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
|
||||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- mobile expenses button -->
|
<!-- mobile expenses button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.screen.lt.md && mode === 'normal'"
|
v-if="($q.platform.is.mobile && ($q.screen.width < $q.screen.height))"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet_approvals.table.expenses')"
|
||||||
class="q-mt-sm"
|
class="col-auto"
|
||||||
@click="expenses_store.open"
|
@click="expenses_store.open"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-space />
|
<q-space v-if="!$q.platform.is.mobile" />
|
||||||
|
|
||||||
<!-- save timesheet changes button -->
|
<!-- desktop save timesheet changes button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="mode === 'normal'"
|
v-if="mode === 'normal' && !is_timesheets_approved && !$q.platform.is.mobile"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
:label="$t('shared.label.save')"
|
:label="$t('shared.label.save')"
|
||||||
class="q-mr-md"
|
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-mr-md'"
|
||||||
@click="shift_api.saveShiftChanges"
|
@click="shift_api.saveShiftChanges"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- desktop expenses button -->
|
<!-- desktop expenses button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="mode === 'normal'"
|
v-if="mode === 'normal' && $q.screen.width > $q.screen.height"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="expenses_store.open"
|
@click="expenses_store.open"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</q-card-section>
|
<TimesheetErrorWidget class="col-auto"/>
|
||||||
|
|
||||||
|
<ShiftList />
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="mode === 'approval' || $q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
:disable="timesheet_store.is_loading"
|
||||||
|
color="accent"
|
||||||
|
icon="upload"
|
||||||
|
:label="$t('shared.label.save')"
|
||||||
|
class="col-auto"
|
||||||
|
:class="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'full-width q-mt-sm' : 'q-mr-md'"
|
||||||
|
@click="shift_api.saveShiftChanges"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-card-section class="q-pa-none">
|
<ExpenseDialog :is-approved="is_timesheets_approved" />
|
||||||
<TimesheetErrorWidget />
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<ShiftList :mode="mode" />
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn
|
|
||||||
v-if="mode === 'approval'"
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
:disable="timesheet_store.is_loading"
|
|
||||||
color="accent"
|
|
||||||
icon="upload"
|
|
||||||
:label="$t('shared.label.save')"
|
|
||||||
class="q-mr-md"
|
|
||||||
@click="shift_api.saveShiftChanges"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
<ExpenseDialog />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { useExpensesStore } from "src/stores/expense-store";
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
import { date } from "quasar";
|
||||||
|
|
||||||
export const useExpensesApi = () => {
|
export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
|
|
@ -10,6 +11,7 @@ export const useExpensesApi = () => {
|
||||||
const upsertExpense = async (expense: Expense): Promise<void> => {
|
const upsertExpense = async (expense: Expense): Promise<void> => {
|
||||||
const success = await expenses_store.upsertExpense(expense);
|
const success = await expenses_store.upsertExpense(expense);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
|
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||||
|
|
||||||
export const useTimesheetApi = () => {
|
export const useTimesheetApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -17,7 +18,7 @@ export const useTimesheetApi = () => {
|
||||||
|
|
||||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||||
if (timesheet_store.pay_period === undefined) return false;
|
if (timesheet_store.pay_period === undefined) return false;
|
||||||
|
|
||||||
timesheet_store.is_loading = true;
|
timesheet_store.is_loading = true;
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
||||||
|
|
||||||
|
|
@ -29,8 +30,41 @@ export const useTimesheetApi = () => {
|
||||||
timesheet_store.is_loading = false;
|
timesheet_store.is_loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string) => {
|
||||||
|
if (timesheet_store.timesheets.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (week_day_index && date)
|
||||||
|
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date);
|
||||||
|
else
|
||||||
|
response = await timesheetService.applyPresetToWeek(timesheet_id);
|
||||||
|
|
||||||
|
if (response.success)
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying weekly timesheet: ', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimesheetsBySwiping = async( direction: number ) => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
|
||||||
|
timesheet_store.getNextOrPreviousPayPeriod(direction);
|
||||||
|
await timesheet_store.getPayPeriodByDateOrYearAndNumber();
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTimesheetsByDate,
|
getTimesheetsByDate,
|
||||||
getTimesheetsByCurrentPayPeriod,
|
getTimesheetsByCurrentPayPeriod,
|
||||||
|
getTimesheetsBySwiping,
|
||||||
|
applyPreset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -14,7 +14,6 @@ export const ShiftService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
|
updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
|
||||||
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.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,41 @@ import { api } from "src/boot/axios";
|
||||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||||
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
|
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
|
||||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
|
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
|
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
|
||||||
const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/date/${date_string}`);
|
const response = await api.get<{ success: boolean, data: PayPeriod, error?: string }>(`pay-periods/date/${date_string}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
|
getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
|
||||||
const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/${year}/${period_number}`);
|
const response = await api.get<{ success: boolean, data: PayPeriod, error?: string }>(`pay-periods/${year}/${period_number}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
|
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
|
||||||
const response = await api.get<{success: boolean, data: TimesheetOverview[], error? : string}>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
const response = await api.get<{ success: boolean, data: TimesheetOverview[], error?: string }>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<TimesheetResponse> => {
|
getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<BackendResponse<TimesheetResponse>> => {
|
||||||
if (employee_email !== undefined) {
|
if (employee_email !== undefined) {
|
||||||
const response = await api.get<{success: boolean, data: TimesheetResponse, error? : string}>(`timesheets/${year}/${period_number}?employee_email=${employee_email}`);
|
const response = await api.get<BackendResponse<TimesheetResponse>>(`timesheets/${year}/${period_number}?employee_email=${employee_email}`);
|
||||||
return response.data.data;
|
return response.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await api.get<{success: boolean, data: TimesheetResponse, error? : string}>(`timesheets/${year}/${period_number}`);
|
const response = await api.get<BackendResponse<TimesheetResponse>>(`timesheets/${year}/${period_number}`);
|
||||||
return response.data.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
applyPresetToWeek: async (timesheet_id: number): Promise<BackendResponse<boolean>> => {
|
||||||
|
const response = await api.post<BackendResponse<boolean>>(`schedule-presets/apply-preset`, { timesheet_id });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyPresetToDay: async (timesheet_id: number, week_day_index: number, date: string): Promise<BackendResponse<boolean>> => {
|
||||||
|
const response = await api.post<BackendResponse<boolean>>('schedule-presets/apply-day-preset', { timesheet_id, week_day_index, date });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||||
export const getExpenseIcon = (type: ExpenseType) => {
|
export const getExpenseIcon = (type: ExpenseType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'MILEAGE': return 'time_to_leave';
|
case 'MILEAGE': return 'time_to_leave';
|
||||||
case 'EXPENSES': return 'receipt_long';
|
case 'EXPENSES': return 'las la-coins';
|
||||||
case 'PER_DIEM': return 'hotel';
|
case 'PER_DIEM': return 'hotel';
|
||||||
case 'ON_CALL': return 'phone_android';
|
case 'ON_CALL': return 'phone_android';
|
||||||
default: return 'help_outline';
|
default: return 'help_outline';
|
||||||
|
|
@ -15,7 +15,7 @@ export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.hints.comment_required');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
typeRequired,
|
typeRequired,
|
||||||
|
|
@ -23,21 +23,4 @@ export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
mileageRequired,
|
mileageRequired,
|
||||||
commentRequired,
|
commentRequired,
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
export const convertToMonetaryAmount = (amount: number | string): number => {
|
|
||||||
if (typeof amount === 'number') return Number(amount.toFixed(2));
|
|
||||||
|
|
||||||
if (typeof amount === 'string') {
|
|
||||||
try {
|
|
||||||
const single_decimal_amount = amount.replace(/\.(?=.*\.)/g, '');
|
|
||||||
const numbers_only_decimal = single_decimal_amount.replace(/[^0-9.]/g, '');
|
|
||||||
|
|
||||||
return Number(numbers_only_decimal);
|
|
||||||
} catch(error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
};
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { date, patterns, type ValidationRule } from "quasar";
|
import { date, patterns, type ValidationRule } from "quasar";
|
||||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||||
|
import type { Shift, ShiftOption } 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;
|
if (shifts.length < 2) return false;
|
||||||
|
|
||||||
const parsed_shifts = shifts.map(shift => ({
|
const parsed_shifts = shifts.map(shift => ({
|
||||||
start: date.extractDate(`${shift.date} ${shift.start_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(`${shift.date} ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (let i = 0; i < parsed_shifts.length; i++) {
|
for (let i = 0; i < parsed_shifts.length; i++) {
|
||||||
|
|
@ -25,10 +26,21 @@ export const isShiftOverlap = (shifts: Shift[]): boolean => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useShiftRules = (time_required_error: string) => {
|
export const useShiftRules = (time_required_error: string, overlap_error_string: string, day_shifts: Shift[]) => {
|
||||||
const isTimeRequired: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
const isTimeRequiredRule: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
||||||
|
const isShiftOverlapRule: ValidationRule<string> = (_time_string: string) => !isShiftOverlap(day_shifts) || overlap_error_string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isTimeRequired,
|
isTimeRequiredRule,
|
||||||
|
isShiftOverlapRule
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
||||||
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||||
|
{ label: 'timesheet.shift.types.EMERGENCY', value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-5' },
|
||||||
|
{ label: 'timesheet.shift.types.VACATION', value: 'VACATION', icon: 'beach_access', icon_color: 'deep-orange-5' },
|
||||||
|
{ label: 'timesheet.shift.types.HOLIDAY', value: 'HOLIDAY', icon: 'event', icon_color: 'purple-5' },
|
||||||
|
{ label: 'timesheet.shift.types.SICK', value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
<script
|
<script setup lang="ts">
|
||||||
setup
|
/* eslint-disable */
|
||||||
lang="ts"
|
import { onMounted } from 'vue';
|
||||||
>
|
import { date } from 'quasar';
|
||||||
/* eslint-disable */
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
import { onMounted, ref } from 'vue';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { date } from 'quasar';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||||
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
|
import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
|
||||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
|
||||||
|
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -66,20 +64,23 @@
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="col-auto row no-wrap flex-center" :class="$q.screen.lt.md ? 'q-mb-md' : ''">
|
<div
|
||||||
<q-btn-dropdown
|
class="col-auto row no-wrap flex-center"
|
||||||
|
:class="$q.screen.lt.md ? 'q-mb-md' : ''"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
icon="filter_alt"
|
icon="download"
|
||||||
color="accent"
|
color="accent"
|
||||||
:label="$q.screen.lt.md ? '' : $t('shared.label.filter')"
|
:label="$q.screen.lt.md ? '' : $t('shared.label.download')"
|
||||||
class="col-auto q-mr-sm"
|
class="col-auto q-mr-sm"
|
||||||
|
@click="timesheet_store.is_report_dialog_open = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QTableFilters v-model:search="timesheet_store.search_filter" />
|
<QTableFilters v-model:search="timesheet_store.search_filter" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<OverviewReport/>
|
||||||
<OverviewList class="col" />
|
<OverviewList class="col" />
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,21 +2,14 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,11 +26,10 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col"
|
class="col column fit"
|
||||||
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
|
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
||||||
>
|
>
|
||||||
<TimesheetWrapper :employee-email="user?.email ?? ''" />
|
<TimesheetWrapper :employee-email="user?.email ?? ''" class="col"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -33,7 +33,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||||
|
|
||||||
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
|
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
|
||||||
console.log('no user account found');
|
console.error('no user account found');
|
||||||
return { name: 'login' };
|
return { name: 'login' };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
import { ref } from "vue";
|
import { date } from "quasar";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { Expense } from "src/modules/timesheets/models/expense.models";
|
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||||
import { date } from "quasar";
|
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const is_hiding_create_form = ref(false);
|
const is_showing_create_form = ref(true);
|
||||||
const mode = ref<'create' | 'update' | 'delete'>('create');
|
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||||
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
|
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
|
||||||
|
|
||||||
const open = (): void => {
|
const open = (): void => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
if (timesheet_store.pay_period !== undefined) {
|
if (timesheet_store.pay_period !== undefined) {
|
||||||
current_expense.value = new Expense(timesheet_store.pay_period.period_start);
|
current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
initial_expense.value = new Expense(timesheet_store.pay_period.period_start);
|
initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
}
|
}
|
||||||
mode.value = 'create';
|
mode.value = 'create';
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
is_hiding_create_form.value = false;
|
is_showing_create_form.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsertExpense = async (expense: Expense): Promise<boolean> => {
|
const upsertExpense = async (expense: Expense): Promise<boolean> => {
|
||||||
|
|
@ -34,8 +35,8 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const data = await ExpenseService.createExpense(expense);
|
const data = await ExpenseService.createExpense(expense);
|
||||||
return data.success;
|
return data.success;
|
||||||
}
|
}
|
||||||
const data = await ExpenseService.updateExpense(expense);
|
const data = await ExpenseService.updateExpense(expense);
|
||||||
return data.success;
|
return data.success;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// setErrorFrom(err);
|
// setErrorFrom(err);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
@ -51,10 +52,11 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
is_hiding_create_form,
|
is_showing_create_form,
|
||||||
mode,
|
mode,
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
|
is_save_disabled,
|
||||||
open,
|
open,
|
||||||
upsertExpense,
|
upsertExpense,
|
||||||
deleteExpenseById,
|
deleteExpenseById,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable */
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
|
import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
|
||||||
|
|
@ -12,11 +11,17 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
||||||
const is_manager_open = ref(false);
|
const is_manager_open = ref(false);
|
||||||
|
|
||||||
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 {
|
else if (schedule_preset_dialog_mode.value === 'copy') {
|
||||||
setCurrentSchedulePreset(preset_id);
|
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;
|
is_manager_open.value = true;
|
||||||
};
|
};
|
||||||
|
|
@ -26,7 +31,7 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
||||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id)!)
|
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id))
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
|
const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
|
||||||
|
|
@ -73,15 +78,6 @@ 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 {
|
return {
|
||||||
schedule_presets,
|
schedule_presets,
|
||||||
current_schedule_preset,
|
current_schedule_preset,
|
||||||
|
|
@ -93,6 +89,5 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
||||||
updateSchedulePreset,
|
updateSchedulePreset,
|
||||||
deleteSchedulePreset,
|
deleteSchedulePreset,
|
||||||
findSchedulePresetList,
|
findSchedulePresetList,
|
||||||
applySchedulePreset,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -3,7 +3,7 @@ import { defineStore } from 'pinia';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
||||||
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
||||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { PayPeriodOverviewResponse, TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||||
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
|
|
@ -15,9 +15,11 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const timesheets = ref<Timesheet[]>([]);
|
const timesheets = ref<Timesheet[]>([]);
|
||||||
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
|
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
|
||||||
const initial_timesheets = ref<Timesheet[]>([]);
|
const initial_timesheets = ref<Timesheet[]>([]);
|
||||||
|
|
||||||
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
||||||
|
const pay_period_infos = ref<PayPeriodOverviewResponse>();
|
||||||
|
const is_report_dialog_open = ref(false);
|
||||||
|
|
||||||
const is_details_dialog_open = ref(false);
|
const is_details_dialog_open = ref(false);
|
||||||
const selected_employee_name = ref<string>();
|
const selected_employee_name = ref<string>();
|
||||||
const current_pay_period_overview = ref<TimesheetOverview>();
|
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||||
|
|
@ -25,6 +27,22 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const is_approval_grid_mode = ref<boolean>(true);
|
const is_approval_grid_mode = ref<boolean>(true);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
|
|
||||||
|
const getNextOrPreviousPayPeriod = (direction: number) => {
|
||||||
|
if (!pay_period.value) return;
|
||||||
|
|
||||||
|
pay_period.value.pay_period_no += direction;
|
||||||
|
|
||||||
|
if (pay_period.value.pay_period_no > 26) {
|
||||||
|
pay_period.value.pay_period_no = 1;
|
||||||
|
pay_period.value.pay_year += direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pay_period.value.pay_period_no < 1) {
|
||||||
|
pay_period.value.pay_period_no = 26;
|
||||||
|
pay_period.value.pay_year += direction;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
|
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
if (date !== undefined) {
|
if (date !== undefined) {
|
||||||
|
|
@ -70,7 +88,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
|
|
||||||
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
|
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
|
||||||
if (pay_period.value === undefined) return;
|
if (pay_period.value === undefined) return;
|
||||||
console.log('pay period: ', pay_period.value);
|
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
|
|
@ -80,10 +97,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
} else {
|
} else {
|
||||||
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
|
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||||
}
|
}
|
||||||
|
|
||||||
selected_employee_name.value = response.employee_fullname;
|
if (response.success && response.data) {
|
||||||
timesheets.value = response.timesheets;
|
selected_employee_name.value = response.data.employee_fullname;
|
||||||
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
timesheets.value = response.data.timesheets;
|
||||||
|
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
||||||
|
} else {
|
||||||
|
selected_employee_name.value = '';
|
||||||
|
timesheets.value = [];
|
||||||
|
initial_timesheets.value = [];
|
||||||
|
}
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
|
|
@ -93,15 +116,12 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
||||||
try {
|
try {
|
||||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
if (!pay_period.value) return false;
|
||||||
year,
|
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no, report_filters);
|
||||||
period_number,
|
|
||||||
report_filters
|
|
||||||
);
|
|
||||||
pay_period_report.value = response;
|
pay_period_report.value = response;
|
||||||
return true;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving the report CSV: ', error);
|
console.error('There was an error retrieving the report CSV: ', error);
|
||||||
// TODO: More in-depth error-handling here
|
// TODO: More in-depth error-handling here
|
||||||
|
|
@ -110,21 +130,38 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openReportDialog = () => {
|
||||||
|
is_report_dialog_open.value = true;
|
||||||
|
is_loading.value = true;
|
||||||
|
|
||||||
|
|
||||||
|
is_loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeReportDialog = () => {
|
||||||
|
is_report_dialog_open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_loading,
|
is_loading,
|
||||||
|
is_report_dialog_open,
|
||||||
is_approval_grid_mode,
|
is_approval_grid_mode,
|
||||||
is_details_dialog_open,
|
is_details_dialog_open,
|
||||||
search_filter,
|
search_filter,
|
||||||
pay_period,
|
pay_period,
|
||||||
pay_period_overviews,
|
pay_period_overviews,
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
|
pay_period_infos,
|
||||||
selected_employee_name,
|
selected_employee_name,
|
||||||
timesheets,
|
timesheets,
|
||||||
all_current_shifts,
|
all_current_shifts,
|
||||||
initial_timesheets,
|
initial_timesheets,
|
||||||
|
getNextOrPreviousPayPeriod,
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getTimesheetOverviews,
|
getTimesheetOverviews,
|
||||||
getTimesheetsByOptionalEmployeeEmail,
|
getTimesheetsByOptionalEmployeeEmail,
|
||||||
getPayPeriodReportByYearAndPeriodNumber,
|
getPayPeriodReport,
|
||||||
|
openReportDialog,
|
||||||
|
closeReportDialog,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -68,7 +68,6 @@ export const useUiStore = defineStore('ui', () => {
|
||||||
Dark.set(user_preferences.value.is_dark_mode ?? "auto");
|
Dark.set(user_preferences.value.is_dark_mode ?? "auto");
|
||||||
locale.value = user_preferences.value.display_language;
|
locale.value = user_preferences.value.display_language;
|
||||||
}
|
}
|
||||||
console.log('quasar dark mode: ', q.dark.mode, 'preferences: ', user_preferences.value.is_dark_mode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
25
src/utils/boolean-utils.ts
Normal file
25
src/utils/boolean-utils.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// export const createDefaultBooleanValue = <T extends PropertyKey>(keys_list: PropertyKey[]): Record<T, boolean> =>
|
||||||
|
// keys_list.reduce((acc, mod) => {
|
||||||
|
// acc[mod] = false;
|
||||||
|
// return acc;
|
||||||
|
// }, {} as Record<T, boolean>);
|
||||||
|
|
||||||
|
|
||||||
|
// export const toBooleanFromKeys = <T extends PropertyKey>(keys_list: PropertyKey[], arr?: readonly PropertyKey[] | null): Record<T, boolean> => {
|
||||||
|
// const result = createDefaultBooleanValue(keys_list);
|
||||||
|
// if (!arr || !Array.isArray(arr)) return result;
|
||||||
|
// for (const item of arr) {
|
||||||
|
// if (typeof item !== 'string') continue;
|
||||||
|
// const trimmed = item.trim();
|
||||||
|
// if ((keys_list as readonly PropertyKey[]).includes(trimmed)) {
|
||||||
|
// result[trimmed as T] = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const toKeysFromBoolean = <T extends PropertyKey>(boolean_values: Record<T, boolean>): T[] => {
|
||||||
|
const values_array = Object.entries(boolean_values);
|
||||||
|
const values = values_array.filter(([_key, value]) => value === true);
|
||||||
|
return values.map(([key]) => key as T);
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,6 @@ export const getCurrentPayPeriod = (today = new Date()): number => {
|
||||||
const periods_since_anchor = Math.floor(days_since_anchor / period_length);
|
const periods_since_anchor = Math.floor(days_since_anchor / period_length);
|
||||||
|
|
||||||
const current_period = (periods_since_anchor % periods_per_year) + 1;
|
const current_period = (periods_since_anchor % periods_per_year) + 1;
|
||||||
|
|
||||||
console.log(current_period);
|
|
||||||
return current_period;
|
return current_period;
|
||||||
}
|
}
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal recursive function comparing two plain values.
|
|
||||||
*/
|
|
||||||
const _deepEqualRecursive = (a: unknown, b: unknown): boolean => {
|
|
||||||
if (a === b) return true;
|
|
||||||
|
|
||||||
if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle arrays
|
|
||||||
if (Array.isArray(a) && Array.isArray(b)) {
|
|
||||||
if (a.length !== b.length) return false;
|
|
||||||
return a.every((val, i) => _deepEqualRecursive(val, b[i]));
|
|
||||||
} else if (Array.isArray(a) || Array.isArray(b)) {
|
|
||||||
return false; // one is array, other is not
|
|
||||||
}
|
|
||||||
|
|
||||||
const aKeys = Object.keys(a as Record<string, unknown>);
|
|
||||||
const bKeys = Object.keys(b as Record<string, unknown>);
|
|
||||||
|
|
||||||
if (aKeys.length !== bKeys.length) return false;
|
|
||||||
|
|
||||||
return aKeys.every((key) =>
|
|
||||||
_deepEqualRecursive(
|
|
||||||
(a as Record<string, unknown>)[key],
|
|
||||||
(b as Record<string, unknown>)[key]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep equality check that normalizes reactive objects first.
|
|
||||||
*/
|
|
||||||
export const deepEqual = (given: unknown, expected: unknown): boolean => {
|
|
||||||
const a = unwrapAndClone(given as object);
|
|
||||||
const b = unwrapAndClone(expected as object);
|
|
||||||
return _deepEqualRecursive(a, b);
|
|
||||||
};
|
|
||||||
|
|
@ -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