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;
|
||||
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
||||
|
||||
$input-text-color : #455A64;
|
||||
$input-autofill-color : #AAD5C4;
|
||||
|
||||
|
||||
$dark : #42444b;
|
||||
$dark-page : #343434;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,25 +9,21 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
dark
|
||||
class="q-pa-none q-mt-sm"
|
||||
<q-btn
|
||||
flat
|
||||
transparent
|
||||
dense
|
||||
: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
|
||||
v-if="notification_count > 0"
|
||||
floating
|
||||
color="negative"
|
||||
class="text-weight-bolder absolute"
|
||||
class="text-weight-bolder q-mt-xs"
|
||||
>
|
||||
{{ notification_count }}
|
||||
</q-badge>
|
||||
|
||||
</q-item>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
|
@ -32,7 +32,8 @@
|
|||
<template>
|
||||
<q-drawer
|
||||
v-model="ui_store.isRightDrawerOpen"
|
||||
overlay
|
||||
persistent
|
||||
mini-to-overlay
|
||||
elevated
|
||||
side="left"
|
||||
:mini="is_mini"
|
||||
|
|
@ -105,7 +106,7 @@
|
|||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
||||
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable */
|
||||
import { api } from 'src/boot/axios';
|
||||
import type { User } from 'src/modules/shared/models/user.models';
|
||||
|
||||
export const AuthService = {
|
||||
// Will likely be deprecated and relegated to Authentik
|
||||
|
|
@ -17,7 +18,7 @@ export const AuthService = {
|
|||
api.post('/auth/refresh')
|
||||
},
|
||||
|
||||
getProfile: async () => {
|
||||
getProfile: async (): Promise<User> => {
|
||||
const response = await api.get('/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<q-avatar
|
||||
color="primary"
|
||||
size="8em"
|
||||
class="shadow-3"
|
||||
class="shadow-3 q-mb-md"
|
||||
>
|
||||
<img
|
||||
src="src/assets/targo-default-avatar.png"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-lg col">
|
||||
<div class="q-pa-lg">
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
|
|
@ -49,6 +49,7 @@
|
|||
:filter="filter"
|
||||
class="q-pa-md bg-transparent"
|
||||
: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'"
|
||||
color="primary"
|
||||
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,56 +1,58 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import { deepEqual } from 'src/utils/deep-equal';
|
||||
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
||||
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue';
|
||||
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-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 { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
|
||||
const { employeeProfile } = defineProps<{
|
||||
employeeProfile: EmployeeProfile;
|
||||
}>();
|
||||
const employee_profile = defineModel<EmployeeProfile>({required: true});
|
||||
|
||||
let initial_info: EmployeeProfile = employeeProfile;
|
||||
let employee_form_data = ref<EmployeeProfile>({ ...employeeProfile });
|
||||
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 onSubmit = () => {
|
||||
const onSubmit = () => {
|
||||
if (!is_editing.value) {
|
||||
is_editing.value = true;
|
||||
console.log('clicky!');
|
||||
return;
|
||||
}
|
||||
|
||||
is_editing.value = false;
|
||||
initial_info = { ...employee_form_data.value }; // update initial value for future possible resets
|
||||
is_editing.value = false;
|
||||
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
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
employee_form_data = ref<EmployeeProfile>(initial_info);
|
||||
employee_profile.value = unwrapAndClone(initial_info);
|
||||
is_editing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form
|
||||
<q-form
|
||||
class="q-pa-md full-height"
|
||||
@submit="onSubmit"
|
||||
@reset="onReset"
|
||||
>
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<ProfileInputField
|
||||
v-model="employee_form_data.job_title"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.job_title"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.employee.job_title')"
|
||||
/>
|
||||
<ProfileInputField
|
||||
v-model="employee_form_data.company_name"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.company_name"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.employee.company')"
|
||||
|
|
@ -58,8 +60,8 @@
|
|||
</div>
|
||||
|
||||
<div class="q-mx-xs">
|
||||
<ProfileSelectField
|
||||
v-model="employee_form_data.supervisor_full_name"
|
||||
<MenuPanelSelectField
|
||||
v-model="employee_profile.supervisor_full_name"
|
||||
:options="supervisor_options"
|
||||
:label-string="$t('profile.employee.supervisor')"
|
||||
:is-editing="is_editing"
|
||||
|
|
@ -68,14 +70,14 @@
|
|||
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<ProfileInputField
|
||||
v-model="employee_form_data.email"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.email"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.employee.email')"
|
||||
/>
|
||||
<ProfileInputField
|
||||
v-model="employee_form_data.first_work_day"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.first_work_day"
|
||||
readonly
|
||||
class="col"
|
||||
type="date"
|
||||
|
|
@ -84,7 +86,10 @@
|
|||
/>
|
||||
</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-btn
|
||||
v-if="is_editing"
|
||||
|
|
@ -100,6 +105,7 @@
|
|||
push
|
||||
size="sm"
|
||||
color="primary"
|
||||
type="submit"
|
||||
:icon="is_editing ? 'save_alt' : 'create'"
|
||||
class="q-ma-sm"
|
||||
: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 { 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 { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
|
||||
const { employeeProfile } = defineProps<{
|
||||
employeeProfile: EmployeeProfile;
|
||||
}>();
|
||||
const employee_profile = defineModel<EmployeeProfile>({required: true});
|
||||
|
||||
const is_editing = ref<boolean>(false);
|
||||
|
||||
let initial_info: EmployeeProfile = employeeProfile;
|
||||
const personal_form_data = ref<EmployeeProfile>({ ...employeeProfile });
|
||||
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!is_editing.value) {
|
||||
|
|
@ -19,37 +20,37 @@
|
|||
return;
|
||||
}
|
||||
|
||||
is_editing.value = false;
|
||||
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets
|
||||
is_editing.value = false;
|
||||
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
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
personal_form_data.value= { ...initial_info };
|
||||
employee_profile.value = unwrapAndClone(initial_info);
|
||||
is_editing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form
|
||||
<q-form
|
||||
class="q-pa-md full-height"
|
||||
@submit="onSubmit"
|
||||
@reset="onReset"
|
||||
>
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<ProfileInputField
|
||||
v-model="personal_form_data.first_name"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.first_name"
|
||||
type="text"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.first_name')"
|
||||
/>
|
||||
<ProfileInputField
|
||||
v-model="personal_form_data.last_name"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.last_name"
|
||||
class="col"
|
||||
type="text"
|
||||
:is-editing="is_editing"
|
||||
|
|
@ -58,15 +59,15 @@
|
|||
</div>
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<ProfileInputField
|
||||
v-model="personal_form_data.phone_number"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.phone_number"
|
||||
class="col"
|
||||
type="text"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.phone_number')"
|
||||
/>
|
||||
<ProfileInputField
|
||||
v-model="personal_form_data.birth_date"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.birth_date"
|
||||
class="col"
|
||||
mask="#### / ## / ##"
|
||||
hint="ex: 1970 / 01 / 01"
|
||||
|
|
@ -76,8 +77,8 @@
|
|||
</div>
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<ProfileInputField
|
||||
v-model="personal_form_data.residence"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_profile.residence"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.address')"
|
||||
|
|
@ -85,7 +86,10 @@
|
|||
/>
|
||||
</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-btn
|
||||
v-if="is_editing"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
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<{
|
||||
firstName: string;
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
|
||||
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
|
||||
>
|
||||
<ProfileHeader
|
||||
<MenuHeader
|
||||
:user-first-name="firstName"
|
||||
: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">
|
||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
|
||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||
title: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
|
|
@ -9,22 +14,22 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
||||
<span class="col">{{ $t(title) }}</span>
|
||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
||||
<span class="col">{{ $t(title) }}</span>
|
||||
|
||||
<div
|
||||
v-if="startDate.length > 0"
|
||||
class="col row flex-center full-width q-py-none q-my-none"
|
||||
>
|
||||
<div class="text-primary text-weight-bold text-h6">
|
||||
{{ $d(new Date(startDate), date_format_options) }}
|
||||
</div>
|
||||
<div class="text-body2 q-mx-md text-weight-medium">
|
||||
{{ $t('shared.misc.to') }}
|
||||
</div>
|
||||
<div class="text-primary text-weight-bold text-h6">
|
||||
{{ $d(new Date(endDate), date_format_options) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="startDate.length > 0"
|
||||
class="col row flex-center full-width q-py-none q-my-none"
|
||||
>
|
||||
<div class="text-primary text-weight-bold text-h6">
|
||||
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||
</div>
|
||||
<div class="text-body2 q-mx-md text-weight-medium">
|
||||
{{ $t('shared.misc.to') }}
|
||||
</div>
|
||||
<div class="text-primary text-weight-bold text-h6">
|
||||
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,10 +3,14 @@
|
|||
import { date} from 'quasar';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const NEXT = 1;
|
||||
const PREVIOUS = -1;
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const is_showing_calendar_picker = ref(false);
|
||||
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
|
||||
const is_disabled = computed(() => timesheet_store.pay_period === undefined);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'date-selected': [ value: string ]
|
||||
|
|
@ -15,8 +19,8 @@
|
|||
}>();
|
||||
|
||||
const is_previous_pay_period_limit = computed( ()=>
|
||||
timesheet_store.pay_period.pay_year === 2024 &&
|
||||
timesheet_store.pay_period.pay_period_no <= 1
|
||||
( timesheet_store.pay_period?.pay_year === 2024 &&
|
||||
timesheet_store.pay_period?.pay_period_no <= 1 ) ?? false
|
||||
);
|
||||
|
||||
const onDateSelected = (value: string) => {
|
||||
|
|
@ -24,6 +28,33 @@
|
|||
is_showing_calendar_picker.value = false;
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -33,8 +64,8 @@
|
|||
push rounded
|
||||
icon="keyboard_arrow_left"
|
||||
color="primary"
|
||||
@click="emit('pressed-previous-button')"
|
||||
:disable="is_previous_pay_period_limit || timesheet_store.is_loading"
|
||||
@click="getPreviousPayPeriod"
|
||||
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
|
||||
class="q-mr-sm q-px-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
|
|
@ -52,7 +83,7 @@
|
|||
icon="calendar_month"
|
||||
color="primary"
|
||||
@click="is_showing_calendar_picker = true"
|
||||
:disable="timesheet_store.is_loading"
|
||||
:disable="timesheet_store.is_loading || is_disabled"
|
||||
class="q-px-xl"
|
||||
>
|
||||
<q-tooltip
|
||||
|
|
@ -69,8 +100,8 @@
|
|||
push rounded
|
||||
icon="keyboard_arrow_right"
|
||||
color="primary"
|
||||
@click="emit('pressed-next-button')"
|
||||
:disable="timesheet_store.is_loading"
|
||||
@click="getNextPayPeriod"
|
||||
:disable="timesheet_store.is_loading || is_disabled"
|
||||
class="q-ml-sm q-px-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable */
|
||||
|
||||
export interface User {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
|
|
|
|||
|
|
@ -25,28 +25,28 @@
|
|||
const expenses_labels = ref<string[]>([]);
|
||||
|
||||
const getExpensesData = (): ChartData<'bar'> => {
|
||||
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 = 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_costs = all_days.map(day => day.total_expenses);
|
||||
console.log('costs, ', all_costs);
|
||||
const all_mileage = all_days.map(day => day.total_mileage);
|
||||
// const all_costs = all_days.map(day => day.total_expenses);
|
||||
// console.log('costs, ', all_costs);
|
||||
// const all_mileage = all_days.map(day => day.total_mileage);
|
||||
|
||||
|
||||
expenses_dataset.value = [
|
||||
{
|
||||
label: t('timesheet_approvals.table.expenses'),
|
||||
data: all_costs,
|
||||
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
||||
},
|
||||
{
|
||||
label: t('timesheet_approvals.table.mileage'),
|
||||
data: all_mileage,
|
||||
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
|
||||
}
|
||||
]
|
||||
// expenses_dataset.value = [
|
||||
// {
|
||||
// label: t('timesheet_approvals.table.expenses'),
|
||||
// data: all_costs,
|
||||
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
||||
// },
|
||||
// {
|
||||
// label: t('timesheet_approvals.table.mileage'),
|
||||
// data: all_mileage,
|
||||
// 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 {
|
||||
datasets: expenses_dataset.value,
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
/* eslint-disable */
|
||||
import { ref } from 'vue';
|
||||
import { colors } from 'quasar';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
|
|
@ -22,37 +23,37 @@
|
|||
|
||||
const getHoursWorkedData = (): ChartData<'bar'> => {
|
||||
|
||||
const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
||||
const datasetConfig = [
|
||||
{
|
||||
key: 'regular_hours',
|
||||
label: t('shared.shift_type.regular'),
|
||||
color: colors.getPaletteColor('green-5'),
|
||||
},
|
||||
{
|
||||
key: 'evening_hours',
|
||||
label: t('shared.shift_type.evening'),
|
||||
color: colors.getPaletteColor('green-9'),
|
||||
},
|
||||
{
|
||||
key: 'emergency_hours',
|
||||
label: t('shared.shift_type.emergency'),
|
||||
color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
|
||||
},
|
||||
{
|
||||
key: 'overtime_hours',
|
||||
label: t('shared.shift_type.overtime'),
|
||||
color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
|
||||
},
|
||||
] as const;
|
||||
// const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
||||
// const datasetConfig = [
|
||||
// {
|
||||
// key: 'regular_hours',
|
||||
// label: t('shared.shift_type.regular'),
|
||||
// color: colors.getPaletteColor('green-5'),
|
||||
// },
|
||||
// {
|
||||
// key: 'evening_hours',
|
||||
// label: t('shared.shift_type.evening'),
|
||||
// color: colors.getPaletteColor('green-9'),
|
||||
// },
|
||||
// {
|
||||
// key: 'emergency_hours',
|
||||
// label: t('shared.shift_type.emergency'),
|
||||
// color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
|
||||
// },
|
||||
// {
|
||||
// key: 'overtime_hours',
|
||||
// label: t('shared.shift_type.overtime'),
|
||||
// color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
|
||||
// },
|
||||
// ] as const;
|
||||
|
||||
hours_worked_dataset.value = datasetConfig.map(cfg => ({
|
||||
label: cfg.label,
|
||||
data: all_days.map(day => day[ cfg.key ]),
|
||||
backgroundColor: cfg.color,
|
||||
}));
|
||||
// hours_worked_dataset.value = datasetConfig.map(cfg => ({
|
||||
// label: cfg.label,
|
||||
// data: all_days.map(day => day[ cfg.key ]),
|
||||
// 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 {
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { ref } from 'vue';
|
||||
import { colors } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
|
@ -22,27 +23,27 @@
|
|||
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
|
||||
|
||||
|
||||
shift_type_totals.value = [{
|
||||
data: [
|
||||
current_pay_period_overview.regular_hours,
|
||||
current_pay_period_overview.other_hours.evening_hours,
|
||||
current_pay_period_overview.other_hours.emergency_hours,
|
||||
current_pay_period_overview.other_hours.overtime_hours,
|
||||
],
|
||||
backgroundColor: [
|
||||
colors.getPaletteColor('green-5'), // Regular
|
||||
colors.getPaletteColor('green-9'), // Evening
|
||||
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
||||
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
||||
]
|
||||
}];
|
||||
// shift_type_totals.value = [{
|
||||
// data: [
|
||||
// current_pay_period_overview.regular_hours,
|
||||
// current_pay_period_overview.other_hours.evening_hours,
|
||||
// current_pay_period_overview.other_hours.emergency_hours,
|
||||
// current_pay_period_overview.other_hours.overtime_hours,
|
||||
// ],
|
||||
// backgroundColor: [
|
||||
// colors.getPaletteColor('green-5'), // Regular
|
||||
// colors.getPaletteColor('green-9'), // Evening
|
||||
// getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
||||
// getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
||||
// ]
|
||||
// }];
|
||||
|
||||
shift_type_labels.value = [
|
||||
current_pay_period_overview.regular_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.overtime_hours.toString() + 'h',
|
||||
]
|
||||
// shift_type_labels.value = [
|
||||
// current_pay_period_overview.regular_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.overtime_hours.toString() + 'h',
|
||||
// ]
|
||||
|
||||
|
||||
const data = {
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { provide, ref } from 'vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
<q-card-section
|
||||
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>
|
||||
|
||||
<!-- employee pay period details using chart -->
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { computed, ref } from 'vue';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
|
@ -38,8 +39,8 @@
|
|||
timesheet_store.current_pay_period_overview = row;
|
||||
emit('clickedDetailsButton', employee_email);
|
||||
|
||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
||||
await expenses_store.getPayPeriodExpensesByEmployeeEmail(employee_email);
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
||||
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getPayPeriodOverviewsBySupervisorEmail(
|
||||
timesheet_store.pay_period.pay_year,
|
||||
timesheet_store.pay_period.pay_period_no,
|
||||
auth_store.user.email
|
||||
await timesheet_store.getTimesheetOverviewsByPayPeriod(
|
||||
timesheet_store.pay_period?.pay_year ?? 1,
|
||||
timesheet_store.pay_period?.pay_period_no ?? 1,
|
||||
auth_store.user?.email
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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 ) => {
|
||||
if (timesheet_store.pay_period === undefined) return;
|
||||
|
||||
const [ targo, solucom ] = report_filter_company;
|
||||
const [ shifts, expenses, holiday, vacation ] = report_filter_type;
|
||||
const options = {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
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 { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
|
@ -17,27 +18,27 @@
|
|||
const expenses_api = useExpensesApi();
|
||||
const files = defineModel<File[] | null>('files');
|
||||
const is_navigator_open = ref(false);
|
||||
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
const employee_email = inject<string>('employeeEmail');
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
const cancelUpdateMode = () => {
|
||||
expenses_store.current_expense = default_expense;
|
||||
expenses_store.initial_expense = default_expense;
|
||||
expenses_store.mode = 'create';
|
||||
expenses_store.current_expense = empty_expense;
|
||||
expenses_store.initial_expense = empty_expense;
|
||||
}
|
||||
|
||||
const requestExpenseCreationOrUpdate = async () => {
|
||||
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date);
|
||||
else await expenses_api.updateExpenseByEmployeeEmail(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 ?? '');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form
|
||||
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"
|
||||
>
|
||||
<div class="text-subtitle2 q-py-sm">
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
</div>
|
||||
<div
|
||||
class="row justify-between rounded-5"
|
||||
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
||||
:class="mode === 'update' ? 'bg-accent' : ''"
|
||||
>
|
||||
|
||||
<!-- date selection input -->
|
||||
|
|
@ -93,7 +94,7 @@
|
|||
/>
|
||||
|
||||
<!-- 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
|
||||
key="amount"
|
||||
v-model.number="expenses_store.current_expense.amount"
|
||||
|
|
@ -176,7 +177,7 @@
|
|||
<!-- add btn section -->
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="expenses_store.mode === 'update'"
|
||||
v-if="mode === 'update'"
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
||||
const expense_store = useExpensesStore();
|
||||
|
|
@ -15,7 +16,8 @@
|
|||
>
|
||||
{{ $t('timesheet.expense.title') }}
|
||||
</q-item-label>
|
||||
<q-item-section
|
||||
|
||||
<!-- <q-item-section
|
||||
no-wrap
|
||||
class="col-auto items-center"
|
||||
>
|
||||
|
|
@ -23,7 +25,7 @@
|
|||
outline
|
||||
class="q-py-xs q-px-md"
|
||||
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>
|
||||
|
||||
|
|
@ -35,8 +37,8 @@
|
|||
outline
|
||||
class="q-py-xs q-px-md"
|
||||
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>
|
||||
</template>
|
||||
|
|
@ -2,15 +2,16 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
import { empty_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||
|
||||
const { expense, horizontal = false } = defineProps<{
|
||||
expense: Expense;
|
||||
|
|
@ -26,10 +27,10 @@
|
|||
const is_approved = defineModel<boolean>({ required: true });
|
||||
const is_selected = ref(false);
|
||||
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 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' : '')
|
||||
|
||||
|
||||
|
|
@ -37,15 +38,15 @@
|
|||
|
||||
|
||||
const setExpenseToModify = () => {
|
||||
expenses_store.mode = 'update';
|
||||
// expenses_store.mode = 'update';
|
||||
expenses_store.current_expense = expense;
|
||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||
};
|
||||
|
||||
const requestExpenseDeletion = async () => {
|
||||
expenses_store.mode = 'delete';
|
||||
// expenses_store.mode = 'delete';
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +67,7 @@
|
|||
:key="refresh_key"
|
||||
:clickable="horizontal"
|
||||
class="row col-4 q-ma-xs shadow-2"
|
||||
:style="expenseItemStyle + highlightClass + approvedClass"
|
||||
:style="expenseItemStyle + approvedClass"
|
||||
@click="onExpenseClicked"
|
||||
>
|
||||
<q-badge
|
||||
|
|
@ -84,7 +85,7 @@
|
|||
<!-- avatar type icon section -->
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="getExpenseTypeIcon(expense.type)"
|
||||
:name="getExpenseIcon(expense.type)"
|
||||
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||
size="lg"
|
||||
>
|
||||
|
|
@ -174,7 +175,6 @@
|
|||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="(!timesheet_store.pay_period_details.weeks[0]?.is_approved && !expense.is_approved) || horizontal"
|
||||
side
|
||||
class="q-pa-none"
|
||||
>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
lang="ts"
|
||||
>
|
||||
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();
|
||||
|
||||
|
|
@ -20,14 +20,14 @@
|
|||
:class="horizontal ? 'row flex-center' : ''"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{{ $t('timesheet.expense.empty_list') }}
|
||||
</q-item-label>
|
||||
|
||||
<ExpenseCrudDialogListItem
|
||||
v-for="(expense, index) in expenses_store.pay_period_expenses.expenses"
|
||||
<ExpenseDialogListItem
|
||||
v-for="(expense, index) in expenses_store.pay_period_expenses"
|
||||
:key="index"
|
||||
v-model="expense.is_approved"
|
||||
:index="index"
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
lang="ts"
|
||||
>
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||
import ExpenseCrudDialogForm from 'src/modules/timesheets/components/expense-crud-dialog-form.vue';
|
||||
import ExpenseCrudDialogHeader from 'src/modules/timesheets/components/expense-crud-dialog-header.vue';
|
||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||
|
||||
const expense_store = useExpensesStore();
|
||||
</script>
|
||||
|
|
@ -32,11 +32,17 @@
|
|||
{{ expenses_error }}
|
||||
</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 />
|
||||
|
||||
|
|
@ -4,9 +4,7 @@
|
|||
>
|
||||
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
defineProps<{
|
||||
shift: Shift;
|
||||
}>();
|
||||
const shift = defineModel<Shift>({ required: true });
|
||||
|
||||
defineEmits<{
|
||||
'onCommentBlur': [void];
|
||||
|
|
|
|||
|
|
@ -2,77 +2,84 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { ref } from 'vue';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const q = useQuasar();
|
||||
|
||||
const { shift, dense = false } = defineProps<{
|
||||
shift: Shift;
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
defineEmits<{
|
||||
'save-comment': [comment: string, shift: Shift];
|
||||
'request-update': [shift: Shift];
|
||||
'request-delete': [shift: Shift];
|
||||
}>();
|
||||
|
||||
const hour_font_size = computed(() => dense ? 'font-size: 1em;' : 'font-size: 1.5em;')
|
||||
const is_hovering = ref(false);
|
||||
const font_color = computed(() => shift.type === 'REGULAR' ? (q.dark.isActive ? ' text-blue-grey-2' : ' text-grey-8') : ' text-white')
|
||||
const shift_start = ref(shift.start_time);
|
||||
const shift_end = ref(shift.end_time);
|
||||
const time_picker_model = ref('');
|
||||
const is_showing_time_picker = ref(false);
|
||||
|
||||
const get_shift_color = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'REGULAR': return 'secondary';
|
||||
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 showTimePicker = (time: string) => {
|
||||
is_showing_time_picker.value = true;
|
||||
time_picker_model.value = time;
|
||||
};
|
||||
|
||||
const onClickUpdate = (type: string) => {
|
||||
if (type !== '') { emit('request-update', shift) };
|
||||
}
|
||||
|
||||
const onClickDelete = () => emit('request-delete', shift);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card-section
|
||||
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;"
|
||||
@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">
|
||||
<!-- punch-in timestamp -->
|
||||
<q-card-section
|
||||
class="col q-pa-none"
|
||||
:class="dense ? 'q-px-xs q-mx-xs' : ''"
|
||||
<q-input
|
||||
dense
|
||||
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
|
||||
class="text-weight-bolder q-pa-xs rounded-5"
|
||||
:class="'bg-' + get_shift_color(shift.type) + font_color"
|
||||
:style="hour_font_size + ' line-height: 80% !important;'"
|
||||
>
|
||||
{{ shift.start_time }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
|
||||
<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 -->
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="col items-center justify-center q-mx-sm"
|
||||
class="col-auto items-center justify-center q-mx-sm"
|
||||
>
|
||||
<div
|
||||
v-for="icon_data, index in [
|
||||
|
|
@ -96,40 +103,49 @@
|
|||
</q-card-section>
|
||||
|
||||
<!-- punch-out timestamps -->
|
||||
<q-card-section
|
||||
class="col q-pa-none"
|
||||
:class="dense ? 'q-px-xs q-mx-xs' : ''"
|
||||
<q-input
|
||||
dense
|
||||
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
|
||||
class="text-weight-bolder q-pa-xs rounded-5"
|
||||
:class="'bg-' + get_shift_color(shift.type) + font_color"
|
||||
:style="hour_font_size + ' line-height: 80% !important;'"
|
||||
>
|
||||
{{ shift.end_time }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
<div class="col-1"></div>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click="showTimePicker(shift_end)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</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 -->
|
||||
<q-icon
|
||||
v-if="shift.type"
|
||||
name="comment"
|
||||
v-if="shift.type && dense"
|
||||
:name="shift.comment ? 'comment' : ''"
|
||||
color="primary"
|
||||
:size="dense ? 'xs' : 'sm'"
|
||||
class="q-pa-none q-mr-xs"
|
||||
:class="shift.comment ? '' : 'invisible'"
|
||||
/>
|
||||
|
||||
<!-- delete btn -->
|
||||
<q-btn
|
||||
v-if="shift.type"
|
||||
v-else
|
||||
flat
|
||||
dense
|
||||
:size="dense ? 'xs' : 'sm'"
|
||||
color="negative"
|
||||
icon="clear"
|
||||
@click.stop="onClickDelete"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? 'primary' : 'grey-8'"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { date } from 'quasar';
|
||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||
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';
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
|
@ -18,74 +18,87 @@
|
|||
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||
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[] => {
|
||||
return shifts.length > 0 ? shifts : [default_shift];
|
||||
};
|
||||
|
||||
const getDate = (shift_date: string): Date => {
|
||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + shift_date);
|
||||
return shifts.length > 0 ? shifts : [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
||||
v-for="week, index in timesheet_store.timesheets"
|
||||
:key="index"
|
||||
class="column col q-mx-xs"
|
||||
>
|
||||
<q-card
|
||||
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"
|
||||
>
|
||||
<div class="col row shadow-2 rounded-10 q-my-xs">
|
||||
|
||||
<!-- Dates column -->
|
||||
<q-card-section class="col-auto q-pa-none text-white">
|
||||
<div
|
||||
class="bg-primary rounded-10 q-pa-xs text-center"
|
||||
:style="date_box_size"
|
||||
>
|
||||
<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>
|
||||
<q-card
|
||||
v-for="day, day_index in week.days"
|
||||
:key="day_index + index"
|
||||
class="row col items-center no-shadow"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
>
|
||||
|
||||
<!-- Dates column -->
|
||||
<q-card-section class="col-auto q-pa-none text-white">
|
||||
<div
|
||||
class="bg-primary rounded-10 q-pa-xs text-center q-ml-sm"
|
||||
:style="date_box_size"
|
||||
>
|
||||
<q-item-label
|
||||
v-if="!dense"
|
||||
:style="'font-size: ' + weekday_font_size"
|
||||
class="text-uppercase"
|
||||
>{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), { 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;'"
|
||||
>{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}</q-item-label>
|
||||
<q-item-label
|
||||
: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>
|
||||
|
||||
<!-- 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=""
|
||||
@request-delete=""
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
import GenericLoader from 'src/modules/shared/components/generic-loader.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 ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
>
|
||||
|
||||
<q-card-section
|
||||
v-if="!dense"
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-px-md items-center"
|
||||
:class="$q.screen.lt.md ? 'column' : ''"
|
||||
|
|
@ -46,40 +47,37 @@
|
|||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
v-if="!dense"
|
||||
@date-selected="timesheet_api.getPayPeriodDetailsByDate(employeeEmail)"
|
||||
@pressed-previous-button="timesheet_api.getPreviousPayPeriodDetails(employeeEmail)"
|
||||
@pressed-next-button="timesheet_api.getNextPayPeriodDetails(employeeEmail)"
|
||||
@date-selected="timesheet_api.getTimesheetsByDate(employeeEmail)"
|
||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||
/>
|
||||
|
||||
<!-- mobile expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.lt.md && !dense"
|
||||
v-if="$q.screen.lt.md"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
class="q-mt-sm"
|
||||
@click="open(employeeEmail)"
|
||||
@click="open"
|
||||
/>
|
||||
|
||||
<!-- shift's colored legend -->
|
||||
<ShiftListLegend
|
||||
v-if="!dense"
|
||||
:is-loading="false"
|
||||
/>
|
||||
<ShiftListLegend :is-loading="false" />
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.gt.sm && !dense"
|
||||
v-if="$q.screen.gt.sm"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="open(employeeEmail)"
|
||||
@click="open"
|
||||
/>
|
||||
|
||||
</q-card-section>
|
||||
|
|
@ -92,6 +90,6 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<ExpenseCrudDialog />
|
||||
<ExpenseDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { normalizeObject } from "src/utils/normalize-object";
|
||||
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";
|
||||
|
||||
export const useExpensesApi = () => {
|
||||
|
|
|
|||
|
|
@ -4,50 +4,27 @@ import { useTimesheetStore } from "src/stores/timesheet-store"
|
|||
export const useTimesheetApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
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);
|
||||
|
||||
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 { pay_period } = timesheet_store;
|
||||
let new_number = pay_period.pay_period_no + direction;
|
||||
let new_year = pay_period.pay_year;
|
||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||
if (timesheet_store.pay_period === undefined) return false;
|
||||
|
||||
if (new_number > 26) {
|
||||
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);
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
|
||||
|
||||
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 {
|
||||
getPayPeriodDetailsByDate,
|
||||
getNextPayPeriodDetails,
|
||||
getPreviousPayPeriodDetails,
|
||||
getTimesheetsByDate,
|
||||
getTimesheetsByCurrentPayPeriod,
|
||||
};
|
||||
};
|
||||
|
|
@ -6,20 +6,42 @@ export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXP
|
|||
|
||||
export interface Expense {
|
||||
id: number;
|
||||
date: string;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
mileage?: number;
|
||||
comment: string;
|
||||
supervisor_comment?: string;
|
||||
is_approved: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const default_expense: Expense = {
|
||||
export const empty_expense: Expense = {
|
||||
id: -1,
|
||||
date: '',
|
||||
type: 'EXPENSES',
|
||||
amount: 0,
|
||||
comment: '',
|
||||
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 {
|
||||
id: number;
|
||||
date: string;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ShiftType;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
start_time: string; //HH:mm:ss
|
||||
end_time: string; //HH:mm:ss
|
||||
comment: string | undefined;
|
||||
is_approved: 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 {
|
||||
id: number;
|
||||
is_approved: boolean;
|
||||
weekly_hours: TotalHours;
|
||||
weekly_expenses: TotalExpenses;
|
||||
days: TimesheetDay[];
|
||||
}
|
||||
|
||||
export interface TimesheetDay {
|
||||
date: string;
|
||||
date: string; // YYYY-MM-DD
|
||||
daily_hours: TotalHours;
|
||||
daily_expenses: TotalExpenses;
|
||||
shifts: Shift[];
|
||||
|
|
@ -33,4 +34,79 @@ export interface TotalHours {
|
|||
export interface TotalExpenses {
|
||||
expenses: 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";
|
||||
|
||||
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> => {
|
||||
const response = await api.get(`pay-periods/date/${date_string}`);
|
||||
return response.data;
|
||||
|
|
@ -23,20 +18,20 @@ export const timesheetService = {
|
|||
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}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPayPeriodDetailsByPayPeriodAndEmployeeEmail: async (year: number, period_no: number, email: string): Promise<Timesheet[]> => {
|
||||
const response = await api.get('timesheets', { params: { year, period_no, email, } });
|
||||
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<Timesheet[]> => {
|
||||
const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||
// const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
||||
// return response.data;
|
||||
// },
|
||||
getExpensesByTimesheetId: async (timesheet_id: number): Promise<Expense[]> => {
|
||||
const response = await api.get(`/expenses/list/${timesheet_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/shifts/upsert/${email}`, payload);
|
||||
|
|
@ -48,8 +43,8 @@ export const timesheetService = {
|
|||
return response;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: Expense[]): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload);
|
||||
upsertOrDeleteExpenseByEmailAndExpenseId: async (email: string, expense_id: number): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/expenses/upsert/${email}`, expense_id);
|
||||
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>
|
||||
|
||||
<template>
|
||||
<q-page>
|
||||
<q-page class="column flex-center">
|
||||
<EmployeeListAddModifyDialog />
|
||||
|
||||
<PageHeaderTemplate title="employee_list.page_header" />
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import MenuEmployee from 'src/modules/profile/components/employee/menu-employee.vue';
|
||||
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 employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
|
||||
const employee_roles = ['SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING'];
|
||||
|
||||
defineProps<{
|
||||
employeeProfile?: EmployeeProfile | undefined;
|
||||
}>();
|
||||
// const employee_profile = defineModel<EmployeeProfile>({ required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="bg-secondary column items-center justify-center">
|
||||
<ProfileEmployee
|
||||
v-if="employee_roles.includes( auth_store.user.role.toUpperCase() )"
|
||||
<q-page class="bg-secondary column items-center justify-center">
|
||||
<MenuEmployee
|
||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||
class="col-auto"
|
||||
:employee-profile="employeeProfile"
|
||||
/>
|
||||
</q-page>
|
||||
</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 PageHeaderTemplate from 'src/modules/shared/components/page-header-template.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_store = useTimesheetStore();
|
||||
|
|
@ -29,16 +29,16 @@
|
|||
>
|
||||
<PageHeaderTemplate
|
||||
title="timesheet_approvals.page_title"
|
||||
:start-date="timesheet_store.pay_period.period_start"
|
||||
:end-date="timesheet_store.pay_period.period_end"
|
||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||
/>
|
||||
|
||||
<DetailscrudDialog
|
||||
<DetailsDialog
|
||||
v-model:dialog="is_details_dialog_open"
|
||||
:employee-email="employee_email"
|
||||
:is-loading="timesheet_store.is_loading"
|
||||
:employee-overview="timesheet_store.current_pay_period_overview"
|
||||
:timesheet-details="timesheet_store.pay_period_details"
|
||||
:timesheets="timesheet_store.timesheets"
|
||||
/>
|
||||
|
||||
<OverviewList @clickedDetailsButton="onDetailsClicked"/>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
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>
|
||||
|
||||
|
|
@ -23,16 +23,23 @@
|
|||
<template>
|
||||
<q-page
|
||||
padding
|
||||
class="column q-pa-md bg-secondary flex-center"
|
||||
class="column q-pa-md bg-secondary items-center"
|
||||
>
|
||||
<PageHeaderTemplate
|
||||
:title="'timesheet.page_header'"
|
||||
:start-date="timesheet_store.pay_period.period_start"
|
||||
:end-date="timesheet_store.pay_period.period_end"
|
||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<div class="col column flex-center" :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
||||
<TimesheetWrapper class="col" :employee-email="user.email" />
|
||||
<div
|
||||
class="col column items-center"
|
||||
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
|
||||
>
|
||||
<TimesheetWrapper
|
||||
class="col-auto"
|
||||
:employee-email="user?.email ?? ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { defineRouter } from '#q-app/wrappers';
|
|||
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router';
|
||||
import routes from './routes';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
|
||||
/*
|
||||
* 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 } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
Router.beforeEach((destinationPage) => {
|
||||
const authStore = useAuthStore();
|
||||
Router.beforeEach(async (destinationPage) => {
|
||||
const authStore = useAuthStore();
|
||||
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||
|
||||
if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) {
|
||||
return { name: 'login' };
|
||||
}
|
||||
})
|
||||
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.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',
|
||||
EMPLOYEE_LIST = 'employee-list',
|
||||
PROFILE = 'user/profile',
|
||||
TIMESHEET_TEMP = 'timesheet-temp'
|
||||
TIMESHEET = 'timesheet'
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
|||
{
|
||||
path: '',
|
||||
name: RouteNames.DASHBOARD,
|
||||
component: () => import('src/pages/test-page.vue'),
|
||||
component: () => import('src/pages/dashboard-page.vue'),
|
||||
},
|
||||
{
|
||||
path: 'timesheet-approvals',
|
||||
|
|
@ -23,8 +23,8 @@ const routes: RouteRecordRaw[] = [
|
|||
component: () => import('src/pages/employee-list-page.vue'),
|
||||
},
|
||||
{
|
||||
path: 'timesheet-temp',
|
||||
name: RouteNames.TIMESHEET_TEMP,
|
||||
path: 'timesheet',
|
||||
name: RouteNames.TIMESHEET,
|
||||
component: () => import('src/pages/timesheet-page.vue')
|
||||
},
|
||||
{
|
||||
|
|
@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
|
|||
{
|
||||
path: '/v1/login',
|
||||
name: RouteNames.LOGIN,
|
||||
component: () => import('src/modules/auth/pages/auth-login.vue'),
|
||||
component: () => import('src/pages/login-page.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -15,19 +15,9 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
//TODO: manage customer login process
|
||||
};
|
||||
|
||||
const oidcLogin = async (): Promise<void> => {
|
||||
window.addEventListener('message', async (event) => {
|
||||
if (event.data.type === 'authSuccess') {
|
||||
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 oidcLogin = () => {
|
||||
window.addEventListener('message', (event) => {
|
||||
void handleAuthMessage(event);
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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 { 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 { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||
import type { CrudAction } from "src/modules/timesheets/models/shift.models";
|
||||
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
|
||||
import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
|
||||
|
||||
export const useExpensesStore = defineStore('expenses', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const is_open = ref(false);
|
||||
const is_loading = ref(false);
|
||||
const mode = ref<CrudAction>('create');
|
||||
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
||||
const current_expense = ref<Expense>(default_expense);
|
||||
const initial_expense = ref<Expense>(default_expense);
|
||||
const pay_period_expenses = ref<Expense[]>(test_expenses);
|
||||
const current_expense = ref<Expense>(empty_expense);
|
||||
const initial_expense = ref<Expense>(empty_expense);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// const setErrorFrom = (err: unknown) => {
|
||||
|
|
@ -23,14 +19,14 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
// error.value = e?.message || 'Unknown error';
|
||||
// };
|
||||
|
||||
const open = async (employee_email: string): Promise<void> => {
|
||||
const open = (): void => {
|
||||
is_open.value = true;
|
||||
is_loading.value = true;
|
||||
error.value = null;
|
||||
current_expense.value = default_expense;
|
||||
initial_expense.value = default_expense;
|
||||
current_expense.value = empty_expense;
|
||||
initial_expense.value = empty_expense;
|
||||
|
||||
await getPayPeriodExpensesByEmployeeEmail(employee_email);
|
||||
// await getPayPeriodExpensesByTimesheetId(timesheet_id);
|
||||
is_loading.value = false;
|
||||
}
|
||||
|
||||
|
|
@ -39,16 +35,12 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
is_open.value = false;
|
||||
};
|
||||
|
||||
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||
const getPayPeriodExpensesByTimesheetId = async (timesheet_id: number): Promise<void> => {
|
||||
is_loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(
|
||||
encodeURIComponent(employee_email),
|
||||
encodeURIComponent(timesheet_store.pay_period.pay_year),
|
||||
encodeURIComponent(timesheet_store.pay_period.pay_period_no),
|
||||
);
|
||||
const expenses = await timesheetService.getExpensesByTimesheetId(timesheet_id);
|
||||
pay_period_expenses.value = expenses;
|
||||
} catch (err: unknown) {
|
||||
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;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(
|
||||
await timesheetService.upsertOrDeleteExpenseByEmailAndExpenseId(
|
||||
encodeURIComponent(employee_email),
|
||||
encodeURIComponent(date),
|
||||
expense,
|
||||
expense_id,
|
||||
);
|
||||
console.log('updated expenses received: ', updated_expenses)
|
||||
pay_period_expenses.value.expenses = updated_expenses;
|
||||
// TODO: Save response data into proper ref
|
||||
} catch (err) {
|
||||
// setErrorFrom(err);
|
||||
console.log('error doing some expense thing: ', err)
|
||||
console.error(err);
|
||||
} finally {
|
||||
is_loading.value = false;
|
||||
}
|
||||
|
|
@ -93,14 +83,13 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
return {
|
||||
is_open,
|
||||
is_loading,
|
||||
mode,
|
||||
pay_period_expenses,
|
||||
current_expense,
|
||||
initial_expense,
|
||||
error,
|
||||
open,
|
||||
getPayPeriodExpensesByEmployeeEmail,
|
||||
upsertOrDeleteExpensesByEmployeeEmail,
|
||||
getPayPeriodExpensesByTimesheetId,
|
||||
upsertOrDeleteExpensesById,
|
||||
close,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
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 { 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 { default_pay_period, 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 type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||
import { test_timesheets, type Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||
const is_loading = ref<boolean>(false);
|
||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
||||
const pay_period_overviews = ref<TimesheetOverview[]>([default_pay_period_overview,]);
|
||||
const current_pay_period_overview = ref<TimesheetOverview>(default_pay_period_overview);
|
||||
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
||||
const pay_period = ref<PayPeriod>();
|
||||
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
||||
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||
const timesheets = ref<Timesheet[]>(test_timesheets);
|
||||
const pay_period_report = ref();
|
||||
const is_calendar_limit = computed(() =>
|
||||
pay_period.value.pay_year === 2024 &&
|
||||
pay_period.value.pay_period_no <= 1
|
||||
);
|
||||
const is_calendar_limit = computed(() => (pay_period.value?.pay_year === 2024 && pay_period.value?.pay_period_no <= 1) ?? false);
|
||||
|
||||
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
||||
is_loading.value = true;
|
||||
|
|
@ -30,14 +30,14 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
else if (typeof date_or_year === 'number' && 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;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Could not get current pay period: ', error);
|
||||
pay_period.value = default_pay_period;
|
||||
pay_period_overviews.value = [default_pay_period_overview,];
|
||||
pay_period.value = undefined;
|
||||
pay_period_overviews.value = [];
|
||||
//TODO: More in-depth error-handling here
|
||||
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;
|
||||
|
||||
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;
|
||||
is_loading.value = false;
|
||||
|
||||
return true;
|
||||
} catch (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
|
||||
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;
|
||||
if (pay_period.value === undefined) return;
|
||||
try {
|
||||
const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
|
||||
pay_period.value.pay_year,
|
||||
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)
|
||||
const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||
timesheets.value = response;
|
||||
is_loading.value = false;
|
||||
} catch (error) {
|
||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||
// TODO: More in-depth error-handling here
|
||||
pay_period_details.value = default_pay_period_details;
|
||||
timesheets.value = [];
|
||||
is_loading.value = false;
|
||||
}
|
||||
};
|
||||
|
|
@ -109,10 +105,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
pay_period,
|
||||
pay_period_overviews,
|
||||
current_pay_period_overview,
|
||||
pay_period_details,
|
||||
timesheets,
|
||||
getPayPeriodByDateOrYearAndNumber,
|
||||
getPayPeriodOverviewsBySupervisorEmail,
|
||||
getPayPeriodDetailsByEmployeeEmail,
|
||||
getTimesheetOverviewsByPayPeriod,
|
||||
getTimesheetsByEmployeeEmail,
|
||||
getPayPeriodReportByYearAndPeriodNumber,
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user