feat(docker): Add/Correct Dockerfile for remote Docker Lab deployment

This commit is contained in:
Nicolas Drolet 2025-10-29 15:19:10 -04:00
parent 33061ef2ab
commit 6c6cecbe7d
25 changed files with 689 additions and 437 deletions

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# targo-frontend
FROM node:22
# Set working directory inside container
WORKDIR /app
# Set environment variables
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
# Copy package.json & package-lock.json first (for caching)
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the code
COPY . .
# Expose Quasar dev port
EXPOSE 9000
# Default command
CMD ["quasar", "dev"]

View File

@ -15,7 +15,7 @@ declare module 'vue' {
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
const api = axios.create({ const api = axios.create({
baseURL: import.meta.env.VITE_TARGO_BACKEND_AUTH_URL, baseURL: import.meta.env.VITE_TARGO_BACKEND_URL,
withCredentials: true withCredentials: true
}); });

View File

@ -1,5 +1,5 @@
// app global css in SCSS form // app global css in SCSS form
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) { @each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
.rounded-#{$size} { .rounded-#{$size} {
border-radius: #{$size}px !important; border-radius: #{$size}px !important;
} }
@ -37,3 +37,8 @@ body.body--dark {
.shift-highlight { .shift-highlight {
background: #0195462a; background: #0195462a;
} }
.frosted-glass {
background-color: #FFFA !important;
backdrop-filter: blur(5px);
}

View File

@ -27,6 +27,8 @@ $layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
$input-text-color : #455A64; $input-text-color : #455A64;
$input-autofill-color : #AAD5C4; $input-autofill-color : #AAD5C4;
$field-dense-label-top : 5px !default;
$field-dense-label-font-size : 16px !default;
$dark : #42444b; $dark : #42444b;

View File

@ -31,7 +31,7 @@
<template> <template>
<q-drawer <q-drawer
v-model="ui_store.isRightDrawerOpen" v-model="ui_store.is_left_drawer_open"
persistent persistent
mini-to-overlay mini-to-overlay
elevated elevated

View File

@ -1,4 +1,7 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api'; import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
@ -10,9 +13,13 @@
</script> </script>
<template> <template>
<q-card class="rounded-15"> <q-card class="rounded-15 shadow-10">
<q-card-section class="text-center bg-primary q-pa-lg"> <q-card-section class="text-center bg-primary q-pa-lg">
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" /> <q-img
src="/src/assets/logo-targo-white.svg"
ratio="4.6"
fit="contain"
/>
</q-card-section> </q-card-section>
<div class="q-pt-sm q-px-xl q-pb-lg "> <div class="q-pt-sm q-px-xl q-pb-lg ">
@ -28,8 +35,14 @@
dense dense
outlined outlined
label-color="primary" label-color="primary"
:label="$t('login.email')" class="rounded-5 inset-shadow bg-blue-grey-1"
/> label-slot
input-class="text-weight-medium text-h6"
>
<template #label>
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
</template>
</q-input>
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium"> <q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
<q-toggle <q-toggle
@ -58,9 +71,16 @@
</q-form> </q-form>
<q-card-section class="row q-pt-sm"> <q-card-section class="row q-pt-sm">
<q-separator color="primary" class="col self-center"/> <q-separator
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{ $t('shared.misc.or') }}</span> color="primary"
<q-separator color="primary" class="col self-center"/> class="col self-center"
/>
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
$t('shared.misc.or') }}</span>
<q-separator
color="primary"
class="col self-center"
/>
</q-card-section> </q-card-section>
<q-card-section class="column q-px-sm q-pt-none"> <q-card-section class="column q-px-sm q-pt-none">
@ -73,7 +93,10 @@
:label="$t('login.button.facebook')" :label="$t('login.button.facebook')"
class="full-width row q-mb-sm" class="full-width row q-mb-sm"
> >
<q-tooltip anchor="top middle" class="bg-primary">{{$t('login.tooltip.coming_soon')}}</q-tooltip> <q-tooltip
anchor="top middle"
class="bg-primary"
>{{ $t('login.tooltip.coming_soon') }}</q-tooltip>
</q-btn> </q-btn>
<q-slide-transition> <q-slide-transition>
<div v-if="is_employee_email"> <div v-if="is_employee_email">

View File

@ -1,27 +0,0 @@
<script
setup
lang="ts"
>
defineProps<{
isLoading: boolean;
}>();
</script>
<template>
<q-card
v-if="isLoading"
flat
class="column flex-center rounded-10"
style="width: 20vw !important; height: 20vh !important;"
>
<q-spinner
color="primary"
size="5em"
:thickness="10"
class="col-auto"
/>
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
{{ $t('shared.label.loading') }}
</div>
</q-card>
</template>

View File

@ -72,7 +72,7 @@
> >
<q-badge <q-badge
v-if="expense.is_approved" v-if="expense.is_approved"
class="absolute z-top rounded-20 bg-white q-pa-none" class="absolute z-top rounded-20 bg-dark q-pa-none"
style="transform: translate(-15px, -15px);" style="transform: translate(-15px, -15px);"
> >
<q-icon <q-icon

View File

@ -13,7 +13,6 @@
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' }, { type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' }, { type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' }, { type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' }, { type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' }, { type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' }, { type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },

View File

@ -2,60 +2,111 @@
setup setup
lang="ts" lang="ts"
> >
import { ref } from 'vue'; /* eslint-disable*/
import type { Shift } from 'src/modules/timesheets/models/shift.models'; import { onMounted, ref, useTemplateRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { QSelect } from 'quasar';
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
import { useUiStore } from 'src/stores/ui-store';
const { shift, dense = false } = defineProps<{ const { t } = useI18n();
shift: Shift; const ui_store = useUiStore();
const shift = defineModel<Shift>('shift', { required: true });
const { dense = false } = defineProps<{
dense?: boolean; dense?: boolean;
}>(); }>();
defineEmits<{ defineEmits<{
'save-comment': [comment: string, shift: Shift]; 'saveComment': [comment: string, shift_id: number];
'request-update': [shift: Shift]; 'requestUpdate': [shift_id: number];
'requestDelete': [void];
}>(); }>();
const shift_start = ref(shift.start_time);
const shift_end = ref(shift.end_time);
const time_picker_model = ref(''); const time_picker_model = ref('');
const is_showing_time_picker = ref(false); const is_showing_time_picker = ref(false);
const select_ref = useTemplateRef<QSelect>('select');
const options: { label: string, value: ShiftType, icon: string, icon_color: string }[] = [
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
{ 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: 'cyan-8' },
];
const shift_type_selected = ref(options.find(option => option.value == shift.value.type));
const showTimePicker = (time: string) => { const showTimePicker = (time: string) => {
is_showing_time_picker.value = true; is_showing_time_picker.value = true;
time_picker_model.value = time; time_picker_model.value = time;
}; };
onMounted(() => {
if (ui_store.focus_next_component) {
select_ref.value?.focus();
select_ref.value?.showPopup();
ui_store.focus_next_component = false;
}
});
</script> </script>
<template> <template>
<q-card-section <div
horizontal v-if="shift.shift_id !== 0"
class="q-py-none q-mx-md text-uppercase text-center items-center rounded-10" class="col row flex-center text-uppercase rounded-10"
style="line-height: 1;"
> >
<!-- time-picker for mobile timesheet --> <!-- shift type -->
<q-dialog <q-select
v-model="is_showing_time_picker" ref="select"
class="z-max" v-model="shift_type_selected"
standout="bg-blue-grey-9"
dense
options-dense
hide-dropdown-icon
:menu-offset="[0, 10]"
:options="options"
class="rounded-5 q-mx-xs shadow-1"
:class="ui_store.is_mobile_mode ? 'col-auto' : 'col'"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-primary)"
> >
<q-time <template #selected-item="scope">
v-model="time_picker_model" <div
format24h class="row text-weight-bold q-ma-none q-pa-none no-wrap ellipsis"
color="primary" :class="ui_store.is_mobile_mode ? 'items-center' : 'flex-center'"
:tabindex="scope.tabindex"
>
<q-icon
:name="scope.opt.icon"
:color="scope.opt.icon_color"
size="sm"
class="col-auto q-mx-xs"
/> />
</q-dialog> <span
v-if="$q.screen.gt.md"
style="line-height: 0.9em;"
class="col ellipsis"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
<div class="col row">
<!-- punch-in timestamp --> <!-- punch-in timestamp -->
<q-input <q-input
v-model="shift.start_time"
dense dense
type="time"
standout="bg-blue-grey-9" standout="bg-blue-grey-9"
label-slot label-slot
v-model="shift_start"
class="col q-pa-none q-my-xs"
input-class="text-weight-bold text-h6"
label-color="primary" label-color="primary"
mask="## : ##" input-class="text-weight-medium"
lazy-rules input-style="font-size: 1.2em;"
class="col q-mx-xs"
> >
<template #label> <template #label>
<span <span
@ -66,51 +117,28 @@
<template #append> <template #append>
<q-btn <q-btn
v-if="ui_store.is_mobile_mode"
dense dense
flat flat
icon="access_time" icon="access_time"
color="primary" color="primary"
class="q-ma-none" @click.stop="showTimePicker(shift.start_time)"
@click.stop="showTimePicker(shift_start)"
/> />
</template> </template>
</q-input> </q-input>
<!-- arrows pointing to punch-out timestamps -->
<q-card-section
horizontal
class="col-auto items-center justify-center q-mx-sm"
>
<div
v-for="icon_data, index in [
{ transform: 'transform: translateX(5px);', color: 'accent' },
{ transform: 'transform: translateX(-5px);', color: 'primary' }]"
:key="index"
>
<q-icon
v-if="shift.type && !dense"
name="double_arrow"
:color="icon_data.color"
:size="dense ? '16px' : '24px'"
:style="icon_data.transform"
/>
<span
v-else
class="text-primary"
> > </span>
</div>
</q-card-section>
<!-- punch-out timestamps --> <!-- punch-out timestamps -->
<q-input <q-input
v-model="shift.end_time"
dense dense
type="time"
standout="bg-blue-grey-9" standout="bg-blue-grey-9"
label-slot label-slot
v-model="shift_end"
class="col q-pa-none q-my-xs"
input-class="text-weight-bold text-h6"
label-color="primary" label-color="primary"
input-class="text-weight-medium"
input-style="font-size: 1.2em;"
class="col q-mx-xs"
lazy-rules
> >
<template #label> <template #label>
<span <span
@ -121,32 +149,49 @@
<template #append> <template #append>
<q-btn <q-btn
v-if="ui_store.is_mobile_mode"
dense dense
flat flat
icon="access_time" icon="access_time"
color="primary" color="primary"
@click="showTimePicker(shift_end)" @click="showTimePicker(shift.end_time)"
/> />
</template> </template>
</q-input> </q-input>
</div>
<q-card-section class="col-grow q-pa-none no-wrap q-mx-xs"> <!-- comment and delete buttons -->
<!-- comment btn --> <div
v-if="$q.screen.gt.sm"
class="col-auto"
>
<q-icon <q-icon
v-if="shift.type && dense" v-if="shift.type && dense"
:name="shift.comment ? 'comment' : ''" :name="shift.comment ? 'comment' : ''"
color="primary" color="primary"
:size="dense ? 'xs' : 'sm'" :size="dense ? 'xs' : 'sm'"
class="q-pa-none q-mr-xs" class="col-auto q-pa-none q-mr-xs"
/> />
<q-btn <q-btn
v-else v-else
flat flat
dense dense
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'" :icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? 'primary' : 'grey-8'" :text-color="shift.comment ? 'primary' : 'grey-8'"
class="col-auto q-ma-none q-pl-md full-height"
/> />
</q-card-section>
</q-card-section> <q-btn
dense
flat
round
unelevated
tabindex="-1"
icon="cancel"
color="negative"
class="q-pa-none q-mr-xs"
@click="$emit('requestDelete')"
/>
</div>
</div>
</template> </template>

View File

@ -2,103 +2,123 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { date } from 'quasar'; import { date } from 'quasar';
import { computed } from 'vue';
import { useQuasar } from 'quasar';
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue'; import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { type Shift } from 'src/modules/timesheets/models/shift.models'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
import { computed } from 'vue'; import { Shift } from 'src/modules/timesheets/models/shift.models';
import { useUiStore } from 'src/stores/ui-store';
const q = useQuasar();
const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const shift_api = useShiftApi();
const { dense = false } = defineProps<{ const { dense = false } = defineProps<{
dense?: boolean; dense?: boolean;
}>(); }>();
const is_mobile = computed(() => q.screen.lt.md);
const date_font_size = computed(() => dense ? '1.5em' : '2.5em'); const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;'); const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
const date_box_size = computed(() => dense ? 'width: 50px;' : 'width: 75px;'); const date_box_size = computed(() => dense || is_mobile.value ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true;
// const get_date_from_short = (short_date: string): Date => { const new_shift = new Shift;
// if (timesheet_store.pay_period === undefined) return new Date(); new_shift.date = date;
// return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date); new_shift.timesheet_id = timesheet_id;
// }; day_shifts.push(new_shift);
// const to_iso_date = (short_date: string): string => {
// return date.formatDate(get_date_from_short(short_date), 'YYYY-MM-DD');
// };
const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
return shifts.length > 0 ? shifts : [];
}; };
const deleteCurrentShift = async (shift: Shift) => {
console.log('shift to delete: ', shift);
if (shift.shift_id < 0) {
shift.shift_id = 0;
return;
}
await shift_api.deleteShiftById(shift.shift_id);
}
</script> </script>
<template> <template>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<div <div
v-for="week, index in timesheet_store.timesheets" v-for="timesheet in timesheet_store.timesheets"
:key="index" :key="timesheet.timesheet_id"
class="column col q-mx-xs" class="col column"
> >
<div class="col row shadow-2 rounded-10 q-my-xs"> <div
v-for="day in timesheet.days"
<q-card :key="day.date"
v-for="day, day_index in week.days" class="col-auto row shadow-2 rounded-10 q-ma-xs"
:key="day_index + index" >
class="row col items-center no-shadow" <div
class="col row bg-dark"
style="border-radius: 10px 0 0 10px;" style="border-radius: 10px 0 0 10px;"
> >
<!-- Dates column --> <!-- Dates column -->
<q-card-section class="col-auto q-pa-none text-white">
<div <div
class="bg-primary rounded-10 q-pa-xs text-center q-ml-sm" class="col-auto column flex-center bg-primary rounded-10 text-center q-ma-sm self-center"
:class="$q.screen.lt.md ? '' : ''"
:style="date_box_size" :style="date_box_size"
> >
<q-item-label <span
v-if="!dense" v-if="!dense"
class="col-auto text-uppercase text-white"
:style="'font-size: ' + weekday_font_size" :style="'font-size: ' + weekday_font_size"
class="text-uppercase" >
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { weekday: $q.screen.lt.md ? 'short' : 'long' }) {{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
}}</q-item-label> weekday: $q.screen.lt.md ? 'short' :
<q-item-label 'long'
class="text-weight-bolder" })
}}
</span>
<span
class="col-auto text-weight-bolder text-grey-1"
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'" :style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
>{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</q-item-label> >
<q-item-label {{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}
</span>
<span
class="col-auto text-uppercase text-white"
:style="'font-size: ' + weekday_font_size" :style="'font-size: ' + weekday_font_size"
class="text-uppercase" >
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { month: $q.screen.lt.md ? 'short' : 'long' }) {{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
}}</q-item-label> month: $q.screen.lt.md ? 'short' : 'long'
})
}}
</span>
</div> </div>
</q-card-section>
<!-- List of shifts column --> <!-- List of shifts column -->
<q-card-section class="col column q-pa-none full-height"> <div class="col column">
<div
v-if="day.shifts.length > 0"
class="col-grow column justify-center"
>
<ShiftListRow <ShiftListRow
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)" v-for="shift, shift_index in day.shifts"
:key="shift_index" :key="shift_index"
class="col" v-model:shift="day.shifts[shift_index]!"
:dense="dense" :dense="dense"
:shift="shift" @request-delete="deleteCurrentShift(shift)"
@request-update=""
/> />
</div> </div>
</q-card-section> </div>
</q-card>
<div class="col-auto self-stretch">
<q-btn <q-btn
unelevated unelevated
class="col-auto bg-primary"
icon="more_time" icon="more_time"
size="lg" :size="$q.screen.lt.md ? 'md' : 'lg'"
color="primary"
text-color="white" text-color="white"
class="full-height"
:class="$q.screen.lt.md ? 'q-px-xs' : ''"
style="border-radius: 0 10px 10px 0;" style="border-radius: 0 10px 10px 0;"
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
/> />
</div> </div>
</div> </div>
</div>
</div>
</template> </template>

View File

@ -2,17 +2,18 @@
setup setup
lang="ts" lang="ts"
> >
import GenericLoader from 'src/modules/shared/components/generic-loader.vue';
import ShiftList from 'src/modules/timesheets/components/shift-list.vue'; import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue'; import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue'; import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue'; // import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
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 { provide } from 'vue'; import { provide } from 'vue';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
const { open } = useExpensesStore(); const { open } = useExpensesStore();
const shift_api = useShiftApi();
const { employeeEmail, dense = false } = defineProps<{ const { employeeEmail, dense = false } = defineProps<{
employeeEmail: string; employeeEmail: string;
@ -27,21 +28,28 @@
<template> <template>
<div class="column flex-center full-width"> <div class="column flex-center full-width">
<GenericLoader
:is-loading="timesheet_store.is_loading" <q-dialog
class="col-auto text-center" v-model="timesheet_store.is_loading"
transition-show="jump-down"
transition-hide="jump-down"
>
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
<q-spinner-radio
color="primary"
size="20vh"
/> />
</q-card>
</q-dialog>
<q-card <q-card
v-if="!timesheet_store.is_loading"
flat flat
class="transparent full-width" class="transparent full-width"
> >
<q-card-section <q-card-section
v-if="!dense" v-if="!dense"
:horizontal="$q.screen.gt.sm" :horizontal="$q.screen.gt.sm"
class="q-px-md items-center" class="q-px-md items-center q-mb-md"
:class="$q.screen.lt.md ? 'column' : ''" :class="$q.screen.lt.md ? 'column' : ''"
> >
<!-- navigation btn --> <!-- navigation btn -->
@ -65,9 +73,21 @@
/> />
<!-- shift's colored legend --> <!-- shift's colored legend -->
<ShiftListLegend :is-loading="false" /> <!-- <ShiftListLegend :is-loading="false" /> -->
<q-space /> <q-space />
<!-- save timesheet changes button -->
<q-btn
v-if="$q.screen.gt.sm"
push
rounded
:disable="timesheet_store.is_loading"
color="primary"
icon="upload"
:label="$t('shared.label.save')"
class="q-mr-md"
@click="shift_api.saveShiftChanges"
/>
<!-- desktop expenses button --> <!-- desktop expenses button -->
<q-btn <q-btn
@ -82,14 +102,8 @@
</q-card-section> </q-card-section>
<q-card-section
:horizontal="$q.screen.gt.sm"
class="bg-secondary q-pa-sm rounded-10"
>
<ShiftList :dense="dense" /> <ShiftList :dense="dense" />
</q-card-section>
</q-card> </q-card>
<ExpenseDialog /> <ExpenseDialog />
</div> </div>
</template> </template>

View File

@ -0,0 +1,39 @@
import { useAuthStore } from "src/stores/auth-store";
import { useShiftStore } from "src/stores/shift-store";
import { useTimesheetStore } from "src/stores/timesheet-store";
export const useShiftApi = () => {
const timesheet_store = useTimesheetStore();
const shift_store = useShiftStore();
const auth_store = useAuthStore();
const deleteShiftById = async (shift_id: number) => {
timesheet_store.is_loading = true;
const success = await shift_store.deleteShiftById(shift_id);
if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '');
}
timesheet_store.is_loading = false;
};
const saveShiftChanges = async () => {
timesheet_store.is_loading = true;
const create_success = await shift_store.createNewShifts();
if (create_success) {
const update_success = await shift_store.updateShifts();
if (update_success) {
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '')
}
}
timesheet_store.is_loading = false;
}
return {
deleteShiftById,
saveShiftChanges,
};
}

View File

@ -6,21 +6,29 @@ export const useTimesheetApi = () => {
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => { const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
if (success) { if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
timesheet_store.is_loading = false;
} }
timesheet_store.is_loading = false;
} }
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;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no ); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
if (success) { if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
timesheet_store.is_loading = false;
} }
timesheet_store.is_loading = false;
}; };
return { return {

View File

@ -2,13 +2,12 @@ export const SHIFT_TYPES: ShiftType[] = [
'REGULAR', 'REGULAR',
'EVENING', 'EVENING',
'EMERGENCY', 'EMERGENCY',
'OVERTIME',
'HOLIDAY', 'HOLIDAY',
'VACATION', 'VACATION',
'SICK' 'SICK'
]; ];
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ; export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
export type ShiftLegendItem = { export type ShiftLegendItem = {
type: ShiftType; type: ShiftType;
@ -17,8 +16,9 @@ export type ShiftLegendItem = {
text_color?: string; text_color?: string;
}; };
export interface Shift { export class Shift {
id: number; shift_id: number;
timesheet_id: number;
date: string; //YYYY-MM-DD date: string; //YYYY-MM-DD
type: ShiftType; type: ShiftType;
start_time: string; //HH:mm:ss start_time: string; //HH:mm:ss
@ -26,4 +26,21 @@ export interface Shift {
comment: string | undefined; comment: string | undefined;
is_approved: boolean; is_approved: boolean;
is_remote: boolean; is_remote: boolean;
constructor() {
this.shift_id = -1;
this.timesheet_id = -1;
this.date = '';
this.type = 'REGULAR';
this.start_time = '';
this.end_time = '';
this.comment = undefined;
this.is_approved = false;
this.is_remote = false;
}
}
export interface NewShift {
timesheet_id: number;
shifts: Shift[];
} }

View File

@ -4,8 +4,13 @@ import type { Expense } from "src/modules/timesheets/models/expense.models";
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/; export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/; export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export interface TimesheetResponse {
employee_full_name: string;
timesheets: Timesheet[];
}
export interface Timesheet { export interface Timesheet {
id: number; timesheet_id: number;
is_approved: boolean; is_approved: boolean;
weekly_hours: TotalHours; weekly_hours: TotalHours;
weekly_expenses: TotalExpenses; weekly_expenses: TotalExpenses;
@ -36,77 +41,77 @@ export interface TotalExpenses {
mileage: number; mileage: number;
} }
export const test_timesheets: Timesheet[] = [ // export const test_timesheets: Timesheet[] = [
{ // {
id: 1, // timehsid: 1,
is_approved: false, // is_approved: false,
weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 }, // weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
weekly_expenses: { expenses: 15.5, mileage: 0 }, // weekly_expenses: { expenses: 15.5, mileage: 0 },
days: [ // days: [
{ // {
date: '2025-10-18', // date: '2025-10-18',
daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 }, // daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
daily_expenses: { expenses: 15.5, mileage: 0 }, // daily_expenses: { expenses: 15.5, mileage: 0 },
shifts: [ // shifts: [
{ id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, }, // { id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, },
{ id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, }, // { id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
], // ],
expenses: [ // expenses: [
{ id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, }, // { id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
], // ],
}, // },
], // ],
}, // },
{ // {
id: 2, // id: 2,
is_approved: true, // is_approved: true,
weekly_hours: { // weekly_hours: {
regular: 0, // regular: 0,
evening: 0, // evening: 0,
emergency: 0, // emergency: 0,
overtime: 8, // overtime: 8,
vacation: 0, // vacation: 0,
holiday: 0, // holiday: 0,
sick: 0, // sick: 0,
absent: 0, // absent: 0,
}, // },
weekly_expenses: { // weekly_expenses: {
expenses: 0, // expenses: 0,
mileage: 32.4, // mileage: 32.4,
}, // },
days: [ // days: [
{ // {
date: '2025-10-27', // date: '2025-10-27',
daily_hours: { // daily_hours: {
regular: 0, // regular: 0,
evening: 0, // evening: 0,
emergency: 0, // emergency: 0,
overtime: 8, // overtime: 8,
vacation: 0, // vacation: 0,
holiday: 0, // holiday: 0,
sick: 0, // sick: 0,
absent: 0, // absent: 0,
}, // },
daily_expenses: { // daily_expenses: {
expenses: 0, // expenses: 0,
mileage: 32.4, // mileage: 32.4,
}, // },
shifts: [ // shifts: [
{ id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, }, // { id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, },
{ id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, }, // { id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
], // ],
expenses: [ // expenses: [
{ // {
id: 202, // id: 202,
date: '2025-10-27', // date: '2025-10-27',
type: 'MILEAGE', // type: 'MILEAGE',
amount: 0, // amount: 0,
mileage: 32.4, // mileage: 32.4,
comment: 'Travel to client site', // comment: 'Travel to client site',
is_approved: true, // is_approved: true,
}, // },
], // ],
}, // },
], // ],
}, // },
]; // ];

View File

@ -0,0 +1,13 @@
import { api } from "src/boot/axios";
export const ExpenseService = {
getExpensesByTimesheetId: async (timesheet_id: number) => {
const response = await api.get(`timesheet/${timesheet_id}`);
return response.data;
},
upsertOrDeleteExpenseById: async (expense_id: number) => {
const response = await api.post(`epxense/${expense_id}`);
return response.data;
},
};

View File

@ -0,0 +1,24 @@
/* eslint-disable */
import { api } from "src/boot/axios";
import type { Shift } from "src/modules/timesheets/models/shift.models";
export const ShiftService = {
deleteShiftById: async (shift_id: number) => {
const response = await api.delete(`/shift/${shift_id}`);
return response.data;
},
createNewShifts: async (new_shifts: Shift[]) => {
// const response = await api.post(`/shift/`, { dtos: new_shifts });
// return response;
console.log('create shift payload: ', new_shifts);
return {status: 200};
},
updateShifts: async (existing_shifts: Shift[]) => {
// const response = await api.patch(`/shift/`, { dtos: existing_shifts });
// return response;
console.log('update shift payload: ', existing_shifts);
return {status: 200};
}
};

View File

@ -1,11 +1,7 @@
/* eslint-disable */
import { api } from "src/boot/axios"; 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 { Timesheet } 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 { Expense } from "src/modules/timesheets/models/expense.models";
import { Shift } from "src/modules/timesheets/models/shift.models";
export const timesheetService = { export const timesheetService = {
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => { getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
@ -23,28 +19,8 @@ export const timesheetService = {
return response.data; return response.data;
}, },
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<Timesheet[]> => { getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<TimesheetResponse> => {
const response = await api.get('timesheets', { params: { employee_email, year, period_number } }); const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
return response.data; return response.data;
}, },
getExpensesByTimesheetId: async (timesheet_id: number): Promise<Expense[]> => {
const response = await api.get(`/expenses/list/${timesheet_id}`);
return response.data;
},
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
const response = await api.put(`/shifts/upsert/${email}`, payload);
return response.data;
},
deleteShiftsByEmployeeEmailAndDate: async (email: string, date: string, payload: Shift[]) => {
const response = await api.delete(`/shifts/delete/${email}/${date}`, { data: payload });
return response;
},
upsertOrDeleteExpenseByEmailAndExpenseId: async (email: string, expense_id: number): Promise<Timesheet[]> => {
const response = await api.put(`/expenses/upsert/${email}`, expense_id);
return response.data;
},
}; };

View File

@ -33,13 +33,10 @@
/> />
<div <div
class="col column items-center" class="col"
:style="$q.screen.gt.sm ? 'width: 90vw' : ''" :style="$q.screen.gt.sm ? 'width: 90vw' : ''"
> >
<TimesheetWrapper <TimesheetWrapper :employee-email="user?.email ?? ''" />
class="col-auto"
:employee-email="user?.email ?? ''"
/>
</div> </div>
</q-page> </q-page>

View File

@ -20,7 +20,7 @@ export const useAuthStore = defineStore('auth', () => {
void handleAuthMessage(event); void handleAuthMessage(event);
}); });
const oidc_popup = window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800'); const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_AUTH_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
if (!oidc_popup) if (!oidc_popup)
Notify.create({ Notify.create({

View File

@ -1,8 +1,8 @@
import { ref } from "vue"; import { ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models"; import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models"; import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
@ -40,7 +40,7 @@ export const useExpensesStore = defineStore('expenses', () => {
error.value = null; error.value = null;
try { try {
const expenses = await timesheetService.getExpensesByTimesheetId(timesheet_id); const expenses = await ExpenseService.getExpensesByTimesheetId(timesheet_id);
pay_period_expenses.value = expenses; pay_period_expenses.value = expenses;
} catch (err: unknown) { } catch (err: unknown) {
if (typeof err === 'object') { if (typeof err === 'object') {
@ -62,15 +62,12 @@ export const useExpensesStore = defineStore('expenses', () => {
} }
}; };
const upsertOrDeleteExpensesById = async (employee_email: string, date: string, expense_id: number): Promise<void> => { const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
is_loading.value = true; is_loading.value = true;
error.value = null; error.value = null;
try { try {
await timesheetService.upsertOrDeleteExpenseByEmailAndExpenseId( await ExpenseService.upsertOrDeleteExpenseById(expense_id);
encodeURIComponent(employee_email),
expense_id,
);
// TODO: Save response data into proper ref // TODO: Save response data into proper ref
} catch (err) { } catch (err) {
// setErrorFrom(err); // setErrorFrom(err);

71
src/stores/shift-store.ts Normal file
View File

@ -0,0 +1,71 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { ShiftService } from "src/modules/timesheets/services/shift-service";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { Notify } from "quasar";
export const useShiftStore = defineStore('shift_store', () => {
const timesheet_store = useTimesheetStore();
const shift_error = ref();
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
try {
await ShiftService.deleteShiftById(shift_id);
return true;
} catch (error) {
console.error('DEV ERROR || error while deleting shift: ', error);
return false;
}
};
const createNewShifts = async (): Promise<boolean> => {
if (timesheet_store.timesheets === undefined) return false;
try {
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id < 0);
if (new_shifts?.length > 0) {
const response = await ShiftService.createNewShifts(new_shifts);
if (response.status <= 200) {
return true;
}
}
console.log('No new shifts to save');
Notify.create('no new shifts to save')
return false;
} catch (error) {
console.error('Error creating new shifts: ', error);
return false;
}
};
const updateShifts = async (): Promise<boolean> => {
if (timesheet_store.timesheets === undefined) return false;
try {
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id > 0);
if (existing_shifts?.length > 0) {
const response = await ShiftService.updateShifts(existing_shifts);
if (response.status <= 200) {
return true;
}
}
console.log('No shifts to update');
Notify.create('no shifts to update')
return false;
} catch (error) {
console.error('Error updating shifts: ', error);
return false;
}
}
return {
shift_error,
deleteShiftById,
createNewShifts,
updateShifts,
}
})

View File

@ -1,28 +1,24 @@
import { ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { withLoading } from 'src/utils/store-helpers';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
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 { 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 { test_timesheets, 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';
const auth_store = useAuthStore();
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const auth_store = useAuthStore();
const is_loading = ref<boolean>(false); const is_loading = ref<boolean>(false);
const pay_period = ref<PayPeriod>(); const pay_period = ref<PayPeriod>();
const pay_period_overviews = ref<TimesheetOverview[]>([]); const pay_period_overviews = ref<TimesheetOverview[]>([]);
const current_pay_period_overview = ref<TimesheetOverview>(); const current_pay_period_overview = ref<TimesheetOverview>();
const timesheets = ref<Timesheet[]>(test_timesheets); const timesheets = ref<Timesheet[]>();
const pay_period_report = ref(); const pay_period_report = ref();
const is_calendar_limit = computed(() => (pay_period.value?.pay_year === 2024 && pay_period.value?.pay_period_no <= 1) ?? false);
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => { const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
is_loading.value = true;
try { try {
if (typeof date_or_year === 'string') { if (typeof date_or_year === 'string') {
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year); pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
@ -31,7 +27,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number); pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
} }
else pay_period.value = undefined; else pay_period.value = undefined;
is_loading.value = false;
return true; return true;
} catch (error) { } catch (error) {
@ -39,7 +34,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
pay_period.value = undefined; pay_period.value = undefined;
pay_period_overviews.value = []; pay_period_overviews.value = [];
//TODO: More in-depth error-handling here //TODO: More in-depth error-handling here
is_loading.value = false;
return false; return false;
} }
@ -66,10 +60,11 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const getTimesheetsByEmployeeEmail = async (employee_email: string) => { const getTimesheetsByEmployeeEmail = async (employee_email: string) => {
is_loading.value = true; is_loading.value = true;
if (pay_period.value === undefined) return; if (pay_period.value === undefined) return;
try { try {
const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no); const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
timesheets.value = response; timesheets.value = response.timesheets;
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);
@ -80,7 +75,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
}; };
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => { const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
return withLoading(is_loading.value, async () => {
try { try {
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber( const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
year, year,
@ -88,7 +82,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
report_filters report_filters
); );
pay_period_report.value = response; pay_period_report.value = response;
return true; return true;
} 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);
@ -96,12 +89,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
return false; return false;
});
}; };
return { return {
is_loading, is_loading,
is_calendar_limit,
pay_period, pay_period,
pay_period_overviews, pay_period_overviews,
current_pay_period_overview, current_pay_period_overview,

View File

@ -1,12 +1,22 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { useQuasar } from 'quasar';
import { computed, ref } from 'vue';
export const useUiStore = defineStore('ui', () => { export const useUiStore = defineStore('ui', () => {
const isRightDrawerOpen = ref(true); const q = useQuasar();
const is_left_drawer_open = ref(true);
const focus_next_component = ref(false);
const is_mobile_mode = computed(() => q.screen.lt.md);
const toggleRightDrawer = () => { const toggleRightDrawer = () => {
isRightDrawerOpen.value = !isRightDrawerOpen.value; is_left_drawer_open.value = !is_left_drawer_open.value;
} }
return { isRightDrawerOpen, toggleRightDrawer }; return {
is_mobile_mode,
focus_next_component,
is_left_drawer_open,
toggleRightDrawer
};
}); });