BREAKING(login): implement full auth process using Authentik, remove files containing deprecated code
This commit is contained in:
parent
702a977fce
commit
c1c0faeaf1
|
|
@ -2,10 +2,10 @@ import { defineBoot } from '#q-app/wrappers';
|
|||
import axios, { type AxiosInstance } from 'axios';
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
$api: AxiosInstance;
|
||||
}
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
$api: AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
|
|
@ -14,18 +14,21 @@ declare module 'vue' {
|
|||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({ baseURL: import.meta.env.VITE_TARGO_BACKEND_AUTH_URL });
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_TARGO_BACKEND_AUTH_URL,
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
});
|
||||
|
||||
export { api };
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export default {
|
|||
tooltip: {
|
||||
coming_soon: "coming soon!",
|
||||
},
|
||||
error: {
|
||||
login_failed: "Failed to login",
|
||||
popups_blocked: "Popups are blocked on this device",
|
||||
},
|
||||
},
|
||||
|
||||
nav_bar: {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export default {
|
|||
tooltip: {
|
||||
coming_soon: "à venir!",
|
||||
},
|
||||
error: {
|
||||
login_failed: "Échec à la connexion",
|
||||
popups_blocked: "Les fenêtres contextuelles sont bloqués sur cet appareil",
|
||||
},
|
||||
},
|
||||
|
||||
nav_bar: {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const currentUser = authStore.user;
|
||||
|
||||
// Will need to implement this eventually, just testing the look for now
|
||||
const notifAmount = ref(7);
|
||||
const notification_count = ref(7);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-item clickable v-ripple dark class="q-pa-none">
|
||||
<q-item-section :side="$q.screen.gt.sm">
|
||||
<q-avatar rounded >
|
||||
<q-img src="src/assets/targo-default-avatar.png" />
|
||||
<q-badge floating color="negative" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
dark
|
||||
class="q-pa-none q-mt-sm"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
|
@ -1,24 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { ref } from 'vue';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const uiStore = useUiStore();
|
||||
const auth_store = useAuthStore();
|
||||
const ui_store = useUiStore();
|
||||
const router = useRouter();
|
||||
const miniState = ref(true);
|
||||
const is_mini = ref(true);
|
||||
|
||||
const goToPageName = (pageName: string) => {
|
||||
router.push({ name: pageName }).catch(err => {
|
||||
const goToPageName = (page_name: string) => {
|
||||
router.push({ name: page_name }).catch(err => {
|
||||
console.error('Error with Vue Router: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout();
|
||||
auth_store.logout();
|
||||
|
||||
router.push({ name: 'login' }).catch(err => {
|
||||
console.log('could not log you out: ', err);
|
||||
|
|
@ -27,22 +30,30 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer
|
||||
v-model="uiStore.isRightDrawerOpen"
|
||||
overlay
|
||||
elevated
|
||||
side="left"
|
||||
:mini="miniState"
|
||||
@mouseenter="miniState = false"
|
||||
@mouseleave="miniState = true"
|
||||
class="bg-dark"
|
||||
<q-drawer
|
||||
v-model="ui_store.isRightDrawerOpen"
|
||||
overlay
|
||||
elevated
|
||||
side="left"
|
||||
:mini="is_mini"
|
||||
@mouseenter="is_mini = false"
|
||||
@mouseleave="is_mini = true"
|
||||
class="bg-dark"
|
||||
>
|
||||
<q-scroll-area class="fit">
|
||||
<q-list>
|
||||
<!-- Home -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.DASHBOARD)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.DASHBOARD)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="home" color="primary" />
|
||||
<q-icon
|
||||
name="home"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
|
||||
|
|
@ -50,42 +61,77 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
|||
</q-item>
|
||||
|
||||
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="event_available" color="primary" />
|
||||
<q-icon
|
||||
name="event_available"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="view_list" color="primary" />
|
||||
<q-icon
|
||||
name="view_list"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||
<q-item
|
||||
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-icon name="punch_clock" color="primary" />
|
||||
<q-icon
|
||||
name="punch_clock"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet') }}</q-item-label>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet')
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<!-- Profile -->
|
||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.PROFILE)">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.PROFILE)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="account_box" color="primary" />
|
||||
<q-icon
|
||||
name="account_box"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
|
||||
|
|
@ -93,9 +139,16 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
|||
</q-item>
|
||||
|
||||
<!-- Help -->
|
||||
<q-item v-ripple clickable @click="goToPageName('help')">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goToPageName('help')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="contact_support" color="primary" />
|
||||
<q-icon
|
||||
name="contact_support"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
|
||||
|
|
@ -104,9 +157,17 @@ import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
|||
</q-list>
|
||||
|
||||
<!-- Logout -->
|
||||
<q-item v-ripple clickable @click="handleLogout" class="absolute-bottom">
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="handleLogout"
|
||||
class="absolute-bottom"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="exit_to_app" color="primary" />
|
||||
<q-icon
|
||||
name="exit_to_app"
|
||||
color="primary"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
|
||||
|
|
|
|||
|
|
@ -1,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 type { UserRole } from "src/modules/shared/models/user.models";
|
||||
|
||||
export const useAuthApi = () => {
|
||||
const authStore = useAuthStore();
|
||||
|
|
@ -16,19 +15,9 @@ export const useAuthApi = () => {
|
|||
authStore.logout();
|
||||
};
|
||||
|
||||
const isAuthorizedUser = () => {
|
||||
return authStore.isAuthorizedUser;
|
||||
};
|
||||
|
||||
const setUser = (bypassRole: UserRole) => {
|
||||
authStore.setUser(bypassRole);
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
oidcLogin,
|
||||
logout,
|
||||
isAuthorizedUser,
|
||||
setUser,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import LoginConnectionPanel from 'src/modules/auth/components/login-connection-panel.vue';
|
||||
import LoginDevBypass from 'src/modules/auth/components/login-dev-bypass.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -11,9 +10,6 @@
|
|||
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
|
||||
<LoginConnectionPanel />
|
||||
</transition>
|
||||
|
||||
<!-- DEV TOOLS -->
|
||||
<LoginDevBypass />
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,6 @@ export const AuthService = {
|
|||
//TODO: OIDC customer sign-in, eventually
|
||||
},
|
||||
|
||||
oidcLogin: (): Window | null => {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'authSuccess') {
|
||||
//some kind of logic here to set user in store
|
||||
}
|
||||
})
|
||||
|
||||
return window.open('http://localhost:3000/auth/v1/login', 'authPopup', 'width=600,height=800');
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
// TODO: logout logic
|
||||
api.post('/auth/logout')
|
||||
|
|
@ -27,8 +17,8 @@ export const AuthService = {
|
|||
api.post('/auth/refresh')
|
||||
},
|
||||
|
||||
getProfile: () => {
|
||||
// TODO: user info fetch logic
|
||||
api.get('/auth/me')
|
||||
getProfile: async () => {
|
||||
const response = await 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 */
|
||||
|
||||
export interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
role: UserRole;
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = 'ADMIN',
|
||||
SUPERVISOR = 'SUPERVISOR',
|
||||
HR = 'HR',
|
||||
ACCOUNTING = 'ACCOUNTING',
|
||||
EMPLOYEE = 'EMPLOYEE',
|
||||
DEALER = 'DEALER',
|
||||
CUSTOMER = 'CUSTOMER',
|
||||
GUEST = 'GUEST',
|
||||
}
|
||||
export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
||||
|
||||
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
||||
UserRole.ADMIN,
|
||||
UserRole.SUPERVISOR,
|
||||
UserRole.HR,
|
||||
UserRole.ACCOUNTING,
|
||||
'ADMIN',
|
||||
'SUPERVISOR',
|
||||
'HR',
|
||||
'ACCOUNTING',
|
||||
]
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { Bar } from 'vue-chartjs';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
|
|
|||
|
|
@ -30,31 +30,7 @@
|
|||
transition-hide="jump-down"
|
||||
@show="render_key += 1"
|
||||
>
|
||||
<!-- loader -->
|
||||
<transition
|
||||
enter-active-class="animated faster zoomIn"
|
||||
leave-active-class="animated faster zoomOut"
|
||||
mode="out-in"
|
||||
>
|
||||
<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"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
setup
|
||||
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 { row } = defineProps<{ row: PayPeriodOverview; }>();
|
||||
const { row } = defineProps<{ row: TimesheetOverview; }>();
|
||||
const emit = defineEmits<{
|
||||
'clickDetails': [overview: PayPeriodOverview];
|
||||
'clickDetails': [overview: TimesheetOverview];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||
import { pay_period_overview_columns, type PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||
import { pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||
|
||||
const expenses_store = useExpensesStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
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;
|
||||
timesheet_store.current_pay_period_overview = row;
|
||||
emit('clickedDetailsButton', employee_email);
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
</template>
|
||||
|
||||
<!-- Template for individual employee cards -->
|
||||
<template #item="props: { row: PayPeriodOverview, key: string }">
|
||||
<template #item="props: { row: TimesheetOverview, key: string }">
|
||||
<OverviewListItem
|
||||
v-model="props.row.is_approved"
|
||||
:row="props.row"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { useAuthStore } from "src/stores/auth-store";
|
||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||
import { NavigatorConstants } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import { NavigatorConstants } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
export const useTimesheetApprovalApi = () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export enum NavigatorConstants {
|
|||
PREVIOUS_PERIOD = -1,
|
||||
}
|
||||
|
||||
export interface PayPeriodOverview {
|
||||
export interface TimesheetOverview {
|
||||
email: string;
|
||||
employee_name: string;
|
||||
regular_hours: number;
|
||||
|
|
@ -31,10 +31,10 @@ export interface PayPeriodOverviewResponse {
|
|||
period_end: string;
|
||||
payday: string;
|
||||
label: string;
|
||||
employees_overview: PayPeriodOverview[];
|
||||
employees_overview: TimesheetOverview[];
|
||||
}
|
||||
|
||||
export const default_pay_period_overview: PayPeriodOverview = {
|
||||
export const default_pay_period_overview: TimesheetOverview = {
|
||||
email: '',
|
||||
employee_name: '',
|
||||
regular_hours: -1,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
export const timesheetApprovalService = {
|
||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||
import { makeExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
const employee_email = inject<string>('employeeEmail');
|
||||
const rules = makeExpenseRules(t);
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
const cancelUpdateMode = () => {
|
||||
expenses_store.current_expense = default_expense;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
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 { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
<q-item
|
||||
:key="refresh_key"
|
||||
:clickable="horizontal"
|
||||
class="row q-mx-xs shadow-2"
|
||||
class="row col-4 q-ma-xs shadow-2"
|
||||
:style="expenseItemStyle + highlightClass + approvedClass"
|
||||
@click="onExpenseClicked"
|
||||
>
|
||||
|
|
@ -126,6 +126,8 @@
|
|||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-space v-if="horizontal" />
|
||||
|
||||
<!-- attachment file icon -->
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<q-list
|
||||
padding
|
||||
class="rounded-borders"
|
||||
:class="horizontal ? 'row justify-center' : ''"
|
||||
:class="horizontal ? 'row flex-center' : ''"
|
||||
>
|
||||
<q-item-label
|
||||
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { useShiftStore } from 'src/stores/shift-store';
|
||||
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const shift_store = useShiftStore();
|
||||
defineProps<{
|
||||
shift: Shift;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
'onCommentBlur': [void];
|
||||
|
|
@ -13,10 +14,21 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="col-xs-6 col-sm-4 col-md-3 row q-mx-xs q-my-none">
|
||||
<div class="row full-width justify-center">
|
||||
<div class="col-sm-6 col-md-3 row q-mx-xs q-my-none">
|
||||
<div class="col-auto column items-center">
|
||||
<span
|
||||
class="text-caption q-pa-none q-ma-none"
|
||||
style="line-height: 0.7em; font-size: 0.7em;"
|
||||
>{{ $t('timesheet.shift.types.REMOTE') }}</span>
|
||||
<q-toggle
|
||||
v-model="shift.is_remote"
|
||||
class="q-pa-none q-ma-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
v-model="shift_store.current_shift.type"
|
||||
v-model="shift.type"
|
||||
options-dense
|
||||
:options="SHIFT_TYPES"
|
||||
:label="$t('timesheet.shift.types.label')"
|
||||
|
|
@ -29,22 +41,11 @@
|
|||
emit-value
|
||||
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 class="col-auto row q-mx-xs">
|
||||
<q-input
|
||||
v-model="shift_store.current_shift.start_time"
|
||||
v-model="shift.start_time"
|
||||
:label="$t('timesheet.shift.fields.start')"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -55,7 +56,7 @@
|
|||
/>
|
||||
|
||||
<q-input
|
||||
v-model="shift_store.current_shift.end_time"
|
||||
v-model="shift.end_time"
|
||||
:label="$t('timesheet.shift.fields.end')"
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
</div>
|
||||
|
||||
<q-input
|
||||
v-model="shift_store.current_shift.comment"
|
||||
v-model="shift.comment"
|
||||
type="textarea"
|
||||
autogrow
|
||||
filled
|
||||
|
|
@ -76,7 +77,7 @@
|
|||
:label="$t('timesheet.shift.fields.header_comment')"
|
||||
:counter="true"
|
||||
:maxlength="512"
|
||||
class="col-auto"
|
||||
class="col-grow"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
|
||||
const q = useQuasar();
|
||||
|
||||
const { shift, dense = false } = defineProps<{
|
||||
|
|
@ -19,15 +19,9 @@
|
|||
'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 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')
|
||||
|
||||
const get_shift_color = (type: string): string => {
|
||||
switch (type) {
|
||||
|
|
@ -58,16 +52,7 @@
|
|||
@mouseenter="is_hovering = true"
|
||||
@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">
|
||||
<!-- punch-in timestamp -->
|
||||
|
|
@ -130,10 +115,11 @@
|
|||
<!-- comment btn -->
|
||||
<q-icon
|
||||
v-if="shift.type"
|
||||
:name="comment_icon"
|
||||
:color="comment_color"
|
||||
class="q-pa-none q-mr-xs"
|
||||
name="comment"
|
||||
color="primary"
|
||||
:size="dense ? 'xs' : 'sm'"
|
||||
class="q-pa-none q-mr-xs"
|
||||
:class="shift.comment ? '' : 'invisible'"
|
||||
/>
|
||||
|
||||
<!-- delete btn -->
|
||||
|
|
|
|||
|
|
@ -2,16 +2,14 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable */
|
||||
import { date } from 'quasar';
|
||||
import ShiftListHeader from 'src/modules/timesheets/components/shift-list-header.vue';
|
||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||
import { useShiftStore } from 'src/stores/shift-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { type Shift, default_shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
||||
const { dense = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
}>();
|
||||
|
|
@ -72,33 +70,22 @@ import { computed } from 'vue';
|
|||
</q-card-section>
|
||||
|
||||
<!-- List of shifts column -->
|
||||
<q-card-section class="col q-pa-none">
|
||||
<ShiftListHeader v-if="day.shifts.length > 0 && !dense"/>
|
||||
<q-card-section class="col column q-pa-none full-height">
|
||||
<div
|
||||
v-if="day.shifts.length > 0"
|
||||
class="q-gutter-xs"
|
||||
class="col-grow column justify-center"
|
||||
>
|
||||
<ShiftListRow
|
||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||
:key="shift_index"
|
||||
class="col"
|
||||
:dense="dense"
|
||||
:shift="shift"
|
||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
||||
@request-update=""
|
||||
@request-delete=""
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,13 +2,13 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import GenericLoader from 'src/modules/shared/components/generic-loader.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 PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||
import ShiftListLegend from 'src/modules/timesheets/components/shift-list-legend.vue';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useExpensesStore } from 'src/stores/expense-store';
|
||||
import { provide } from 'vue';
|
||||
|
||||
|
|
@ -19,71 +19,79 @@
|
|||
dense?: boolean;
|
||||
}>();
|
||||
|
||||
const { is_loading } = useTimesheetStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
provide('employeeEmail', employeeEmail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
flat
|
||||
class="transparent full-width"
|
||||
>
|
||||
<q-inner-loading
|
||||
:showing="is_loading"
|
||||
color="primary"
|
||||
<div class="column flex-center full-width">
|
||||
<GenericLoader
|
||||
:is-loading="timesheet_store.is_loading"
|
||||
class="col-auto text-center"
|
||||
/>
|
||||
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-px-md items-center"
|
||||
:class="$q.screen.lt.md ? 'column' : ''"
|
||||
<q-card
|
||||
v-if="!timesheet_store.is_loading"
|
||||
flat
|
||||
class="transparent full-width"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
v-if="!dense"
|
||||
@date-selected="timesheet_api.getPayPeriodDetailsByDate(employeeEmail)"
|
||||
@pressed-previous-button="timesheet_api.getPreviousPayPeriodDetails(employeeEmail)"
|
||||
@pressed-next-button="timesheet_api.getNextPayPeriodDetails(employeeEmail)"
|
||||
/>
|
||||
|
||||
<!-- mobile expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.lt.md && !dense"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
class="q-mt-sm"
|
||||
@click="open(employeeEmail)"
|
||||
/>
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="q-px-md items-center"
|
||||
:class="$q.screen.lt.md ? 'column' : ''"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
v-if="!dense"
|
||||
@date-selected="timesheet_api.getPayPeriodDetailsByDate(employeeEmail)"
|
||||
@pressed-previous-button="timesheet_api.getPreviousPayPeriodDetails(employeeEmail)"
|
||||
@pressed-next-button="timesheet_api.getNextPayPeriodDetails(employeeEmail)"
|
||||
/>
|
||||
|
||||
<!-- shift's colored legend -->
|
||||
<ShiftListLegend v-if="!dense" :is-loading="false" />
|
||||
<!-- mobile expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.lt.md && !dense"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
class="q-mt-sm"
|
||||
@click="open(employeeEmail)"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
<!-- shift's colored legend -->
|
||||
<ShiftListLegend
|
||||
v-if="!dense"
|
||||
:is-loading="false"
|
||||
/>
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.gt.sm && !dense"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="open(employeeEmail)"
|
||||
/>
|
||||
<q-space />
|
||||
|
||||
</q-card-section>
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
v-if="$q.screen.gt.sm && !dense"
|
||||
push
|
||||
rounded
|
||||
color="primary"
|
||||
icon="receipt_long"
|
||||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="open(employeeEmail)"
|
||||
/>
|
||||
|
||||
<q-card-section :horizontal="$q.screen.gt.sm" class="bg-secondary q-pa-sm rounded-10">
|
||||
<ShiftList :dense="dense"/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
<ExpenseCrudDialog />
|
||||
<q-card-section
|
||||
:horizontal="$q.screen.gt.sm"
|
||||
class="bg-secondary q-pa-sm rounded-10"
|
||||
>
|
||||
<ShiftList :dense="dense" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<ShiftCrudDialog :employee-email="employeeEmail" />
|
||||
<ExpenseCrudDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { normalizeObject } from "src/utils/normalize-object";
|
||||
import { useExpensesStore } from "src/stores/expense-store";
|
||||
import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation";
|
||||
import type { Expense, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
export const useExpensesApi = () => {
|
||||
const expenses_store = useExpensesStore();
|
||||
|
||||
const toUpsertExpense = (obj: {
|
||||
old_expense?: Expense;
|
||||
new_expense?: Expense;
|
||||
}) => obj as UpsertExpense;
|
||||
|
||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||
const upsert_expense = toUpsertExpense({
|
||||
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||
});
|
||||
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||
};
|
||||
|
||||
return {
|
||||
createExpenseByEmployeeEmail,
|
||||
updateExpenseByEmployeeEmail,
|
||||
deleteExpenseByEmployeeEmail,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||
// import { TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
||||
// import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||
import { useShiftStore } from "src/stores/shift-store";
|
||||
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||
import { deepEqual } from "src/utils/deep-equal";
|
||||
|
||||
export const useShiftApi = () => {
|
||||
const shift_store = useShiftStore();
|
||||
|
||||
const normalizeShiftPayload = (shift: Shift): Shift => {
|
||||
const comment = shift.comment?.trim() || undefined;
|
||||
|
||||
return {
|
||||
date: shift.date,
|
||||
start_time: shift.start_time,
|
||||
end_time: shift.end_time,
|
||||
type: shift.type,
|
||||
is_approved: 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,42 +5,21 @@ export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
|||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||
|
||||
export interface Expense {
|
||||
date: string;
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
mileage?: number;
|
||||
comment: string;
|
||||
id: number;
|
||||
date: string;
|
||||
type: ExpenseType;
|
||||
amount: number;
|
||||
mileage?: number;
|
||||
comment: string;
|
||||
supervisor_comment?: string;
|
||||
is_approved: boolean;
|
||||
}
|
||||
|
||||
export type ExpenseTotals = {
|
||||
amount: number;
|
||||
mileage: number;
|
||||
reimburseable_total?: number;
|
||||
};
|
||||
|
||||
export interface PayPeriodExpenses {
|
||||
expenses: Expense[];
|
||||
total_expense: number;
|
||||
total_mileage: number;
|
||||
}
|
||||
|
||||
export interface UpsertExpense {
|
||||
old_expense: Expense;
|
||||
new_expense: Expense;
|
||||
}
|
||||
|
||||
export const default_expense: Expense = {
|
||||
id: -1,
|
||||
date: '',
|
||||
type: 'EXPENSES',
|
||||
amount: 0,
|
||||
comment: '',
|
||||
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> = {
|
||||
id: v => typeof v === 'number' ? v : -1,
|
||||
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||
amount: v => typeof v === "number" ? v : -1,
|
||||
|
|
@ -1,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',
|
||||
'EVENING',
|
||||
'EMERGENCY',
|
||||
|
|
@ -10,8 +10,6 @@ export const SHIFT_TYPES = [
|
|||
|
||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
||||
|
||||
export type CrudAction = 'create' | 'update' | 'delete';
|
||||
|
||||
export type ShiftLegendItem = {
|
||||
type: ShiftType;
|
||||
color: string;
|
||||
|
|
@ -20,6 +18,7 @@ export type ShiftLegendItem = {
|
|||
};
|
||||
|
||||
export interface Shift {
|
||||
id: number;
|
||||
date: string;
|
||||
type: ShiftType;
|
||||
start_time: string;
|
||||
|
|
@ -29,17 +28,8 @@ export interface Shift {
|
|||
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> = {
|
||||
id: -1,
|
||||
date: '',
|
||||
start_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 type { CrudAction, UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||
import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
|
||||
import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import type { Expense, PayPeriodExpenses, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||
import type { Timesheet } from "src/modules/timesheets/models/timesheet.models";
|
||||
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const timesheetService = {
|
||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<Timesheet[]> => {
|
||||
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
|
||||
return response.data;
|
||||
},
|
||||
|
|
@ -21,36 +23,33 @@ export const timesheetService = {
|
|||
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}`);
|
||||
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, } });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||
const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
||||
// getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||
// const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: Shift[]): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/shifts/upsert/${email}`, payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
upsertShiftsByEmployeeEmailAndAction: async (email: string, payload: UpsertShift, action: CrudAction): Promise<PayPeriodDetails> => {
|
||||
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);
|
||||
deleteShiftsByEmployeeEmailAndDate: async (email: string, date: string, payload: Shift[]) => {
|
||||
const response = await api.delete(`/shifts/delete/${email}/${date}`, { data: payload });
|
||||
return response;
|
||||
},
|
||||
|
||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: UpsertExpense): Promise<Expense[]> => {
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
|
||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload, { headers });
|
||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: Expense[]): Promise<Timesheet[]> => {
|
||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,46 +1,21 @@
|
|||
import type { Expense, ExpenseTotals } from "src/modules/timesheets/models/expense.models";
|
||||
import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||
|
||||
//------------------ normalization / icons ------------------
|
||||
export const normExpenseType = (type: unknown): string =>
|
||||
typeof type === 'string' ? type.trim() : '';
|
||||
|
||||
const icon_map: Record<string,string> = {
|
||||
MILEAGE: 'time_to_leave',
|
||||
EXPENSES: 'receipt_long',
|
||||
PER_DIEM: 'hotel',
|
||||
ON_CALL: 'phone_android',
|
||||
export const getExpenseIcon = (type: ExpenseType) => {
|
||||
switch (type) {
|
||||
case 'MILEAGE': return 'time_to_leave';
|
||||
case 'EXPENSES': return 'receipt_long';
|
||||
case 'PER_DIEM': return 'hotel';
|
||||
case 'ON_CALL': return 'phone_android';
|
||||
default: return 'help_outline';
|
||||
}
|
||||
};
|
||||
|
||||
export const getExpenseTypeIcon = (type: unknown): 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) => {
|
||||
export const useExpenseRules = (t: (_key: string) => string) => {
|
||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||
|
||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||
|
||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||
|
||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||
|
||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false ) || t('timesheet.expense.errors.comment_required');
|
||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
||||
|
||||
return {
|
||||
typeRequired,
|
||||
|
|
|
|||
|
|
@ -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 { onMounted } from 'vue';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/api/use-timesheet-api';
|
||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
|
|
@ -31,8 +31,8 @@
|
|||
:end-date="timesheet_store.pay_period.period_end"
|
||||
/>
|
||||
|
||||
<div :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
||||
<TimesheetWrapper :employee-email="user.email" />
|
||||
<div class="col column flex-center" :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
||||
<TimesheetWrapper class="col" :employee-email="user.email" />
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,49 @@
|
|||
import { computed, ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { AuthService } from "../modules/auth/services/services-auth";
|
||||
import { CAN_APPROVE_PAY_PERIODS, UserRole, type User } from "src/modules/shared/models/user.models";
|
||||
|
||||
const TestUsers: Record<UserRole, User> = {
|
||||
[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 },
|
||||
}
|
||||
import { CAN_APPROVE_PAY_PERIODS, type User } from "src/modules/shared/models/user.models";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User>(TestUsers.GUEST);
|
||||
const user = ref<User>();
|
||||
const authError = ref("");
|
||||
const isAuthorizedUser = computed(() => 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 = () => {
|
||||
//TODO: manage customer login process
|
||||
};
|
||||
|
||||
const oidcLogin = () => {
|
||||
const oidcPopup = AuthService.oidcLogin();
|
||||
if (!oidcPopup) {
|
||||
authError.value = "You have popups blocked on this website!";
|
||||
}
|
||||
const oidcLogin = async (): Promise<void> => {
|
||||
window.addEventListener('message', async (event) => {
|
||||
if (event.data.type === 'authSuccess') {
|
||||
const new_user = await AuthService.getProfile();
|
||||
user.value = new_user;
|
||||
router.push('/');
|
||||
} else {
|
||||
Notify.create({
|
||||
message: "You have popups blocked on this website!",
|
||||
color: 'negative',
|
||||
textColor: 'white',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const 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 = () => {
|
||||
user.value = TestUsers.GUEST;
|
||||
user.value = undefined;
|
||||
};
|
||||
|
||||
const setUser = (bypassRole: UserRole) => {
|
||||
if (bypassRole in TestUsers) {
|
||||
user.value = TestUsers[bypassRole];
|
||||
}
|
||||
else {
|
||||
user.value = TestUsers.GUEST;
|
||||
}
|
||||
};
|
||||
|
||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
|
||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout };
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
|
||||
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
|
||||
import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||
import { default_pay_period_overview, type TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||
import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/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';
|
||||
|
||||
export const useTimesheetStore = defineStore('timesheet', () => {
|
||||
const is_loading = ref<boolean>(false);
|
||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
||||
const pay_period_overviews = ref<PayPeriodOverview[]>([default_pay_period_overview,]);
|
||||
const current_pay_period_overview = ref<PayPeriodOverview>(default_pay_period_overview);
|
||||
const pay_period_overviews = ref<TimesheetOverview[]>([default_pay_period_overview,]);
|
||||
const current_pay_period_overview = ref<TimesheetOverview>(default_pay_period_overview);
|
||||
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
||||
const pay_period_report = ref();
|
||||
const is_calendar_limit = computed(() =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user