BREAKING(timesheet): Overhaul timesheet UI, refactor to increase efficiency, complete OIDC login

Change timesheet UI to better fit current app model and avoid adding extra clicks and interactions to add new shifts and expenses. Also refactoring calls to backend to be more efficient and use recently-finalized OIDC implementation and integration.
This commit is contained in:
Nicolas Drolet 2025-10-22 08:59:40 -04:00
parent c1c0faeaf1
commit 33061ef2ab
54 changed files with 809 additions and 661 deletions

View File

@ -25,6 +25,10 @@ $elevation-dark-ambient : rgba($dark-shadow-color, 0.2);
$dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient; $dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5); $layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
$input-text-color : #455A64;
$input-autofill-color : #AAD5C4;
$dark : #42444b; $dark : #42444b;
$dark-page : #343434; $dark-page : #343434;

View File

@ -9,25 +9,21 @@
</script> </script>
<template> <template>
<q-item <q-btn
clickable flat
v-ripple transparent
dark dense
class="q-pa-none q-mt-sm" :icon="notification_count > 0 ? 'notifications_active' : 'notifications_off'"
>
<q-icon
:name="notification_count > 0 ? 'notifications_active' : 'notifications_off'"
size="lg" size="lg"
color="white" color="white"
/> >
<q-badge <q-badge
v-if="notification_count > 0" v-if="notification_count > 0"
floating floating
color="negative" color="negative"
class="text-weight-bolder absolute" class="text-weight-bolder q-mt-xs"
> >
{{ notification_count }} {{ notification_count }}
</q-badge> </q-badge>
</q-btn>
</q-item>
</template> </template>

View File

@ -32,7 +32,8 @@
<template> <template>
<q-drawer <q-drawer
v-model="ui_store.isRightDrawerOpen" v-model="ui_store.isRightDrawerOpen"
overlay persistent
mini-to-overlay
elevated elevated
side="left" side="left"
:mini="is_mini" :mini="is_mini"
@ -105,7 +106,7 @@
v-ripple v-ripple
clickable clickable
side side
@click="goToPageName(RouteNames.TIMESHEET_TEMP)" @click="goToPageName(RouteNames.TIMESHEET)"
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')" v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
> >
<q-item-section avatar> <q-item-section avatar>

View File

@ -1,5 +1,6 @@
/* eslint-disable */ /* eslint-disable */
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import type { User } from 'src/modules/shared/models/user.models';
export const AuthService = { export const AuthService = {
// Will likely be deprecated and relegated to Authentik // Will likely be deprecated and relegated to Authentik
@ -17,7 +18,7 @@ export const AuthService = {
api.post('/auth/refresh') api.post('/auth/refresh')
}, },
getProfile: async () => { getProfile: async (): Promise<User> => {
const response = await api.get('/auth/me'); const response = await api.get('/auth/me');
return response.data; return response.data;
}, },

View File

@ -25,7 +25,7 @@
<q-avatar <q-avatar
color="primary" color="primary"
size="8em" size="8em"
class="shadow-3" class="shadow-3 q-mb-md"
> >
<img <img
src="src/assets/targo-default-avatar.png" src="src/assets/targo-default-avatar.png"

View File

@ -33,7 +33,7 @@
</script> </script>
<template> <template>
<div class="q-pa-lg col"> <div class="q-pa-lg">
<q-table <q-table
dense dense
flat flat
@ -49,6 +49,7 @@
:filter="filter" :filter="filter"
class="q-pa-md bg-transparent" class="q-pa-md bg-transparent"
:class="is_grid_mode ? '': 'sticky-header-table'" :class="is_grid_mode ? '': 'sticky-header-table'"
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'" :table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
color="primary" color="primary"
table-header-class="text-primary text-uppercase" table-header-class="text-primary text-uppercase"

View File

@ -0,0 +1,73 @@
<script
setup
lang="ts"
>
import MenuPanelPersonal from 'src/modules/profile/components/employee/menu-panel-personal.vue';
import MenuPanelEmployee from 'src/modules/profile/components/employee/menu-panel-employee.vue';
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.vue';
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const PanelNames = {
PERSONAL_INFO: 'personal_info',
EMPLOYEE_INFO: 'employee_info',
PREFERENCES: 'references',
};
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
</script>
<template>
<q-card
flat
class="rounded-5 bg-transparent q-pa-none"
>
<MenuTemplate
:first-name="employee_profile.first_name"
:last-name="employee_profile.last_name"
:initial-menu="PanelNames.PERSONAL_INFO"
>
<template #tabs>
<q-tab
:name='PanelNames.PERSONAL_INFO'
icon='person_outline'
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
/>
<q-tab
:name="PanelNames.EMPLOYEE_INFO"
icon="work_outline"
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
/>
<q-tab
:name="PanelNames.PREFERENCES"
icon="display_settings"
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
/>
</template>
<template #panels>
<q-tab-panel
:name="PanelNames.PERSONAL_INFO"
class="q-pa-none"
>
<MenuPanelPersonal v-model="employee_profile" />
</q-tab-panel>
<q-tab-panel
:name="PanelNames.EMPLOYEE_INFO"
class="q-pa-none"
>
<MenuPanelEmployee v-model="employee_profile" />
</q-tab-panel>
<q-tab-panel
:name="PanelNames.PREFERENCES"
class="q-pa-none"
>
<MenuPanelPreferences />
</q-tab-panel>
</template>
</MenuTemplate>
</q-card>
</template>

View File

@ -1,37 +1,39 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
import { ref } from 'vue'; import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal'; import { deepEqual } from 'src/utils/deep-equal';
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue'; import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue'; import MenuPanelSelectField from 'src/modules/profile/components/shared/menu-panel-select-field.vue';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
const { employeeProfile } = defineProps<{ const employee_profile = defineModel<EmployeeProfile>({required: true});
employeeProfile: EmployeeProfile;
}>();
let initial_info: EmployeeProfile = employeeProfile;
let employee_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const is_editing = ref<boolean>(false); const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }]; const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
const onSubmit = () => { const onSubmit = () => {
if (!is_editing.value) { if (!is_editing.value) {
is_editing.value = true; is_editing.value = true;
console.log('clicky!');
return; return;
} }
is_editing.value = false; is_editing.value = false;
initial_info = { ...employee_form_data.value }; // update initial value for future possible resets initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
if (!deepEqual(employee_form_data, initial_info)) { if (!deepEqual(employee_profile.value, initial_info)) {
// save the new data here // save the new data here
return; return;
} }
}; };
const onReset = () => { const onReset = () => {
employee_form_data = ref<EmployeeProfile>(initial_info); employee_profile.value = unwrapAndClone(initial_info);
is_editing.value = false; is_editing.value = false;
} }
</script> </script>
@ -43,14 +45,14 @@
@reset="onReset" @reset="onReset"
> >
<div :class="$q.screen.lt.md ? 'column' : 'row'"> <div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField <MenuPanelInputField
v-model="employee_form_data.job_title" v-model="employee_profile.job_title"
class="col" class="col"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.employee.job_title')" :label-string="$t('profile.employee.job_title')"
/> />
<ProfileInputField <MenuPanelInputField
v-model="employee_form_data.company_name" v-model="employee_profile.company_name"
class="col" class="col"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.employee.company')" :label-string="$t('profile.employee.company')"
@ -58,8 +60,8 @@
</div> </div>
<div class="q-mx-xs"> <div class="q-mx-xs">
<ProfileSelectField <MenuPanelSelectField
v-model="employee_form_data.supervisor_full_name" v-model="employee_profile.supervisor_full_name"
:options="supervisor_options" :options="supervisor_options"
:label-string="$t('profile.employee.supervisor')" :label-string="$t('profile.employee.supervisor')"
:is-editing="is_editing" :is-editing="is_editing"
@ -68,14 +70,14 @@
<div :class="$q.screen.lt.md ? 'column' : 'row'"> <div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField <MenuPanelInputField
v-model="employee_form_data.email" v-model="employee_profile.email"
class="col" class="col"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.employee.email')" :label-string="$t('profile.employee.email')"
/> />
<ProfileInputField <MenuPanelInputField
v-model="employee_form_data.first_work_day" v-model="employee_profile.first_work_day"
readonly readonly
class="col" class="col"
type="date" type="date"
@ -84,7 +86,10 @@
/> />
</div> </div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'"> <div
class="absolute-bottom"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-space /> <q-space />
<q-btn <q-btn
v-if="is_editing" v-if="is_editing"
@ -100,6 +105,7 @@
push push
size="sm" size="sm"
color="primary" color="primary"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'" :icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm" class="q-ma-sm"
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')" :label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"

View File

@ -1,17 +1,18 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
import { ref } from 'vue'; import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal'; import { deepEqual } from 'src/utils/deep-equal';
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue'; import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
const { employeeProfile } = defineProps<{ const employee_profile = defineModel<EmployeeProfile>({required: true});
employeeProfile: EmployeeProfile;
}>();
const is_editing = ref<boolean>(false); const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = employeeProfile; let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
const personal_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const onSubmit = () => { const onSubmit = () => {
if (!is_editing.value) { if (!is_editing.value) {
@ -20,16 +21,16 @@
} }
is_editing.value = false; is_editing.value = false;
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
if (!deepEqual(personal_form_data.value, initial_info)) { if (!deepEqual(employee_profile.value, initial_info)) {
// save the new data here // save the new data here
return; return;
} }
}; };
const onReset = () => { const onReset = () => {
personal_form_data.value= { ...initial_info }; employee_profile.value = unwrapAndClone(initial_info);
is_editing.value = false; is_editing.value = false;
} }
</script> </script>
@ -41,15 +42,15 @@
@reset="onReset" @reset="onReset"
> >
<div :class="$q.screen.lt.md ? 'column' : 'row'"> <div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField <MenuPanelInputField
v-model="personal_form_data.first_name" v-model="employee_profile.first_name"
type="text" type="text"
class="col" class="col"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.personal.first_name')" :label-string="$t('profile.personal.first_name')"
/> />
<ProfileInputField <MenuPanelInputField
v-model="personal_form_data.last_name" v-model="employee_profile.last_name"
class="col" class="col"
type="text" type="text"
:is-editing="is_editing" :is-editing="is_editing"
@ -58,15 +59,15 @@
</div> </div>
<div :class="$q.screen.lt.md ? 'column' : 'row'"> <div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField <MenuPanelInputField
v-model="personal_form_data.phone_number" v-model="employee_profile.phone_number"
class="col" class="col"
type="text" type="text"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.personal.phone_number')" :label-string="$t('profile.personal.phone_number')"
/> />
<ProfileInputField <MenuPanelInputField
v-model="personal_form_data.birth_date" v-model="employee_profile.birth_date"
class="col" class="col"
mask="#### / ## / ##" mask="#### / ## / ##"
hint="ex: 1970 / 01 / 01" hint="ex: 1970 / 01 / 01"
@ -76,8 +77,8 @@
</div> </div>
<div :class="$q.screen.lt.md ? 'column' : 'row'"> <div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField <MenuPanelInputField
v-model="personal_form_data.residence" v-model="employee_profile.residence"
class="col" class="col"
:is-editing="is_editing" :is-editing="is_editing"
:label-string="$t('profile.personal.address')" :label-string="$t('profile.personal.address')"
@ -85,7 +86,10 @@
/> />
</div> </div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'"> <div
class="absolute-bottom"
:class="$q.screen.lt.md ? 'column' : 'row'"
>
<q-space /> <q-space />
<q-btn <q-btn
v-if="is_editing" v-if="is_editing"

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import ProfileHeader from 'src/modules/profile/components/shared/profile-header.vue'; import MenuHeader from 'src/modules/profile/components/shared/menu-header.vue';
const { firstName, lastName, initialMenu } = defineProps<{ const { firstName, lastName, initialMenu } = defineProps<{
firstName: string; firstName: string;
@ -16,7 +16,7 @@
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'" :class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'" :style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
> >
<ProfileHeader <MenuHeader
:user-first-name="firstName" :user-first-name="firstName"
:user-last-name="lastName" :user-last-name="lastName"
/> />

View File

@ -1,60 +0,0 @@
<script setup lang="ts">
import PanelInfoPersonal from 'src/modules/profile/components/employee/profile-panel-info-personal.vue';
import PanelInfoEmployee from 'src/modules/profile/components/employee/profile-panel-info-employee.vue';
import PanelPreferences from 'src/modules/profile/components/shared/profile-panel-preferences.vue';
import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue';
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const PanelNames = {
PERSONAL_INFO: 'personal_info',
EMPLOYEE_INFO: 'employee_info',
PREFERENCES: 'references',
};
const { employeeProfile = default_employee_profile } = defineProps<{
employeeProfile?: EmployeeProfile | undefined;
}>();
</script>
<template>
<q-card flat class="rounded-5 bg-transparent q-pa-none">
<ProfileTabMenuTemplate
:first-name="employeeProfile.first_name"
:last-name="employeeProfile.last_name"
:initial-menu="PanelNames.PERSONAL_INFO"
>
<template #tabs>
<q-tab
:name='PanelNames.PERSONAL_INFO'
icon='person_outline'
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
/>
<q-tab
:name="PanelNames.EMPLOYEE_INFO"
icon="work_outline"
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
/>
<q-tab
:name="PanelNames.PREFERENCES"
icon="display_settings"
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
/>
</template>
<template #panels>
<q-tab-panel :name="PanelNames.PERSONAL_INFO" class="q-pa-none">
<PanelInfoPersonal :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.EMPLOYEE_INFO" class="q-pa-none">
<PanelInfoEmployee :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.PREFERENCES" class="q-pa-none">
<PanelPreferences />
</q-tab-panel>
</template>
</ProfileTabMenuTemplate>
</q-card>
</template>

View File

@ -1,4 +1,9 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
import { date } from 'quasar';
const { title, startDate = "", endDate = "" } = defineProps<{ const { title, startDate = "", endDate = "" } = defineProps<{
title: string; title: string;
startDate?: string; startDate?: string;
@ -17,13 +22,13 @@
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"
> >
<div class="text-primary text-weight-bold text-h6"> <div class="text-primary text-weight-bold text-h6">
{{ $d(new Date(startDate), date_format_options) }} {{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
</div> </div>
<div class="text-body2 q-mx-md text-weight-medium"> <div class="text-body2 q-mx-md text-weight-medium">
{{ $t('shared.misc.to') }} {{ $t('shared.misc.to') }}
</div> </div>
<div class="text-primary text-weight-bold text-h6"> <div class="text-primary text-weight-bold text-h6">
{{ $d(new Date(endDate), date_format_options) }} {{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,10 +3,14 @@
import { date} from 'quasar'; import { date} from 'quasar';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
const NEXT = 1;
const PREVIOUS = -1;
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const is_showing_calendar_picker = ref(false); const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' )); const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
const is_disabled = computed(() => timesheet_store.pay_period === undefined);
const emit = defineEmits<{ const emit = defineEmits<{
'date-selected': [ value: string ] 'date-selected': [ value: string ]
@ -15,8 +19,8 @@
}>(); }>();
const is_previous_pay_period_limit = computed( ()=> const is_previous_pay_period_limit = computed( ()=>
timesheet_store.pay_period.pay_year === 2024 && ( timesheet_store.pay_period?.pay_year === 2024 &&
timesheet_store.pay_period.pay_period_no <= 1 timesheet_store.pay_period?.pay_period_no <= 1 ) ?? false
); );
const onDateSelected = (value: string) => { const onDateSelected = (value: string) => {
@ -24,6 +28,33 @@
is_showing_calendar_picker.value = false; is_showing_calendar_picker.value = false;
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 = () => {
getNextOrPreviousPayPeriod(NEXT);
emit('pressed-next-button');
}
const getPreviousPayPeriod = () => {
getNextOrPreviousPayPeriod(PREVIOUS);
emit('pressed-previous-button');
};
</script> </script>
<template> <template>
@ -33,8 +64,8 @@
push rounded push rounded
icon="keyboard_arrow_left" icon="keyboard_arrow_left"
color="primary" color="primary"
@click="emit('pressed-previous-button')" @click="getPreviousPayPeriod"
:disable="is_previous_pay_period_limit || timesheet_store.is_loading" :disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
class="q-mr-sm q-px-sm" class="q-mr-sm q-px-sm"
> >
<q-tooltip <q-tooltip
@ -52,7 +83,7 @@
icon="calendar_month" icon="calendar_month"
color="primary" color="primary"
@click="is_showing_calendar_picker = true" @click="is_showing_calendar_picker = true"
:disable="timesheet_store.is_loading" :disable="timesheet_store.is_loading || is_disabled"
class="q-px-xl" class="q-px-xl"
> >
<q-tooltip <q-tooltip
@ -69,8 +100,8 @@
push rounded push rounded
icon="keyboard_arrow_right" icon="keyboard_arrow_right"
color="primary" color="primary"
@click="emit('pressed-next-button')" @click="getNextPayPeriod"
:disable="timesheet_store.is_loading" :disable="timesheet_store.is_loading || is_disabled"
class="q-ml-sm q-px-sm" class="q-ml-sm q-px-sm"
> >
<q-tooltip <q-tooltip

View File

@ -1,5 +1,3 @@
/* eslint-disable */
export interface User { export interface User {
first_name: string; first_name: string;
last_name: string; last_name: string;

View File

@ -25,28 +25,28 @@
const expenses_labels = ref<string[]>([]); const expenses_labels = ref<string[]>([]);
const getExpensesData = (): ChartData<'bar'> => { const getExpensesData = (): ChartData<'bar'> => {
const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses)); // const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses));
const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts)) // const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
const all_costs = all_days.map(day => day.total_expenses); // const all_costs = all_days.map(day => day.total_expenses);
console.log('costs, ', all_costs); // console.log('costs, ', all_costs);
const all_mileage = all_days.map(day => day.total_mileage); // const all_mileage = all_days.map(day => day.total_mileage);
expenses_dataset.value = [ // expenses_dataset.value = [
{ // {
label: t('timesheet_approvals.table.expenses'), // label: t('timesheet_approvals.table.expenses'),
data: all_costs, // data: all_costs,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), // backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
}, // },
{ // {
label: t('timesheet_approvals.table.mileage'), // label: t('timesheet_approvals.table.mileage'),
data: all_mileage, // data: all_mileage,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(), // backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
} // }
] // ]
expenses_labels.value = all_days_dates.map(day => day.short_date); // expenses_labels.value = all_days_dates.map(day => day.short_date);
return { return {
datasets: expenses_dataset.value, datasets: expenses_dataset.value,

View File

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable */
import { ref } from 'vue'; import { ref } from 'vue';
import { colors } from 'quasar'; import { colors } from 'quasar';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
@ -22,37 +23,37 @@
const getHoursWorkedData = (): ChartData<'bar'> => { const getHoursWorkedData = (): ChartData<'bar'> => {
const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts)); // const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
const datasetConfig = [ // const datasetConfig = [
{ // {
key: 'regular_hours', // key: 'regular_hours',
label: t('shared.shift_type.regular'), // label: t('shared.shift_type.regular'),
color: colors.getPaletteColor('green-5'), // color: colors.getPaletteColor('green-5'),
}, // },
{ // {
key: 'evening_hours', // key: 'evening_hours',
label: t('shared.shift_type.evening'), // label: t('shared.shift_type.evening'),
color: colors.getPaletteColor('green-9'), // color: colors.getPaletteColor('green-9'),
}, // },
{ // {
key: 'emergency_hours', // key: 'emergency_hours',
label: t('shared.shift_type.emergency'), // label: t('shared.shift_type.emergency'),
color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
}, // },
{ // {
key: 'overtime_hours', // key: 'overtime_hours',
label: t('shared.shift_type.overtime'), // label: t('shared.shift_type.overtime'),
color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
}, // },
] as const; // ] as const;
hours_worked_dataset.value = datasetConfig.map(cfg => ({ // hours_worked_dataset.value = datasetConfig.map(cfg => ({
label: cfg.label, // label: cfg.label,
data: all_days.map(day => day[ cfg.key ]), // data: all_days.map(day => day[ cfg.key ]),
backgroundColor: cfg.color, // backgroundColor: cfg.color,
})); // }));
hours_worked_labels.value = all_days.map(day => day.short_date); // hours_worked_labels.value = all_days.map(day => day.short_date);
return { return {

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { ref } from 'vue'; import { ref } from 'vue';
import { colors } from 'quasar'; import { colors } from 'quasar';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -22,27 +23,27 @@
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]); const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
shift_type_totals.value = [{ // shift_type_totals.value = [{
data: [ // data: [
current_pay_period_overview.regular_hours, // current_pay_period_overview.regular_hours,
current_pay_period_overview.other_hours.evening_hours, // current_pay_period_overview.other_hours.evening_hours,
current_pay_period_overview.other_hours.emergency_hours, // current_pay_period_overview.other_hours.emergency_hours,
current_pay_period_overview.other_hours.overtime_hours, // current_pay_period_overview.other_hours.overtime_hours,
], // ],
backgroundColor: [ // backgroundColor: [
colors.getPaletteColor('green-5'), // Regular // colors.getPaletteColor('green-5'), // Regular
colors.getPaletteColor('green-9'), // Evening // colors.getPaletteColor('green-9'), // Evening
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency // getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime // getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
] // ]
}]; // }];
shift_type_labels.value = [ // shift_type_labels.value = [
current_pay_period_overview.regular_hours.toString() + 'h', // current_pay_period_overview.regular_hours.toString() + 'h',
current_pay_period_overview.other_hours.evening_hours.toString() + 'h', // current_pay_period_overview.other_hours.evening_hours.toString() + 'h',
current_pay_period_overview.other_hours.emergency_hours.toString() + 'h', // current_pay_period_overview.other_hours.emergency_hours.toString() + 'h',
current_pay_period_overview.other_hours.overtime_hours.toString() + 'h', // current_pay_period_overview.other_hours.overtime_hours.toString() + 'h',
] // ]
const data = { const data = {

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { provide, ref } from 'vue'; import { provide, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue'; import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
@ -39,7 +40,7 @@
<q-card-section <q-card-section
class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto" class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto"
> >
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span> <span>TODO: Name goes here</span>
</q-card-section> </q-card-section>
<!-- employee pay period details using chart --> <!-- employee pay period details using chart -->

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
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';
@ -38,8 +39,8 @@
timesheet_store.current_pay_period_overview = row; timesheet_store.current_pay_period_overview = row;
emit('clickedDetailsButton', employee_email); emit('clickedDetailsButton', employee_email);
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email); await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
await expenses_store.getPayPeriodExpensesByEmployeeEmail(employee_email); // await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
}; };
const getListModeTextColor = (type: string): string => { const getListModeTextColor = (type: string): string => {

View File

@ -13,16 +13,18 @@ export const useTimesheetApprovalApi = () => {
else if (typeof date_or_year === 'number' && period_number) success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year, period_number); else if (typeof date_or_year === 'number' && period_number) success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year, period_number);
if (success) { if (success) {
await timesheet_store.getPayPeriodOverviewsBySupervisorEmail( await timesheet_store.getTimesheetOverviewsByPayPeriod(
timesheet_store.pay_period.pay_year, timesheet_store.pay_period?.pay_year ?? 1,
timesheet_store.pay_period.pay_period_no, timesheet_store.pay_period?.pay_period_no ?? 1,
auth_store.user.email auth_store.user?.email
); );
} }
}; };
const getNextOrPreviousPayPeriodOverview = async (direction: number) => { const getNextOrPreviousPayPeriodOverview = async (direction: number) => {
let new_period_number = timesheet_store.pay_period.pay_period_no + direction; if (timesheet_store.pay_period === undefined) return;
let new_period_number = (timesheet_store.pay_period.pay_period_no) + direction;
let new_year = timesheet_store.pay_period.pay_year; let new_year = timesheet_store.pay_period.pay_year;
if ( new_period_number > 26 || new_period_number < 1) { if ( new_period_number > 26 || new_period_number < 1) {
@ -42,6 +44,8 @@ export const useTimesheetApprovalApi = () => {
}; };
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[], year?: number, period_number?: number ) => {
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 = {

View File

@ -2,10 +2,11 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { inject, ref } from 'vue'; import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; import { empty_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
@ -17,27 +18,27 @@
const expenses_api = useExpensesApi(); const expenses_api = useExpensesApi();
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 mode = ref<'create' | 'update' | 'delete'>('create');
const COMMENT_MAX_LENGTH = 280; const COMMENT_MAX_LENGTH = 280;
const employee_email = inject<string>('employeeEmail'); const employee_email = inject<string>('employeeEmail');
const rules = useExpenseRules(t); const rules = useExpenseRules(t);
const cancelUpdateMode = () => { const cancelUpdateMode = () => {
expenses_store.current_expense = default_expense; expenses_store.current_expense = empty_expense;
expenses_store.initial_expense = default_expense; expenses_store.initial_expense = empty_expense;
expenses_store.mode = 'create';
} }
const requestExpenseCreationOrUpdate = async () => { const requestExpenseCreationOrUpdate = async () => {
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date); if (mode.value === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date); else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
} }
</script> </script>
<template> <template>
<q-form <q-form
flat flat
v-if="!timesheet_store.pay_period_details.weeks[0]?.is_approved" v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
@submit.prevent="requestExpenseCreationOrUpdate" @submit.prevent="requestExpenseCreationOrUpdate"
> >
<div class="text-subtitle2 q-py-sm"> <div class="text-subtitle2 q-py-sm">
@ -45,7 +46,7 @@
</div> </div>
<div <div
class="row justify-between rounded-5" class="row justify-between rounded-5"
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''" :class="mode === 'update' ? 'bg-accent' : ''"
> >
<!-- date selection input --> <!-- date selection input -->
@ -93,7 +94,7 @@
/> />
<!-- amount input --> <!-- amount input -->
<template v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense.type)"> <template v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')">
<q-input <q-input
key="amount" key="amount"
v-model.number="expenses_store.current_expense.amount" v-model.number="expenses_store.current_expense.amount"
@ -176,7 +177,7 @@
<!-- add btn section --> <!-- add btn section -->
<div> <div>
<q-btn <q-btn
v-if="expenses_store.mode === 'update'" v-if="mode === 'update'"
flat flat
dense dense
size="sm" size="sm"

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
const expense_store = useExpensesStore(); const expense_store = useExpensesStore();
@ -15,7 +16,8 @@
> >
{{ $t('timesheet.expense.title') }} {{ $t('timesheet.expense.title') }}
</q-item-label> </q-item-label>
<q-item-section
<!-- <q-item-section
no-wrap no-wrap
class="col-auto items-center" class="col-auto items-center"
> >
@ -23,7 +25,7 @@
outline outline
class="q-py-xs q-px-md" class="q-py-xs q-px-md"
color="primary" color="primary"
:label="$t('timesheet.expense.total_amount') + ': $' + expense_store.pay_period_expenses.total_expense.toFixed(2)" :label="$t('timesheet.expense.total_amount') + ': $' + expense_store.pay_period_expenses?.toFixed(2)"
/> />
</q-item-section> </q-item-section>
@ -35,8 +37,8 @@
outline outline
class="q-py-xs q-px-md" class="q-py-xs q-px-md"
color="primary" color="primary"
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses.total_mileage.toFixed(1) + ' km'" :label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses?.total_mileage.toFixed(1) + ' km'"
/> />
</q-item-section> </q-item-section> -->
</q-item> </q-item>
</template> </template>

View File

@ -2,15 +2,16 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */
import { computed, inject, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone'; import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
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 { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util'; import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models'; import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
import { empty_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
const { expense, horizontal = false } = defineProps<{ const { expense, horizontal = false } = defineProps<{
expense: Expense; expense: Expense;
@ -26,10 +27,10 @@
const is_approved = defineModel<boolean>({ required: true }); const is_approved = defineModel<boolean>({ required: true });
const is_selected = ref(false); const is_selected = ref(false);
const refresh_key = ref(1); const refresh_key = ref(1);
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user.role)) const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
const expenseItemStyle = computed(() => is_approved.value ? 'border: solid 2px var(--q-primary);' : 'border: solid 2px grey;'); const expenseItemStyle = computed(() => is_approved.value ? 'border: solid 2px var(--q-primary);' : 'border: solid 2px grey;');
const highlightClass = computed(() => (expenses_store.mode === 'update' && is_selected) ? 'bg-accent' : ''); // const highlightClass = computed(() => (expenses_store.mode === 'update' && is_selected) ? 'bg-accent' : '');
const approvedClass = computed(() => horizontal ? ' q-mx-xs q-pa-xs cursor-pointer' : '') const approvedClass = computed(() => horizontal ? ' q-mx-xs q-pa-xs cursor-pointer' : '')
@ -37,15 +38,15 @@
const setExpenseToModify = () => { const setExpenseToModify = () => {
expenses_store.mode = 'update'; // expenses_store.mode = 'update';
expenses_store.current_expense = expense; expenses_store.current_expense = expense;
expenses_store.initial_expense = unwrapAndClone(expense); expenses_store.initial_expense = unwrapAndClone(expense);
}; };
const requestExpenseDeletion = async () => { const requestExpenseDeletion = async () => {
expenses_store.mode = 'delete'; // expenses_store.mode = 'delete';
expenses_store.initial_expense = expense; expenses_store.initial_expense = expense;
expenses_store.current_expense = default_expense; expenses_store.current_expense = empty_expense;
await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date); await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date);
} }
@ -66,7 +67,7 @@
:key="refresh_key" :key="refresh_key"
:clickable="horizontal" :clickable="horizontal"
class="row col-4 q-ma-xs shadow-2" class="row col-4 q-ma-xs shadow-2"
:style="expenseItemStyle + highlightClass + approvedClass" :style="expenseItemStyle + approvedClass"
@click="onExpenseClicked" @click="onExpenseClicked"
> >
<q-badge <q-badge
@ -84,7 +85,7 @@
<!-- avatar type icon section --> <!-- avatar type icon section -->
<q-item-section avatar> <q-item-section avatar>
<q-icon <q-icon
:name="getExpenseTypeIcon(expense.type)" :name="getExpenseIcon(expense.type)"
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')" :color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
size="lg" size="lg"
> >
@ -174,7 +175,6 @@
</q-item-section> </q-item-section>
<q-item-section <q-item-section
v-if="(!timesheet_store.pay_period_details.weeks[0]?.is_approved && !expense.is_approved) || horizontal"
side side
class="q-pa-none" class="q-pa-none"
> >

View File

@ -3,7 +3,7 @@
lang="ts" lang="ts"
> >
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import ExpenseCrudDialogListItem from 'src/modules/timesheets/components/expense-crud-dialog-list-item.vue'; import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
const expenses_store = useExpensesStore(); const expenses_store = useExpensesStore();
@ -20,14 +20,14 @@
:class="horizontal ? 'row flex-center' : ''" :class="horizontal ? 'row flex-center' : ''"
> >
<q-item-label <q-item-label
v-if="expenses_store.pay_period_expenses.expenses.length === 0" v-if="expenses_store.pay_period_expenses?.length === 0"
class="text-italic q-px-sm" class="text-italic q-px-sm"
> >
{{ $t('timesheet.expense.empty_list') }} {{ $t('timesheet.expense.empty_list') }}
</q-item-label> </q-item-label>
<ExpenseCrudDialogListItem <ExpenseDialogListItem
v-for="(expense, index) in expenses_store.pay_period_expenses.expenses" v-for="(expense, index) in expenses_store.pay_period_expenses"
:key="index" :key="index"
v-model="expense.is_approved" v-model="expense.is_approved"
:index="index" :index="index"

View File

@ -3,9 +3,9 @@
lang="ts" lang="ts"
> >
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue'; import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
import ExpenseCrudDialogForm from 'src/modules/timesheets/components/expense-crud-dialog-form.vue'; import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import ExpenseCrudDialogHeader from 'src/modules/timesheets/components/expense-crud-dialog-header.vue'; import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
const expense_store = useExpensesStore(); const expense_store = useExpensesStore();
</script> </script>
@ -32,11 +32,17 @@
{{ expenses_error }} {{ expenses_error }}
</q-banner> --> </q-banner> -->
<ExpenseCrudDialogHeader /> <ExpenseDialogHeader />
<ExpenseCrudDialogList /> <ExpenseDialogList />
<ExpenseCrudDialogForm /> <ExpenseDialogForm v-if="!expense_store.current_expense.is_approved" />
<q-icon
v-else
name="block"
color="negative"
size="lg"
/>
<q-separator spaced /> <q-separator spaced />

View File

@ -4,9 +4,7 @@
> >
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models'; import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
defineProps<{ const shift = defineModel<Shift>({ required: true });
shift: Shift;
}>();
defineEmits<{ defineEmits<{
'onCommentBlur': [void]; 'onCommentBlur': [void];

View File

@ -2,77 +2,84 @@
setup setup
lang="ts" lang="ts"
> >
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { useQuasar } from 'quasar';
import type { Shift } from 'src/modules/timesheets/models/shift.models'; import type { Shift } from 'src/modules/timesheets/models/shift.models';
const q = useQuasar();
const { shift, dense = false } = defineProps<{ const { shift, dense = false } = defineProps<{
shift: Shift; shift: Shift;
dense?: boolean; dense?: boolean;
}>(); }>();
const emit = defineEmits<{ defineEmits<{
'save-comment': [comment: string, shift: Shift]; 'save-comment': [comment: string, shift: Shift];
'request-update': [shift: Shift]; 'request-update': [shift: Shift];
'request-delete': [shift: Shift];
}>(); }>();
const hour_font_size = computed(() => dense ? 'font-size: 1em;' : 'font-size: 1.5em;') const shift_start = ref(shift.start_time);
const is_hovering = ref(false); const shift_end = ref(shift.end_time);
const font_color = computed(() => shift.type === 'REGULAR' ? (q.dark.isActive ? ' text-blue-grey-2' : ' text-grey-8') : ' text-white') const time_picker_model = ref('');
const is_showing_time_picker = ref(false);
const get_shift_color = (type: string): string => { const showTimePicker = (time: string) => {
switch (type) { is_showing_time_picker.value = true;
case 'REGULAR': return 'secondary'; time_picker_model.value = time;
case 'EVENING': return 'warning';
case 'EMERGENCY': return 'amber-10';
case 'OVERTIME': return 'negative';
case 'VACATION': return 'purple-10';
case 'HOLIDAY': return 'purple-5';
case 'SICK': return 'grey-8';
default: return 'transparent';
}
}; };
const onClickUpdate = (type: string) => {
if (type !== '') { emit('request-update', shift) };
}
const onClickDelete = () => emit('request-delete', shift);
</script> </script>
<template> <template>
<q-card-section <q-card-section
horizontal horizontal
class="q-py-none q-mx-md text-uppercase text-center items-center rounded-10 cursor-pointer" class="q-py-none q-mx-md text-uppercase text-center items-center rounded-10"
style="line-height: 1;" style="line-height: 1;"
@click.stop="onClickUpdate(shift.type)"
@mouseenter="is_hovering = true"
@mouseleave="is_hovering = false"
> >
<!-- time-picker for mobile timesheet -->
<q-dialog
v-model="is_showing_time_picker"
class="z-max"
>
<q-time
v-model="time_picker_model"
format24h
color="primary"
/>
</q-dialog>
<div class="col row"> <div class="col row">
<!-- punch-in timestamp --> <!-- punch-in timestamp -->
<q-card-section <q-input
class="col q-pa-none" dense
:class="dense ? 'q-px-xs q-mx-xs' : ''" standout="bg-blue-grey-9"
label-slot
v-model="shift_start"
class="col q-pa-none q-my-xs"
input-class="text-weight-bold text-h6"
label-color="primary"
mask="## : ##"
lazy-rules
> >
<q-item-label <template #label>
class="text-weight-bolder q-pa-xs rounded-5" <span
:class="'bg-' + get_shift_color(shift.type) + font_color" class="text-weight-bolder"
:style="hour_font_size + ' line-height: 80% !important;'" style="font-size: 0.95em;"
> >{{ $t('shared.misc.in') }}</span>
{{ shift.start_time }} </template>
</q-item-label>
</q-card-section> <template #append>
<q-btn
dense
flat
icon="access_time"
color="primary"
class="q-ma-none"
@click.stop="showTimePicker(shift_start)"
/>
</template>
</q-input>
<!-- arrows pointing to punch-out timestamps --> <!-- arrows pointing to punch-out timestamps -->
<q-card-section <q-card-section
horizontal horizontal
class="col items-center justify-center q-mx-sm" class="col-auto items-center justify-center q-mx-sm"
> >
<div <div
v-for="icon_data, index in [ v-for="icon_data, index in [
@ -96,40 +103,49 @@
</q-card-section> </q-card-section>
<!-- punch-out timestamps --> <!-- punch-out timestamps -->
<q-card-section <q-input
class="col q-pa-none" dense
:class="dense ? 'q-px-xs q-mx-xs' : ''" standout="bg-blue-grey-9"
label-slot
v-model="shift_end"
class="col q-pa-none q-my-xs"
input-class="text-weight-bold text-h6"
label-color="primary"
> >
<q-item-label <template #label>
class="text-weight-bolder q-pa-xs rounded-5" <span
:class="'bg-' + get_shift_color(shift.type) + font_color" class="text-weight-bolder"
:style="hour_font_size + ' line-height: 80% !important;'" style="font-size: 0.95em;"
> >{{ $t('shared.misc.out') }}</span>
{{ shift.end_time }} </template>
</q-item-label>
</q-card-section> <template #append>
<div class="col-1"></div> <q-btn
dense
flat
icon="access_time"
color="primary"
@click="showTimePicker(shift_end)"
/>
</template>
</q-input>
</div> </div>
<q-card-section class="col-auto q-pa-none no-wrap q-mx-xs"> <q-card-section class="col-grow q-pa-none no-wrap q-mx-xs">
<!-- comment btn --> <!-- comment btn -->
<q-icon <q-icon
v-if="shift.type" v-if="shift.type && dense"
name="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="q-pa-none q-mr-xs"
:class="shift.comment ? '' : 'invisible'"
/> />
<!-- delete btn -->
<q-btn <q-btn
v-if="shift.type" v-else
flat
dense dense
:size="dense ? 'xs' : 'sm'" :icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
color="negative" :text-color="shift.comment ? 'primary' : 'grey-8'"
icon="clear"
@click.stop="onClickDelete"
/> />
</q-card-section> </q-card-section>
</q-card-section> </q-card-section>

View File

@ -6,7 +6,7 @@
import { date } from 'quasar'; import { date } 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, default_shift } from 'src/modules/timesheets/models/shift.models'; import { type Shift } from 'src/modules/timesheets/models/shift.models';
import { computed } from 'vue'; import { computed } from 'vue';
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
@ -18,54 +18,58 @@
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 ? 'width: 50px;' : 'width: 75px;');
const get_date_from_short = (short_date: string): Date => {
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
};
const to_iso_date = (short_date: string): string => {
return date.formatDate(get_date_from_short(short_date), 'YYYY-MM-DD'); // const get_date_from_short = (short_date: string): Date => {
}; // if (timesheet_store.pay_period === undefined) return new Date();
// return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
// };
// 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[] => { const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
return shifts.length > 0 ? shifts : [default_shift]; return shifts.length > 0 ? shifts : [];
};
const getDate = (shift_date: string): Date => {
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + shift_date);
}; };
</script> </script>
<template> <template>
<div <div
v-for="week, index in timesheet_store.pay_period_details.weeks" v-for="week, index in timesheet_store.timesheets"
:key="index" :key="index"
class="column col q-mx-xs" class="column col q-mx-xs"
> >
<div class="col row shadow-2 rounded-10 q-my-xs">
<q-card <q-card
v-for="day, day_index in week.shifts" v-for="day, day_index in week.days"
:key="day_index + index" :key="day_index + index"
class="row col items-center rounded-10 q-my-xs q-pa-xs" class="row col items-center no-shadow"
style="border-radius: 10px 0 0 10px;"
> >
<!-- Dates column --> <!-- Dates column -->
<q-card-section class="col-auto q-pa-none text-white"> <q-card-section class="col-auto q-pa-none text-white">
<div <div
class="bg-primary rounded-10 q-pa-xs text-center" class="bg-primary rounded-10 q-pa-xs text-center q-ml-sm"
:style="date_box_size" :style="date_box_size"
> >
<q-item-label <q-item-label
v-if="!dense" v-if="!dense"
:style="'font-size: ' + weekday_font_size" :style="'font-size: ' + weekday_font_size"
class="text-uppercase" class="text-uppercase"
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label> >{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { weekday: $q.screen.lt.md ? 'short' : 'long' })
}}</q-item-label>
<q-item-label <q-item-label
class="text-weight-bolder" class="text-weight-bolder"
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'" :style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
>{{ day.short_date.split('/')[1] }}</q-item-label> >{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</q-item-label>
<q-item-label <q-item-label
:style="'font-size: ' + weekday_font_size" :style="'font-size: ' + weekday_font_size"
class="text-uppercase" class="text-uppercase"
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label> >{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { month: $q.screen.lt.md ? 'short' : 'long' })
}}</q-item-label>
</div> </div>
</q-card-section> </q-card-section>
@ -82,10 +86,19 @@
:dense="dense" :dense="dense"
:shift="shift" :shift="shift"
@request-update="" @request-update=""
@request-delete=""
/> />
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-btn
unelevated
class="col-auto bg-primary"
icon="more_time"
size="lg"
text-color="white"
style="border-radius: 0 10px 10px 0;"
/>
</div>
</div> </div>
</template> </template>

View File

@ -4,7 +4,7 @@
> >
import GenericLoader from 'src/modules/shared/components/generic-loader.vue'; 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 ExpenseCrudDialog from 'src/modules/timesheets/components/expense-crud-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';
@ -39,6 +39,7 @@
> >
<q-card-section <q-card-section
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"
:class="$q.screen.lt.md ? 'column' : ''" :class="$q.screen.lt.md ? 'column' : ''"
@ -46,40 +47,37 @@
<!-- navigation btn --> <!-- navigation btn -->
<PayPeriodNavigator <PayPeriodNavigator
v-if="!dense" v-if="!dense"
@date-selected="timesheet_api.getPayPeriodDetailsByDate(employeeEmail)" @date-selected="timesheet_api.getTimesheetsByDate(employeeEmail)"
@pressed-previous-button="timesheet_api.getPreviousPayPeriodDetails(employeeEmail)" @pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
@pressed-next-button="timesheet_api.getNextPayPeriodDetails(employeeEmail)" @pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
/> />
<!-- mobile expenses button --> <!-- mobile expenses button -->
<q-btn <q-btn
v-if="$q.screen.lt.md && !dense" v-if="$q.screen.lt.md"
push push
rounded rounded
color="primary" color="primary"
icon="receipt_long" icon="receipt_long"
:label="$t('timesheet.expense.open_btn')" :label="$t('timesheet.expense.open_btn')"
class="q-mt-sm" class="q-mt-sm"
@click="open(employeeEmail)" @click="open"
/> />
<!-- shift's colored legend --> <!-- shift's colored legend -->
<ShiftListLegend <ShiftListLegend :is-loading="false" />
v-if="!dense"
:is-loading="false"
/>
<q-space /> <q-space />
<!-- desktop expenses button --> <!-- desktop expenses button -->
<q-btn <q-btn
v-if="$q.screen.gt.sm && !dense" v-if="$q.screen.gt.sm"
push push
rounded rounded
color="primary" color="primary"
icon="receipt_long" icon="receipt_long"
:label="$t('timesheet.expense.open_btn')" :label="$t('timesheet.expense.open_btn')"
@click="open(employeeEmail)" @click="open"
/> />
</q-card-section> </q-card-section>
@ -92,6 +90,6 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
<ExpenseCrudDialog /> <ExpenseDialog />
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
import { normalizeObject } from "src/utils/normalize-object"; import { normalizeObject } from "src/utils/normalize-object";
import { useExpensesStore } from "src/stores/expense-store"; import { useExpensesStore } from "src/stores/expense-store";
import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation.models"; import { expense_validation_schema } from "src/modules/timesheets/models/expense-validation.models";
import type { Expense } from "src/modules/timesheets/models/expense.models"; import type { Expense } from "src/modules/timesheets/models/expense.models";
export const useExpensesApi = () => { export const useExpensesApi = () => {

View File

@ -4,50 +4,27 @@ import { useTimesheetStore } from "src/stores/timesheet-store"
export const useTimesheetApi = () => { export const useTimesheetApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const NEXT = 1;
const PREVIOUS = -1;
const getPayPeriodDetailsByDate = async (date_string: string, employee_email?: string) => { const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
if (success) { if (success) {
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email) await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
} }
} }
const getNextOrPreviousPayPeriodDetails = async (direction: number, employee_email?: string) => { const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
const { pay_period } = timesheet_store; if (timesheet_store.pay_period === undefined) return false;
let new_number = pay_period.pay_period_no + direction;
let new_year = pay_period.pay_year;
if (new_number > 26) { const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
new_number = 1;
new_year += 1;
}
if (new_number < 1) {
new_number = 26;
new_year -= 1;
}
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(new_year, new_number);
if (success) { if (success) {
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email); await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
} }
}; };
const getNextPayPeriodDetails = async (employee_email?: string) => {
await getNextOrPreviousPayPeriodDetails(NEXT, employee_email ?? auth_store.user.email);
}
const getPreviousPayPeriodDetails = async (employee_email?: string) => {
await getNextOrPreviousPayPeriodDetails(PREVIOUS, employee_email ?? auth_store.user.email);
}
return { return {
getPayPeriodDetailsByDate, getTimesheetsByDate,
getNextPayPeriodDetails, getTimesheetsByCurrentPayPeriod,
getPreviousPayPeriodDetails,
}; };
}; };

View File

@ -6,16 +6,16 @@ export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXP
export interface Expense { export interface Expense {
id: number; id: number;
date: string; date: string; //YYYY-MM-DD
type: ExpenseType; type: ExpenseType;
amount: number; amount: number;
mileage?: number; mileage?: number;
comment: string; comment: string;
supervisor_comment?: string; supervisor_comment?: string;
is_approved: boolean; is_approved: boolean;
} };
export const default_expense: Expense = { export const empty_expense: Expense = {
id: -1, id: -1,
date: '', date: '',
type: 'EXPENSES', type: 'EXPENSES',
@ -23,3 +23,25 @@ export const default_expense: Expense = {
comment: '', comment: '',
is_approved: false, is_approved: false,
}; };
export const test_expenses: Expense[] = [
{
id: 201,
date: '2025-01-06',
type: 'EXPENSES',
amount: 15.5,
comment: 'Lunch receipt',
is_approved: false,
},
{
id: 202,
date: '2025-01-07',
type: 'MILEAGE',
amount: 0,
mileage: 32.4,
comment: 'Travel to client site',
is_approved: true,
},
];

View File

@ -19,22 +19,11 @@ export type ShiftLegendItem = {
export interface Shift { export interface Shift {
id: number; id: number;
date: string; date: string; //YYYY-MM-DD
type: ShiftType; type: ShiftType;
start_time: string; start_time: string; //HH:mm:ss
end_time: string; end_time: string; //HH:mm:ss
comment: string | undefined; comment: string | undefined;
is_approved: boolean; is_approved: boolean;
is_remote: boolean; is_remote: boolean;
} }
export const default_shift: Readonly<Shift> = {
id: -1,
date: '',
start_time: '',
end_time: '',
type: 'REGULAR',
comment: '',
is_approved: false,
is_remote: false,
};

View File

@ -6,13 +6,14 @@ export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export interface Timesheet { export interface Timesheet {
id: number; id: number;
is_approved: boolean;
weekly_hours: TotalHours; weekly_hours: TotalHours;
weekly_expenses: TotalExpenses; weekly_expenses: TotalExpenses;
days: TimesheetDay[]; days: TimesheetDay[];
} }
export interface TimesheetDay { export interface TimesheetDay {
date: string; date: string; // YYYY-MM-DD
daily_hours: TotalHours; daily_hours: TotalHours;
daily_expenses: TotalExpenses; daily_expenses: TotalExpenses;
shifts: Shift[]; shifts: Shift[];
@ -34,3 +35,78 @@ export interface TotalExpenses {
expenses: number; expenses: number;
mileage: number; mileage: number;
} }
export const test_timesheets: Timesheet[] = [
{
id: 1,
is_approved: false,
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 },
days: [
{
date: '2025-10-18',
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 },
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: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
],
expenses: [
{ id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
],
},
],
},
{
id: 2,
is_approved: true,
weekly_hours: {
regular: 0,
evening: 0,
emergency: 0,
overtime: 8,
vacation: 0,
holiday: 0,
sick: 0,
absent: 0,
},
weekly_expenses: {
expenses: 0,
mileage: 32.4,
},
days: [
{
date: '2025-10-27',
daily_hours: {
regular: 0,
evening: 0,
emergency: 0,
overtime: 8,
vacation: 0,
holiday: 0,
sick: 0,
absent: 0,
},
daily_expenses: {
expenses: 0,
mileage: 32.4,
},
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: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
],
expenses: [
{
id: 202,
date: '2025-10-27',
type: 'MILEAGE',
amount: 0,
mileage: 32.4,
comment: 'Travel to client site',
is_approved: true,
},
],
},
],
},
];

View File

@ -8,11 +8,6 @@ import type { Expense } from "src/modules/timesheets/models/expense.models";
import { Shift } from "src/modules/timesheets/models/shift.models"; import { Shift } from "src/modules/timesheets/models/shift.models";
export const timesheetService = { export const timesheetService = {
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<Timesheet[]> => {
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
return response.data;
},
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => { getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date_string}`); const response = await api.get(`pay-periods/date/${date_string}`);
return response.data; return response.data;
@ -23,20 +18,20 @@ export const timesheetService = {
return response.data; return response.data;
}, },
getPayPeriodOverviewsBySupervisorEmail: 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(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data; return response.data;
}, },
getPayPeriodDetailsByPayPeriodAndEmployeeEmail: async (year: number, period_no: number, email: string): Promise<Timesheet[]> => { getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<Timesheet[]> => {
const response = await api.get('timesheets', { params: { year, period_no, email, } }); const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
return response.data; return response.data;
}, },
// getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => { getExpensesByTimesheetId: async (timesheet_id: number): Promise<Expense[]> => {
// const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`); const response = await api.get(`/expenses/list/${timesheet_id}`);
// return response.data; return response.data;
// }, },
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => { upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
const response = await api.put(`/shifts/upsert/${email}`, payload); const response = await api.put(`/shifts/upsert/${email}`, payload);
@ -48,8 +43,8 @@ export const timesheetService = {
return response; return response;
}, },
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: Expense[]): Promise<Timesheet[]> => { upsertOrDeleteExpenseByEmailAndExpenseId: async (email: string, expense_id: number): Promise<Timesheet[]> => {
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload); const response = await api.put(`/expenses/upsert/${email}`, expense_id);
return response.data; return response.data;
}, },
}; };

View File

@ -0,0 +1,47 @@
<script
setup
lang="ts"
>
import { Notify } from 'quasar';
const clickNotify = () => {
Notify.create({
message: 'You clicked the little click button!',
color: 'info'
})
}
</script>
<template>
<q-page
padding
class="q-pa-md row items-center justify-center"
>
<q-card class="shadow-2 col-9 dark-font">
<q-img src="src/assets/line-truck-1.jpg">
<div class="absolute-bottom text-h5">
Welcome to App Targo, !
</div>
</q-img>
<q-card-section class="text-center">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
</q-card-section>
<q-separator />
<q-card-actions align="center">
<q-btn
color="primary"
label="Click Me"
@click="clickNotify"
/>
</q-card-actions>
</q-card>
</q-page>
</template>

View File

@ -8,7 +8,7 @@
</script> </script>
<template> <template>
<q-page> <q-page class="column flex-center">
<EmployeeListAddModifyDialog /> <EmployeeListAddModifyDialog />
<PageHeaderTemplate title="employee_list.page_header" /> <PageHeaderTemplate title="employee_list.page_header" />

View File

@ -1,22 +1,22 @@
<script setup lang="ts"> <script
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue'; setup
lang="ts"
>
import MenuEmployee from 'src/modules/profile/components/employee/menu-employee.vue';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models'; // import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ]; const employee_roles = ['SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING'];
defineProps<{ // const employee_profile = defineModel<EmployeeProfile>({ required: true });
employeeProfile?: EmployeeProfile | undefined;
}>();
</script> </script>
<template> <template>
<q-page class="bg-secondary column items-center justify-center"> <q-page class="bg-secondary column items-center justify-center">
<ProfileEmployee <MenuEmployee
v-if="employee_roles.includes( auth_store.user.role.toUpperCase() )" v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
class="col-auto" class="col-auto"
:employee-profile="employeeProfile"
/> />
</q-page> </q-page>
</template> </template>

View File

@ -1,73 +0,0 @@
<script
setup
lang="ts"
>
import { useQuasar } from 'quasar';
import type { QVueGlobals } from 'quasar';
const q: QVueGlobals = useQuasar();
const clickNotify = () => {
q.notify({
message: 'Nick pinged you.',
})
}
</script>
<template>
<q-page
padding
class="q-pa-md row items-center justify-center"
>
<q-card class="shadow-2 col-9 dark-font">
<q-img src="src/assets/line-truck-1.jpg">
<div class="absolute-bottom text-h5">
Welcome to App Targo!
</div>
</q-img>
<q-card-section class="text-center">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui
dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem
vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum
qui
dolorem eum fugiat quo voluptas nulla pariatur?
</q-card-section>
<q-card-section class="text-center">
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas
assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum
necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut
perferendis doloribus asperiores repellat.
</q-card-section>
<q-card-section class="text-center">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
</q-card-section>
<q-separator />
<q-card-actions align="center">
<q-btn
color="primary"
label="Click Me"
@click="clickNotify"
/>
</q-card-actions>
</q-card>
</q-page>
</template>

View File

@ -5,7 +5,7 @@
import { useTimesheetStore } from 'src/stores/timesheet-store'; 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 OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue'; import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
import DetailscrudDialog from 'src/modules/timesheet-approval/components/details-crud-dialog.vue'; import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
@ -29,16 +29,16 @@
> >
<PageHeaderTemplate <PageHeaderTemplate
title="timesheet_approvals.page_title" title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period.period_start" :start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period.period_end" :end-date="timesheet_store.pay_period?.period_end ?? ''"
/> />
<DetailscrudDialog <DetailsDialog
v-model:dialog="is_details_dialog_open" v-model:dialog="is_details_dialog_open"
:employee-email="employee_email" :employee-email="employee_email"
:is-loading="timesheet_store.is_loading" :is-loading="timesheet_store.is_loading"
:employee-overview="timesheet_store.current_pay_period_overview" :employee-overview="timesheet_store.current_pay_period_overview"
:timesheet-details="timesheet_store.pay_period_details" :timesheets="timesheet_store.timesheets"
/> />
<OverviewList @clickedDetailsButton="onDetailsClicked"/> <OverviewList @clickedDetailsButton="onDetailsClicked"/>

View File

@ -15,7 +15,7 @@
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
onMounted(async () => { onMounted(async () => {
await timesheet_api.getPayPeriodDetailsByDate(date.formatDate(new Date(), 'YYYY-MM-DD')); await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
}); });
</script> </script>
@ -23,16 +23,23 @@
<template> <template>
<q-page <q-page
padding padding
class="column q-pa-md bg-secondary flex-center" class="column q-pa-md bg-secondary items-center"
> >
<PageHeaderTemplate <PageHeaderTemplate
:title="'timesheet.page_header'" :title="'timesheet.page_header'"
:start-date="timesheet_store.pay_period.period_start" :start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period.period_end" :end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col-auto"
/> />
<div class="col column flex-center" :style="$q.screen.gt.sm ? 'width: 70vw': ''"> <div
<TimesheetWrapper class="col" :employee-email="user.email" /> class="col column items-center"
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
>
<TimesheetWrapper
class="col-auto"
:employee-email="user?.email ?? ''"
/>
</div> </div>
</q-page> </q-page>

View File

@ -2,6 +2,7 @@ import { defineRouter } from '#q-app/wrappers';
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router'; import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router';
import routes from './routes'; import routes from './routes';
import { useAuthStore } from 'src/stores/auth-store'; import { useAuthStore } from 'src/stores/auth-store';
import { RouteNames } from 'src/router/router-constants';
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
@ -27,10 +28,12 @@ export default defineRouter(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE), history: createHistory(process.env.VUE_ROUTER_BASE),
}); });
Router.beforeEach((destinationPage) => { Router.beforeEach(async (destinationPage) => {
const authStore = useAuthStore(); const authStore = useAuthStore();
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) { if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
console.log('no user account found');
return { name: 'login' }; return { name: 'login' };
} }
}) })

View File

@ -6,5 +6,5 @@ export enum RouteNames {
TIMESHEET_APPROVALS = 'timesheet-approvals', TIMESHEET_APPROVALS = 'timesheet-approvals',
EMPLOYEE_LIST = 'employee-list', EMPLOYEE_LIST = 'employee-list',
PROFILE = 'user/profile', PROFILE = 'user/profile',
TIMESHEET_TEMP = 'timesheet-temp' TIMESHEET = 'timesheet'
} }

View File

@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: '', path: '',
name: RouteNames.DASHBOARD, name: RouteNames.DASHBOARD,
component: () => import('src/pages/test-page.vue'), component: () => import('src/pages/dashboard-page.vue'),
}, },
{ {
path: 'timesheet-approvals', path: 'timesheet-approvals',
@ -23,8 +23,8 @@ const routes: RouteRecordRaw[] = [
component: () => import('src/pages/employee-list-page.vue'), component: () => import('src/pages/employee-list-page.vue'),
}, },
{ {
path: 'timesheet-temp', path: 'timesheet',
name: RouteNames.TIMESHEET_TEMP, name: RouteNames.TIMESHEET,
component: () => import('src/pages/timesheet-page.vue') component: () => import('src/pages/timesheet-page.vue')
}, },
{ {
@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/v1/login', path: '/v1/login',
name: RouteNames.LOGIN, name: RouteNames.LOGIN,
component: () => import('src/modules/auth/pages/auth-login.vue'), component: () => import('src/pages/login-page.vue'),
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },

View File

@ -15,19 +15,9 @@ export const useAuthStore = defineStore('auth', () => {
//TODO: manage customer login process //TODO: manage customer login process
}; };
const oidcLogin = async (): Promise<void> => { const oidcLogin = () => {
window.addEventListener('message', async (event) => { window.addEventListener('message', (event) => {
if (event.data.type === 'authSuccess') { void handleAuthMessage(event);
const new_user = await AuthService.getProfile();
user.value = new_user;
router.push('/');
} else {
Notify.create({
message: "You have popups blocked on this website!",
color: 'negative',
textColor: 'white',
});
}
}); });
const oidc_popup = window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800'); const oidc_popup = window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800');
@ -44,6 +34,34 @@ export const useAuthStore = defineStore('auth', () => {
user.value = undefined; user.value = undefined;
}; };
return { user, authError, isAuthorizedUser, login, oidcLogin, logout }; const handleAuthMessage = async (event: MessageEvent) => {
if (event.data.type === 'authSuccess') {
try {
await getProfile();
await router.push('/');
} catch (error) {
console.error('failed to login: ', error);
}
} else {
Notify.create({
message: "You have popups blocked on this website!",
color: 'negative',
textColor: 'white',
});
}
};
const getProfile = async (): Promise<{ status: number, message: string }> => {
try {
const new_user = await AuthService.getProfile();
user.value = new_user;
return { status: 200, message: 'profile retrieved successfully' };
} catch (error) {
console.error('error while retrieving profile: ', error);
}
return { status: 400, message: 'unknown error occured' };
}
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, getProfile };
}); });

View File

@ -1,21 +1,17 @@
import { ref } from "vue"; import { ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
import { timesheetService } from "src/modules/timesheets/services/timesheet-service"; import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation"; import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
import type { CrudAction } from "src/modules/timesheets/models/shift.models"; import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
export const useExpensesStore = defineStore('expenses', () => { export const useExpensesStore = defineStore('expenses', () => {
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 mode = ref<CrudAction>('create'); const pay_period_expenses = ref<Expense[]>(test_expenses);
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses); const current_expense = ref<Expense>(empty_expense);
const current_expense = ref<Expense>(default_expense); const initial_expense = ref<Expense>(empty_expense);
const initial_expense = ref<Expense>(default_expense);
const error = ref<string | null>(null); const error = ref<string | null>(null);
// const setErrorFrom = (err: unknown) => { // const setErrorFrom = (err: unknown) => {
@ -23,14 +19,14 @@ export const useExpensesStore = defineStore('expenses', () => {
// error.value = e?.message || 'Unknown error'; // error.value = e?.message || 'Unknown error';
// }; // };
const open = async (employee_email: string): Promise<void> => { const open = (): void => {
is_open.value = true; is_open.value = true;
is_loading.value = true; is_loading.value = true;
error.value = null; error.value = null;
current_expense.value = default_expense; current_expense.value = empty_expense;
initial_expense.value = default_expense; initial_expense.value = empty_expense;
await getPayPeriodExpensesByEmployeeEmail(employee_email); // await getPayPeriodExpensesByTimesheetId(timesheet_id);
is_loading.value = false; is_loading.value = false;
} }
@ -39,16 +35,12 @@ export const useExpensesStore = defineStore('expenses', () => {
is_open.value = false; is_open.value = false;
}; };
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => { const getPayPeriodExpensesByTimesheetId = async (timesheet_id: number): Promise<void> => {
is_loading.value = true; is_loading.value = true;
error.value = null; error.value = null;
try { try {
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail( const expenses = await timesheetService.getExpensesByTimesheetId(timesheet_id);
encodeURIComponent(employee_email),
encodeURIComponent(timesheet_store.pay_period.pay_year),
encodeURIComponent(timesheet_store.pay_period.pay_period_no),
);
pay_period_expenses.value = expenses; pay_period_expenses.value = expenses;
} catch (err: unknown) { } catch (err: unknown) {
if (typeof err === 'object') { if (typeof err === 'object') {
@ -70,21 +62,19 @@ export const useExpensesStore = defineStore('expenses', () => {
} }
}; };
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, date: string, expense: UpsertExpense): Promise<void> => { const upsertOrDeleteExpensesById = async (employee_email: string, date: string, expense_id: number): Promise<void> => {
is_loading.value = true; is_loading.value = true;
error.value = null; error.value = null;
try { try {
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail( await timesheetService.upsertOrDeleteExpenseByEmailAndExpenseId(
encodeURIComponent(employee_email), encodeURIComponent(employee_email),
encodeURIComponent(date), expense_id,
expense,
); );
console.log('updated expenses received: ', updated_expenses) // TODO: Save response data into proper ref
pay_period_expenses.value.expenses = updated_expenses;
} catch (err) { } catch (err) {
// setErrorFrom(err); // setErrorFrom(err);
console.log('error doing some expense thing: ', err) console.error(err);
} finally { } finally {
is_loading.value = false; is_loading.value = false;
} }
@ -93,14 +83,13 @@ export const useExpensesStore = defineStore('expenses', () => {
return { return {
is_open, is_open,
is_loading, is_loading,
mode,
pay_period_expenses, pay_period_expenses,
current_expense, current_expense,
initial_expense, initial_expense,
error, error,
open, open,
getPayPeriodExpensesByEmployeeEmail, getPayPeriodExpensesByTimesheetId,
upsertOrDeleteExpensesByEmployeeEmail, upsertOrDeleteExpensesById,
close, close,
}; };
}); });

View File

@ -1,24 +1,24 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { withLoading } from 'src/utils/store-helpers'; import { withLoading } from 'src/utils/store-helpers';
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 { default_pay_period_overview, type TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models"; import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models'; import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/timesheet.models'; import { test_timesheets, 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 is_loading = ref<boolean>(false); const is_loading = ref<boolean>(false);
const pay_period = ref<PayPeriod>(default_pay_period); const pay_period = ref<PayPeriod>();
const pay_period_overviews = ref<TimesheetOverview[]>([default_pay_period_overview,]); const pay_period_overviews = ref<TimesheetOverview[]>([]);
const current_pay_period_overview = ref<TimesheetOverview>(default_pay_period_overview); const current_pay_period_overview = ref<TimesheetOverview>();
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details); const timesheets = ref<Timesheet[]>(test_timesheets);
const pay_period_report = ref(); const pay_period_report = ref();
const is_calendar_limit = computed(() => const is_calendar_limit = computed(() => (pay_period.value?.pay_year === 2024 && pay_period.value?.pay_period_no <= 1) ?? false);
pay_period.value.pay_year === 2024 &&
pay_period.value.pay_period_no <= 1
);
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; is_loading.value = true;
@ -30,14 +30,14 @@ export const useTimesheetStore = defineStore('timesheet', () => {
else if (typeof date_or_year === 'number' && period_number) { else if (typeof date_or_year === 'number' && period_number) {
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 = default_pay_period; else pay_period.value = undefined;
is_loading.value = false; is_loading.value = false;
return true; return true;
} catch (error) { } catch (error) {
console.error('Could not get current pay period: ', error); console.error('Could not get current pay period: ', error);
pay_period.value = default_pay_period; pay_period.value = undefined;
pay_period_overviews.value = [default_pay_period_overview,]; pay_period_overviews.value = [];
//TODO: More in-depth error-handling here //TODO: More in-depth error-handling here
is_loading.value = false; is_loading.value = false;
@ -45,18 +45,18 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
}; };
const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise<boolean> => { const getTimesheetOverviewsByPayPeriod = async (pay_year: number, period_number: number, supervisor_email?: string): Promise<boolean> => {
is_loading.value = true; is_loading.value = true;
try { try {
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email); const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email ?? auth_store.user?.email ?? '');
pay_period_overviews.value = response.employees_overview; pay_period_overviews.value = response.employees_overview;
is_loading.value = false; is_loading.value = false;
return true; return true;
} catch (error) { } catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error); console.error('There was an error retrieving Employee Pay Period overviews: ', error);
pay_period_overviews.value = [default_pay_period_overview,]; pay_period_overviews.value = [];
// TODO: More in-depth error-handling here // TODO: More in-depth error-handling here
is_loading.value = false; is_loading.value = false;
@ -64,21 +64,17 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
}; };
const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => { const getTimesheetsByEmployeeEmail = async (employee_email: string) => {
is_loading.value = true; is_loading.value = true;
if (pay_period.value === undefined) return;
try { try {
const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail( const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
pay_period.value.pay_year, timesheets.value = response;
pay_period.value.pay_period_no,
employee_email
);
pay_period_details.value = response;
console.log('pay period details: ', response, pay_period_details.value.employee_full_name)
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);
// TODO: More in-depth error-handling here // TODO: More in-depth error-handling here
pay_period_details.value = default_pay_period_details; timesheets.value = [];
is_loading.value = false; is_loading.value = false;
} }
}; };
@ -109,10 +105,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
pay_period, pay_period,
pay_period_overviews, pay_period_overviews,
current_pay_period_overview, current_pay_period_overview,
pay_period_details, timesheets,
getPayPeriodByDateOrYearAndNumber, getPayPeriodByDateOrYearAndNumber,
getPayPeriodOverviewsBySupervisorEmail, getTimesheetOverviewsByPayPeriod,
getPayPeriodDetailsByEmployeeEmail, getTimesheetsByEmployeeEmail,
getPayPeriodReportByYearAndPeriodNumber, getPayPeriodReportByYearAndPeriodNumber,
}; };
}); });