Merge pull request 'dev/nicolas/timesheet-gui-refactor' (#22) from dev/nicolas/timesheet-gui-refactor into main
Reviewed-on: Targo/targo_frontend#22
This commit is contained in:
commit
52984c88e9
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# targo-frontend
|
||||
FROM node:22
|
||||
|
||||
# Set working directory inside container
|
||||
WORKDIR /app
|
||||
|
||||
# Set environment variables
|
||||
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
|
||||
|
||||
# Copy package.json & package-lock.json first (for caching)
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the code
|
||||
COPY . .
|
||||
|
||||
# Expose Quasar dev port
|
||||
EXPOSE 9000
|
||||
|
||||
# Default command
|
||||
CMD ["quasar", "dev"]
|
||||
|
|
@ -14,7 +14,10 @@ declare module 'vue' {
|
|||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({ baseURL: import.meta.env.VITE_TARGO_BACKEND_AUTH_URL });
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_TARGO_BACKEND_URL,
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// app global css in SCSS form
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) {
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
|
||||
body.body--dark {
|
||||
--q-secondary: #0f1114;
|
||||
--q-secondary: #2b2f34;
|
||||
color: $grey-2;
|
||||
}
|
||||
|
||||
|
|
@ -33,3 +33,12 @@ body.body--dark {
|
|||
--q-dark: #FFF;
|
||||
color: $blue-grey-8;
|
||||
}
|
||||
|
||||
.shift-highlight {
|
||||
background: #0195462a;
|
||||
}
|
||||
|
||||
.frosted-glass {
|
||||
background-color: #FFFA !important;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
|
@ -16,16 +16,22 @@ $primary : #019547;
|
|||
$secondary : #DAE0E7;
|
||||
$accent : #AAD5C4;
|
||||
|
||||
$dark-shadow-color : #019547;
|
||||
$dark-shadow-color : #00220f;
|
||||
|
||||
$elevation-dark-umbra : rgba($dark-shadow-color, 0.4);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0);
|
||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0);
|
||||
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.2);
|
||||
$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);
|
||||
|
||||
$dark : #333;
|
||||
$input-text-color : #455A64;
|
||||
$input-autofill-color : #AAD5C4;
|
||||
$field-dense-label-top : 5px !default;
|
||||
$field-dense-label-font-size : 16px !default;
|
||||
|
||||
|
||||
$dark : #42444b;
|
||||
$dark-page : #343434;
|
||||
|
||||
$positive : #21ba45;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export default {
|
|||
tooltip: {
|
||||
coming_soon: "coming soon!",
|
||||
},
|
||||
error: {
|
||||
login_failed: "Failed to login",
|
||||
popups_blocked: "Popups are blocked on this device",
|
||||
},
|
||||
},
|
||||
|
||||
nav_bar: {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export default {
|
|||
tooltip: {
|
||||
coming_soon: "à venir!",
|
||||
},
|
||||
error: {
|
||||
login_failed: "Échec à la connexion",
|
||||
popups_blocked: "Les fenêtres contextuelles sont bloqués sur cet appareil",
|
||||
},
|
||||
},
|
||||
|
||||
nav_bar: {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const currentUser = authStore.user;
|
||||
|
||||
// Will need to implement this eventually, just testing the look for now
|
||||
const notifAmount = ref(7);
|
||||
const notification_count = ref(7);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-item clickable v-ripple dark class="q-pa-none">
|
||||
<q-item-section :side="$q.screen.gt.sm">
|
||||
<q-avatar rounded >
|
||||
<q-img src="src/assets/targo-default-avatar.png" />
|
||||
<q-badge floating color="negative" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section v-if="$q.screen.gt.sm">
|
||||
<q-item-label>{{ currentUser.firstName }} {{ currentUser.lastName }}</q-item-label>
|
||||
<q-item-label caption>{{ notifAmount }} new messages</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-btn
|
||||
flat
|
||||
transparent
|
||||
dense
|
||||
:icon="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 q-mt-xs"
|
||||
>
|
||||
{{ notification_count }}
|
||||
</q-badge>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { ref } from 'vue';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const auth_store = useAuthStore();
|
||||
const ui_store = useUiStore();
|
||||
const router = useRouter();
|
||||
const miniState = ref(true);
|
||||
const is_mini = ref(true);
|
||||
|
||||
const goToPageName = (pageName: string) => {
|
||||
router.push({ name: pageName }).catch(err => {
|
||||
const goToPageName = (page_name: string) => {
|
||||
router.push({ name: page_name }).catch(err => {
|
||||
console.error('Error with Vue Router: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout();
|
||||
auth_store.logout();
|
||||
|
||||
router.push({ name: 'login' }).catch(err => {
|
||||
console.log('could not log you out: ', err);
|
||||
|
|
@ -27,21 +31,30 @@
|
|||
|
||||
<template>
|
||||
<q-drawer
|
||||
v-model="uiStore.isRightDrawerOpen"
|
||||
overlay
|
||||
v-model="ui_store.is_left_drawer_open"
|
||||
persistent
|
||||
mini-to-overlay
|
||||
elevated
|
||||
side="left"
|
||||
:mini="miniState"
|
||||
@mouseenter="miniState = false"
|
||||
@mouseleave="miniState = true"
|
||||
:mini="is_mini"
|
||||
@mouseenter="is_mini = false"
|
||||
@mouseleave="is_mini = true"
|
||||
class="bg-dark"
|
||||
>
|
||||
<q-scroll-area class="fit">
|
||||
<q-list>
|
||||
<!-- Home -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.DASHBOARD)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.DASHBOARD)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="home" color="primary" />
|
||||
<q-icon
|
||||
name="home"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
|
||||
|
|
@ -49,42 +62,77 @@
|
|||
</q-item>
|
||||
|
||||
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||
v-if="['supervisor', 'accounting'].includes(authStore.user.role)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="event_available" color="primary" />
|
||||
<q-icon
|
||||
name="event_available"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="view_list" color="primary" />
|
||||
<q-icon
|
||||
name="view_list"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
||||
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="punch_clock" color="primary" />
|
||||
<q-icon
|
||||
name="punch_clock"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Profile -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.PROFILE)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.PROFILE)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="account_box" color="primary" />
|
||||
<q-icon
|
||||
name="account_box"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
|
||||
|
|
@ -92,9 +140,16 @@
|
|||
</q-item>
|
||||
|
||||
<!-- Help -->
|
||||
<q-item v-ripple clickable @click="goToPageName('help')">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goToPageName('help')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="contact_support" color="primary" />
|
||||
<q-icon
|
||||
name="contact_support"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
|
||||
|
|
@ -103,9 +158,17 @@
|
|||
</q-list>
|
||||
|
||||
<!-- Logout -->
|
||||
<q-item v-ripple clickable @click="handleLogout" class="absolute-bottom">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="handleLogout"
|
||||
class="absolute-bottom"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="exit_to_app" color="primary" />
|
||||
<q-icon
|
||||
name="exit_to_app"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||
|
||||
|
|
@ -10,9 +13,13 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-card class="rounded-15">
|
||||
<q-card class="rounded-15 shadow-10">
|
||||
<q-card-section class="text-center bg-primary q-pa-lg">
|
||||
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" />
|
||||
<q-img
|
||||
src="/src/assets/logo-targo-white.svg"
|
||||
ratio="4.6"
|
||||
fit="contain"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<div class="q-pt-sm q-px-xl q-pb-lg ">
|
||||
|
|
@ -28,8 +35,14 @@
|
|||
dense
|
||||
outlined
|
||||
label-color="primary"
|
||||
:label="$t('login.email')"
|
||||
/>
|
||||
class="rounded-5 inset-shadow bg-blue-grey-1"
|
||||
label-slot
|
||||
input-class="text-weight-medium text-h6"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
|
||||
<q-toggle
|
||||
|
|
@ -58,9 +71,16 @@
|
|||
</q-form>
|
||||
|
||||
<q-card-section class="row q-pt-sm">
|
||||
<q-separator color="primary" class="col self-center"/>
|
||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{ $t('shared.misc.or') }}</span>
|
||||
<q-separator color="primary" class="col self-center"/>
|
||||
<q-separator
|
||||
color="primary"
|
||||
class="col self-center"
|
||||
/>
|
||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
|
||||
$t('shared.misc.or') }}</span>
|
||||
<q-separator
|
||||
color="primary"
|
||||
class="col self-center"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="column q-px-sm q-pt-none">
|
||||
|
|
@ -73,7 +93,10 @@
|
|||
:label="$t('login.button.facebook')"
|
||||
class="full-width row q-mb-sm"
|
||||
>
|
||||
<q-tooltip anchor="top middle" class="bg-primary">{{$t('login.tooltip.coming_soon')}}</q-tooltip>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
class="bg-primary"
|
||||
>{{ $t('login.tooltip.coming_soon') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-slide-transition>
|
||||
<div v-if="is_employee_email">
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthApi } from '../composables/use-auth-api';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const auth_api = useAuthApi();
|
||||
const router = useRouter();
|
||||
|
||||
const setBypassUser = (bypassRole: string) => {
|
||||
auth_api.setUser(bypassRole);
|
||||
|
||||
router.push({ name: 'dashboard' }).catch( err => {
|
||||
console.error('Router navigation failed: ', err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card class="absolute-bottom-right q-ma-sm">
|
||||
<q-card-section class="q-pa-sm text-uppercase text-center"> impersonate </q-card-section>
|
||||
<q-card-actions vertical>
|
||||
<q-btn
|
||||
v-for="role, index in [ 'supervisor', 'accounting', 'human_resources', 'employee' ]"
|
||||
:key="index"
|
||||
push
|
||||
color="primary"
|
||||
text-color="white"
|
||||
:label="role"
|
||||
class="text-uppercase"
|
||||
@click="setBypassUser(role)"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
|
@ -3,8 +3,6 @@ import { useAuthStore } from "../../../stores/auth-store";
|
|||
export const useAuthApi = () => {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
|
||||
|
||||
const login = () => {
|
||||
authStore.login();
|
||||
};
|
||||
|
|
@ -17,19 +15,9 @@ export const useAuthApi = () => {
|
|||
authStore.logout();
|
||||
};
|
||||
|
||||
const isAuthorizedUser = () => {
|
||||
return authStore.isAuthorizedUser;
|
||||
};
|
||||
|
||||
const setUser = (bypassRole: string) => {
|
||||
authStore.setUser(bypassRole);
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
oidcLogin,
|
||||
logout,
|
||||
isAuthorizedUser,
|
||||
setUser,
|
||||
};
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -7,16 +8,6 @@ export const AuthService = {
|
|||
//TODO: OIDC customer sign-in, eventually
|
||||
},
|
||||
|
||||
oidcLogin: (): Window | null => {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'authSuccess') {
|
||||
//some kind of logic here to set user in store
|
||||
}
|
||||
})
|
||||
|
||||
return window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800');
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
// TODO: logout logic
|
||||
api.post('/auth/logout')
|
||||
|
|
@ -27,8 +18,8 @@ export const AuthService = {
|
|||
api.post('/auth/refresh')
|
||||
},
|
||||
|
||||
getProfile: () => {
|
||||
// TODO: user info fetch logic
|
||||
api.get('/auth/me')
|
||||
getProfile: async (): Promise<User> => {
|
||||
const response = await api.get('/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
||||
// // add logic here to see if user has an avatar image and return that instead of initials
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
// };
|
||||
|
||||
const { row } = defineProps<{
|
||||
row: EmployeeListTableItem
|
||||
row: EmployeeProfile
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
onProfileClick: [email: string]
|
||||
|
|
@ -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"
|
||||
|
|
@ -3,9 +3,8 @@
|
|||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
|
||||
|
||||
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
|
||||
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import type { QTableColumn } from 'quasar';
|
||||
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
|
@ -17,7 +16,7 @@
|
|||
const is_grid_mode = ref(true);
|
||||
const pagination = ref({ rowsPerPage: 0 });
|
||||
|
||||
const employee_list_columns = computed((): QTableColumn<EmployeeListTableItem>[] => [
|
||||
const employee_list_columns = computed((): QTableColumn<EmployeeProfile>[] => [
|
||||
{name: 'first_name', label: t('employee_list.table.first_name'), field: 'first_name', align: 'left'},
|
||||
{name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
|
||||
{name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
|
||||
|
|
@ -34,7 +33,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-lg col">
|
||||
<div class="q-pa-lg">
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
|
|
@ -49,7 +48,8 @@
|
|||
:rows-per-page-options="[0]"
|
||||
:filter="filter"
|
||||
class="q-pa-md bg-transparent"
|
||||
:class="is_grid_mode ? '': 'my-sticky-header-table'"
|
||||
:class="is_grid_mode ? '': 'sticky-header-table'"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
|
||||
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
|
||||
color="primary"
|
||||
table-header-class="text-primary text-uppercase"
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
@row-click="() => console.log('click!')"
|
||||
>
|
||||
<template v-slot:item="props">
|
||||
<SupervisorCrewTableItem :row="props.row"/>
|
||||
<EmployeeListTableItem :row="props.row"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:top>
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.my-sticky-header-table
|
||||
.sticky-header-table
|
||||
thead tr:first-child th
|
||||
background-color: var(--q-dark)
|
||||
margin-top: none
|
||||
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { api } from 'src/boot/axios';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
export const EmployeeListService = {
|
||||
getEmployeeList: async (): Promise<EmployeeProfile[]> => {
|
||||
const response = await api.get<EmployeeProfile[]>('/employees/employee-list')
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
||||
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
// /* eslint-disable */
|
||||
import { api } from 'src/boot/axios';
|
||||
import type { EmployeeListTableItem } from '../types/employee-list-table-interface';
|
||||
import type { EmployeeProfile } from '../types/employee-profile-interface';
|
||||
|
||||
|
||||
export const EmployeeListService = {
|
||||
getEmployeeList: async (): Promise<EmployeeListTableItem[]> => {
|
||||
const response = await api.get<EmployeeListTableItem[]>('/employees/employee-list')
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
||||
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface EmployeeListTableItem {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
supervisor_full_name: string | null;
|
||||
company_name: number;
|
||||
job_title: string;
|
||||
};
|
||||
73
src/modules/profile/components/employee/menu-employee.vue
Normal file
73
src/modules/profile/components/employee/menu-employee.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import MenuPanelPersonal from 'src/modules/profile/components/employee/menu-panel-personal.vue';
|
||||
import MenuPanelEmployee from 'src/modules/profile/components/employee/menu-panel-employee.vue';
|
||||
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
|
||||
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.vue';
|
||||
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const PanelNames = {
|
||||
PERSONAL_INFO: 'personal_info',
|
||||
EMPLOYEE_INFO: 'employee_info',
|
||||
PREFERENCES: 'references',
|
||||
};
|
||||
|
||||
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
flat
|
||||
class="rounded-5 bg-transparent q-pa-none"
|
||||
>
|
||||
<MenuTemplate
|
||||
:first-name="employee_profile.first_name"
|
||||
:last-name="employee_profile.last_name"
|
||||
:initial-menu="PanelNames.PERSONAL_INFO"
|
||||
>
|
||||
<template #tabs>
|
||||
<q-tab
|
||||
:name='PanelNames.PERSONAL_INFO'
|
||||
icon='person_outline'
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
icon="work_outline"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.PREFERENCES"
|
||||
icon="display_settings"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #panels>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PERSONAL_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPersonal v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelEmployee v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PREFERENCES"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPreferences />
|
||||
</q-tab-panel>
|
||||
</template>
|
||||
</MenuTemplate>
|
||||
</q-card>
|
||||
</template>
|
||||
|
|
@ -1,37 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import { 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 { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
||||
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 = () => {
|
||||
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
|
||||
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>
|
||||
|
|
@ -43,14 +45,14 @@
|
|||
@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 { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
||||
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) {
|
||||
|
|
@ -20,16 +21,16 @@
|
|||
}
|
||||
|
||||
is_editing.value = false;
|
||||
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets
|
||||
initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
|
||||
|
||||
if (!deepEqual(personal_form_data.value, initial_info)) {
|
||||
if (!deepEqual(employee_profile.value, initial_info)) {
|
||||
// save the new data here
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
personal_form_data.value= { ...initial_info };
|
||||
employee_profile.value = unwrapAndClone(initial_info);
|
||||
is_editing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -41,15 +42,15 @@
|
|||
@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/types/employee-profile-interface';
|
||||
|
||||
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,4 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
|
||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||
title: string;
|
||||
startDate?: string;
|
||||
|
|
@ -17,13 +22,13 @@
|
|||
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) }}
|
||||
{{ $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(new Date(endDate), date_format_options) }}
|
||||
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,6 +1,15 @@
|
|||
export interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
||||
|
||||
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
||||
'ADMIN',
|
||||
'SUPERVISOR',
|
||||
'HR',
|
||||
'ACCOUNTING',
|
||||
]
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { 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';
|
||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
|
||||
defineProps<{
|
||||
employeeEmail: string;
|
||||
}>();
|
||||
|
||||
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
// const timesheet_store = useTimesheetStore();
|
||||
const is_showing_graph = ref(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="dialog_model"
|
||||
full-width
|
||||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
>
|
||||
<!-- loader -->
|
||||
<q-card
|
||||
v-if="timesheet_store.is_loading"
|
||||
class="column flex-center text-center"
|
||||
>
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="5em"
|
||||
:thickness="10"
|
||||
class="col-auto"
|
||||
/>
|
||||
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
||||
{{ $t('shared.loading') }}
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
v-else
|
||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||
>
|
||||
|
||||
<!-- employee name -->
|
||||
<q-card-section
|
||||
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
|
||||
>
|
||||
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span>
|
||||
|
||||
<q-separator
|
||||
spaced
|
||||
size="2px"
|
||||
/>
|
||||
|
||||
<q-card-actions
|
||||
align="center"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<q-btn-toggle
|
||||
v-model="is_showing_graph"
|
||||
color="white"
|
||||
text-color="primary"
|
||||
toggle-color="primary"
|
||||
:options="[
|
||||
{ icon: 'bar_chart', value: true },
|
||||
{ icon: 'edit', value: false },
|
||||
]"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
|
||||
<!-- employee timesheet for supervisor editting -->
|
||||
<q-card-section
|
||||
v-if="!is_showing_graph"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<!-- list of shifts -->
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-pa-none rounded-10"
|
||||
>
|
||||
<TimesheetWrapper
|
||||
dense
|
||||
:employee-email="employeeEmail"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
|
||||
<!-- employee timesheet details with chart -->
|
||||
<q-card-section
|
||||
v-if="is_showing_graph"
|
||||
class="q-pa-md col column full-width no-wrap"
|
||||
>
|
||||
<q-card-section
|
||||
:horizontal="!$q.screen.lt.md"
|
||||
class="q-pa-none col no-wrap"
|
||||
style="min-height: 300px;"
|
||||
>
|
||||
<DetailedDialogChartHoursWorked class="col-7" />
|
||||
|
||||
<q-separator
|
||||
spaced
|
||||
:vertical="!$q.screen.lt.md"
|
||||
/>
|
||||
|
||||
<div class="column col justify-center no-wrap q-pa-none">
|
||||
<DetailedDialogChartShiftTypes class="col-5" />
|
||||
|
||||
<q-separator
|
||||
spaced
|
||||
:vertical="!$q.screen.lt.md"
|
||||
/>
|
||||
|
||||
<DetailedDialogChartExpenses class="col" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
|
@ -23,27 +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);
|
||||
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';
|
||||
|
|
@ -15,44 +16,44 @@
|
|||
ChartJS.defaults.maintainAspectRatio = false;
|
||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||
|
||||
const { pay_period_details } = useTimesheetStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const hours_worked_labels = ref<string[]>([]);
|
||||
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
||||
|
||||
const getHoursWorkedData = (): ChartData<'bar'> => {
|
||||
|
||||
const all_days = 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.evening_hours,
|
||||
current_pay_period_overview.emergency_hours,
|
||||
current_pay_period_overview.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.evening_hours.toString() + 'h',
|
||||
current_pay_period_overview.emergency_hours.toString() + 'h',
|
||||
current_pay_period_overview.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 = {
|
||||
88
src/modules/timesheet-approval/components/details-dialog.vue
Normal file
88
src/modules/timesheet-approval/components/details-dialog.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<script
|
||||
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';
|
||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||
|
||||
const { employeeEmail } = defineProps<{
|
||||
employeeEmail: string;
|
||||
}>();
|
||||
|
||||
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const render_key = ref(1);
|
||||
|
||||
provide('employeeEmail', employeeEmail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="dialog_model"
|
||||
full-width
|
||||
full-height
|
||||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
@show="render_key += 1"
|
||||
>
|
||||
<q-card
|
||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||
>
|
||||
|
||||
<!-- employee name -->
|
||||
<q-card-section
|
||||
class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto"
|
||||
>
|
||||
<span>TODO: Name goes here</span>
|
||||
</q-card-section>
|
||||
|
||||
<!-- employee pay period details using chart -->
|
||||
<q-card-section
|
||||
:horizontal="!$q.screen.lt.md"
|
||||
class=" col-auto q-px-sm no-wrap"
|
||||
>
|
||||
<DetailedDialogChartHoursWorked
|
||||
:key="render_key"
|
||||
class="col"
|
||||
/>
|
||||
|
||||
<DetailedDialogChartShiftTypes
|
||||
:key="render_key + 1"
|
||||
class="col-2 q-ma-lg"
|
||||
/>
|
||||
|
||||
<DetailedDialogChartExpenses
|
||||
:key="render_key + 2"
|
||||
class="col"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="col-auto">
|
||||
<q-separator />
|
||||
<ExpenseCrudDialogList
|
||||
horizontal
|
||||
:employee-email="employeeEmail"
|
||||
/>
|
||||
<q-separator />
|
||||
</q-card-section>
|
||||
|
||||
<!-- list of shifts -->
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="col-auto q-px-sm rounded-5 no-wrap"
|
||||
>
|
||||
<TimesheetWrapper
|
||||
dense
|
||||
:employee-email="employeeEmail"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -1,21 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import type { TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||
|
||||
const modelApproval = defineModel<boolean>();
|
||||
const { row } = defineProps<{ row: PayPeriodOverview; }>();
|
||||
const { row } = defineProps<{ row: TimesheetOverview; }>();
|
||||
const emit = defineEmits<{
|
||||
'clickDetails': [overview: PayPeriodOverview];
|
||||
'clickDetails': [overview: TimesheetOverview];
|
||||
}>();
|
||||
|
||||
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
|
||||
<q-card class="rounded-10">
|
||||
|
||||
<!-- Card header with employee name and details button-->
|
||||
<q-card-section horizontal class="q-py-none q-px-sm q-ma-none justify-between items-center">
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="q-py-none q-px-sm q-ma-none justify-between items-center"
|
||||
>
|
||||
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
|
||||
|
||||
<!-- Buttons to view detailed shifts or view employee timesheet -->
|
||||
|
|
@ -42,35 +45,33 @@
|
|||
<q-separator size="2px" />
|
||||
|
||||
<!-- Main body of pay period card -->
|
||||
<q-card-section class="q-py-none q-px-sm q-mt-sm q-mb-md">
|
||||
<div class="row no-wrap">
|
||||
|
||||
<q-card-section class="q-py-none q-px-sm q-my-sm">
|
||||
<div class="row">
|
||||
<!-- left portion of pay period card -->
|
||||
<div class="col column no-wrap q-px-sm">
|
||||
|
||||
<div class="col column q-px-sm">
|
||||
<!-- Regular hours segment -->
|
||||
<div class="column" :class="$q.screen.lt.md ? 'col' : 'col-8'">
|
||||
<span :class="stack_label_class"> {{ $t('shared.shift_type.regular') }} </span>
|
||||
<div class="col column">
|
||||
<span class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"> {{ $t('shared.shift_type.regular') }} </span>
|
||||
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-mx-sm" />
|
||||
</div>
|
||||
|
||||
<!-- Other hour types segment -->
|
||||
<div class="row q-px-xs">
|
||||
<div class="col column no-wrap">
|
||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.evening') }} </span>
|
||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.evening_hours }} </span>
|
||||
</div>
|
||||
|
||||
<div class="col column no-wrap">
|
||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.emergency') }} </span>
|
||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.emergency_hours }} </span>
|
||||
</div>
|
||||
|
||||
<div class="col column no-wrap">
|
||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.overtime') }} </span>
|
||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.overtime_hours }} </span>
|
||||
<div class="col-auto row ellipsis q-mt-xs">
|
||||
<div
|
||||
v-for="hour_type, index in row.other_hours"
|
||||
:key="index"
|
||||
class="col-4 column ellipsis"
|
||||
:class="hour_type === 0 ? 'invisible' : ''"
|
||||
>
|
||||
<span
|
||||
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||
style="font-size: 0.7em;"
|
||||
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
|
||||
<span
|
||||
class="text-weight-bolder q-pa-none q-mb-xs"
|
||||
style="font-size: 1.2em; line-height: 1em;"
|
||||
> {{ hour_type }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -83,19 +84,34 @@
|
|||
<!-- Right portion of pay period card -->
|
||||
<div class="col-auto column q-px-sm">
|
||||
<div class="col column no-wrap">
|
||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.expenses }} </span>
|
||||
<span
|
||||
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||
style="font-size: 0.8em;"
|
||||
> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
||||
<span
|
||||
class="text-weight-bolder text-h6 q-pa-none"
|
||||
style="line-height: 0.9em;"
|
||||
> {{ row.expenses }} </span>
|
||||
</div>
|
||||
|
||||
<div class="col column no-wrap">
|
||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.mileage }} </span>
|
||||
<span
|
||||
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||
style="font-size: 0.8em;"
|
||||
> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
||||
<span
|
||||
class="text-weight-bolder text-h6 q-pa-none"
|
||||
style="line-height: 0.9em;"
|
||||
> {{ row.mileage }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator color="primary" size="2px" />
|
||||
<q-separator
|
||||
color="primary"
|
||||
size="2px"
|
||||
/>
|
||||
|
||||
<!-- Validate Pay Period section -->
|
||||
<q-card-section
|
||||
|
|
|
|||
|
|
@ -2,18 +2,29 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { computed, ref } from 'vue';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||
import { pay_period_overview_columns, type PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||
import { pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||
|
||||
const filter = ref<string | number | null>('');
|
||||
const is_grid_mode = ref(true);
|
||||
const IS_ABNORMAL_SHIFT = ['OVERTIME', 'EMERGENCY'];
|
||||
const IS_PTO = ['HOLIDAY', 'VACATION', 'SICK'];
|
||||
|
||||
const employeeEmail = defineModel();
|
||||
|
||||
const visible_columns = ref<string[]>(['REGULAR', 'email']);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'clickedDetailsButton': [email: string];
|
||||
}>();
|
||||
|
|
@ -23,28 +34,46 @@
|
|||
timesheet_store.pay_period_overviews
|
||||
)
|
||||
|
||||
const onClickedDetails = async (employee_email: string, row: PayPeriodOverview) => {
|
||||
const onClickedDetails = async (employee_email: string, row: TimesheetOverview) => {
|
||||
employeeEmail.value = employee_email;
|
||||
emit('clickedDetailsButton', employee_email);
|
||||
timesheet_store.current_pay_period_overview = row;
|
||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
||||
emit('clickedDetailsButton', employee_email);
|
||||
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
||||
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
||||
};
|
||||
|
||||
const getListModeTextColor = (type: string): string => {
|
||||
console.log('type: ', type);
|
||||
if (IS_ABNORMAL_SHIFT.includes(type)) {
|
||||
return ' text-negative text-weight-bolder';
|
||||
}
|
||||
else if (IS_PTO.includes(type)) {
|
||||
return ' text-warning text-weight-bold';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<q-table
|
||||
:visible-columns="visible_columns"
|
||||
:rows="overview_rows"
|
||||
:columns="pay_period_overview_columns"
|
||||
row-key="email"
|
||||
:filter="filter"
|
||||
grid
|
||||
:grid="is_grid_mode"
|
||||
dense
|
||||
hide-pagination
|
||||
color="primary"
|
||||
:rows-per-page-options="[0]"
|
||||
card-container-class="justify-center"
|
||||
:loading="timesheet_store.is_loading"
|
||||
class="q-py-md bg-transparent"
|
||||
:class="is_grid_mode ? '' : 'sticky-header-table no-shadow'"
|
||||
table-class="q-pa-none q-py-none q-mx-md rounded-10 bg-dark shadow-4'"
|
||||
:no-data-label="$t('shared.error.no_data_found')"
|
||||
:no-results-label="$t('shared.error.no_search_results')"
|
||||
:loading-label="$t('shared.label.loading')"
|
||||
|
|
@ -54,18 +83,69 @@
|
|||
class="full-width"
|
||||
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
||||
>
|
||||
<PayPeriodNavigator />
|
||||
<PayPeriodNavigator
|
||||
@date-selected="timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
|
||||
<!-- Grid-or-List toggle goes here -->
|
||||
<q-btn-toggle
|
||||
v-model="is_grid_mode"
|
||||
push
|
||||
color="white"
|
||||
text-color="primary"
|
||||
toggle-color="primary"
|
||||
class="q-mr-md"
|
||||
:options="[
|
||||
{ icon: 'grid_view', value: true },
|
||||
{ icon: 'view_list', value: false },
|
||||
]"
|
||||
/>
|
||||
|
||||
<QTableFilters v-model="filter" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #header="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="bg-primary"
|
||||
>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<span
|
||||
v-if="col.label !== 'timesheet_approvals.table.is_approved'"
|
||||
class="text-uppercase text-weight-bolder text-white"
|
||||
>
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body-cell="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
class="text-weight-medium"
|
||||
>
|
||||
<span
|
||||
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
||||
:class="getListModeTextColor(props.col.name)"
|
||||
>{{ props.value }}</span>
|
||||
<q-icon
|
||||
v-if="typeof props.value === 'boolean'"
|
||||
:name="props.value ? 'verified' : 'fiber_manual_record'"
|
||||
:color="props.value ? 'primary' : 'grey-5'"
|
||||
size="sm"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- Template for individual employee cards -->
|
||||
<template #item="props: { row: PayPeriodOverview, key: string }">
|
||||
<template #item="props: { row: TimesheetOverview, key: string }">
|
||||
<OverviewListItem
|
||||
v-model="props.row.is_approved"
|
||||
:row="props.row"
|
||||
|
|
@ -89,3 +169,22 @@
|
|||
</q-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.sticky-header-table
|
||||
thead tr:first-child th
|
||||
background-color: var(--q-primary)
|
||||
margin-top: none
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
thead tr:first-child th
|
||||
top: 0
|
||||
|
||||
&.q-table--loading thead tr:last-child th
|
||||
top: 48px
|
||||
|
||||
tbody
|
||||
scroll-margin-top: 48px
|
||||
</style>
|
||||
|
|
@ -1,24 +1,51 @@
|
|||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { useAuthStore } from "src/stores/auth-store";
|
||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||
import { NavigatorConstants } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
export const useTimesheetApprovalApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
const getPayPeriodOverviewsByDate = async (date_string: string): Promise<void> => {
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
const getPayPeriodOverviewsByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<void> => {
|
||||
let success = false;
|
||||
if (typeof date_or_year === 'string') success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year);
|
||||
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) => {
|
||||
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) {
|
||||
new_period_number = 1;
|
||||
new_year += direction;
|
||||
}
|
||||
|
||||
await getPayPeriodOverviewsByDateOrYearAndNumber(new_year, new_period_number);
|
||||
};
|
||||
|
||||
const getNextPayPeriodOverview = async () => {
|
||||
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.NEXT_PERIOD);
|
||||
};
|
||||
|
||||
const getPreviousPayPeriodOverview = async () => {
|
||||
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.PREVIOUS_PERIOD);
|
||||
};
|
||||
|
||||
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 = {
|
||||
|
|
@ -34,7 +61,9 @@ export const useTimesheetApprovalApi = () => {
|
|||
};
|
||||
|
||||
return {
|
||||
getPayPeriodOverviewsByDate,
|
||||
getPayPeriodOverviewsByDateOrYearAndNumber,
|
||||
getTimesheetApprovalCSVReport,
|
||||
getNextPayPeriodOverview,
|
||||
getPreviousPayPeriodOverview,
|
||||
}
|
||||
};
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
export interface PayPeriodOverview {
|
||||
email: string;
|
||||
employee_name: string;
|
||||
regular_hours: number;
|
||||
evening_hours: number;
|
||||
emergency_hours: number;
|
||||
overtime_hours: number;
|
||||
total_hours: number;
|
||||
expenses: number;
|
||||
mileage: number;
|
||||
is_approved: boolean;
|
||||
};
|
||||
|
||||
export interface PayPeriodOverviewResponse {
|
||||
pay_period_no: number;
|
||||
pay_year: number;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
payday: string;
|
||||
label: string;
|
||||
employees_overview: PayPeriodOverview[];
|
||||
}
|
||||
|
||||
export const default_pay_period_overview: PayPeriodOverview = {
|
||||
email: '',
|
||||
employee_name: '',
|
||||
regular_hours: -1,
|
||||
evening_hours: -1,
|
||||
emergency_hours: -1,
|
||||
overtime_hours: -1,
|
||||
total_hours: -1,
|
||||
expenses: -1,
|
||||
mileage: -1,
|
||||
is_approved: false
|
||||
}
|
||||
|
||||
export const pay_period_overview_columns = [
|
||||
{
|
||||
name: 'employee_name',
|
||||
label: 'timesheet_approvals.table.full_name',
|
||||
field: 'employee_name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'timesheet_approvals.table.email',
|
||||
field: 'email',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'regular_hours',
|
||||
label: 'shared.shift_type.regular',
|
||||
field: 'regular_hours',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'evening_hours',
|
||||
label: 'shared.shift_type.evening',
|
||||
field: 'evening_hours',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'emergency_hours',
|
||||
label: 'shared.shift_type.emergency',
|
||||
field: 'emergency_hours',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'overtime_hours',
|
||||
label: 'shared.shift_type.overtime',
|
||||
field: 'overtime_hours',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'expenses',
|
||||
label: 'timesheet_approvals.table.expenses',
|
||||
field: 'expenses',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'mileage',
|
||||
label: 'timesheet_approvals.table.mileage',
|
||||
field: 'mileage',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_approved',
|
||||
label: 'timesheet_approvals.table.is_approved',
|
||||
field: 'is_approved',
|
||||
sortable: true,
|
||||
}
|
||||
];
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import type { QTableColumn } from "quasar";
|
||||
|
||||
/* eslint-disable */
|
||||
export enum NavigatorConstants {
|
||||
NEXT_PERIOD = 1,
|
||||
PREVIOUS_PERIOD = -1,
|
||||
}
|
||||
|
||||
export interface TimesheetOverview {
|
||||
email: string;
|
||||
employee_name: string;
|
||||
regular_hours: number;
|
||||
other_hours: {
|
||||
evening_hours: number;
|
||||
emergency_hours: number;
|
||||
overtime_hours: number;
|
||||
sick_hours: number;
|
||||
holiday_hours: number;
|
||||
vacation_hours: number;
|
||||
};
|
||||
total_hours: number;
|
||||
expenses: number;
|
||||
mileage: number;
|
||||
is_approved: boolean;
|
||||
}
|
||||
|
||||
export interface PayPeriodOverviewResponse {
|
||||
pay_period_no: number;
|
||||
pay_year: number;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
payday: string;
|
||||
label: string;
|
||||
employees_overview: TimesheetOverview[];
|
||||
}
|
||||
|
||||
export const default_pay_period_overview: TimesheetOverview = {
|
||||
email: '',
|
||||
employee_name: '',
|
||||
regular_hours: -1,
|
||||
other_hours: {
|
||||
evening_hours: -1,
|
||||
emergency_hours: -1,
|
||||
overtime_hours: -1,
|
||||
sick_hours: -1,
|
||||
holiday_hours: -1,
|
||||
vacation_hours: -1,
|
||||
},
|
||||
total_hours: -1,
|
||||
expenses: -1,
|
||||
mileage: -1,
|
||||
is_approved: false
|
||||
}
|
||||
|
||||
export const pay_period_overview_columns: QTableColumn[] = [
|
||||
{
|
||||
name: 'employee_name',
|
||||
label: 'timesheet_approvals.table.full_name',
|
||||
align: 'left',
|
||||
field: 'employee_name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'timesheet_approvals.table.email',
|
||||
field: 'email',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'REGULAR',
|
||||
label: 'shared.shift_type.regular',
|
||||
field: 'regular_hours',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'EVENING',
|
||||
label: 'shared.shift_type.evening',
|
||||
field: row => row.other_hours.evening_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'EMERGENCY',
|
||||
label: 'shared.shift_type.emergency',
|
||||
field: row => row.other_hours.emergency_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'SICK',
|
||||
label: 'shared.shift_type.sick',
|
||||
field: row => row.other_hours.sick_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'HOLIDAY',
|
||||
label: 'shared.shift_type.holiday',
|
||||
field: row => row.other_hours.holiday_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'VACATION',
|
||||
label: 'shared.shift_type.vacation',
|
||||
field: row => row.other_hours.vacation_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'OVERTIME',
|
||||
label: 'shared.shift_type.overtime',
|
||||
field: row => row.other_hours.overtime_hours,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'expenses',
|
||||
label: 'timesheet_approvals.table.expenses',
|
||||
field: 'expenses',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'mileage',
|
||||
label: 'timesheet_approvals.table.mileage',
|
||||
field: 'mileage',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_approved',
|
||||
label: 'timesheet_approvals.table.is_approved',
|
||||
field: 'is_approved',
|
||||
sortable: true,
|
||||
}
|
||||
]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
export const timesheetApprovalService = {
|
||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
||||
const expense_store = useExpensesStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-item class="row justify-between">
|
||||
<q-item-label
|
||||
header
|
||||
class="text-h6 col-auto"
|
||||
>
|
||||
{{ $t('timesheet.expense.title') }}
|
||||
</q-item-label>
|
||||
<q-item-section class="items-center col-auto">
|
||||
<q-badge
|
||||
lines="1"
|
||||
class="q-pa-sm q-px-md"
|
||||
:label="$t('timesheet.expense.total_amount') + ': ' + expense_store.pay_period_expenses_totals.amount.toFixed(2)"
|
||||
/>
|
||||
|
||||
<q-separator spaced />
|
||||
|
||||
<q-badge
|
||||
lines="2"
|
||||
class="q-pa-sm q-px-md"
|
||||
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses_totals.mileage.toFixed(1)"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { expenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
|
||||
const expenses_list = computed(() => timesheet_store.pay_period_details.weeks.flatMap(week =>
|
||||
Object.values(week.expenses).flatMap(day => day.expenses)));
|
||||
|
||||
const employee_email = inject('employeeEmail', '');
|
||||
|
||||
const setExpenseToModify = (expense: Expense) => {
|
||||
expenses_store.mode = 'update';
|
||||
expenses_store.current_expense = expense;
|
||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||
};
|
||||
|
||||
const requestExpenseDeletion = async (expense: Expense) => {
|
||||
expenses_store.mode = 'delete';
|
||||
expenses_store.initial_expense = expense;
|
||||
expenses_store.current_expense = default_expense;
|
||||
await expenses_api.deleteExpenseByEmployeeEmail(employee_email, expenses_store.initial_expense.date);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- liste des dépenses pré existantes -->
|
||||
<q-list
|
||||
padding
|
||||
class="rounded-borders"
|
||||
>
|
||||
<q-item-label
|
||||
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
||||
class="text-italic q-px-sm"
|
||||
>
|
||||
{{ $t('timesheet.expense.empty_list') }}
|
||||
</q-item-label>
|
||||
<q-item
|
||||
style="border: solid 1px lightgrey; border-radius: 7px;"
|
||||
v-for="(expense, index) in expenses_list"
|
||||
:key="index"
|
||||
class="q-my-xs shadow-1"
|
||||
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
||||
>
|
||||
<!-- avatar type icon section -->
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="expenseTypeIcon(expense.type)"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<!-- amount or mileage section -->
|
||||
<q-item-section top>
|
||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||
<template v-if="typeof expense.mileage === 'number'">
|
||||
{{ expense.mileage?.toFixed(1) }} km
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ expense.amount.toFixed(2) }} $
|
||||
</template>
|
||||
</q-item-label>
|
||||
<q-item-label v-else>
|
||||
{{ expense.amount.toFixed(2) }} $
|
||||
</q-item-label>
|
||||
|
||||
<!-- date label -->
|
||||
<q-item-label
|
||||
caption
|
||||
lines="2"
|
||||
>
|
||||
{{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' })
|
||||
}}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- attachment file icon -->
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="md"
|
||||
color="primary"
|
||||
class="q-mx-lg"
|
||||
icon="attach_file"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<!-- comment section -->
|
||||
<q-item-section top>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.employee_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
lines="2"
|
||||
>
|
||||
{{ expense.comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- supervisor comment section -->
|
||||
<q-item-section top>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
v-if="expense.supervisor_comment"
|
||||
caption
|
||||
lines="2"
|
||||
>
|
||||
{{ expense.supervisor_comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
v-if="!expenses_store.pay_period_expenses.is_approved && !expense.is_approved"
|
||||
side
|
||||
>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="xs"
|
||||
color="primary"
|
||||
icon="edit"
|
||||
@click="setExpenseToModify(expense)"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="xs"
|
||||
color="negative"
|
||||
icon="close"
|
||||
@click="requestExpenseDeletion(expense)"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
</template>
|
||||
|
|
@ -2,40 +2,43 @@
|
|||
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 { makeExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||
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';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
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 = makeExpenseRules(t);
|
||||
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="!expenses_store.pay_period_expenses.is_approved"
|
||||
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||
>
|
||||
<div class="text-subtitle2 q-py-sm">
|
||||
|
|
@ -43,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 -->
|
||||
|
|
@ -87,11 +90,11 @@
|
|||
map-options
|
||||
:label="$t('timesheet.expense.type')"
|
||||
:rules="[rules.typeRequired]"
|
||||
:option-label="label => $t(label)"
|
||||
:option-label="label => $t(`timesheet.expense.types.${label}`)"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
|
|
@ -174,7 +177,7 @@
|
|||
<!-- add btn section -->
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="expenses_store.mode === 'update'"
|
||||
v-if="mode === 'update'"
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
44
src/modules/timesheets/components/expense-dialog-header.vue
Normal file
44
src/modules/timesheets/components/expense-dialog-header.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
|
||||
const expense_store = useExpensesStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-item class="row justify-between items-center q-pa-none">
|
||||
<q-item-label
|
||||
header
|
||||
class="text-h6 col q-pa-none"
|
||||
>
|
||||
{{ $t('timesheet.expense.title') }}
|
||||
</q-item-label>
|
||||
|
||||
<!-- <q-item-section
|
||||
no-wrap
|
||||
class="col-auto items-center"
|
||||
>
|
||||
<q-badge
|
||||
outline
|
||||
class="q-py-xs q-px-md"
|
||||
color="primary"
|
||||
:label="$t('timesheet.expense.total_amount') + ': $' + expense_store.pay_period_expenses?.toFixed(2)"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
no-wrap
|
||||
class="col-auto items-center"
|
||||
>
|
||||
<q-badge
|
||||
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'"
|
||||
/>
|
||||
</q-item-section> -->
|
||||
</q-item>
|
||||
</template>
|
||||
203
src/modules/timesheets/components/expense-dialog-list-item.vue
Normal file
203
src/modules/timesheets/components/expense-dialog-list-item.vue
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<script
|
||||
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 { 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;
|
||||
index: number;
|
||||
horizontal?: boolean;
|
||||
}>();
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const auth_store = useAuthStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
|
||||
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 ?? '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 approvedClass = computed(() => horizontal ? ' q-mx-xs q-pa-xs cursor-pointer' : '')
|
||||
|
||||
|
||||
const employeeEmail = inject<string>('employeeEmail') ?? '';
|
||||
|
||||
|
||||
const setExpenseToModify = () => {
|
||||
// expenses_store.mode = 'update';
|
||||
expenses_store.current_expense = expense;
|
||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||
};
|
||||
|
||||
const requestExpenseDeletion = async () => {
|
||||
// expenses_store.mode = 'delete';
|
||||
expenses_store.initial_expense = expense;
|
||||
expenses_store.current_expense = empty_expense;
|
||||
await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date);
|
||||
}
|
||||
|
||||
function onExpenseClicked() {
|
||||
if (is_authorized_to_approve.value) {
|
||||
is_approved.value = !is_approved.value;
|
||||
refresh_key.value += 1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
enter-active-class="animated pulse"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-item
|
||||
:key="refresh_key"
|
||||
:clickable="horizontal"
|
||||
class="row col-4 q-ma-xs shadow-2"
|
||||
:style="expenseItemStyle + approvedClass"
|
||||
@click="onExpenseClicked"
|
||||
>
|
||||
<q-badge
|
||||
v-if="expense.is_approved"
|
||||
class="absolute z-top rounded-20 bg-dark q-pa-none"
|
||||
style="transform: translate(-15px, -15px);"
|
||||
>
|
||||
<q-icon
|
||||
name="verified"
|
||||
color="primary"
|
||||
size="md"
|
||||
/>
|
||||
</q-badge>
|
||||
|
||||
<!-- avatar type icon section -->
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="getExpenseIcon(expense.type)"
|
||||
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||
size="lg"
|
||||
>
|
||||
<q-badge
|
||||
v-if="expense.type === 'ON_CALL'"
|
||||
floating
|
||||
class="q-pa-none rounded-50 bg-white z-top"
|
||||
>
|
||||
<q-icon
|
||||
name="shield"
|
||||
size="xs"
|
||||
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||
/>
|
||||
</q-badge>
|
||||
</q-icon>
|
||||
</q-item-section>
|
||||
|
||||
<!-- amount or mileage section -->
|
||||
<q-item-section class="col-auto">
|
||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||
<template v-if="typeof expense.mileage === 'number'">
|
||||
{{ expense.mileage?.toFixed(1) }} km
|
||||
</template>
|
||||
<template v-else>
|
||||
${{ expense.amount.toFixed(2) }}
|
||||
</template>
|
||||
</q-item-label>
|
||||
<q-item-label v-else>
|
||||
${{ expense.amount.toFixed(2) }}
|
||||
</q-item-label>
|
||||
|
||||
<!-- date label -->
|
||||
<q-item-label
|
||||
caption
|
||||
lines="1"
|
||||
>
|
||||
<!-- {{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' }) }} -->
|
||||
{{ expense.date }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-space v-if="horizontal" />
|
||||
|
||||
<!-- attachment file icon -->
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="md"
|
||||
color="primary"
|
||||
class="q-mx-lg"
|
||||
icon="attach_file"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<!-- comment section -->
|
||||
<q-item-section
|
||||
v-if="!horizontal"
|
||||
top
|
||||
>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.employee_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
lines="1"
|
||||
>
|
||||
{{ expense.comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- supervisor comment section -->
|
||||
<q-item-section
|
||||
v-if="expense.supervisor_comment && !horizontal"
|
||||
top
|
||||
>
|
||||
<q-item-label lines="1">
|
||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
v-if="expense.supervisor_comment"
|
||||
caption
|
||||
lines="2"
|
||||
>
|
||||
{{ expense.supervisor_comment }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section
|
||||
side
|
||||
class="q-pa-none"
|
||||
>
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="xs"
|
||||
color="primary"
|
||||
icon="edit"
|
||||
class="q-mb-xs z-top"
|
||||
@click.stop="setExpenseToModify"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
size="xs"
|
||||
color="negative"
|
||||
icon="close"
|
||||
class="z-top"
|
||||
@click.stop="requestExpenseDeletion"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</transition>
|
||||
</template>
|
||||
39
src/modules/timesheets/components/expense-dialog-list.vue
Normal file
39
src/modules/timesheets/components/expense-dialog-list.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
||||
|
||||
const expenses_store = useExpensesStore();
|
||||
|
||||
const { horizontal = false } = defineProps<{
|
||||
horizontal?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- liste des dépenses pré existantes -->
|
||||
<q-list
|
||||
padding
|
||||
class="rounded-borders"
|
||||
:class="horizontal ? 'row flex-center' : ''"
|
||||
>
|
||||
<q-item-label
|
||||
v-if="expenses_store.pay_period_expenses?.length === 0"
|
||||
class="text-italic q-px-sm"
|
||||
>
|
||||
{{ $t('timesheet.expense.empty_list') }}
|
||||
</q-item-label>
|
||||
|
||||
<ExpenseDialogListItem
|
||||
v-for="(expense, index) in expenses_store.pay_period_expenses"
|
||||
:key="index"
|
||||
v-model="expense.is_approved"
|
||||
:index="index"
|
||||
:expense="expense"
|
||||
:horizontal="horizontal"
|
||||
/>
|
||||
</q-list>
|
||||
|
||||
</template>
|
||||
|
|
@ -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 />
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const shift = defineModel<Shift>({ required: true });
|
||||
|
||||
defineEmits<{
|
||||
'onCommentBlur': [void];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row full-width justify-center">
|
||||
<div class="col-sm-6 col-md-3 row q-mx-xs q-my-none">
|
||||
<div class="col-auto column items-center">
|
||||
<span
|
||||
class="text-caption q-pa-none q-ma-none"
|
||||
style="line-height: 0.7em; font-size: 0.7em;"
|
||||
>{{ $t('timesheet.shift.types.REMOTE') }}</span>
|
||||
<q-toggle
|
||||
v-model="shift.is_remote"
|
||||
class="q-pa-none q-ma-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
v-model="shift.type"
|
||||
options-dense
|
||||
:options="SHIFT_TYPES"
|
||||
:label="$t('timesheet.shift.types.label')"
|
||||
class="col q-pa-none"
|
||||
color="primary"
|
||||
outlined
|
||||
dense
|
||||
square
|
||||
hide-dropdown-icon
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row q-mx-xs">
|
||||
<q-input
|
||||
v-model="shift.start_time"
|
||||
:label="$t('timesheet.shift.fields.start')"
|
||||
outlined
|
||||
dense
|
||||
square
|
||||
inputmode="numeric"
|
||||
mask="##:##"
|
||||
class="col-auto q-mx-xs"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
:label="$t('timesheet.shift.fields.end')"
|
||||
outlined
|
||||
dense
|
||||
square
|
||||
inputmode="numeric"
|
||||
mask="##:##"
|
||||
class="col-auto q-mx-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
v-model="shift.comment"
|
||||
type="textarea"
|
||||
autogrow
|
||||
filled
|
||||
dense
|
||||
square
|
||||
:label="$t('timesheet.shift.fields.header_comment')"
|
||||
:counter="true"
|
||||
:maxlength="512"
|
||||
class="col-grow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useShiftStore } from 'src/stores/shift-store';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/api/use-shift-api';
|
||||
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const { date_iso, mode, current_shift, is_open, close } = useShiftStore();
|
||||
const { upsertOrDeleteShiftByEmployeeEmail } = useShiftApi();
|
||||
|
||||
const { employeeEmail } = defineProps<{
|
||||
employeeEmail: string;
|
||||
}>();
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
const errorBanner = ref<string | null>(null);
|
||||
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
|
||||
|
||||
const canSubmit = computed(() =>
|
||||
mode === 'delete' ||
|
||||
(current_shift.start_time.trim().length === 5 &&
|
||||
current_shift.end_time.trim().length === 5 &&
|
||||
current_shift.type !== undefined)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model=" is_open"
|
||||
persistent
|
||||
transition-show="fade"
|
||||
transition-hide="fade"
|
||||
>
|
||||
|
||||
<q-card class="q-pa-md">
|
||||
<div class="row items-center q-mb-sm">
|
||||
<q-icon
|
||||
name="schedule"
|
||||
size="24px"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
<div class="text-h6">
|
||||
{{
|
||||
mode === 'create'
|
||||
? $t('timesheet.shift.actions.add')
|
||||
: mode === 'update'
|
||||
? $t('timesheet.shift.actions.edit')
|
||||
: $t('timesheet.shift.actions.delete')
|
||||
}}
|
||||
</div>
|
||||
<q-space />
|
||||
<q-badge
|
||||
outline
|
||||
color="primary"
|
||||
>
|
||||
{{ date_iso }}
|
||||
</q-badge>
|
||||
</div>
|
||||
|
||||
<q-separator spaced />
|
||||
|
||||
<div
|
||||
v-if="mode !== 'delete'"
|
||||
class="column q-gutter-md"
|
||||
>
|
||||
<div class="row ">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="current_shift.start_time"
|
||||
:label="$t('timesheet.shift.fields.start')"
|
||||
filled
|
||||
dense
|
||||
inputmode="numeric"
|
||||
mask="##:##"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="current_shift.end_time"
|
||||
:label="$t('timesheet.shift.fields.end')"
|
||||
filled
|
||||
dense
|
||||
inputmode="numeric"
|
||||
mask="##:##"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row items-center">
|
||||
<q-select
|
||||
v-model="current_shift.type"
|
||||
options-dense
|
||||
:options="SHIFT_TYPES"
|
||||
:label="$t('timesheet.shift.types.label')"
|
||||
class="col"
|
||||
color="primary"
|
||||
filled
|
||||
dense
|
||||
hide-dropdown-icon
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-toggle
|
||||
v-model="current_shift.is_remote"
|
||||
:label="$t('timesheet.shift.types.REMOTE')"
|
||||
class="col-auto"
|
||||
/>
|
||||
</div>
|
||||
<q-input
|
||||
v-model="current_shift.comment"
|
||||
type="textarea"
|
||||
autogrow
|
||||
filled
|
||||
dense
|
||||
:label="$t('timesheet.shift.fields.header_comment')"
|
||||
:counter="true"
|
||||
:maxlength="512"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="q-pa-md"
|
||||
>
|
||||
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorBanner"
|
||||
class="q-mt-md"
|
||||
>
|
||||
<q-banner
|
||||
dense
|
||||
class="bg-red-2 text-negative"
|
||||
>{{ errorBanner }}</q-banner>
|
||||
<div
|
||||
v-if="conflicts.length"
|
||||
class="q-mt-xs"
|
||||
>
|
||||
<div class="text-caption">Conflits :</div>
|
||||
<ul class="q-pl-md q-mt-xs">
|
||||
<li
|
||||
v-for="(c, i) in conflicts"
|
||||
:key="i"
|
||||
>
|
||||
{{ c.start_time }}–{{ c.end_time }} ({{ c.type }})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator spaced />
|
||||
|
||||
<div class="row justify-end q-gutter-sm">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey-8"
|
||||
:label="$t('timesheet.cancel_button')"
|
||||
@click="close"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save_alt"
|
||||
:label="mode === 'delete' ? $t('timesheet.delete_button') : $t('timesheet.save_button')"
|
||||
:loading="isSubmitting"
|
||||
:disable="!canSubmit"
|
||||
@click="upsertOrDeleteShiftByEmployeeEmail(employeeEmail)"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<template>
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="text-uppercase text-center items-center q-pa-none"
|
||||
>
|
||||
<!-- shift row itself -->
|
||||
<q-card-section class="col q-pa-none">
|
||||
<q-card-section horizontal class="col q-pa-none">
|
||||
<!-- punch-in timestamps -->
|
||||
<q-card-section class="col q-pa-none">
|
||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
||||
{{ $t('shared.misc.in') }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<!-- arrows pointing to punch-out timestamps -->
|
||||
<q-card-section class="col q-py-none q-px-sm">
|
||||
</q-card-section>
|
||||
|
||||
<!-- punch-out timestamps -->
|
||||
<q-card-section class="col q-pa-none">
|
||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
||||
{{ $t('shared.misc.out') }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<!-- comment button -->
|
||||
<q-card-section class="col column q-pa-none">
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</template>
|
||||
|
|
@ -10,10 +10,9 @@
|
|||
const is_showing_legend = ref(false);
|
||||
|
||||
const legend: ShiftLegendItem[] = [
|
||||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR', text_color: 'grey-8' },
|
||||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
|
||||
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
||||
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
||||
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
|
||||
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
|
||||
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
|
||||
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
|
||||
|
|
@ -35,7 +34,7 @@
|
|||
dense
|
||||
rounded
|
||||
color="primary"
|
||||
class="col-auto q-ma-sm"
|
||||
class="col-auto q-my-sm"
|
||||
@click="is_showing_legend = !is_showing_legend"
|
||||
>
|
||||
<template #default>
|
||||
|
|
@ -55,7 +54,7 @@
|
|||
>
|
||||
<div
|
||||
v-if="is_showing_legend"
|
||||
class="q-pa-xs bg-white rounded-5 shadow-2 text-center q-ma-xs"
|
||||
class="q-py-xs bg-white rounded-5 shadow-2 text-center q-my-xs"
|
||||
>
|
||||
<q-badge
|
||||
v-for="shift_type in shift_type_legend"
|
||||
|
|
@ -63,7 +62,7 @@
|
|||
:color="shift_type.color"
|
||||
:label="shift_type.label"
|
||||
:text-color="shift_type.text_color || 'white'"
|
||||
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||
class="q-pa-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||
style="font-size: 0.8em;"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,136 +1,197 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable*/
|
||||
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect } from 'quasar';
|
||||
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const ui_store = useUiStore();
|
||||
|
||||
const { shift, dense = false } = defineProps<{
|
||||
shift: Shift;
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
|
||||
const { dense = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'save-comment': [comment: string, shift: Shift];
|
||||
'request-update': [shift: Shift];
|
||||
'request-delete': [shift: Shift];
|
||||
defineEmits<{
|
||||
'saveComment': [comment: string, shift_id: number];
|
||||
'requestUpdate': [shift_id: number];
|
||||
'requestDelete': [void];
|
||||
}>();
|
||||
|
||||
const has_comment = computed(() => {
|
||||
const comment = shift.comment ?? '';
|
||||
return typeof comment === 'string' && comment.trim().length > 0;
|
||||
})
|
||||
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
||||
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
||||
const hour_font_size = computed(() => dense ? '0.9em' : '1.5em' )
|
||||
const time_picker_model = ref('');
|
||||
const is_showing_time_picker = ref(false);
|
||||
const select_ref = useTemplateRef<QSelect>('select');
|
||||
|
||||
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 options: { label: string, value: ShiftType, icon: string, icon_color: string }[] = [
|
||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
||||
];
|
||||
|
||||
const shift_type_selected = ref(options.find(option => option.value == shift.value.type));
|
||||
|
||||
const showTimePicker = (time: string) => {
|
||||
is_showing_time_picker.value = true;
|
||||
time_picker_model.value = time;
|
||||
};
|
||||
|
||||
const get_text_color = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'REGULAR': return 'grey-8';
|
||||
case '': return 'grey-5';
|
||||
default: return 'white';
|
||||
onMounted(() => {
|
||||
if (ui_store.focus_next_component) {
|
||||
select_ref.value?.focus();
|
||||
select_ref.value?.showPopup();
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onClickUpdate = (type: string) => {
|
||||
if (type !== '') { emit('request-update', shift) };
|
||||
}
|
||||
|
||||
const onClickDelete = () => emit('request-delete', shift);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
||||
:class="shift.type"
|
||||
style="line-height: 1;"
|
||||
@click.stop="onClickUpdate(shift.type)"
|
||||
>
|
||||
<!-- punch-in timestamps -->
|
||||
<q-card-section class="q-pa-none col">
|
||||
<q-item-label
|
||||
class="text-weight-bolder q-pa-xs rounded-5"
|
||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
||||
:style="'font-size: ' + hour_font_size + '; line-height: 80% !important;'"
|
||||
>
|
||||
{{ shift.start_time }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
|
||||
<!-- arrows pointing to punch-out timestamps -->
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="items-center justify-center q-mx-sm col"
|
||||
>
|
||||
<div
|
||||
v-for="icon_data, index in [
|
||||
{ transform: 'transform: translateX(5px);', color: 'accent' },
|
||||
{ transform: 'transform: translateX(-5px);', color: 'primary' }]"
|
||||
:key="index"
|
||||
v-if="shift.shift_id !== 0"
|
||||
class="col row flex-center text-uppercase rounded-10"
|
||||
>
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
v-model="shift_type_selected"
|
||||
standout="bg-blue-grey-9"
|
||||
dense
|
||||
options-dense
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
:options="options"
|
||||
class="rounded-5 q-mx-xs shadow-1"
|
||||
:class="ui_store.is_mobile_mode ? 'col-auto' : 'col'"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
popup-content-style="border: 2px solid var(--q-primary)"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
class="row text-weight-bold q-ma-none q-pa-none no-wrap ellipsis"
|
||||
:class="ui_store.is_mobile_mode ? 'items-center' : 'flex-center'"
|
||||
:tabindex="scope.tabindex"
|
||||
>
|
||||
<q-icon
|
||||
v-if="shift.type"
|
||||
name="double_arrow"
|
||||
:color="icon_data.color"
|
||||
size="24px"
|
||||
:style="icon_data.transform"
|
||||
:name="scope.opt.icon"
|
||||
:color="scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto q-mx-xs"
|
||||
/>
|
||||
<span
|
||||
v-if="$q.screen.gt.md"
|
||||
style="line-height: 0.9em;"
|
||||
class="col ellipsis"
|
||||
>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- punch-in timestamp -->
|
||||
<q-input
|
||||
v-model="shift.start_time"
|
||||
dense
|
||||
type="time"
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
label-color="primary"
|
||||
input-class="text-weight-medium"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col q-mx-xs"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
v-if="ui_store.is_mobile_mode"
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click.stop="showTimePicker(shift.start_time)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- punch-out timestamps -->
|
||||
<q-card-section class="q-pa-none col">
|
||||
<q-item-label
|
||||
class="text-weight-bolder text-white q-pa-xs rounded-5"
|
||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
||||
style="font-size: 1.5em; line-height: 80% !important;"
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
dense
|
||||
type="time"
|
||||
standout="bg-blue-grey-9"
|
||||
label-slot
|
||||
label-color="primary"
|
||||
input-class="text-weight-medium"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="col q-mx-xs"
|
||||
lazy-rules
|
||||
>
|
||||
{{ shift.end_time }}
|
||||
</q-item-label>
|
||||
</q-card-section>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
|
||||
<!-- comment and expenses buttons -->
|
||||
<q-card-section class="col q-pa-none text-right">
|
||||
<!-- comment btn -->
|
||||
<q-icon
|
||||
v-if="shift.type"
|
||||
:name="comment_icon"
|
||||
:color="comment_color"
|
||||
class="q-pa-none q-mx-xs"
|
||||
size="sm"
|
||||
/>
|
||||
<!-- expenses btn -->
|
||||
<template #append>
|
||||
<q-btn
|
||||
v-if="shift.type"
|
||||
v-if="ui_store.is_mobile_mode"
|
||||
dense
|
||||
flat
|
||||
icon="access_time"
|
||||
color="primary"
|
||||
@click="showTimePicker(shift.end_time)"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- comment and delete buttons -->
|
||||
<div
|
||||
v-if="$q.screen.gt.sm"
|
||||
class="col-auto"
|
||||
>
|
||||
<q-icon
|
||||
v-if="shift.type && dense"
|
||||
:name="shift.comment ? 'comment' : ''"
|
||||
color="primary"
|
||||
:size="dense ? 'xs' : 'sm'"
|
||||
class="col-auto q-pa-none q-mr-xs"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
flat
|
||||
dense
|
||||
color='grey-8'
|
||||
icon="attach_money"
|
||||
class="q-pa-none q-mx-xs"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? 'primary' : 'grey-8'"
|
||||
class="col-auto q-ma-none q-pl-md full-height"
|
||||
/>
|
||||
<!-- delete btn -->
|
||||
|
||||
<q-btn
|
||||
v-if="shift.type"
|
||||
push
|
||||
dense
|
||||
size="sm"
|
||||
color="red-6"
|
||||
icon="close"
|
||||
class="q-ml-xs"
|
||||
@click.stop="onClickDelete"
|
||||
flat
|
||||
round
|
||||
unelevated
|
||||
tabindex="-1"
|
||||
icon="cancel"
|
||||
color="negative"
|
||||
class="q-pa-none q-mr-xs"
|
||||
@click="$emit('requestDelete')"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card-section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,96 +3,122 @@
|
|||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
import ShiftListHeader from 'src/modules/timesheets/components/shift-list-header.vue';
|
||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||
import { useShiftStore } from 'src/stores/shift-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { computed } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
const q = useQuasar();
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
||||
const shift_api = useShiftApi();
|
||||
|
||||
const { dense = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const font_size = computed(() => dense ? '1.5em' : '2.5em')
|
||||
const is_mobile = computed(() => q.screen.lt.md);
|
||||
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||
const date_box_size = computed(() => dense || is_mobile.value ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
|
||||
|
||||
const get_date_from_short = (short_date: string): Date => {
|
||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||
ui_store.focus_next_component = true;
|
||||
const new_shift = new Shift;
|
||||
new_shift.date = date;
|
||||
new_shift.timesheet_id = timesheet_id;
|
||||
day_shifts.push(new_shift);
|
||||
};
|
||||
|
||||
const to_iso_date = (short_date: string): string => {
|
||||
return date.formatDate(get_date_from_short(short_date), 'YYYY-MM-DD');
|
||||
};
|
||||
|
||||
const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
|
||||
return shifts.length > 0 ? shifts : [default_shift];
|
||||
};
|
||||
|
||||
const getDate = (shift_date: string): Date => {
|
||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + shift_date);
|
||||
};
|
||||
const deleteCurrentShift = async (shift: Shift) => {
|
||||
console.log('shift to delete: ', shift);
|
||||
if (shift.shift_id < 0) {
|
||||
shift.shift_id = 0;
|
||||
return;
|
||||
}
|
||||
await shift_api.deleteShiftById(shift.shift_id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<div
|
||||
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
||||
:key="index"
|
||||
class="col q-px-xs q-pt-xs q-mx-sm rounded-5"
|
||||
v-for="timesheet in timesheet_store.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="col column"
|
||||
>
|
||||
<q-card
|
||||
v-for="day, day_index in week.shifts"
|
||||
:key="day_index + index"
|
||||
class="row items-center rounded-10 q-mb-xs"
|
||||
<div
|
||||
v-for="day in timesheet.days"
|
||||
:key="day.date"
|
||||
class="col-auto row shadow-2 rounded-10 q-ma-xs"
|
||||
>
|
||||
<div
|
||||
class="col row bg-dark"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
>
|
||||
|
||||
<!-- Dates column -->
|
||||
<q-card-section class="col-auto q-pa-xs text-white q-mr-md">
|
||||
<div
|
||||
class="bg-primary rounded-10 q-pa-xs text-center"
|
||||
:style="'width: ' + dense? '60px' : '75px;'"
|
||||
class="col-auto column flex-center bg-primary rounded-10 text-center q-ma-sm self-center"
|
||||
:class="$q.screen.lt.md ? '' : ''"
|
||||
:style="date_box_size"
|
||||
>
|
||||
<q-item-label
|
||||
style="font-size: 0.7em;"
|
||||
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: ' + font_size + '; line-height: 90% !important;'"
|
||||
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
||||
<q-item-label
|
||||
style="font-size: 0.7em;"
|
||||
class="text-uppercase"
|
||||
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
||||
<span
|
||||
v-if="!dense"
|
||||
class="col-auto text-uppercase text-white"
|
||||
:style="'font-size: ' + weekday_font_size"
|
||||
>
|
||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
weekday: $q.screen.lt.md ? 'short' :
|
||||
'long'
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="col-auto text-weight-bolder text-grey-1"
|
||||
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||
>
|
||||
{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}
|
||||
</span>
|
||||
<span
|
||||
class="col-auto text-uppercase text-white"
|
||||
:style="'font-size: ' + weekday_font_size"
|
||||
>
|
||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||
month: $q.screen.lt.md ? 'short' : 'long'
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- List of shifts column -->
|
||||
<q-card-section class="col q-pa-none">
|
||||
<ShiftListHeader v-if="day.shifts.length > 0"/>
|
||||
<div
|
||||
v-if="day.shifts.length > 0"
|
||||
>
|
||||
<div class="col column">
|
||||
<ShiftListRow
|
||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
:shift="shift"
|
||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:dense="dense"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<!-- add shift btn column -->
|
||||
<q-card-section class="q-pr-xs col-auto">
|
||||
</div>
|
||||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-btn
|
||||
push
|
||||
color="primary"
|
||||
unelevated
|
||||
icon="more_time"
|
||||
class="q-pa-sm"
|
||||
@click="openCreate(to_iso_date(day.short_date))"
|
||||
:size="$q.screen.lt.md ? 'md' : 'lg'"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:class="$q.screen.lt.md ? 'q-px-xs' : ''"
|
||||
style="border-radius: 0 10px 10px 0;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,48 +3,61 @@
|
|||
lang="ts"
|
||||
>
|
||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||
import ShiftCrudDialog from 'src/modules/timesheets/components/shift-crud-dialog.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 ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { provide } from 'vue';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
||||
const { open } = useExpensesStore();
|
||||
const shift_api = useShiftApi();
|
||||
|
||||
const { employeeEmail, dense = false } = defineProps<{
|
||||
employeeEmail: string;
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const { is_loading } = useTimesheetStore();
|
||||
const { getPayPeriodDetailsByDate, getPreviousPayPeriodDetails, getNextPayPeriodDetails } = useTimesheetApi();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
provide('employeeEmail', employeeEmail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column flex-center full-width">
|
||||
|
||||
<q-dialog
|
||||
v-model="timesheet_store.is_loading"
|
||||
transition-show="jump-down"
|
||||
transition-hide="jump-down"
|
||||
>
|
||||
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
|
||||
<q-spinner-radio
|
||||
color="primary"
|
||||
size="20vh"
|
||||
/>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
class="q-mt-md bg-secondary full-width"
|
||||
class="transparent full-width"
|
||||
>
|
||||
<q-inner-loading
|
||||
:showing="is_loading"
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<q-card-section
|
||||
v-if="!dense"
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-px-lg items-center"
|
||||
class="q-px-md items-center q-mb-md"
|
||||
:class="$q.screen.lt.md ? 'column' : ''"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
@date-selected="getPayPeriodDetailsByDate"
|
||||
@pressed-previous-button="getPreviousPayPeriodDetails"
|
||||
@pressed-next-button="getNextPayPeriodDetails"
|
||||
v-if="!dense"
|
||||
@date-selected="timesheet_api.getTimesheetsByDate(employeeEmail)"
|
||||
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||
/>
|
||||
|
||||
<!-- mobile expenses button -->
|
||||
|
|
@ -56,13 +69,25 @@
|
|||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
class="q-mt-sm"
|
||||
@click="open(employeeEmail)"
|
||||
@click="open"
|
||||
/>
|
||||
|
||||
<!-- shift's colored legend -->
|
||||
<ShiftListLegend :is-loading="false" />
|
||||
<!-- <ShiftListLegend :is-loading="false" /> -->
|
||||
|
||||
<q-space />
|
||||
<!-- save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.gt.sm"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading"
|
||||
color="primary"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
class="q-mr-md"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
|
|
@ -72,17 +97,13 @@
|
|||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="open(employeeEmail)"
|
||||
@click="open"
|
||||
/>
|
||||
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section :horizontal="$q.screen.gt.sm">
|
||||
<ShiftList :dense="dense" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<ExpenseCrudDialog />
|
||||
|
||||
<ShiftCrudDialog :employee-email="employeeEmail" />
|
||||
<ExpenseDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
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";
|
||||
import type { Expense, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const useExpensesApi = () => {
|
||||
const expenses_store = useExpensesStore();
|
||||
|
||||
const toUpsertExpense = (obj: {
|
||||
old_expense?: Expense;
|
||||
new_expense?: Expense;
|
||||
}) => obj as UpsertExpense;
|
||||
|
||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
return {
|
||||
createExpenseByEmployeeEmail,
|
||||
updateExpenseByEmployeeEmail,
|
||||
deleteExpenseByEmployeeEmail,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||
import { TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
||||
import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||
import { useShiftStore } from "src/stores/shift-store";
|
||||
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||
import { deepEqual } from "src/utils/deep-equal";
|
||||
|
||||
export const useShiftApi = () => {
|
||||
const shift_store = useShiftStore();
|
||||
|
||||
const normalizeShiftPayload = (shift: Shift): Shift => {
|
||||
const comment = shift.comment?.trim() || undefined;
|
||||
|
||||
return {
|
||||
date: shift.date,
|
||||
start_time: shift.start_time,
|
||||
end_time: shift.end_time,
|
||||
type: shift.type,
|
||||
is_approved: false,
|
||||
is_remote: shift.is_remote,
|
||||
comment: comment,
|
||||
};
|
||||
};
|
||||
|
||||
const parseHHMM = (s: string): [number, number] => {
|
||||
const m = /^(\d{2}):(\d{2})$/.exec(s);
|
||||
|
||||
if (!m) {
|
||||
throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` });
|
||||
}
|
||||
|
||||
const h = Number(m[1]);
|
||||
const min = Number(m[2]);
|
||||
|
||||
if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) {
|
||||
throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` })
|
||||
}
|
||||
return [h, min];
|
||||
};
|
||||
|
||||
const toMinutes = (hhmm: string): number => {
|
||||
const [h, m] = parseHHMM(hhmm);
|
||||
return h * 60 + m;
|
||||
};
|
||||
|
||||
const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => {
|
||||
if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) {
|
||||
throw new GenericApiError({
|
||||
status_code: 400,
|
||||
message: `Invalid time format in ${label}. Expected HH:MM`,
|
||||
context: { [label]: shift }
|
||||
});
|
||||
}
|
||||
|
||||
if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) {
|
||||
throw new GenericApiError({
|
||||
status_code: 400,
|
||||
message: `Invalid time range in ${label}. The End time must be after the Start time`,
|
||||
context: { [label]: shift }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||
const flat_upsert_shift: UpsertShift = {
|
||||
...(deepEqual(shift_store.initial_shift, default_shift) ? { old_shift: unwrapAndClone(shift_store.initial_shift) } : {}),
|
||||
...(deepEqual(shift_store.current_shift, default_shift) ? { new_shift: unwrapAndClone(shift_store.current_shift) } : {}),
|
||||
};
|
||||
|
||||
const normalized_upsert_shift: UpsertShift = {
|
||||
...(flat_upsert_shift.old_shift ? { old_shift: normalizeShiftPayload(flat_upsert_shift.old_shift) } : {}),
|
||||
...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}),
|
||||
};
|
||||
|
||||
if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift');
|
||||
if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift');
|
||||
|
||||
await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift);
|
||||
};
|
||||
|
||||
return {
|
||||
upsertOrDeleteShiftByEmployeeEmail,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import { useAuthStore } from "src/stores/auth-store";
|
||||
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 success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(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;
|
||||
|
||||
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);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(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,
|
||||
};
|
||||
};
|
||||
28
src/modules/timesheets/composables/use-expense-api.ts
Normal file
28
src/modules/timesheets/composables/use-expense-api.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable */
|
||||
|
||||
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 type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const useExpensesApi = () => {
|
||||
const expenses_store = useExpensesStore();
|
||||
|
||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
return {
|
||||
createExpenseByEmployeeEmail,
|
||||
updateExpenseByEmployeeEmail,
|
||||
deleteExpenseByEmployeeEmail,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { ref, computed } from "vue";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const useExpenseDraft = (initial?: Partial<Expense>) => {
|
||||
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
||||
|
||||
const draft = ref<Partial<Expense>>({
|
||||
date: '',
|
||||
type: DEFAULT_TYPE,
|
||||
comment: '',
|
||||
...(initial ?? {}),
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
draft.value = {
|
||||
date: '',
|
||||
type: DEFAULT_TYPE,
|
||||
comment: '',
|
||||
}
|
||||
}
|
||||
|
||||
const setType = (value: ExpenseType) => {
|
||||
draft.value.type = value;
|
||||
if(value === 'MILEAGE') {
|
||||
delete draft.value.amount;
|
||||
} else {
|
||||
delete draft.value.mileage;
|
||||
}
|
||||
};
|
||||
|
||||
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
||||
const showAmount = computed(()=> !showMileage.value);
|
||||
|
||||
return { draft, setType, reset, showMileage, showAmount };
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { ref } from "vue";
|
||||
import type { QForm } from "quasar"
|
||||
|
||||
|
||||
export const useExpenseForm = () => {
|
||||
const formRef = ref<QForm | null>(null);
|
||||
const triedSubmit = ref(false);
|
||||
|
||||
const validateAnd = async (fn: ()=> void | Promise<void>) => {
|
||||
triedSubmit.value = true;
|
||||
const ok = await formRef.value?.validate(true);
|
||||
if(!ok) return false;
|
||||
await fn();
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
formRef,
|
||||
validateAnd,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// import { ref, type Ref } from "vue";
|
||||
// import { normalizeObject } from "src/utils/normalize-object";
|
||||
// import { normExpenseType } from "../utils/expense.util";
|
||||
// import type { Expense, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
||||
// import { useExpensesStore } from "src/stores/expense-store";
|
||||
// import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||
// import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation";
|
||||
|
||||
// const expenses_store = useExpensesStore();
|
||||
|
||||
// export const useExpenseItems = () => {
|
||||
// let expenses = unwrapAndClone(expenses_store.pay_period_expenses.expenses.map(normalizeExpense));
|
||||
|
||||
// const normalizePayload = (expense: Expense): Expense => {
|
||||
// const exp = normalizeObject(expense, expense_validation_schema);
|
||||
// const out: Expense = {
|
||||
// date: exp.date,
|
||||
// type: exp.type as ExpenseType,
|
||||
// comment: exp.comment || '',
|
||||
// };
|
||||
// if(typeof exp.amount === 'number') out.amount = exp.amount;
|
||||
// if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
|
||||
// return out;
|
||||
// }
|
||||
|
||||
// const addFromDraft = () => {
|
||||
// const candidate: Expense = normalizeExpense({
|
||||
// date: draft.date,
|
||||
// type: normExpenseType(draft.type),
|
||||
// ...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
|
||||
// ...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
|
||||
// comment: String(draft.comment ?? '').trim(),
|
||||
// } as Expense);
|
||||
|
||||
// validateExpenseUI(candidate, 'expense_draft');
|
||||
// expenses = [ ...expenses, candidate];
|
||||
// };
|
||||
|
||||
// const removeAt = (index: number) => {
|
||||
// if(index < 0 || index >= expenses.length) return;
|
||||
// expenses = expenses.filter((_,i)=> i !== index);
|
||||
// };
|
||||
|
||||
// const validateAll = () => {
|
||||
// for (const expense of expenses) {
|
||||
// validateExpenseUI(expense, 'expense_item');
|
||||
// }
|
||||
// };
|
||||
|
||||
// const payload = () => expenses.map(normalizeExpense);
|
||||
|
||||
// return {
|
||||
// expenses,
|
||||
// addFromDraft,
|
||||
// removeAt,
|
||||
// validateAll,
|
||||
// payload,
|
||||
// };
|
||||
// };
|
||||
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useAuthStore } from "src/stores/auth-store";
|
||||
import { useShiftStore } from "src/stores/shift-store";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
|
||||
export const useShiftApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_store = useShiftStore();
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
const deleteShiftById = async (shift_id: number) => {
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await shift_store.deleteShiftById(shift_id);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '');
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
const saveShiftChanges = async () => {
|
||||
timesheet_store.is_loading = true;
|
||||
const create_success = await shift_store.createNewShifts();
|
||||
|
||||
if (create_success) {
|
||||
const update_success = await shift_store.updateShifts();
|
||||
|
||||
if (update_success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '')
|
||||
}
|
||||
}
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
deleteShiftById,
|
||||
saveShiftChanges,
|
||||
};
|
||||
}
|
||||
38
src/modules/timesheets/composables/use-timesheet-api.ts
Normal file
38
src/modules/timesheets/composables/use-timesheet-api.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useAuthStore } from "src/stores/auth-store";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
||||
|
||||
export const useTimesheetApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const auth_store = useAuthStore();
|
||||
|
||||
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||
if (timesheet_store.pay_period === undefined) return false;
|
||||
|
||||
timesheet_store.is_loading = true;
|
||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
|
||||
|
||||
if (success) {
|
||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||
timesheet_store.is_loading = false;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
return {
|
||||
getTimesheetsByDate,
|
||||
getTimesheetsByCurrentPayPeriod,
|
||||
};
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
|
@ -43,6 +43,7 @@ export class ExpensesApiError extends ApiError {
|
|||
};
|
||||
|
||||
export const expense_validation_schema: Normalizer<Expense> = {
|
||||
id: v => typeof v === 'number' ? v : -1,
|
||||
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||
amount: v => typeof v === "number" ? v : -1,
|
||||
|
|
@ -1,37 +1,22 @@
|
|||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'PRIME_GARDE';
|
||||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'ON_CALL';
|
||||
|
||||
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'PRIME_GARDE',];
|
||||
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'ON_CALL',];
|
||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE',];
|
||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||
|
||||
export interface Expense {
|
||||
date: string;
|
||||
id: number;
|
||||
date: string; //YYYY-MM-DD
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
mileage?: number;
|
||||
comment: string;
|
||||
supervisor_comment?: string;
|
||||
is_approved: boolean;
|
||||
}
|
||||
|
||||
export type ExpenseTotals = {
|
||||
amount: number;
|
||||
mileage: number;
|
||||
reimburseable_total?: number;
|
||||
};
|
||||
|
||||
export interface PayPeriodExpenses {
|
||||
is_approved: boolean;
|
||||
expenses: Expense[];
|
||||
totals?: ExpenseTotals;
|
||||
}
|
||||
|
||||
export interface UpsertExpense {
|
||||
old_expense: Expense;
|
||||
new_expense: Expense;
|
||||
}
|
||||
|
||||
export const default_expense: Expense = {
|
||||
export const empty_expense: Expense = {
|
||||
id: -1,
|
||||
date: '',
|
||||
type: 'EXPENSES',
|
||||
amount: 0,
|
||||
|
|
@ -39,7 +24,24 @@ export const default_expense: Expense = {
|
|||
is_approved: false,
|
||||
};
|
||||
|
||||
export const default_pay_period_expenses: PayPeriodExpenses = {
|
||||
export const test_expenses: Expense[] = [
|
||||
{
|
||||
id: 201,
|
||||
date: '2025-01-06',
|
||||
type: 'EXPENSES',
|
||||
amount: 15.5,
|
||||
comment: 'Lunch receipt',
|
||||
is_approved: false,
|
||||
expenses: [],
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
date: '2025-01-07',
|
||||
type: 'MILEAGE',
|
||||
amount: 0,
|
||||
mileage: 32.4,
|
||||
comment: 'Travel to client site',
|
||||
is_approved: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
import type { Shift } from "./shift.models";
|
||||
import { default_expense, type Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export type Week<T> = {
|
||||
sun: T;
|
||||
mon: T;
|
||||
tue: T;
|
||||
wed: T;
|
||||
thu: T;
|
||||
fri: T;
|
||||
sat: T;
|
||||
};
|
||||
|
||||
export interface PayPeriodDetails {
|
||||
weeks: PayPeriodDetailsWeek[];
|
||||
employee_full_name: string;
|
||||
}
|
||||
|
||||
export interface PayPeriodDetailsWeek {
|
||||
is_approved: boolean;
|
||||
shifts: Week<PayPeriodDetailsWeekDayShifts>
|
||||
expenses: Week<PayPeriodDetailsWeekDayExpenses>;
|
||||
}
|
||||
|
||||
export interface PayPeriodDetailsWeekDayShifts {
|
||||
shifts: Shift[];
|
||||
regular_hours: number;
|
||||
evening_hours: number;
|
||||
emergency_hours: number;
|
||||
overtime_hours: number;
|
||||
total_hours: number;
|
||||
short_date: string;
|
||||
break_duration?: number;
|
||||
}
|
||||
|
||||
export interface PayPeriodDetailsWeekDayExpenses {
|
||||
expenses: Expense[];
|
||||
total_expenses: number;
|
||||
total_mileage: number;
|
||||
}
|
||||
|
||||
const makeWeek = <T>(factory: ()=> T): Week<T> => ({
|
||||
sun: factory(),
|
||||
mon: factory(),
|
||||
tue: factory(),
|
||||
wed: factory(),
|
||||
thu: factory(),
|
||||
fri: factory(),
|
||||
sat: factory(),
|
||||
});
|
||||
|
||||
const emptyDailySchedule = (): PayPeriodDetailsWeekDayShifts => ({
|
||||
shifts: [],
|
||||
regular_hours: 0,
|
||||
evening_hours: 0,
|
||||
emergency_hours: 0,
|
||||
overtime_hours: 0,
|
||||
total_hours: 0,
|
||||
short_date: "",
|
||||
break_duration: 0,
|
||||
});
|
||||
|
||||
const emptyDailyExpenses = (): PayPeriodDetailsWeekDayExpenses => ({
|
||||
expenses: [default_expense,],
|
||||
total_expenses: -1,
|
||||
total_mileage: -1,
|
||||
});
|
||||
|
||||
export const defaultPayPeriodDetailsWeek = (): PayPeriodDetailsWeek => ({
|
||||
is_approved: false,
|
||||
shifts: makeWeek(emptyDailySchedule),
|
||||
expenses: makeWeek(emptyDailyExpenses),
|
||||
});
|
||||
|
||||
export const default_pay_period_details: PayPeriodDetails = {
|
||||
weeks: [ defaultPayPeriodDetailsWeek(), ],
|
||||
employee_full_name: "",
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
export const SHIFT_TYPES = [
|
||||
export const SHIFT_TYPES: ShiftType[] = [
|
||||
'REGULAR',
|
||||
'EVENING',
|
||||
'EMERGENCY',
|
||||
'OVERTIME',
|
||||
'HOLIDAY',
|
||||
'VACATION',
|
||||
'SICK'
|
||||
];
|
||||
|
||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
||||
|
||||
export type UpsertAction = 'create' | 'update' | 'delete';
|
||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
|
||||
|
||||
export type ShiftLegendItem = {
|
||||
type: ShiftType;
|
||||
|
|
@ -19,32 +16,31 @@ export type ShiftLegendItem = {
|
|||
text_color?: string;
|
||||
};
|
||||
|
||||
export interface Shift {
|
||||
date: string;
|
||||
export class Shift {
|
||||
shift_id: number;
|
||||
timesheet_id: number;
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
this.shift_id = -1;
|
||||
this.timesheet_id = -1;
|
||||
this.date = '';
|
||||
this.type = 'REGULAR';
|
||||
this.start_time = '';
|
||||
this.end_time = '';
|
||||
this.comment = undefined;
|
||||
this.is_approved = false;
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpsertShiftsResponse {
|
||||
action: UpsertAction;
|
||||
day: Shift[];
|
||||
export interface NewShift {
|
||||
timesheet_id: number;
|
||||
shifts: Shift[];
|
||||
}
|
||||
|
||||
export interface UpsertShift {
|
||||
old_shift?: Shift | undefined;
|
||||
new_shift?: Shift | undefined;
|
||||
}
|
||||
|
||||
export const default_shift: Readonly<Shift> = {
|
||||
date: '',
|
||||
start_time: '--:--',
|
||||
end_time: '--:--',
|
||||
type: 'REGULAR',
|
||||
comment: '',
|
||||
is_approved: false,
|
||||
is_remote: false,
|
||||
};
|
||||
117
src/modules/timesheets/models/timesheet.models.ts
Normal file
117
src/modules/timesheets/models/timesheet.models.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import type { Shift } from "./shift.models";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
|
||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export interface TimesheetResponse {
|
||||
employee_full_name: string;
|
||||
timesheets: Timesheet[];
|
||||
}
|
||||
|
||||
export interface Timesheet {
|
||||
timesheet_id: number;
|
||||
is_approved: boolean;
|
||||
weekly_hours: TotalHours;
|
||||
weekly_expenses: TotalExpenses;
|
||||
days: TimesheetDay[];
|
||||
}
|
||||
|
||||
export interface TimesheetDay {
|
||||
date: string; // YYYY-MM-DD
|
||||
daily_hours: TotalHours;
|
||||
daily_expenses: TotalExpenses;
|
||||
shifts: Shift[];
|
||||
expenses: Expense[];
|
||||
}
|
||||
|
||||
export interface TotalHours {
|
||||
regular: number;
|
||||
evening: number;
|
||||
emergency: number;
|
||||
overtime: number;
|
||||
vacation: number;
|
||||
holiday: number;
|
||||
sick: number;
|
||||
absent: number;
|
||||
}
|
||||
|
||||
export interface TotalExpenses {
|
||||
expenses: number;
|
||||
mileage: number;
|
||||
}
|
||||
|
||||
// export const test_timesheets: Timesheet[] = [
|
||||
// {
|
||||
// timehsid: 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,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ];
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export type PayPeriodLabel = {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
|
||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||
13
src/modules/timesheets/services/expense-service.ts
Normal file
13
src/modules/timesheets/services/expense-service.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { api } from "src/boot/axios";
|
||||
|
||||
export const ExpenseService = {
|
||||
getExpensesByTimesheetId: async (timesheet_id: number) => {
|
||||
const response = await api.get(`timesheet/${timesheet_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpenseById: async (expense_id: number) => {
|
||||
const response = await api.post(`epxense/${expense_id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
24
src/modules/timesheets/services/shift-service.ts
Normal file
24
src/modules/timesheets/services/shift-service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable */
|
||||
import { api } from "src/boot/axios";
|
||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const ShiftService = {
|
||||
deleteShiftById: async (shift_id: number) => {
|
||||
const response = await api.delete(`/shift/${shift_id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createNewShifts: async (new_shifts: Shift[]) => {
|
||||
// const response = await api.post(`/shift/`, { dtos: new_shifts });
|
||||
// return response;
|
||||
console.log('create shift payload: ', new_shifts);
|
||||
return {status: 200};
|
||||
},
|
||||
|
||||
updateShifts: async (existing_shifts: Shift[]) => {
|
||||
// const response = await api.patch(`/shift/`, { dtos: existing_shifts });
|
||||
// return response;
|
||||
console.log('update shift payload: ', existing_shifts);
|
||||
return {status: 200};
|
||||
}
|
||||
};
|
||||
|
|
@ -1,16 +1,9 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||
import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
|
||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import type { Expense, PayPeriodExpenses, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
|
||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
export const timesheetService = {
|
||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
||||
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;
|
||||
|
|
@ -21,30 +14,13 @@ export const timesheetService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview[]> => {
|
||||
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<PayPeriodDetails> => {
|
||||
const response = await api.get('timesheets', { params: { year, period_no, email, } });
|
||||
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<TimesheetResponse> => {
|
||||
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/${email}/${year}/${period_number}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertOrDeleteShiftsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[], date: string): Promise<PayPeriodDetails> => {
|
||||
const response = await api.put(`/shifts/upsert/${email}/${date}`, payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: UpsertExpense): Promise<Expense[]> => {
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
|
||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload, { headers });
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,45 +1,20 @@
|
|||
import type { Expense, ExpenseTotals } from "src/modules/timesheets/models/expense.models";
|
||||
import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
//------------------ normalization / icons ------------------
|
||||
export const normExpenseType = (type: unknown): string =>
|
||||
typeof type === 'string' ? type.trim().toUpperCase() : '';
|
||||
|
||||
const icon_map: Record<string,string> = {
|
||||
MILEAGE: 'time_to_leave',
|
||||
EXPENSES: 'receipt_long',
|
||||
PER_DIEM: 'hotel',
|
||||
PRIME_GARDE: 'admin_panel_settings',
|
||||
export const getExpenseIcon = (type: ExpenseType) => {
|
||||
switch (type) {
|
||||
case 'MILEAGE': return 'time_to_leave';
|
||||
case 'EXPENSES': return 'receipt_long';
|
||||
case 'PER_DIEM': return 'hotel';
|
||||
case 'ON_CALL': return 'phone_android';
|
||||
default: return 'help_outline';
|
||||
}
|
||||
};
|
||||
|
||||
export const expenseTypeIcon = (type: unknown): string => {
|
||||
const t = normExpenseType(type);
|
||||
return (
|
||||
icon_map[t.toLowerCase()] ??
|
||||
icon_map[t.replace('-','_').toLowerCase()] ??
|
||||
'help_outline'
|
||||
);
|
||||
};
|
||||
|
||||
//------------------ totals ------------------
|
||||
export const computeExpenseTotals = (items: readonly Expense[]): ExpenseTotals =>
|
||||
items.reduce<ExpenseTotals>(
|
||||
(acc, e) => ({
|
||||
amount: acc.amount + (Number(e.amount) || 0),
|
||||
mileage: acc.mileage + (Number(e.mileage) || 0),
|
||||
}),
|
||||
{ amount: 0, mileage: 0 }
|
||||
);
|
||||
|
||||
//------------------ Quasar :rules=[] ------------------
|
||||
export const makeExpenseRules = (t: (_key: string) => string) => {
|
||||
export const useExpenseRules = (t: (_key: string) => string) => {
|
||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||
|
||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||
|
||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||
|
||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||
|
||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
// import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "src/modules/timesheets/constants/expense.constants";
|
||||
// import { ExpensesValidationError } from "src/modules/timesheets/models/expense.validation";
|
||||
// import { type Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY, TYPES_WITH_MILEAGE_ONLY } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
// //normalization helpers
|
||||
// export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||
// if(value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) return undefined;
|
||||
// const num = Number(value);
|
||||
|
||||
// return Number.isFinite(num) ? num : undefined;
|
||||
// };
|
||||
|
||||
// export const normalizeComment = (input?: string): string | undefined => {
|
||||
// if(typeof input === 'undefined' || input === null) return undefined;
|
||||
// const trimmed = String(input).trim();
|
||||
|
||||
// return trimmed.length ? trimmed : undefined;
|
||||
// };
|
||||
|
||||
// export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
|
||||
|
||||
// export const normalizeExpense = (expense: Expense): Expense => {
|
||||
// const comment = normalizeComment(expense.comment);
|
||||
// const amount = toNumOrUndefined(expense.amount);
|
||||
// const mileage = toNumOrUndefined(expense.mileage);
|
||||
|
||||
// return {
|
||||
// date: (expense.date ?? '').trim(),
|
||||
// type: normalizeType(expense.type),
|
||||
// ...(amount !== undefined ? { amount } : {}),
|
||||
// ...(mileage !== undefined ? { mileage } : {}),
|
||||
// ...(comment !== undefined ? { comment } : {}),
|
||||
// ...(typeof expense.supervisor_comment === 'string' && expense.supervisor_comment.trim().length
|
||||
// ? { supervisor_comment: expense.supervisor_comment.trim() }
|
||||
// : {}),
|
||||
// ...(typeof expense.is_approved === 'boolean' ? { is_approved: expense.is_approved }: {} ),
|
||||
// };
|
||||
// };
|
||||
|
||||
// //UI validation error messages
|
||||
// export const validateExpenseUI = (raw: Expense, label: string = 'expense'): void => {
|
||||
// const expense = normalizeExpense(raw);
|
||||
|
||||
// //Date input validation
|
||||
// if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.date_required_or_invalid',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
|
||||
// //comment input validation
|
||||
// if(!expense.comment) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.comment_required',
|
||||
// context: { [label]: expense },
|
||||
// })
|
||||
// }
|
||||
|
||||
// if((expense.comment.length ?? 0) > COMMENT_MAX_LENGTH) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.comment_too_long',
|
||||
// context: { [label]: { ...expense, comment_length: expense.comment?.length } },
|
||||
// });
|
||||
// }
|
||||
|
||||
// //amount input validation
|
||||
// if(expense.amount !== undefined && expense.amount <= 0) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.amount_must_be_positive',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
|
||||
// //mileage input validation
|
||||
// if(expense.mileage !== undefined && expense.mileage <= 0) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.mileage_must_be_positive',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
|
||||
// //cross origin amount/mileage validation
|
||||
// const has_amount = typeof expense.amount === 'number' && expense.amount > 0;
|
||||
// const has_mileage = typeof expense.mileage === 'number' && expense.mileage > 0;
|
||||
|
||||
// if(has_amount === has_mileage) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.amount_xor_mileage',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
|
||||
// //type constraint validation
|
||||
// const type = expense.type as ExpenseType;
|
||||
// if( TYPES_WITH_MILEAGE_ONLY.includes(type) && !has_mileage ) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.mileage_required_for_type',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
// if(TYPES_WITH_AMOUNT_ONLY.includes(type) && !has_amount) {
|
||||
// throw new ExpensesValidationError({
|
||||
// status_code: 400,
|
||||
// message: 'timesheet.expense.errors.amount_required_for_type',
|
||||
// context: { [label]: expense },
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// <<<<<<< HEAD
|
||||
|
||||
// //totals per pay-period
|
||||
// export const compute_expense_totals = (items: Expense[]) => items.reduce(
|
||||
// (acc, raw) => {
|
||||
// const expense = normalizeExpense(raw);
|
||||
// if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
||||
// if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
||||
// return acc;
|
||||
// },
|
||||
// { amount: 0, mileage: 0 }
|
||||
// );
|
||||
// =======
|
||||
// >>>>>>> 1bdbe021facc85fb50cff6c60053278695df6bdc
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// import type { ShiftKey, ShiftPayload, ShiftSelectOption } from "../types/shift.types";
|
||||
|
||||
// export const toShiftPayload = (shift: any): ShiftPayload => ({
|
||||
// start_time: String(shift.start_time),
|
||||
// end_time: String(shift.end_time),
|
||||
// type: String(shift.type).toUpperCase() as ShiftKey,
|
||||
// is_remote: !!shift.is_remote,
|
||||
// ...(shift.comment ? { comment: String(shift.comment) } : {}),
|
||||
// });
|
||||
|
||||
// export const buildShiftOptions = (
|
||||
// keys: readonly string[],
|
||||
// t:(k: string) => string
|
||||
// ): ShiftSelectOption[] =>
|
||||
// keys.map((key) => ({
|
||||
// value: key as any,
|
||||
// label: t(`timesheet.shift.types.${key}`),
|
||||
// }));
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type { PayPeriodLabel } from "src/modules/timesheets/models/ui.models";
|
||||
|
||||
export const formatPayPeriodLabel = (
|
||||
raw_label: string | undefined,
|
||||
locale: string,
|
||||
extractDate: (_input: string, _mask: string) => Date,
|
||||
opts: Intl.DateTimeFormatOptions
|
||||
): PayPeriodLabel => {
|
||||
const label = raw_label ?? '';
|
||||
const dates = label.split('.');
|
||||
if(dates.length < 2) return { start_date: '—', end_date:'—' };
|
||||
|
||||
const fmt = new Intl.DateTimeFormat(locale, opts);
|
||||
const start = fmt.format(extractDate(dates[0]!, 'YYYY-MM-DD'));
|
||||
const end = fmt.format(extractDate(dates[1]!, 'YYYY-MM-DD'));
|
||||
return { start_date: start, end_date: end };
|
||||
}
|
||||
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>
|
||||
18
src/pages/employee-list-page.vue
Normal file
18
src/pages/employee-list-page.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
|
||||
import EmployeeListAddModifyDialog from 'src/modules/employee-list/components/employee/employee-list-add-modify-dialog.vue';
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="column flex-center">
|
||||
<EmployeeListAddModifyDialog />
|
||||
|
||||
<PageHeaderTemplate title="employee_list.page_header" />
|
||||
|
||||
<EmployeeListTable />
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import LoginConnectionPanel from 'src/modules/auth/components/login-connection-panel.vue';
|
||||
import LoginDevBypass from 'src/modules/auth/components/login-dev-bypass.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,9 +10,6 @@
|
|||
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
|
||||
<LoginConnectionPanel />
|
||||
</transition>
|
||||
|
||||
<!-- DEV TOOLS -->
|
||||
<LoginDevBypass />
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
|
|
@ -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/types/employee-profile-interface';
|
||||
// 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 { employeeProfile } = 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() )"
|
||||
<MenuEmployee
|
||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||
class="col-auto"
|
||||
:employee-profile="employeeProfile"
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import SupervisorCrewTable from '../components/supervisor/supervisor-crew-table.vue';
|
||||
import EmployeeListAddModifyDialog from '../components/employee/employee-list-add-modify-dialog.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page>
|
||||
<EmployeeListAddModifyDialog />
|
||||
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder">
|
||||
{{ $t('employee_list.page_header') }}
|
||||
</div>
|
||||
<SupervisorCrewTable />
|
||||
</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();
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
};
|
||||
|
||||
onMounted( async () => {
|
||||
await timesheet_approval_api.getPayPeriodOverviewsByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||
await timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { date } from 'quasar';
|
||||
import { onMounted } from 'vue';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
|
|
@ -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,15 +23,21 @@
|
|||
<template>
|
||||
<q-page
|
||||
padding
|
||||
class="q-pa-md bg-secondary"
|
||||
class="column q-pa-md bg-secondary items-center"
|
||||
>
|
||||
<PageHeaderTemplate
|
||||
:title="$t('timesheet.page_header')"
|
||||
:start-date="timesheet_store.pay_period.period_start"
|
||||
:end-date="timesheet_store.pay_period.period_end"
|
||||
:title="'timesheet.page_header'"
|
||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<TimesheetWrapper :employee-email="user.email" />
|
||||
<div
|
||||
class="col"
|
||||
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
|
||||
>
|
||||
<TimesheetWrapper :employee-email="user?.email ?? ''" />
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -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
|
||||
|
|
@ -27,10 +28,12 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
Router.beforeEach((destinationPage) => {
|
||||
Router.beforeEach(async (destinationPage) => {
|
||||
const authStore = useAuthStore();
|
||||
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||
|
||||
if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) {
|
||||
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
|
||||
console.log('no user account found');
|
||||
return { name: 'login' };
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -20,11 +20,11 @@ const routes: RouteRecordRaw[] = [
|
|||
{
|
||||
path: 'employees',
|
||||
name: RouteNames.EMPLOYEE_LIST,
|
||||
component: () => import('src/pages/supervisor-crew-page.vue'),
|
||||
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 },
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,67 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { AuthService } from "../modules/auth/services/services-auth";
|
||||
import type { User } from "src/modules/shared/models/user.models";
|
||||
|
||||
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
|
||||
|
||||
const TestUsers: Record<CompanyRole, User> = {
|
||||
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
|
||||
supervisor: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: 'supervisor' },
|
||||
accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
||||
}
|
||||
import { CAN_APPROVE_PAY_PERIODS, type User } from "src/modules/shared/models/user.models";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User>(TestUsers.guest);
|
||||
const user = ref<User>();
|
||||
const authError = ref("");
|
||||
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
|
||||
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value?.role ?? 'GUEST'));
|
||||
const router = useRouter();
|
||||
|
||||
const login = () => {
|
||||
//TODO: manage customer login process
|
||||
};
|
||||
|
||||
const oidcLogin = () => {
|
||||
const oidcPopup = AuthService.oidcLogin();
|
||||
if (!oidcPopup) {
|
||||
authError.value = "You have popups blocked on this website!";
|
||||
}
|
||||
window.addEventListener('message', (event) => {
|
||||
void handleAuthMessage(event);
|
||||
});
|
||||
|
||||
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_AUTH_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
||||
|
||||
if (!oidc_popup)
|
||||
Notify.create({
|
||||
message: "You have popups blocked on this website!",
|
||||
color: 'negative',
|
||||
textColor: 'white',
|
||||
});
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
user.value = TestUsers.guest;
|
||||
user.value = undefined;
|
||||
};
|
||||
|
||||
const setUser = (bypassRole: string) => {
|
||||
if (bypassRole in TestUsers) {
|
||||
user.value = TestUsers[bypassRole as CompanyRole];
|
||||
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 {
|
||||
user.value = TestUsers.guest;
|
||||
} else {
|
||||
Notify.create({
|
||||
message: "You have popups blocked on this website!",
|
||||
color: 'negative',
|
||||
textColor: 'white',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
|
||||
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,12 +1,11 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list";
|
||||
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
|
||||
import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface";
|
||||
import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
|
||||
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
|
||||
|
||||
export const useEmployeeStore = defineStore('employee', () => {
|
||||
const employee = ref<EmployeeProfile>( default_employee_profile );
|
||||
const employeeList = ref<EmployeeListTableItem[]>([]);
|
||||
const employeeList = ref<EmployeeProfile[]>([]);
|
||||
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
||||
const isLoadingEmployeeProfile = ref(false);
|
||||
const isLoadingEmployeeList = ref(false);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
import { computed, ref } from "vue";
|
||||
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 { computeExpenseTotals } from "src/modules/timesheets/utils/expense.util";
|
||||
import type { UpsertAction } 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";
|
||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||
|
||||
|
||||
|
||||
export const useExpensesStore = defineStore('expenses', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const is_open = ref(false);
|
||||
const is_loading = ref(false);
|
||||
const mode = ref<UpsertAction>('create');
|
||||
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
||||
const pay_period_expenses_totals = computed(() => computeExpenseTotals(pay_period_expenses.value.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) => {
|
||||
|
|
@ -25,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;
|
||||
}
|
||||
|
||||
|
|
@ -41,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 ExpenseService.getExpensesByTimesheetId(timesheet_id);
|
||||
pay_period_expenses.value = expenses;
|
||||
} catch (err: unknown) {
|
||||
if (typeof err === 'object') {
|
||||
|
|
@ -72,21 +62,16 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, date: string, expense: UpsertExpense): Promise<void> => {
|
||||
const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
|
||||
is_loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(
|
||||
encodeURIComponent(employee_email),
|
||||
encodeURIComponent(date),
|
||||
expense,
|
||||
);
|
||||
console.log('updated expenses received: ', updated_expenses)
|
||||
pay_period_expenses.value.expenses = updated_expenses;
|
||||
await ExpenseService.upsertOrDeleteExpenseById(expense_id);
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -95,15 +80,13 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
return {
|
||||
is_open,
|
||||
is_loading,
|
||||
mode,
|
||||
pay_period_expenses,
|
||||
pay_period_expenses_totals,
|
||||
current_expense,
|
||||
initial_expense,
|
||||
error,
|
||||
open,
|
||||
getPayPeriodExpensesByEmployeeEmail,
|
||||
upsertOrDeleteExpensesByEmployeeEmail,
|
||||
getPayPeriodExpensesByTimesheetId,
|
||||
upsertOrDeleteExpensesById,
|
||||
close,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,80 +1,71 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { default_shift, type UpsertAction, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const useShiftStore = defineStore('shift', () => {
|
||||
const is_open = ref(false);
|
||||
const mode = ref<UpsertAction>('create');
|
||||
const date_iso = ref<string>('');
|
||||
const current_shift = ref<Shift>(default_shift);
|
||||
const initial_shift = ref<Shift>(default_shift);
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const useShiftStore = defineStore('shift_store', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_error = ref();
|
||||
|
||||
const open = (next_mode: UpsertAction, date: string, current: Shift, initial: Shift) => {
|
||||
mode.value = next_mode;
|
||||
date_iso.value = date;
|
||||
current_shift.value = current; // new shift
|
||||
initial_shift.value = initial; // old shift
|
||||
is_open.value = true;
|
||||
};
|
||||
|
||||
const openCreate = (date: string) => {
|
||||
open('create', date, default_shift, default_shift);
|
||||
};
|
||||
|
||||
const openUpdate = (date: string, shift: Shift) => {
|
||||
open('update', date, shift, unwrapAndClone(shift));
|
||||
};
|
||||
|
||||
const openDelete = (date: string, shift: Shift) => {
|
||||
open('delete', date, default_shift, shift);
|
||||
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
||||
try {
|
||||
await ShiftService.deleteShiftById(shift_id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while deleting shift: ', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
is_open.value = false;
|
||||
mode.value = 'create';
|
||||
date_iso.value = '';
|
||||
current_shift.value = default_shift;
|
||||
initial_shift.value = default_shift;
|
||||
};
|
||||
|
||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
|
||||
const encoded_email = encodeURIComponent(employee_email);
|
||||
const encoded_date = encodeURIComponent(current_shift.value.date);
|
||||
const createNewShifts = async (): Promise<boolean> => {
|
||||
if (timesheet_store.timesheets === undefined) return false;
|
||||
|
||||
try {
|
||||
const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, [ upsert_shift, ], encoded_date);
|
||||
timesheet_store.pay_period_details = result;
|
||||
} catch (err) {
|
||||
console.log('error doing thing: ', err)
|
||||
// const status_code: number = err?.response?.status ?? 500;
|
||||
// const data = err?.response?.data ?? {};
|
||||
// throw new GenericApiError({
|
||||
// status_code,
|
||||
// error_code: data.error_code,
|
||||
// message: data.message || data.error || err.message,
|
||||
// context: data.context,
|
||||
// });
|
||||
} finally {
|
||||
close();
|
||||
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id < 0);
|
||||
|
||||
if (new_shifts?.length > 0) {
|
||||
const response = await ShiftService.createNewShifts(new_shifts);
|
||||
if (response.status <= 200) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No new shifts to save');
|
||||
Notify.create('no new shifts to save')
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error creating new shifts: ', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateShifts = async (): Promise<boolean> => {
|
||||
if (timesheet_store.timesheets === undefined) return false;
|
||||
|
||||
try {
|
||||
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id > 0);
|
||||
|
||||
if (existing_shifts?.length > 0) {
|
||||
const response = await ShiftService.updateShifts(existing_shifts);
|
||||
if (response.status <= 200) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No shifts to update');
|
||||
Notify.create('no shifts to update')
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error updating shifts: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_open,
|
||||
mode,
|
||||
date_iso,
|
||||
current_shift,
|
||||
initial_shift,
|
||||
openCreate,
|
||||
openUpdate,
|
||||
openDelete,
|
||||
close,
|
||||
upsertOrDeleteShiftByEmployeeEmail,
|
||||
};
|
||||
shift_error,
|
||||
deleteShiftById,
|
||||
createNewShifts,
|
||||
updateShifts,
|
||||
}
|
||||
})
|
||||
|
|
@ -1,28 +1,24 @@
|
|||
import { ref } from 'vue';
|
||||
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 PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-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/pay-period-details.models';
|
||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||
|
||||
|
||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||
const auth_store = useAuthStore();
|
||||
const is_loading = ref<boolean>(false);
|
||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
||||
const pay_period_overviews = ref<PayPeriodOverview[]>([default_pay_period_overview,]);
|
||||
const current_pay_period_overview = ref<PayPeriodOverview>(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[]>();
|
||||
const pay_period_report = ref();
|
||||
const is_calendar_limit = computed(() =>
|
||||
pay_period.value.pay_year === 2024 &&
|
||||
pay_period.value.pay_period_no <= 1
|
||||
);
|
||||
|
||||
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
||||
is_loading.value = true;
|
||||
|
||||
try {
|
||||
if (typeof date_or_year === 'string') {
|
||||
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
|
||||
|
|
@ -30,33 +26,31 @@ 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;
|
||||
is_loading.value = false;
|
||||
else pay_period.value = undefined;
|
||||
|
||||
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;
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
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,27 +58,23 @@ 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.timesheets;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
||||
return withLoading(is_loading.value, async () => {
|
||||
try {
|
||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
||||
year,
|
||||
|
|
@ -92,7 +82,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
report_filters
|
||||
);
|
||||
pay_period_report.value = response;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('There was an error retrieving the report CSV: ', error);
|
||||
|
|
@ -100,19 +89,17 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
is_loading,
|
||||
is_calendar_limit,
|
||||
pay_period,
|
||||
pay_period_overviews,
|
||||
current_pay_period_overview,
|
||||
pay_period_details,
|
||||
timesheets,
|
||||
getPayPeriodByDateOrYearAndNumber,
|
||||
getPayPeriodOverviewsBySupervisorEmail,
|
||||
getPayPeriodDetailsByEmployeeEmail,
|
||||
getTimesheetOverviewsByPayPeriod,
|
||||
getTimesheetsByEmployeeEmail,
|
||||
getPayPeriodReportByYearAndPeriodNumber,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const isRightDrawerOpen = ref(true);
|
||||
const q = useQuasar();
|
||||
const is_left_drawer_open = ref(true);
|
||||
const focus_next_component = ref(false);
|
||||
const is_mobile_mode = computed(() => q.screen.lt.md);
|
||||
|
||||
const toggleRightDrawer = () => {
|
||||
isRightDrawerOpen.value = !isRightDrawerOpen.value;
|
||||
is_left_drawer_open.value = !is_left_drawer_open.value;
|
||||
}
|
||||
|
||||
return { isRightDrawerOpen, toggleRightDrawer };
|
||||
return {
|
||||
is_mobile_mode,
|
||||
focus_next_component,
|
||||
is_left_drawer_open,
|
||||
toggleRightDrawer
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user