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:
parent
c1c0faeaf1
commit
33061ef2ab
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'"
|
||||||
|
size="lg"
|
||||||
|
color="white"
|
||||||
>
|
>
|
||||||
<q-icon
|
|
||||||
:name="notification_count > 0 ? 'notifications_active' : 'notifications_off'"
|
|
||||||
size="lg"
|
|
||||||
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
73
src/modules/profile/components/employee/menu-employee.vue
Normal file
73
src/modules/profile/components/employee/menu-employee.vue
Normal 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>
|
||||||
|
|
@ -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')"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
|
|
@ -9,22 +14,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
||||||
<span class="col">{{ $t(title) }}</span>
|
<span class="col">{{ $t(title) }}</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="startDate.length > 0"
|
v-if="startDate.length > 0"
|
||||||
class="col row flex-center full-width q-py-none q-my-none"
|
class="col row flex-center full-width q-py-none q-my-none"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -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 -->
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,74 +18,87 @@
|
||||||
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"
|
||||||
>
|
>
|
||||||
<q-card
|
<div class="col row shadow-2 rounded-10 q-my-xs">
|
||||||
v-for="day, day_index in week.shifts"
|
|
||||||
:key="day_index + index"
|
|
||||||
class="row col items-center rounded-10 q-my-xs q-pa-xs"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Dates column -->
|
<q-card
|
||||||
<q-card-section class="col-auto q-pa-none text-white">
|
v-for="day, day_index in week.days"
|
||||||
<div
|
:key="day_index + index"
|
||||||
class="bg-primary rounded-10 q-pa-xs text-center"
|
class="row col items-center no-shadow"
|
||||||
:style="date_box_size"
|
style="border-radius: 10px 0 0 10px;"
|
||||||
>
|
>
|
||||||
<q-item-label
|
|
||||||
v-if="!dense"
|
|
||||||
:style="'font-size: ' + weekday_font_size"
|
|
||||||
class="text-uppercase"
|
|
||||||
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
class="text-weight-bolder"
|
|
||||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
|
||||||
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
:style="'font-size: ' + weekday_font_size"
|
|
||||||
class="text-uppercase"
|
|
||||||
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- Dates column -->
|
||||||
<q-card-section class="col column q-pa-none full-height">
|
<q-card-section class="col-auto q-pa-none text-white">
|
||||||
<div
|
<div
|
||||||
v-if="day.shifts.length > 0"
|
class="bg-primary rounded-10 q-pa-xs text-center q-ml-sm"
|
||||||
class="col-grow column justify-center"
|
:style="date_box_size"
|
||||||
>
|
>
|
||||||
<ShiftListRow
|
<q-item-label
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-if="!dense"
|
||||||
:key="shift_index"
|
:style="'font-size: ' + weekday_font_size"
|
||||||
class="col"
|
class="text-uppercase"
|
||||||
:dense="dense"
|
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { weekday: $q.screen.lt.md ? 'short' : 'long' })
|
||||||
:shift="shift"
|
}}</q-item-label>
|
||||||
@request-update=""
|
<q-item-label
|
||||||
@request-delete=""
|
class="text-weight-bolder"
|
||||||
/>
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
</div>
|
>{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</q-item-label>
|
||||||
</q-card-section>
|
<q-item-label
|
||||||
</q-card>
|
:style="'font-size: ' + weekday_font_size"
|
||||||
|
class="text-uppercase"
|
||||||
|
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { month: $q.screen.lt.md ? 'short' : 'long' })
|
||||||
|
}}</q-item-label>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- List of shifts column -->
|
||||||
|
<q-card-section class="col column q-pa-none full-height">
|
||||||
|
<div
|
||||||
|
v-if="day.shifts.length > 0"
|
||||||
|
class="col-grow column justify-center"
|
||||||
|
>
|
||||||
|
<ShiftListRow
|
||||||
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
|
:key="shift_index"
|
||||||
|
class="col"
|
||||||
|
:dense="dense"
|
||||||
|
:shift="shift"
|
||||||
|
@request-update=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
47
src/pages/dashboard-page.vue
Normal file
47
src/pages/dashboard-page.vue
Normal 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>
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -13,27 +14,29 @@ import { useAuthStore } from 'src/stores/auth-store';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
||||||
|
|
||||||
const Router = createRouter({
|
const Router = createRouter({
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
routes,
|
routes,
|
||||||
|
|
||||||
// Leave this as is and make changes in quasar.conf.js instead!
|
// Leave this as is and make changes in quasar.conf.js instead!
|
||||||
// quasar.conf.js -> build -> vueRouterMode
|
// quasar.conf.js -> build -> vueRouterMode
|
||||||
// quasar.conf.js -> build -> publicPath
|
// quasar.conf.js -> build -> publicPath
|
||||||
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)) {
|
||||||
return { name: 'login' };
|
console.log('no user account found');
|
||||||
}
|
return { name: 'login' };
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return Router;
|
return Router;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
@ -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 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user