BREAKING(login): implement full auth process using Authentik, remove files containing deprecated code
This commit is contained in:
parent
702a977fce
commit
c1c0faeaf1
|
|
@ -14,7 +14,10 @@ declare module 'vue' {
|
||||||
// good idea to move this instance creation inside of the
|
// good idea to move this instance creation inside of the
|
||||||
// "export default () => {}" function below (which runs individually
|
// "export default () => {}" function below (which runs individually
|
||||||
// for each client)
|
// 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_AUTH_URL,
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
|
||||||
export default defineBoot(({ app }) => {
|
export default defineBoot(({ app }) => {
|
||||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ export default {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
coming_soon: "coming soon!",
|
coming_soon: "coming soon!",
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
login_failed: "Failed to login",
|
||||||
|
popups_blocked: "Popups are blocked on this device",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
nav_bar: {
|
nav_bar: {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ export default {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
coming_soon: "à venir!",
|
coming_soon: "à venir!",
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
login_failed: "Échec à la connexion",
|
||||||
|
popups_blocked: "Les fenêtres contextuelles sont bloqués sur cet appareil",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
nav_bar: {
|
nav_bar: {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,33 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const currentUser = authStore.user;
|
|
||||||
|
|
||||||
// Will need to implement this eventually, just testing the look for now
|
// Will need to implement this eventually, just testing the look for now
|
||||||
const notifAmount = ref(7);
|
const notification_count = ref(7);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-item clickable v-ripple dark class="q-pa-none">
|
<q-item
|
||||||
<q-item-section :side="$q.screen.gt.sm">
|
clickable
|
||||||
<q-avatar rounded >
|
v-ripple
|
||||||
<q-img src="src/assets/targo-default-avatar.png" />
|
dark
|
||||||
<q-badge floating color="negative" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
|
class="q-pa-none q-mt-sm"
|
||||||
</q-avatar>
|
>
|
||||||
</q-item-section>
|
<q-icon
|
||||||
|
:name="notification_count > 0 ? 'notifications_active' : 'notifications_off'"
|
||||||
|
size="lg"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
<q-badge
|
||||||
|
v-if="notification_count > 0"
|
||||||
|
floating
|
||||||
|
color="negative"
|
||||||
|
class="text-weight-bolder absolute"
|
||||||
|
>
|
||||||
|
{{ notification_count }}
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
<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-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
|
@ -6,19 +9,19 @@
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const uiStore = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const miniState = ref(true);
|
const is_mini = ref(true);
|
||||||
|
|
||||||
const goToPageName = (pageName: string) => {
|
const goToPageName = (page_name: string) => {
|
||||||
router.push({ name: pageName }).catch(err => {
|
router.push({ name: page_name }).catch(err => {
|
||||||
console.error('Error with Vue Router: ', err);
|
console.error('Error with Vue Router: ', err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
auth_store.logout();
|
||||||
|
|
||||||
router.push({ name: 'login' }).catch(err => {
|
router.push({ name: 'login' }).catch(err => {
|
||||||
console.log('could not log you out: ', err);
|
console.log('could not log you out: ', err);
|
||||||
|
|
@ -28,21 +31,29 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="uiStore.isRightDrawerOpen"
|
v-model="ui_store.isRightDrawerOpen"
|
||||||
overlay
|
overlay
|
||||||
elevated
|
elevated
|
||||||
side="left"
|
side="left"
|
||||||
:mini="miniState"
|
:mini="is_mini"
|
||||||
@mouseenter="miniState = false"
|
@mouseenter="is_mini = false"
|
||||||
@mouseleave="miniState = true"
|
@mouseleave="is_mini = true"
|
||||||
class="bg-dark"
|
class="bg-dark"
|
||||||
>
|
>
|
||||||
<q-scroll-area class="fit">
|
<q-scroll-area class="fit">
|
||||||
<q-list>
|
<q-list>
|
||||||
<!-- Home -->
|
<!-- 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-item-section avatar>
|
||||||
<q-icon name="home" color="primary" />
|
<q-icon
|
||||||
|
name="home"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
|
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
|
||||||
|
|
@ -50,42 +61,77 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
<q-item
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
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-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-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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
<q-item
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
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-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-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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
<q-item
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
v-ripple
|
||||||
|
clickable
|
||||||
|
side
|
||||||
|
@click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
||||||
|
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||||
|
>
|
||||||
<q-item-section avatar>
|
<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-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-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<!-- Profile -->
|
<!-- 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-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-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
|
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
|
||||||
|
|
@ -93,9 +139,16 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<!-- Help -->
|
<!-- Help -->
|
||||||
<q-item v-ripple clickable @click="goToPageName('help')">
|
<q-item
|
||||||
|
v-ripple
|
||||||
|
clickable
|
||||||
|
@click="goToPageName('help')"
|
||||||
|
>
|
||||||
<q-item-section avatar>
|
<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-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
|
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
|
||||||
|
|
@ -104,9 +157,17 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<!-- Logout -->
|
<!-- 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-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-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
|
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { UserRole } from 'src/modules/shared/models/user.models';
|
|
||||||
import { useAuthApi } from '../composables/use-auth-api';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const auth_api = useAuthApi();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const setBypassUser = (bypassRole: UserRole) => {
|
|
||||||
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 UserRole"
|
|
||||||
:key="index"
|
|
||||||
push
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="role"
|
|
||||||
class="text-uppercase"
|
|
||||||
@click="setBypassUser(role)"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useAuthStore } from "../../../stores/auth-store";
|
import { useAuthStore } from "../../../stores/auth-store";
|
||||||
import type { UserRole } from "src/modules/shared/models/user.models";
|
|
||||||
|
|
||||||
export const useAuthApi = () => {
|
export const useAuthApi = () => {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
@ -16,19 +15,9 @@ export const useAuthApi = () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAuthorizedUser = () => {
|
|
||||||
return authStore.isAuthorizedUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUser = (bypassRole: UserRole) => {
|
|
||||||
authStore.setUser(bypassRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login,
|
login,
|
||||||
oidcLogin,
|
oidcLogin,
|
||||||
logout,
|
logout,
|
||||||
isAuthorizedUser,
|
|
||||||
setUser,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LoginConnectionPanel from 'src/modules/auth/components/login-connection-panel.vue';
|
import LoginConnectionPanel from 'src/modules/auth/components/login-connection-panel.vue';
|
||||||
import LoginDevBypass from 'src/modules/auth/components/login-dev-bypass.vue';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -11,9 +10,6 @@
|
||||||
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
|
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
|
||||||
<LoginConnectionPanel />
|
<LoginConnectionPanel />
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- DEV TOOLS -->
|
|
||||||
<LoginDevBypass />
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,6 @@ export const AuthService = {
|
||||||
//TODO: OIDC customer sign-in, eventually
|
//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: () => {
|
logout: () => {
|
||||||
// TODO: logout logic
|
// TODO: logout logic
|
||||||
api.post('/auth/logout')
|
api.post('/auth/logout')
|
||||||
|
|
@ -27,8 +17,8 @@ export const AuthService = {
|
||||||
api.post('/auth/refresh')
|
api.post('/auth/refresh')
|
||||||
},
|
},
|
||||||
|
|
||||||
getProfile: () => {
|
getProfile: async () => {
|
||||||
// TODO: user info fetch logic
|
const response = await api.get('/auth/me');
|
||||||
api.get('/auth/me')
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
27
src/modules/shared/components/generic-loader.vue
Normal file
27
src/modules/shared/components/generic-loader.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
defineProps<{
|
||||||
|
isLoading: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
v-if="isLoading"
|
||||||
|
flat
|
||||||
|
class="column flex-center rounded-10"
|
||||||
|
style="width: 20vw !important; height: 20vh !important;"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="5em"
|
||||||
|
:thickness="10"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
||||||
|
{{ $t('shared.label.loading') }}
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
@ -1,26 +1,17 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
firstName: string;
|
first_name: string;
|
||||||
lastName: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UserRole {
|
export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
||||||
ADMIN = 'ADMIN',
|
|
||||||
SUPERVISOR = 'SUPERVISOR',
|
|
||||||
HR = 'HR',
|
|
||||||
ACCOUNTING = 'ACCOUNTING',
|
|
||||||
EMPLOYEE = 'EMPLOYEE',
|
|
||||||
DEALER = 'DEALER',
|
|
||||||
CUSTOMER = 'CUSTOMER',
|
|
||||||
GUEST = 'GUEST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
||||||
UserRole.ADMIN,
|
'ADMIN',
|
||||||
UserRole.SUPERVISOR,
|
'SUPERVISOR',
|
||||||
UserRole.HR,
|
'HR',
|
||||||
UserRole.ACCOUNTING,
|
'ACCOUNTING',
|
||||||
]
|
]
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
|
||||||
|
|
@ -29,32 +29,8 @@
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
@show="render_key += 1"
|
@show="render_key += 1"
|
||||||
>
|
|
||||||
<!-- loader -->
|
|
||||||
<transition
|
|
||||||
enter-active-class="animated faster zoomIn"
|
|
||||||
leave-active-class="animated faster zoomOut"
|
|
||||||
mode="out-in"
|
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
v-if="timesheet_store.is_loading"
|
|
||||||
class="column flex-center text-center"
|
|
||||||
style="width: 50vw !important; max-height: 50vh !important;"
|
|
||||||
>
|
|
||||||
<q-spinner
|
|
||||||
color="primary"
|
|
||||||
size="5em"
|
|
||||||
:thickness="10"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
|
||||||
{{ $t('shared.label.loading') }}
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<q-card
|
|
||||||
v-if="!timesheet_store.is_loading"
|
|
||||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
import type { TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||||
|
|
||||||
const modelApproval = defineModel<boolean>();
|
const modelApproval = defineModel<boolean>();
|
||||||
const { row } = defineProps<{ row: PayPeriodOverview; }>();
|
const { row } = defineProps<{ row: TimesheetOverview; }>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clickDetails': [overview: PayPeriodOverview];
|
'clickDetails': [overview: TimesheetOverview];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.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 expenses_store = useExpensesStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
timesheet_store.pay_period_overviews
|
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;
|
employeeEmail.value = employee_email;
|
||||||
timesheet_store.current_pay_period_overview = row;
|
timesheet_store.current_pay_period_overview = row;
|
||||||
emit('clickedDetailsButton', employee_email);
|
emit('clickedDetailsButton', employee_email);
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Template for individual employee cards -->
|
<!-- Template for individual employee cards -->
|
||||||
<template #item="props: { row: PayPeriodOverview, key: string }">
|
<template #item="props: { row: TimesheetOverview, key: string }">
|
||||||
<OverviewListItem
|
<OverviewListItem
|
||||||
v-model="props.row.is_approved"
|
v-model="props.row.is_approved"
|
||||||
:row="props.row"
|
:row="props.row"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { useAuthStore } from "src/stores/auth-store";
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||||
import { NavigatorConstants } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import { NavigatorConstants } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
|
|
||||||
export const useTimesheetApprovalApi = () => {
|
export const useTimesheetApprovalApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export enum NavigatorConstants {
|
||||||
PREVIOUS_PERIOD = -1,
|
PREVIOUS_PERIOD = -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayPeriodOverview {
|
export interface TimesheetOverview {
|
||||||
email: string;
|
email: string;
|
||||||
employee_name: string;
|
employee_name: string;
|
||||||
regular_hours: number;
|
regular_hours: number;
|
||||||
|
|
@ -31,10 +31,10 @@ export interface PayPeriodOverviewResponse {
|
||||||
period_end: string;
|
period_end: string;
|
||||||
payday: string;
|
payday: string;
|
||||||
label: string;
|
label: string;
|
||||||
employees_overview: PayPeriodOverview[];
|
employees_overview: TimesheetOverview[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const default_pay_period_overview: PayPeriodOverview = {
|
export const default_pay_period_overview: TimesheetOverview = {
|
||||||
email: '',
|
email: '',
|
||||||
employee_name: '',
|
employee_name: '',
|
||||||
regular_hours: -1,
|
regular_hours: -1,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||||
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 = {
|
export const timesheetApprovalService = {
|
||||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => {
|
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { 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 { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
const employee_email = inject<string>('employeeEmail');
|
const employee_email = inject<string>('employeeEmail');
|
||||||
const rules = makeExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
const cancelUpdateMode = () => {
|
const cancelUpdateMode = () => {
|
||||||
expenses_store.current_expense = default_expense;
|
expenses_store.current_expense = default_expense;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
>
|
>
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<q-item
|
<q-item
|
||||||
:key="refresh_key"
|
:key="refresh_key"
|
||||||
:clickable="horizontal"
|
:clickable="horizontal"
|
||||||
class="row q-mx-xs shadow-2"
|
class="row col-4 q-ma-xs shadow-2"
|
||||||
:style="expenseItemStyle + highlightClass + approvedClass"
|
:style="expenseItemStyle + highlightClass + approvedClass"
|
||||||
@click="onExpenseClicked"
|
@click="onExpenseClicked"
|
||||||
>
|
>
|
||||||
|
|
@ -126,6 +126,8 @@
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-space v-if="horizontal" />
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<!-- attachment file icon -->
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<q-list
|
<q-list
|
||||||
padding
|
padding
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
:class="horizontal ? 'row justify-center' : ''"
|
:class="horizontal ? 'row flex-center' : ''"
|
||||||
>
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useShiftStore } from 'src/stores/shift-store';
|
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||||
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
|
|
||||||
const shift_store = useShiftStore();
|
defineProps<{
|
||||||
|
shift: Shift;
|
||||||
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
'onCommentBlur': [void];
|
'onCommentBlur': [void];
|
||||||
|
|
@ -13,10 +14,21 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="row full-width justify-center">
|
||||||
<div class="col-xs-6 col-sm-4 col-md-3 row q-mx-xs q-my-none">
|
<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
|
<q-select
|
||||||
v-model="shift_store.current_shift.type"
|
v-model="shift.type"
|
||||||
options-dense
|
options-dense
|
||||||
:options="SHIFT_TYPES"
|
:options="SHIFT_TYPES"
|
||||||
:label="$t('timesheet.shift.types.label')"
|
:label="$t('timesheet.shift.types.label')"
|
||||||
|
|
@ -29,22 +41,11 @@
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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_store.current_shift.is_remote"
|
|
||||||
class="q-pa-none q-ma-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto row q-mx-xs">
|
<div class="col-auto row q-mx-xs">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="shift_store.current_shift.start_time"
|
v-model="shift.start_time"
|
||||||
:label="$t('timesheet.shift.fields.start')"
|
:label="$t('timesheet.shift.fields.start')"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
|
@ -55,7 +56,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="shift_store.current_shift.end_time"
|
v-model="shift.end_time"
|
||||||
:label="$t('timesheet.shift.fields.end')"
|
:label="$t('timesheet.shift.fields.end')"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
|
|
@ -67,7 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="shift_store.current_shift.comment"
|
v-model="shift.comment"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
autogrow
|
autogrow
|
||||||
filled
|
filled
|
||||||
|
|
@ -76,7 +77,7 @@
|
||||||
:label="$t('timesheet.shift.fields.header_comment')"
|
:label="$t('timesheet.shift.fields.header_comment')"
|
||||||
:counter="true"
|
:counter="true"
|
||||||
:maxlength="512"
|
:maxlength="512"
|
||||||
class="col-auto"
|
class="col-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,127 +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 ShiftCrudDialogAddUpdateShift from 'src/modules/timesheets/components/shift-crud-dialog-add-update-shift.vue';
|
|
||||||
|
|
||||||
const shift_store = useShiftStore();
|
|
||||||
const shift_api = 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(() =>
|
|
||||||
shift_store.mode === 'delete' ||
|
|
||||||
(shift_store.current_shift.start_time.trim().length === 5 &&
|
|
||||||
shift_store.current_shift.end_time.trim().length === 5 &&
|
|
||||||
shift_store.current_shift.type !== undefined)
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="shift_store.is_open"
|
|
||||||
persistent
|
|
||||||
full-width
|
|
||||||
transition-show="fade"
|
|
||||||
transition-hide="fade"
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-card
|
|
||||||
class="q-pa-md rounded-10 shadow-5"
|
|
||||||
:style="$q.screen.gt.sm ? 'max-width: 60vw !important;' : ''"
|
|
||||||
>
|
|
||||||
<q-card-section class="row items-center q-mb-sm q-pa-none">
|
|
||||||
<q-icon
|
|
||||||
name="schedule"
|
|
||||||
size="24px"
|
|
||||||
class="q-mr-sm"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<div class="text-h6">
|
|
||||||
{{
|
|
||||||
shift_store.mode === 'create'
|
|
||||||
? $t('timesheet.shift.actions.add')
|
|
||||||
: shift_store.mode === 'update'
|
|
||||||
? $t('timesheet.shift.actions.edit')
|
|
||||||
: $t('timesheet.shift.actions.delete')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<q-space />
|
|
||||||
<q-badge
|
|
||||||
outline
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{{ shift_store.date_iso }}
|
|
||||||
</q-badge>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="shift_store.mode !== 'delete'"
|
|
||||||
class="row no-wrap items-start justify-center"
|
|
||||||
>
|
|
||||||
<ShiftCrudDialogAddUpdateShift />
|
|
||||||
</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="shift_store.close"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
icon="save_alt"
|
|
||||||
:label="shift_store.mode === 'delete' ? $t('timesheet.delete_button') : $t('timesheet.save_button')"
|
|
||||||
:loading="isSubmitting"
|
|
||||||
:disable="!canSubmit"
|
|
||||||
@click="shift_api.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>
|
|
||||||
|
|
@ -19,12 +19,6 @@
|
||||||
'request-delete': [shift: Shift];
|
'request-delete': [shift: Shift];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
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 ? 'font-size: 1em;' : 'font-size: 1.5em;')
|
const hour_font_size = computed(() => dense ? 'font-size: 1em;' : 'font-size: 1.5em;')
|
||||||
const is_hovering = ref(false);
|
const is_hovering = ref(false);
|
||||||
const font_color = computed(() => shift.type === 'REGULAR' ? (q.dark.isActive ? ' text-blue-grey-2' : ' text-grey-8') : ' text-white')
|
const font_color = computed(() => shift.type === 'REGULAR' ? (q.dark.isActive ? ' text-blue-grey-2' : ' text-grey-8') : ' text-white')
|
||||||
|
|
@ -58,16 +52,7 @@
|
||||||
@mouseenter="is_hovering = true"
|
@mouseenter="is_hovering = true"
|
||||||
@mouseleave="is_hovering = false"
|
@mouseleave="is_hovering = false"
|
||||||
>
|
>
|
||||||
<!-- highlight hovering div -->
|
|
||||||
<transition
|
|
||||||
enter-active-class="animated zoomIn"
|
|
||||||
leave-active-class="animated zoomOut"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="is_hovering"
|
|
||||||
class="absolute shift-highlight full-height full-width no-pointer-events"
|
|
||||||
></div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
<!-- punch-in timestamp -->
|
<!-- punch-in timestamp -->
|
||||||
|
|
@ -130,10 +115,11 @@
|
||||||
<!-- comment btn -->
|
<!-- comment btn -->
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="shift.type"
|
v-if="shift.type"
|
||||||
:name="comment_icon"
|
name="comment"
|
||||||
:color="comment_color"
|
color="primary"
|
||||||
class="q-pa-none q-mr-xs"
|
|
||||||
:size="dense ? 'xs' : 'sm'"
|
:size="dense ? 'xs' : 'sm'"
|
||||||
|
class="q-pa-none q-mr-xs"
|
||||||
|
:class="shift.comment ? '' : 'invisible'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- delete btn -->
|
<!-- delete btn -->
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,14 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
import { date } from 'quasar';
|
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 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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
|
||||||
const { dense = false } = defineProps<{
|
const { dense = false } = defineProps<{
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -72,33 +70,22 @@ import { computed } from 'vue';
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- List of shifts column -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col column q-pa-none full-height">
|
||||||
<ShiftListHeader v-if="day.shifts.length > 0 && !dense"/>
|
|
||||||
<div
|
<div
|
||||||
v-if="day.shifts.length > 0"
|
v-if="day.shifts.length > 0"
|
||||||
class="q-gutter-xs"
|
class="col-grow column justify-center"
|
||||||
>
|
>
|
||||||
<ShiftListRow
|
<ShiftListRow
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
|
class="col"
|
||||||
:dense="dense"
|
:dense="dense"
|
||||||
:shift="shift"
|
:shift="shift"
|
||||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
@request-update=""
|
||||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
@request-delete=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- add shift btn column -->
|
|
||||||
<q-card-section class="col-auto q-pa-none">
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
:dense="dense"
|
|
||||||
color="primary"
|
|
||||||
icon="more_time"
|
|
||||||
:class="dense ? '' : 'q-pa-sm q-mr-sm'"
|
|
||||||
@click="openCreate(to_iso_date(day.short_date))"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import GenericLoader from 'src/modules/shared/components/generic-loader.vue';
|
||||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||||
import ShiftCrudDialog from 'src/modules/timesheets/components/shift-crud-dialog.vue';
|
|
||||||
import ExpenseCrudDialog from 'src/modules/timesheets/components/expense-crud-dialog.vue';
|
import ExpenseCrudDialog from 'src/modules/timesheets/components/expense-crud-dialog.vue';
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { provide } from 'vue';
|
import { provide } from 'vue';
|
||||||
|
|
||||||
|
|
@ -19,21 +19,24 @@
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { is_loading } = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
provide('employeeEmail', employeeEmail);
|
provide('employeeEmail', employeeEmail);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="column flex-center full-width">
|
||||||
|
<GenericLoader
|
||||||
|
:is-loading="timesheet_store.is_loading"
|
||||||
|
class="col-auto text-center"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
|
v-if="!timesheet_store.is_loading"
|
||||||
flat
|
flat
|
||||||
class="transparent full-width"
|
class="transparent full-width"
|
||||||
>
|
>
|
||||||
<q-inner-loading
|
|
||||||
:showing="is_loading"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-card-section
|
<q-card-section
|
||||||
:horizontal="$q.screen.gt.sm"
|
:horizontal="$q.screen.gt.sm"
|
||||||
|
|
@ -61,7 +64,10 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- shift's colored legend -->
|
<!-- shift's colored legend -->
|
||||||
<ShiftListLegend v-if="!dense" :is-loading="false" />
|
<ShiftListLegend
|
||||||
|
v-if="!dense"
|
||||||
|
:is-loading="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
|
|
@ -78,12 +84,14 @@
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section :horizontal="$q.screen.gt.sm" class="bg-secondary q-pa-sm rounded-10">
|
<q-card-section
|
||||||
|
:horizontal="$q.screen.gt.sm"
|
||||||
|
class="bg-secondary q-pa-sm rounded-10"
|
||||||
|
>
|
||||||
<ShiftList :dense="dense" />
|
<ShiftList :dense="dense" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<ExpenseCrudDialog />
|
<ExpenseCrudDialog />
|
||||||
|
</div>
|
||||||
<ShiftCrudDialog :employee-email="employeeEmail" />
|
|
||||||
</template>
|
</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: shift.is_approved,
|
|
||||||
is_remote: shift.is_remote,
|
|
||||||
comment: comment,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// const parseHHMM = (s: string): [number, number] => {
|
|
||||||
// const m = TIME_FORMAT_PATTERN.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, got ${shift.start_time} - ${shift.end_time}`,
|
|
||||||
// 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) ? undefined : { old_shift: unwrapAndClone(shift_store.initial_shift) }),
|
|
||||||
...(deepEqual(shift_store.current_shift, default_shift) ? undefined : { 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
|
|
||||||
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
|
|
@ -5,6 +5,7 @@ export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||||
|
|
||||||
export interface Expense {
|
export interface Expense {
|
||||||
|
id: number;
|
||||||
date: string;
|
date: string;
|
||||||
type: ExpenseType;
|
type: ExpenseType;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
@ -14,33 +15,11 @@ export interface Expense {
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExpenseTotals = {
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
reimburseable_total?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PayPeriodExpenses {
|
|
||||||
expenses: Expense[];
|
|
||||||
total_expense: number;
|
|
||||||
total_mileage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpense {
|
|
||||||
old_expense: Expense;
|
|
||||||
new_expense: Expense;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_expense: Expense = {
|
export const default_expense: Expense = {
|
||||||
|
id: -1,
|
||||||
date: '',
|
date: '',
|
||||||
type: 'EXPENSES',
|
type: 'EXPENSES',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
comment: '',
|
comment: '',
|
||||||
is_approved: false,
|
is_approved: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const default_pay_period_expenses: PayPeriodExpenses = {
|
|
||||||
expenses: [],
|
|
||||||
total_expense: -1,
|
|
||||||
total_mileage: -1,
|
|
||||||
}
|
|
||||||
|
|
@ -43,6 +43,7 @@ export class ExpensesApiError extends ApiError {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expense_validation_schema: Normalizer<Expense> = {
|
export const expense_validation_schema: Normalizer<Expense> = {
|
||||||
|
id: v => typeof v === 'number' ? v : -1,
|
||||||
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||||
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||||
amount: v => typeof v === "number" ? v : -1,
|
amount: v => typeof v === "number" ? v : -1,
|
||||||
|
|
@ -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,4 +1,4 @@
|
||||||
export const SHIFT_TYPES = [
|
export const SHIFT_TYPES: ShiftType[] = [
|
||||||
'REGULAR',
|
'REGULAR',
|
||||||
'EVENING',
|
'EVENING',
|
||||||
'EMERGENCY',
|
'EMERGENCY',
|
||||||
|
|
@ -10,8 +10,6 @@ export const SHIFT_TYPES = [
|
||||||
|
|
||||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
||||||
|
|
||||||
export type CrudAction = 'create' | 'update' | 'delete';
|
|
||||||
|
|
||||||
export type ShiftLegendItem = {
|
export type ShiftLegendItem = {
|
||||||
type: ShiftType;
|
type: ShiftType;
|
||||||
color: string;
|
color: string;
|
||||||
|
|
@ -20,6 +18,7 @@ export type ShiftLegendItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Shift {
|
export interface Shift {
|
||||||
|
id: number;
|
||||||
date: string;
|
date: string;
|
||||||
type: ShiftType;
|
type: ShiftType;
|
||||||
start_time: string;
|
start_time: string;
|
||||||
|
|
@ -29,17 +28,8 @@ export interface Shift {
|
||||||
is_remote: boolean;
|
is_remote: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpsertShiftsResponse {
|
|
||||||
action: CrudAction;
|
|
||||||
day: Shift[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertShift {
|
|
||||||
old_shift?: Shift | undefined;
|
|
||||||
new_shift?: Shift | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_shift: Readonly<Shift> = {
|
export const default_shift: Readonly<Shift> = {
|
||||||
|
id: -1,
|
||||||
date: '',
|
date: '',
|
||||||
start_time: '',
|
start_time: '',
|
||||||
end_time: '',
|
end_time: '',
|
||||||
|
|
|
||||||
36
src/modules/timesheets/models/timesheet.models.ts
Normal file
36
src/modules/timesheets/models/timesheet.models.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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 Timesheet {
|
||||||
|
id: number;
|
||||||
|
weekly_hours: TotalHours;
|
||||||
|
weekly_expenses: TotalExpenses;
|
||||||
|
days: TimesheetDay[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDay {
|
||||||
|
date: string;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export type PayPeriodLabel = {
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { CrudAction, UpsertShift } from "src/modules/timesheets/models/shift.models";
|
|
||||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.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 { Timesheet } from "src/modules/timesheets/models/timesheet.models";
|
||||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import type { Expense, PayPeriodExpenses, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
import { Shift } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<Timesheet[]> => {
|
||||||
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
|
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
@ -21,36 +23,33 @@ export const timesheetService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview[]> => {
|
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
|
||||||
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPayPeriodDetailsByPayPeriodAndEmployeeEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodDetails> => {
|
getPayPeriodDetailsByPayPeriodAndEmployeeEmail: async (year: number, period_no: number, email: string): Promise<Timesheet[]> => {
|
||||||
const response = await api.get('timesheets', { params: { year, period_no, email, } });
|
const response = await api.get('timesheets', { params: { year, period_no, email, } });
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
// getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||||
const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
// const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
|
||||||
|
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
|
||||||
|
const response = await api.put(`/shifts/upsert/${email}`, payload);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: UpsertShift, action: CrudAction): Promise<PayPeriodDetails> => {
|
deleteShiftsByEmployeeEmailAndDate: async (email: string, date: string, payload: Shift[]) => {
|
||||||
const response = await api.put(`/shifts/upsert/${email}?action=${action}`, payload);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteShiftsByEmployeeEmailAndDate: async (email: string, date: string, payload: UpsertShift) => {
|
|
||||||
console.log('sent old shift: ', payload);
|
|
||||||
const response = await api.delete(`/shifts/delete/${email}/${date}`, { data: payload });
|
const response = await api.delete(`/shifts/delete/${email}/${date}`, { data: payload });
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: UpsertExpense): Promise<Expense[]> => {
|
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: Expense[]): Promise<Timesheet[]> => {
|
||||||
const headers = { 'Content-Type': 'application/json' }
|
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload);
|
||||||
|
|
||||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload, { headers });
|
|
||||||
return response.data;
|
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 getExpenseIcon = (type: ExpenseType) => {
|
||||||
export const normExpenseType = (type: unknown): string =>
|
switch (type) {
|
||||||
typeof type === 'string' ? type.trim() : '';
|
case 'MILEAGE': return 'time_to_leave';
|
||||||
|
case 'EXPENSES': return 'receipt_long';
|
||||||
const icon_map: Record<string,string> = {
|
case 'PER_DIEM': return 'hotel';
|
||||||
MILEAGE: 'time_to_leave',
|
case 'ON_CALL': return 'phone_android';
|
||||||
EXPENSES: 'receipt_long',
|
default: return 'help_outline';
|
||||||
PER_DIEM: 'hotel',
|
}
|
||||||
ON_CALL: 'phone_android',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getExpenseTypeIcon = (type: unknown): string => {
|
export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
const t = normExpenseType(type);
|
|
||||||
return (
|
|
||||||
icon_map[t] ??
|
|
||||||
icon_map[t.replace('-','_')] ??
|
|
||||||
'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) => {
|
|
||||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
|
|
||||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
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 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 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');
|
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
||||||
|
|
||||||
return {
|
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 };
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
|
@ -31,8 +31,8 @@
|
||||||
:end-date="timesheet_store.pay_period.period_end"
|
:end-date="timesheet_store.pay_period.period_end"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
<div class="col column flex-center" :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
||||||
<TimesheetWrapper :employee-email="user.email" />
|
<TimesheetWrapper class="col" :employee-email="user.email" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,49 @@
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { AuthService } from "../modules/auth/services/services-auth";
|
import { AuthService } from "../modules/auth/services/services-auth";
|
||||||
import { CAN_APPROVE_PAY_PERIODS, UserRole, type User } from "src/modules/shared/models/user.models";
|
import { CAN_APPROVE_PAY_PERIODS, type User } from "src/modules/shared/models/user.models";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
const TestUsers: Record<UserRole, User> = {
|
import { Notify } from "quasar";
|
||||||
[UserRole.ADMIN]: { firstName: 'Alex', lastName: 'Clark', email: 'user1@targointernet.com', role: UserRole.ADMIN },
|
|
||||||
[UserRole.SUPERVISOR]: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: UserRole.SUPERVISOR },
|
|
||||||
[UserRole.HR]: { firstName: 'Avery', lastName: 'Davis', email: 'user5@example.test', role: UserRole.HR },
|
|
||||||
[UserRole.ACCOUNTING]: { firstName: 'Avery', lastName: 'Johnson', email: 'user6@example.test', role: UserRole.ACCOUNTING },
|
|
||||||
[UserRole.EMPLOYEE]: { firstName: 'Alex', lastName: 'Johnson', email: 'user13@example.test', role: UserRole.EMPLOYEE },
|
|
||||||
[UserRole.DEALER]: { firstName: 'Dea', lastName: 'Ler', email: 'dealer@example.test', role: UserRole.DEALER },
|
|
||||||
[UserRole.CUSTOMER]: { firstName: 'Custo', lastName: 'Mer', email: 'customer@example.test', role: UserRole.CUSTOMER },
|
|
||||||
[UserRole.GUEST]: { firstName: 'Guestie', lastName: 'Guesterson', email: 'guest@guest.com', role: UserRole.GUEST },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref<User>(TestUsers.GUEST);
|
const user = ref<User>();
|
||||||
const authError = ref("");
|
const authError = ref("");
|
||||||
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value.role));
|
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value?.role ?? 'GUEST'));
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
//TODO: manage customer login process
|
//TODO: manage customer login process
|
||||||
};
|
};
|
||||||
|
|
||||||
const oidcLogin = () => {
|
const oidcLogin = async (): Promise<void> => {
|
||||||
const oidcPopup = AuthService.oidcLogin();
|
window.addEventListener('message', async (event) => {
|
||||||
if (!oidcPopup) {
|
if (event.data.type === 'authSuccess') {
|
||||||
authError.value = "You have popups blocked on this website!";
|
const new_user = await AuthService.getProfile();
|
||||||
|
user.value = new_user;
|
||||||
|
router.push('/');
|
||||||
|
} else {
|
||||||
|
Notify.create({
|
||||||
|
message: "You have popups blocked on this website!",
|
||||||
|
color: 'negative',
|
||||||
|
textColor: 'white',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oidc_popup = window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800');
|
||||||
|
|
||||||
|
if (!oidc_popup)
|
||||||
|
Notify.create({
|
||||||
|
message: "You have popups blocked on this website!",
|
||||||
|
color: 'negative',
|
||||||
|
textColor: 'white',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
user.value = TestUsers.GUEST;
|
user.value = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUser = (bypassRole: UserRole) => {
|
return { user, authError, isAuthorizedUser, login, oidcLogin, logout };
|
||||||
if (bypassRole in TestUsers) {
|
|
||||||
user.value = TestUsers[bypassRole];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
user.value = TestUsers.GUEST;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
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 { useTimesheetStore } from "src/stores/timesheet-store";
|
|
||||||
import { default_shift, type CrudAction, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
|
||||||
|
|
||||||
export const useShiftStore = defineStore('shift', () => {
|
|
||||||
const is_open = ref(false);
|
|
||||||
const mode = ref<CrudAction>('create');
|
|
||||||
const date_iso = ref<string>('');
|
|
||||||
const current_shift = ref<Shift>(default_shift);
|
|
||||||
const initial_shift = ref<Shift>(default_shift);
|
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
|
|
||||||
const open = (next_mode: CrudAction, 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 close = () => {
|
|
||||||
is_open.value = false;
|
|
||||||
mode.value = 'create';
|
|
||||||
date_iso.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
|
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (mode.value === 'delete') {
|
|
||||||
const result = await timesheetService.deleteShiftsByEmployeeEmailAndDate(encoded_email, upsert_shift.old_shift?.date ?? '', upsert_shift);
|
|
||||||
console.log('result: ', result);
|
|
||||||
if (result.status === 200) {
|
|
||||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
const result = await timesheetService.upsertShiftsByEmployeeEmailAndAction(encoded_email, upsert_shift, mode.value);
|
|
||||||
timesheet_store.pay_period_details = result;
|
|
||||||
}
|
|
||||||
close();
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
is_open,
|
|
||||||
mode,
|
|
||||||
date_iso,
|
|
||||||
current_shift,
|
|
||||||
initial_shift,
|
|
||||||
openCreate,
|
|
||||||
openUpdate,
|
|
||||||
openDelete,
|
|
||||||
close,
|
|
||||||
upsertOrDeleteShiftByEmployeeEmail,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
|
|
@ -3,16 +3,16 @@ import { computed, ref } from 'vue';
|
||||||
import { withLoading } from 'src/utils/store-helpers';
|
import { withLoading } from 'src/utils/store-helpers';
|
||||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
||||||
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
||||||
import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
import { default_pay_period_overview, type TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
import { default_pay_period, 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 { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
||||||
|
|
||||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const is_loading = ref<boolean>(false);
|
const is_loading = ref<boolean>(false);
|
||||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
const pay_period = ref<PayPeriod>(default_pay_period);
|
||||||
const pay_period_overviews = ref<PayPeriodOverview[]>([default_pay_period_overview,]);
|
const pay_period_overviews = ref<TimesheetOverview[]>([default_pay_period_overview,]);
|
||||||
const current_pay_period_overview = ref<PayPeriodOverview>(default_pay_period_overview);
|
const current_pay_period_overview = ref<TimesheetOverview>(default_pay_period_overview);
|
||||||
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
const is_calendar_limit = computed(() =>
|
const is_calendar_limit = computed(() =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user