Merge pull request 'dev/nicolas/timesheet-gui-refactor' (#22) from dev/nicolas/timesheet-gui-refactor into main
Reviewed-on: Targo/targo_frontend#22
This commit is contained in:
commit
52984c88e9
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# targo-frontend
|
||||||
|
FROM node:22
|
||||||
|
|
||||||
|
# Set working directory inside container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
|
||||||
|
|
||||||
|
# Copy package.json & package-lock.json first (for caching)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose Quasar dev port
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["quasar", "dev"]
|
||||||
|
|
@ -2,10 +2,10 @@ import { defineBoot } from '#q-app/wrappers';
|
||||||
import axios, { type AxiosInstance } from 'axios';
|
import axios, { type AxiosInstance } from 'axios';
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
$axios: AxiosInstance;
|
$axios: AxiosInstance;
|
||||||
$api: AxiosInstance;
|
$api: AxiosInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Be careful when using SSR for cross-request state pollution
|
// 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
|
// 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_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
|
||||||
|
|
||||||
app.config.globalProperties.$axios = axios;
|
app.config.globalProperties.$axios = axios;
|
||||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
// ^ ^ ^ 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
|
// so you won't necessarily have to import axios in each vue file
|
||||||
|
|
||||||
app.config.globalProperties.$api = api;
|
app.config.globalProperties.$api = api;
|
||||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||||
// so you can easily perform requests against your app's API
|
// so you can easily perform requests against your app's API
|
||||||
});
|
});
|
||||||
|
|
||||||
export { api };
|
export { api };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// app global css in SCSS form
|
// app global css in SCSS form
|
||||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) {
|
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||||
.rounded-#{$size} {
|
.rounded-#{$size} {
|
||||||
border-radius: #{$size}px !important;
|
border-radius: #{$size}px !important;
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body.body--dark {
|
body.body--dark {
|
||||||
--q-secondary: #0f1114;
|
--q-secondary: #2b2f34;
|
||||||
color: $grey-2;
|
color: $grey-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,3 +33,12 @@ body.body--dark {
|
||||||
--q-dark: #FFF;
|
--q-dark: #FFF;
|
||||||
color: $blue-grey-8;
|
color: $blue-grey-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shift-highlight {
|
||||||
|
background: #0195462a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frosted-glass {
|
||||||
|
background-color: #FFFA !important;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
@ -16,16 +16,22 @@ $primary : #019547;
|
||||||
$secondary : #DAE0E7;
|
$secondary : #DAE0E7;
|
||||||
$accent : #AAD5C4;
|
$accent : #AAD5C4;
|
||||||
|
|
||||||
$dark-shadow-color : #019547;
|
$dark-shadow-color : #00220f;
|
||||||
|
|
||||||
$elevation-dark-umbra : rgba($dark-shadow-color, 0.4);
|
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0);
|
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.2);
|
||||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0);
|
$elevation-dark-ambient : rgba($dark-shadow-color, 0.2);
|
||||||
|
|
||||||
$dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
|
$dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
|
||||||
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
||||||
|
|
||||||
$dark : #333;
|
$input-text-color : #455A64;
|
||||||
|
$input-autofill-color : #AAD5C4;
|
||||||
|
$field-dense-label-top : 5px !default;
|
||||||
|
$field-dense-label-font-size : 16px !default;
|
||||||
|
|
||||||
|
|
||||||
|
$dark : #42444b;
|
||||||
$dark-page : #343434;
|
$dark-page : #343434;
|
||||||
|
|
||||||
$positive : #21ba45;
|
$positive : #21ba45;
|
||||||
|
|
|
||||||
|
|
@ -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,29 @@
|
||||||
<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-btn
|
||||||
<q-item-section :side="$q.screen.gt.sm">
|
flat
|
||||||
<q-avatar rounded >
|
transparent
|
||||||
<q-img src="src/assets/targo-default-avatar.png" />
|
dense
|
||||||
<q-badge floating color="negative" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
|
:icon="notification_count > 0 ? 'notifications_active' : 'notifications_off'"
|
||||||
</q-avatar>
|
size="lg"
|
||||||
</q-item-section>
|
color="white"
|
||||||
|
>
|
||||||
<q-item-section v-if="$q.screen.gt.sm">
|
<q-badge
|
||||||
<q-item-label>{{ currentUser.firstName }} {{ currentUser.lastName }}</q-item-label>
|
v-if="notification_count > 0"
|
||||||
<q-item-label caption>{{ notifAmount }} new messages</q-item-label>
|
floating
|
||||||
</q-item-section>
|
color="negative"
|
||||||
</q-item>
|
class="text-weight-bolder q-mt-xs"
|
||||||
|
>
|
||||||
|
{{ notification_count }}
|
||||||
|
</q-badge>
|
||||||
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,23 +1,27 @@
|
||||||
<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';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
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';
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -27,21 +31,30 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="uiStore.isRightDrawerOpen"
|
v-model="ui_store.is_left_drawer_open"
|
||||||
overlay
|
persistent
|
||||||
elevated
|
mini-to-overlay
|
||||||
side="left"
|
elevated
|
||||||
:mini="miniState"
|
side="left"
|
||||||
@mouseenter="miniState = false"
|
:mini="is_mini"
|
||||||
@mouseleave="miniState = true"
|
@mouseenter="is_mini = false"
|
||||||
class="bg-dark"
|
@mouseleave="is_mini = true"
|
||||||
|
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>
|
||||||
|
|
@ -49,42 +62,77 @@
|
||||||
</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="['supervisor', 'accounting'].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="['supervisor', 'accounting', 'human_resources'].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="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)">
|
v-ripple
|
||||||
|
clickable
|
||||||
|
side
|
||||||
|
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||||
|
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||||
|
>
|
||||||
<q-item-section avatar>
|
<q-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>
|
||||||
|
|
@ -92,9 +140,16 @@
|
||||||
</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>
|
||||||
|
|
@ -103,9 +158,17 @@
|
||||||
</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,4 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
|
||||||
|
|
||||||
|
|
@ -6,16 +9,20 @@
|
||||||
|
|
||||||
const email = defineModel<string>('email', { default: '', });
|
const email = defineModel<string>('email', { default: '', });
|
||||||
const is_remembered = ref<boolean>(false);
|
const is_remembered = ref<boolean>(false);
|
||||||
const is_employee_email = computed( () => email.value.includes('@targ'));
|
const is_employee_email = computed(() => email.value.includes('@targ'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card class="rounded-15">
|
<q-card class="rounded-15 shadow-10">
|
||||||
<q-card-section class="text-center bg-primary q-pa-lg">
|
<q-card-section class="text-center bg-primary q-pa-lg">
|
||||||
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" />
|
<q-img
|
||||||
|
src="/src/assets/logo-targo-white.svg"
|
||||||
|
ratio="4.6"
|
||||||
|
fit="contain"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<div class="q-pt-sm q-px-xl q-pb-lg">
|
<div class="q-pt-sm q-px-xl q-pb-lg ">
|
||||||
<q-card-section class="text-center text-uppercase">
|
<q-card-section class="text-center text-uppercase">
|
||||||
<div class="text-h6 text-weight-bold">
|
<div class="text-h6 text-weight-bold">
|
||||||
{{ $t('login.page_header') }}
|
{{ $t('login.page_header') }}
|
||||||
|
|
@ -28,8 +35,14 @@
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
label-color="primary"
|
label-color="primary"
|
||||||
:label="$t('login.email')"
|
class="rounded-5 inset-shadow bg-blue-grey-1"
|
||||||
/>
|
label-slot
|
||||||
|
input-class="text-weight-medium text-h6"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
|
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
|
||||||
<q-toggle
|
<q-toggle
|
||||||
|
|
@ -58,9 +71,16 @@
|
||||||
</q-form>
|
</q-form>
|
||||||
|
|
||||||
<q-card-section class="row q-pt-sm">
|
<q-card-section class="row q-pt-sm">
|
||||||
<q-separator color="primary" class="col self-center"/>
|
<q-separator
|
||||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{ $t('shared.misc.or') }}</span>
|
color="primary"
|
||||||
<q-separator color="primary" class="col self-center"/>
|
class="col self-center"
|
||||||
|
/>
|
||||||
|
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
|
||||||
|
$t('shared.misc.or') }}</span>
|
||||||
|
<q-separator
|
||||||
|
color="primary"
|
||||||
|
class="col self-center"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section class="column q-px-sm q-pt-none">
|
<q-card-section class="column q-px-sm q-pt-none">
|
||||||
|
|
@ -73,7 +93,10 @@
|
||||||
:label="$t('login.button.facebook')"
|
:label="$t('login.button.facebook')"
|
||||||
class="full-width row q-mb-sm"
|
class="full-width row q-mb-sm"
|
||||||
>
|
>
|
||||||
<q-tooltip anchor="top middle" class="bg-primary">{{$t('login.tooltip.coming_soon')}}</q-tooltip>
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
class="bg-primary"
|
||||||
|
>{{ $t('login.tooltip.coming_soon') }}</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
<div v-if="is_employee_email">
|
<div v-if="is_employee_email">
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useAuthApi } from '../composables/use-auth-api';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
const auth_api = useAuthApi();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const setBypassUser = (bypassRole: string) => {
|
|
||||||
auth_api.setUser(bypassRole);
|
|
||||||
|
|
||||||
router.push({ name: 'dashboard' }).catch( err => {
|
|
||||||
console.error('Router navigation failed: ', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card class="absolute-bottom-right q-ma-sm">
|
|
||||||
<q-card-section class="q-pa-sm text-uppercase text-center"> impersonate </q-card-section>
|
|
||||||
<q-card-actions vertical>
|
|
||||||
<q-btn
|
|
||||||
v-for="role, index in [ 'supervisor', 'accounting', 'human_resources', 'employee' ]"
|
|
||||||
:key="index"
|
|
||||||
push
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="role"
|
|
||||||
class="text-uppercase"
|
|
||||||
@click="setBypassUser(role)"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
@ -3,8 +3,6 @@ import { useAuthStore } from "../../../stores/auth-store";
|
||||||
export const useAuthApi = () => {
|
export const useAuthApi = () => {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
authStore.login();
|
authStore.login();
|
||||||
};
|
};
|
||||||
|
|
@ -17,19 +15,9 @@ export const useAuthApi = () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAuthorizedUser = () => {
|
|
||||||
return authStore.isAuthorizedUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUser = (bypassRole: string) => {
|
|
||||||
authStore.setUser(bypassRole);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login,
|
login,
|
||||||
oidcLogin,
|
oidcLogin,
|
||||||
logout,
|
logout,
|
||||||
isAuthorizedUser,
|
|
||||||
setUser,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from 'src/boot/axios';
|
||||||
|
import type { User } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
export const AuthService = {
|
export const AuthService = {
|
||||||
// Will likely be deprecated and relegated to Authentik
|
// Will likely be deprecated and relegated to Authentik
|
||||||
|
|
@ -7,16 +8,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 +18,8 @@ export const AuthService = {
|
||||||
api.post('/auth/refresh')
|
api.post('/auth/refresh')
|
||||||
},
|
},
|
||||||
|
|
||||||
getProfile: () => {
|
getProfile: async (): Promise<User> => {
|
||||||
// TODO: user info fetch logic
|
const response = await api.get('/auth/me');
|
||||||
api.get('/auth/me')
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
||||||
// // add logic here to see if user has an avatar image and return that instead of initials
|
// // add logic here to see if user has an avatar image and return that instead of initials
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const { row } = defineProps<{
|
const { row } = defineProps<{
|
||||||
row: EmployeeListTableItem
|
row: EmployeeProfile
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
onProfileClick: [email: string]
|
onProfileClick: [email: string]
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<q-avatar
|
<q-avatar
|
||||||
color="primary"
|
color="primary"
|
||||||
size="8em"
|
size="8em"
|
||||||
class="shadow-3"
|
class="shadow-3 q-mb-md"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="src/assets/targo-default-avatar.png"
|
src="src/assets/targo-default-avatar.png"
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
|
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
|
||||||
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
|
|
||||||
import type { QTableColumn } from 'quasar';
|
import type { QTableColumn } from 'quasar';
|
||||||
|
|
||||||
const employee_list_api = useEmployeeListApi();
|
const employee_list_api = useEmployeeListApi();
|
||||||
|
|
@ -17,7 +16,7 @@
|
||||||
const is_grid_mode = ref(true);
|
const is_grid_mode = ref(true);
|
||||||
const pagination = ref({ rowsPerPage: 0 });
|
const pagination = ref({ rowsPerPage: 0 });
|
||||||
|
|
||||||
const employee_list_columns = computed((): QTableColumn<EmployeeListTableItem>[] => [
|
const employee_list_columns = computed((): QTableColumn<EmployeeProfile>[] => [
|
||||||
{name: 'first_name', label: t('employee_list.table.first_name'), field: 'first_name', align: 'left'},
|
{name: 'first_name', label: t('employee_list.table.first_name'), field: 'first_name', align: 'left'},
|
||||||
{name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
|
{name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
|
||||||
{name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
|
{name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
|
||||||
|
|
@ -34,7 +33,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-pa-lg col">
|
<div class="q-pa-lg">
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
|
|
@ -49,7 +48,8 @@
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
class="q-pa-md bg-transparent"
|
class="q-pa-md bg-transparent"
|
||||||
:class="is_grid_mode ? '': 'my-sticky-header-table'"
|
:class="is_grid_mode ? '': 'sticky-header-table'"
|
||||||
|
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
|
||||||
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
|
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
|
||||||
color="primary"
|
color="primary"
|
||||||
table-header-class="text-primary text-uppercase"
|
table-header-class="text-primary text-uppercase"
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
@row-click="() => console.log('click!')"
|
@row-click="() => console.log('click!')"
|
||||||
>
|
>
|
||||||
<template v-slot:item="props">
|
<template v-slot:item="props">
|
||||||
<SupervisorCrewTableItem :row="props.row"/>
|
<EmployeeListTableItem :row="props.row"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:top>
|
<template v-slot:top>
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.my-sticky-header-table
|
.sticky-header-table
|
||||||
thead tr:first-child th
|
thead tr:first-child th
|
||||||
background-color: var(--q-dark)
|
background-color: var(--q-dark)
|
||||||
margin-top: none
|
margin-top: none
|
||||||
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
|
export const EmployeeListService = {
|
||||||
|
getEmployeeList: async (): Promise<EmployeeProfile[]> => {
|
||||||
|
const response = await api.get<EmployeeProfile[]>('/employees/employee-list')
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
||||||
|
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// /* eslint-disable */
|
|
||||||
import { api } from 'src/boot/axios';
|
|
||||||
import type { EmployeeListTableItem } from '../types/employee-list-table-interface';
|
|
||||||
import type { EmployeeProfile } from '../types/employee-profile-interface';
|
|
||||||
|
|
||||||
|
|
||||||
export const EmployeeListService = {
|
|
||||||
getEmployeeList: async (): Promise<EmployeeListTableItem[]> => {
|
|
||||||
const response = await api.get<EmployeeListTableItem[]>('/employees/employee-list')
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
|
||||||
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export interface EmployeeListTableItem {
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
email: string;
|
|
||||||
supervisor_full_name: string | null;
|
|
||||||
company_name: number;
|
|
||||||
job_title: string;
|
|
||||||
};
|
|
||||||
73
src/modules/profile/components/employee/menu-employee.vue
Normal file
73
src/modules/profile/components/employee/menu-employee.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import MenuPanelPersonal from 'src/modules/profile/components/employee/menu-panel-personal.vue';
|
||||||
|
import MenuPanelEmployee from 'src/modules/profile/components/employee/menu-panel-employee.vue';
|
||||||
|
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
|
||||||
|
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.vue';
|
||||||
|
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
|
const PanelNames = {
|
||||||
|
PERSONAL_INFO: 'personal_info',
|
||||||
|
EMPLOYEE_INFO: 'employee_info',
|
||||||
|
PREFERENCES: 'references',
|
||||||
|
};
|
||||||
|
|
||||||
|
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
class="rounded-5 bg-transparent q-pa-none"
|
||||||
|
>
|
||||||
|
<MenuTemplate
|
||||||
|
:first-name="employee_profile.first_name"
|
||||||
|
:last-name="employee_profile.last_name"
|
||||||
|
:initial-menu="PanelNames.PERSONAL_INFO"
|
||||||
|
>
|
||||||
|
<template #tabs>
|
||||||
|
<q-tab
|
||||||
|
:name='PanelNames.PERSONAL_INFO'
|
||||||
|
icon='person_outline'
|
||||||
|
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
|
||||||
|
/>
|
||||||
|
<q-tab
|
||||||
|
:name="PanelNames.EMPLOYEE_INFO"
|
||||||
|
icon="work_outline"
|
||||||
|
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
|
||||||
|
/>
|
||||||
|
<q-tab
|
||||||
|
:name="PanelNames.PREFERENCES"
|
||||||
|
icon="display_settings"
|
||||||
|
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #panels>
|
||||||
|
<q-tab-panel
|
||||||
|
:name="PanelNames.PERSONAL_INFO"
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<MenuPanelPersonal v-model="employee_profile" />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel
|
||||||
|
:name="PanelNames.EMPLOYEE_INFO"
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<MenuPanelEmployee v-model="employee_profile" />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel
|
||||||
|
:name="PanelNames.PREFERENCES"
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<MenuPanelPreferences />
|
||||||
|
</q-tab-panel>
|
||||||
|
</template>
|
||||||
|
</MenuTemplate>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
@ -1,37 +1,39 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
|
||||||
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue';
|
import MenuPanelSelectField from 'src/modules/profile/components/shared/menu-panel-select-field.vue';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
|
||||||
const { employeeProfile } = defineProps<{
|
const employee_profile = defineModel<EmployeeProfile>({required: true});
|
||||||
employeeProfile: EmployeeProfile;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let initial_info: EmployeeProfile = employeeProfile;
|
|
||||||
let employee_form_data = ref<EmployeeProfile>({ ...employeeProfile });
|
|
||||||
const is_editing = ref<boolean>(false);
|
const is_editing = ref<boolean>(false);
|
||||||
|
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
|
||||||
|
|
||||||
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
|
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (!is_editing.value) {
|
if (!is_editing.value) {
|
||||||
is_editing.value = true;
|
is_editing.value = true;
|
||||||
|
console.log('clicky!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
is_editing.value = false;
|
is_editing.value = false;
|
||||||
initial_info = { ...employee_form_data.value }; // update initial value for future possible resets
|
initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
|
||||||
|
|
||||||
if (!deepEqual(employee_form_data, initial_info)) {
|
if (!deepEqual(employee_profile.value, initial_info)) {
|
||||||
// save the new data here
|
// save the new data here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
employee_form_data = ref<EmployeeProfile>(initial_info);
|
employee_profile.value = unwrapAndClone(initial_info);
|
||||||
is_editing.value = false;
|
is_editing.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -43,14 +45,14 @@
|
||||||
@reset="onReset"
|
@reset="onReset"
|
||||||
>
|
>
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="employee_form_data.job_title"
|
v-model="employee_profile.job_title"
|
||||||
class="col"
|
class="col"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.employee.job_title')"
|
:label-string="$t('profile.employee.job_title')"
|
||||||
/>
|
/>
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="employee_form_data.company_name"
|
v-model="employee_profile.company_name"
|
||||||
class="col"
|
class="col"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.employee.company')"
|
:label-string="$t('profile.employee.company')"
|
||||||
|
|
@ -58,8 +60,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="q-mx-xs">
|
<div class="q-mx-xs">
|
||||||
<ProfileSelectField
|
<MenuPanelSelectField
|
||||||
v-model="employee_form_data.supervisor_full_name"
|
v-model="employee_profile.supervisor_full_name"
|
||||||
:options="supervisor_options"
|
:options="supervisor_options"
|
||||||
:label-string="$t('profile.employee.supervisor')"
|
:label-string="$t('profile.employee.supervisor')"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
|
|
@ -68,14 +70,14 @@
|
||||||
|
|
||||||
|
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="employee_form_data.email"
|
v-model="employee_profile.email"
|
||||||
class="col"
|
class="col"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.employee.email')"
|
:label-string="$t('profile.employee.email')"
|
||||||
/>
|
/>
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="employee_form_data.first_work_day"
|
v-model="employee_profile.first_work_day"
|
||||||
readonly
|
readonly
|
||||||
class="col"
|
class="col"
|
||||||
type="date"
|
type="date"
|
||||||
|
|
@ -84,7 +86,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div
|
||||||
|
class="absolute-bottom"
|
||||||
|
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||||
|
>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="is_editing"
|
v-if="is_editing"
|
||||||
|
|
@ -100,6 +105,7 @@
|
||||||
push
|
push
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
type="submit"
|
||||||
:icon="is_editing ? 'save_alt' : 'create'"
|
:icon="is_editing ? 'save_alt' : 'create'"
|
||||||
class="q-ma-sm"
|
class="q-ma-sm"
|
||||||
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
|
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
|
||||||
const { employeeProfile } = defineProps<{
|
const employee_profile = defineModel<EmployeeProfile>({required: true});
|
||||||
employeeProfile: EmployeeProfile;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const is_editing = ref<boolean>(false);
|
const is_editing = ref<boolean>(false);
|
||||||
|
|
||||||
let initial_info: EmployeeProfile = employeeProfile;
|
let initial_info: EmployeeProfile = unwrapAndClone(employee_profile.value);
|
||||||
const personal_form_data = ref<EmployeeProfile>({ ...employeeProfile });
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (!is_editing.value) {
|
if (!is_editing.value) {
|
||||||
|
|
@ -20,16 +21,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
is_editing.value = false;
|
is_editing.value = false;
|
||||||
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets
|
initial_info = unwrapAndClone(employee_profile.value); // update initial value for future possible resets
|
||||||
|
|
||||||
if (!deepEqual(personal_form_data.value, initial_info)) {
|
if (!deepEqual(employee_profile.value, initial_info)) {
|
||||||
// save the new data here
|
// save the new data here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
personal_form_data.value= { ...initial_info };
|
employee_profile.value = unwrapAndClone(initial_info);
|
||||||
is_editing.value = false;
|
is_editing.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -41,15 +42,15 @@
|
||||||
@reset="onReset"
|
@reset="onReset"
|
||||||
>
|
>
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="personal_form_data.first_name"
|
v-model="employee_profile.first_name"
|
||||||
type="text"
|
type="text"
|
||||||
class="col"
|
class="col"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.personal.first_name')"
|
:label-string="$t('profile.personal.first_name')"
|
||||||
/>
|
/>
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="personal_form_data.last_name"
|
v-model="employee_profile.last_name"
|
||||||
class="col"
|
class="col"
|
||||||
type="text"
|
type="text"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
|
|
@ -58,15 +59,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="personal_form_data.phone_number"
|
v-model="employee_profile.phone_number"
|
||||||
class="col"
|
class="col"
|
||||||
type="text"
|
type="text"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.personal.phone_number')"
|
:label-string="$t('profile.personal.phone_number')"
|
||||||
/>
|
/>
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="personal_form_data.birth_date"
|
v-model="employee_profile.birth_date"
|
||||||
class="col"
|
class="col"
|
||||||
mask="#### / ## / ##"
|
mask="#### / ## / ##"
|
||||||
hint="ex: 1970 / 01 / 01"
|
hint="ex: 1970 / 01 / 01"
|
||||||
|
|
@ -76,8 +77,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
<ProfileInputField
|
<MenuPanelInputField
|
||||||
v-model="personal_form_data.residence"
|
v-model="employee_profile.residence"
|
||||||
class="col"
|
class="col"
|
||||||
:is-editing="is_editing"
|
:is-editing="is_editing"
|
||||||
:label-string="$t('profile.personal.address')"
|
:label-string="$t('profile.personal.address')"
|
||||||
|
|
@ -85,7 +86,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div
|
||||||
|
class="absolute-bottom"
|
||||||
|
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||||
|
>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="is_editing"
|
v-if="is_editing"
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import ProfileHeader from 'src/modules/profile/components/shared/profile-header.vue';
|
import MenuHeader from 'src/modules/profile/components/shared/menu-header.vue';
|
||||||
|
|
||||||
const { firstName, lastName, initialMenu } = defineProps<{
|
const { firstName, lastName, initialMenu } = defineProps<{
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
|
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
|
||||||
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
|
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
|
||||||
>
|
>
|
||||||
<ProfileHeader
|
<MenuHeader
|
||||||
:user-first-name="firstName"
|
:user-first-name="firstName"
|
||||||
:user-last-name="lastName"
|
:user-last-name="lastName"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import PanelInfoPersonal from 'src/modules/profile/components/employee/profile-panel-info-personal.vue';
|
|
||||||
import PanelInfoEmployee from 'src/modules/profile/components/employee/profile-panel-info-employee.vue';
|
|
||||||
import PanelPreferences from 'src/modules/profile/components/shared/profile-panel-preferences.vue';
|
|
||||||
import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue';
|
|
||||||
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
|
||||||
|
|
||||||
const PanelNames = {
|
|
||||||
PERSONAL_INFO: 'personal_info',
|
|
||||||
EMPLOYEE_INFO: 'employee_info',
|
|
||||||
PREFERENCES: 'references',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { employeeProfile = default_employee_profile } = defineProps<{
|
|
||||||
employeeProfile?: EmployeeProfile | undefined;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card flat class="rounded-5 bg-transparent q-pa-none">
|
|
||||||
<ProfileTabMenuTemplate
|
|
||||||
:first-name="employeeProfile.first_name"
|
|
||||||
:last-name="employeeProfile.last_name"
|
|
||||||
:initial-menu="PanelNames.PERSONAL_INFO"
|
|
||||||
>
|
|
||||||
<template #tabs>
|
|
||||||
<q-tab
|
|
||||||
:name='PanelNames.PERSONAL_INFO'
|
|
||||||
icon='person_outline'
|
|
||||||
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
|
|
||||||
/>
|
|
||||||
<q-tab
|
|
||||||
:name="PanelNames.EMPLOYEE_INFO"
|
|
||||||
icon="work_outline"
|
|
||||||
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
|
|
||||||
/>
|
|
||||||
<q-tab
|
|
||||||
:name="PanelNames.PREFERENCES"
|
|
||||||
icon="display_settings"
|
|
||||||
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #panels>
|
|
||||||
<q-tab-panel :name="PanelNames.PERSONAL_INFO" class="q-pa-none">
|
|
||||||
<PanelInfoPersonal :employee-profile="employeeProfile" />
|
|
||||||
</q-tab-panel>
|
|
||||||
|
|
||||||
<q-tab-panel :name="PanelNames.EMPLOYEE_INFO" class="q-pa-none">
|
|
||||||
<PanelInfoEmployee :employee-profile="employeeProfile" />
|
|
||||||
</q-tab-panel>
|
|
||||||
|
|
||||||
<q-tab-panel :name="PanelNames.PREFERENCES" class="q-pa-none">
|
|
||||||
<PanelPreferences />
|
|
||||||
</q-tab-panel>
|
|
||||||
</template>
|
|
||||||
</ProfileTabMenuTemplate>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
|
|
@ -9,22 +14,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
||||||
<span class="col">{{ $t(title) }}</span>
|
<span class="col">{{ $t(title) }}</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="startDate.length > 0"
|
v-if="startDate.length > 0"
|
||||||
class="col row flex-center full-width q-py-none q-my-none"
|
class="col row flex-center full-width q-py-none q-my-none"
|
||||||
>
|
>
|
||||||
<div class="text-primary text-weight-bold text-h6">
|
<div class="text-primary text-weight-bold text-h6">
|
||||||
{{ $d(new Date(startDate), date_format_options) }}
|
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body2 q-mx-md text-weight-medium">
|
<div class="text-body2 q-mx-md text-weight-medium">
|
||||||
{{ $t('shared.misc.to') }}
|
{{ $t('shared.misc.to') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-primary text-weight-bold text-h6">
|
<div class="text-primary text-weight-bold text-h6">
|
||||||
{{ $d(new Date(endDate), date_format_options) }}
|
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -3,10 +3,14 @@
|
||||||
import { date} from 'quasar';
|
import { date} from 'quasar';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
|
const NEXT = 1;
|
||||||
|
const PREVIOUS = -1;
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const is_showing_calendar_picker = ref(false);
|
const is_showing_calendar_picker = ref(false);
|
||||||
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
|
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
|
||||||
|
const is_disabled = computed(() => timesheet_store.pay_period === undefined);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'date-selected': [ value: string ]
|
'date-selected': [ value: string ]
|
||||||
|
|
@ -15,8 +19,8 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const is_previous_pay_period_limit = computed( ()=>
|
const is_previous_pay_period_limit = computed( ()=>
|
||||||
timesheet_store.pay_period.pay_year === 2024 &&
|
( timesheet_store.pay_period?.pay_year === 2024 &&
|
||||||
timesheet_store.pay_period.pay_period_no <= 1
|
timesheet_store.pay_period?.pay_period_no <= 1 ) ?? false
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDateSelected = (value: string) => {
|
const onDateSelected = (value: string) => {
|
||||||
|
|
@ -24,6 +28,33 @@
|
||||||
is_showing_calendar_picker.value = false;
|
is_showing_calendar_picker.value = false;
|
||||||
emit('date-selected', value);
|
emit('date-selected', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNextOrPreviousPayPeriod = (direction: number) => {
|
||||||
|
const pay_period = timesheet_store.pay_period;
|
||||||
|
if (!pay_period) return;
|
||||||
|
|
||||||
|
pay_period.pay_period_no += direction;
|
||||||
|
|
||||||
|
if (pay_period.pay_period_no > 26) {
|
||||||
|
pay_period.pay_period_no = 1;
|
||||||
|
pay_period.pay_year += direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pay_period.pay_period_no < 1) {
|
||||||
|
pay_period.pay_period_no = 26;
|
||||||
|
pay_period.pay_year += direction;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPayPeriod = () => {
|
||||||
|
getNextOrPreviousPayPeriod(NEXT);
|
||||||
|
emit('pressed-next-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPreviousPayPeriod = () => {
|
||||||
|
getNextOrPreviousPayPeriod(PREVIOUS);
|
||||||
|
emit('pressed-previous-button');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -33,8 +64,8 @@
|
||||||
push rounded
|
push rounded
|
||||||
icon="keyboard_arrow_left"
|
icon="keyboard_arrow_left"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="emit('pressed-previous-button')"
|
@click="getPreviousPayPeriod"
|
||||||
:disable="is_previous_pay_period_limit || timesheet_store.is_loading"
|
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
|
||||||
class="q-mr-sm q-px-sm"
|
class="q-mr-sm q-px-sm"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|
@ -52,7 +83,7 @@
|
||||||
icon="calendar_month"
|
icon="calendar_month"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="is_showing_calendar_picker = true"
|
@click="is_showing_calendar_picker = true"
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
class="q-px-xl"
|
class="q-px-xl"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|
@ -69,8 +100,8 @@
|
||||||
push rounded
|
push rounded
|
||||||
icon="keyboard_arrow_right"
|
icon="keyboard_arrow_right"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="emit('pressed-next-button')"
|
@click="getNextPayPeriod"
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
class="q-ml-sm q-px-sm"
|
class="q-ml-sm q-px-sm"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
export interface User {
|
export interface User {
|
||||||
firstName: string;
|
first_name: string;
|
||||||
lastName: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
||||||
|
|
||||||
|
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
||||||
|
'ADMIN',
|
||||||
|
'SUPERVISOR',
|
||||||
|
'HR',
|
||||||
|
'ACCOUNTING',
|
||||||
|
]
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
|
||||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
|
||||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
employeeEmail: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
|
|
||||||
// const timesheet_store = useTimesheetStore();
|
|
||||||
const is_showing_graph = ref(true);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="dialog_model"
|
|
||||||
full-width
|
|
||||||
transition-show="jump-down"
|
|
||||||
transition-hide="jump-down"
|
|
||||||
>
|
|
||||||
<!-- loader -->
|
|
||||||
<q-card
|
|
||||||
v-if="timesheet_store.is_loading"
|
|
||||||
class="column flex-center text-center"
|
|
||||||
>
|
|
||||||
<q-spinner
|
|
||||||
color="primary"
|
|
||||||
size="5em"
|
|
||||||
:thickness="10"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
|
||||||
{{ $t('shared.loading') }}
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card
|
|
||||||
v-else
|
|
||||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
|
||||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- employee name -->
|
|
||||||
<q-card-section
|
|
||||||
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
|
|
||||||
>
|
|
||||||
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span>
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
size="2px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-card-actions
|
|
||||||
align="center"
|
|
||||||
class="q-pa-none"
|
|
||||||
>
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="is_showing_graph"
|
|
||||||
color="white"
|
|
||||||
text-color="primary"
|
|
||||||
toggle-color="primary"
|
|
||||||
:options="[
|
|
||||||
{ icon: 'bar_chart', value: true },
|
|
||||||
{ icon: 'edit', value: false },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- employee timesheet for supervisor editting -->
|
|
||||||
<q-card-section
|
|
||||||
v-if="!is_showing_graph"
|
|
||||||
class="q-pa-none"
|
|
||||||
>
|
|
||||||
<!-- list of shifts -->
|
|
||||||
<q-card-section
|
|
||||||
:horizontal="$q.screen.gt.sm"
|
|
||||||
class="q-pa-none rounded-10"
|
|
||||||
>
|
|
||||||
<TimesheetWrapper
|
|
||||||
dense
|
|
||||||
:employee-email="employeeEmail"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- employee timesheet details with chart -->
|
|
||||||
<q-card-section
|
|
||||||
v-if="is_showing_graph"
|
|
||||||
class="q-pa-md col column full-width no-wrap"
|
|
||||||
>
|
|
||||||
<q-card-section
|
|
||||||
:horizontal="!$q.screen.lt.md"
|
|
||||||
class="q-pa-none col no-wrap"
|
|
||||||
style="min-height: 300px;"
|
|
||||||
>
|
|
||||||
<DetailedDialogChartHoursWorked class="col-7" />
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
:vertical="!$q.screen.lt.md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="column col justify-center no-wrap q-pa-none">
|
|
||||||
<DetailedDialogChartShiftTypes class="col-5" />
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
:vertical="!$q.screen.lt.md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailedDialogChartExpenses class="col" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
setup
|
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';
|
||||||
|
|
@ -23,27 +25,28 @@
|
||||||
const expenses_labels = ref<string[]>([]);
|
const expenses_labels = ref<string[]>([]);
|
||||||
|
|
||||||
const getExpensesData = (): ChartData<'bar'> => {
|
const getExpensesData = (): ChartData<'bar'> => {
|
||||||
const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses));
|
// const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses));
|
||||||
const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
|
// const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
|
||||||
|
|
||||||
const all_costs = all_days.map(day => day.total_expenses);
|
// const all_costs = all_days.map(day => day.total_expenses);
|
||||||
const all_mileage = all_days.map(day => day.total_mileage);
|
// console.log('costs, ', all_costs);
|
||||||
|
// const all_mileage = all_days.map(day => day.total_mileage);
|
||||||
|
|
||||||
|
|
||||||
expenses_dataset.value = [
|
// expenses_dataset.value = [
|
||||||
{
|
// {
|
||||||
label: t('timesheet_approvals.table.expenses'),
|
// label: t('timesheet_approvals.table.expenses'),
|
||||||
data: all_costs,
|
// data: all_costs,
|
||||||
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
label: t('timesheet_approvals.table.mileage'),
|
// label: t('timesheet_approvals.table.mileage'),
|
||||||
data: all_mileage,
|
// data: all_mileage,
|
||||||
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
|
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
|
|
||||||
expenses_labels.value = all_days_dates.map(day => day.short_date);
|
// expenses_labels.value = all_days_dates.map(day => day.short_date);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasets: expenses_dataset.value,
|
datasets: expenses_dataset.value,
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/* eslint-disable */
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { colors } from 'quasar';
|
import { colors } from 'quasar';
|
||||||
import { Bar } from 'vue-chartjs';
|
import { Bar } from 'vue-chartjs';
|
||||||
|
|
@ -15,44 +16,44 @@
|
||||||
ChartJS.defaults.maintainAspectRatio = false;
|
ChartJS.defaults.maintainAspectRatio = false;
|
||||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||||
|
|
||||||
const { pay_period_details } = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const hours_worked_labels = ref<string[]>([]);
|
const hours_worked_labels = ref<string[]>([]);
|
||||||
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
||||||
|
|
||||||
const getHoursWorkedData = (): ChartData<'bar'> => {
|
const getHoursWorkedData = (): ChartData<'bar'> => {
|
||||||
|
|
||||||
const all_days = pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
// const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
||||||
const datasetConfig = [
|
// const datasetConfig = [
|
||||||
{
|
// {
|
||||||
key: 'regular_hours',
|
// key: 'regular_hours',
|
||||||
label: t('shared.shift_type.regular'),
|
// label: t('shared.shift_type.regular'),
|
||||||
color: colors.getPaletteColor('green-5'),
|
// color: colors.getPaletteColor('green-5'),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
key: 'evening_hours',
|
// key: 'evening_hours',
|
||||||
label: t('shared.shift_type.evening'),
|
// label: t('shared.shift_type.evening'),
|
||||||
color: colors.getPaletteColor('green-9'),
|
// color: colors.getPaletteColor('green-9'),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
key: 'emergency_hours',
|
// key: 'emergency_hours',
|
||||||
label: t('shared.shift_type.emergency'),
|
// label: t('shared.shift_type.emergency'),
|
||||||
color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
|
// color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
key: 'overtime_hours',
|
// key: 'overtime_hours',
|
||||||
label: t('shared.shift_type.overtime'),
|
// label: t('shared.shift_type.overtime'),
|
||||||
color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
|
// color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
|
||||||
},
|
// },
|
||||||
] as const;
|
// ] as const;
|
||||||
|
|
||||||
hours_worked_dataset.value = datasetConfig.map(cfg => ({
|
// hours_worked_dataset.value = datasetConfig.map(cfg => ({
|
||||||
label: cfg.label,
|
// label: cfg.label,
|
||||||
data: all_days.map(day => day[ cfg.key ]),
|
// data: all_days.map(day => day[ cfg.key ]),
|
||||||
backgroundColor: cfg.color,
|
// backgroundColor: cfg.color,
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
hours_worked_labels.value = all_days.map(day => day.short_date);
|
// hours_worked_labels.value = all_days.map(day => day.short_date);
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { colors } from 'quasar';
|
import { colors } from 'quasar';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
|
@ -22,27 +23,27 @@
|
||||||
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
|
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
|
||||||
|
|
||||||
|
|
||||||
shift_type_totals.value = [{
|
// shift_type_totals.value = [{
|
||||||
data: [
|
// data: [
|
||||||
current_pay_period_overview.regular_hours,
|
// current_pay_period_overview.regular_hours,
|
||||||
current_pay_period_overview.evening_hours,
|
// current_pay_period_overview.other_hours.evening_hours,
|
||||||
current_pay_period_overview.emergency_hours,
|
// current_pay_period_overview.other_hours.emergency_hours,
|
||||||
current_pay_period_overview.overtime_hours,
|
// current_pay_period_overview.other_hours.overtime_hours,
|
||||||
],
|
// ],
|
||||||
backgroundColor: [
|
// backgroundColor: [
|
||||||
colors.getPaletteColor('green-5'), // Regular
|
// colors.getPaletteColor('green-5'), // Regular
|
||||||
colors.getPaletteColor('green-9'), // Evening
|
// colors.getPaletteColor('green-9'), // Evening
|
||||||
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
// getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency
|
||||||
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
// getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime
|
||||||
]
|
// ]
|
||||||
}];
|
// }];
|
||||||
|
|
||||||
shift_type_labels.value = [
|
// shift_type_labels.value = [
|
||||||
current_pay_period_overview.regular_hours.toString() + 'h',
|
// current_pay_period_overview.regular_hours.toString() + 'h',
|
||||||
current_pay_period_overview.evening_hours.toString() + 'h',
|
// current_pay_period_overview.other_hours.evening_hours.toString() + 'h',
|
||||||
current_pay_period_overview.emergency_hours.toString() + 'h',
|
// current_pay_period_overview.other_hours.emergency_hours.toString() + 'h',
|
||||||
current_pay_period_overview.overtime_hours.toString() + 'h',
|
// current_pay_period_overview.other_hours.overtime_hours.toString() + 'h',
|
||||||
]
|
// ]
|
||||||
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
88
src/modules/timesheet-approval/components/details-dialog.vue
Normal file
88
src/modules/timesheet-approval/components/details-dialog.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
/* eslint-disable */
|
||||||
|
import { provide, ref } from 'vue';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
||||||
|
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||||
|
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||||
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||||
|
|
||||||
|
const { employeeEmail } = defineProps<{
|
||||||
|
employeeEmail: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const render_key = ref(1);
|
||||||
|
|
||||||
|
provide('employeeEmail', employeeEmail);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
v-model="dialog_model"
|
||||||
|
full-width
|
||||||
|
full-height
|
||||||
|
transition-show="jump-down"
|
||||||
|
transition-hide="jump-down"
|
||||||
|
@show="render_key += 1"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||||
|
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- employee name -->
|
||||||
|
<q-card-section
|
||||||
|
class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto"
|
||||||
|
>
|
||||||
|
<span>TODO: Name goes here</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- employee pay period details using chart -->
|
||||||
|
<q-card-section
|
||||||
|
:horizontal="!$q.screen.lt.md"
|
||||||
|
class=" col-auto q-px-sm no-wrap"
|
||||||
|
>
|
||||||
|
<DetailedDialogChartHoursWorked
|
||||||
|
:key="render_key"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailedDialogChartShiftTypes
|
||||||
|
:key="render_key + 1"
|
||||||
|
class="col-2 q-ma-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailedDialogChartExpenses
|
||||||
|
:key="render_key + 2"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="col-auto">
|
||||||
|
<q-separator />
|
||||||
|
<ExpenseCrudDialogList
|
||||||
|
horizontal
|
||||||
|
:employee-email="employeeEmail"
|
||||||
|
/>
|
||||||
|
<q-separator />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- list of shifts -->
|
||||||
|
<q-card-section
|
||||||
|
:horizontal="$q.screen.gt.sm"
|
||||||
|
class="col-auto q-px-sm rounded-5 no-wrap"
|
||||||
|
>
|
||||||
|
<TimesheetWrapper
|
||||||
|
dense
|
||||||
|
:employee-email="employeeEmail"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
@ -1,38 +1,41 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
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];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
|
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
|
||||||
<q-card class="rounded-10">
|
<q-card class="rounded-10">
|
||||||
|
|
||||||
<!-- Card header with employee name and details button-->
|
<!-- Card header with employee name and details button-->
|
||||||
<q-card-section horizontal class="q-py-none q-px-sm q-ma-none justify-between items-center">
|
<q-card-section
|
||||||
|
horizontal
|
||||||
|
class="q-py-none q-px-sm q-ma-none justify-between items-center"
|
||||||
|
>
|
||||||
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
|
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
|
||||||
|
|
||||||
<!-- Buttons to view detailed shifts or view employee timesheet -->
|
<!-- Buttons to view detailed shifts or view employee timesheet -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
square
|
square
|
||||||
unelevated
|
unelevated
|
||||||
class="col-auto q-pa-none q-ma-none"
|
class="col-auto q-pa-none q-ma-none"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="work_history"
|
icon="work_history"
|
||||||
@click="emit('clickDetails', row)"
|
@click="emit('clickDetails', row)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
anchor="top middle"
|
anchor="top middle"
|
||||||
self="center middle"
|
self="center middle"
|
||||||
class="bg-primary text-uppercase text-weight-bold"
|
class="bg-primary text-uppercase text-weight-bold"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
|
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|
@ -42,66 +45,79 @@
|
||||||
<q-separator size="2px" />
|
<q-separator size="2px" />
|
||||||
|
|
||||||
<!-- Main body of pay period card -->
|
<!-- Main body of pay period card -->
|
||||||
<q-card-section class="q-py-none q-px-sm q-mt-sm q-mb-md">
|
<q-card-section class="q-py-none q-px-sm q-my-sm">
|
||||||
<div class="row no-wrap">
|
<div class="row">
|
||||||
|
|
||||||
<!-- left portion of pay period card -->
|
<!-- left portion of pay period card -->
|
||||||
<div class="col column no-wrap q-px-sm">
|
<div class="col column q-px-sm">
|
||||||
|
|
||||||
<!-- Regular hours segment -->
|
<!-- Regular hours segment -->
|
||||||
<div class="column" :class="$q.screen.lt.md ? 'col' : 'col-8'">
|
<div class="col column">
|
||||||
<span :class="stack_label_class"> {{ $t('shared.shift_type.regular') }} </span>
|
<span class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"> {{ $t('shared.shift_type.regular') }} </span>
|
||||||
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
|
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
|
||||||
|
<q-separator class="q-mx-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mx-sm" />
|
|
||||||
|
|
||||||
<!-- Other hour types segment -->
|
<!-- Other hour types segment -->
|
||||||
<div class="row q-px-xs">
|
<div class="col-auto row ellipsis q-mt-xs">
|
||||||
<div class="col column no-wrap">
|
<div
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.evening') }} </span>
|
v-for="hour_type, index in row.other_hours"
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.evening_hours }} </span>
|
:key="index"
|
||||||
</div>
|
class="col-4 column ellipsis"
|
||||||
|
:class="hour_type === 0 ? 'invisible' : ''"
|
||||||
<div class="col column no-wrap">
|
>
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.emergency') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.emergency_hours }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
</div>
|
style="font-size: 0.7em;"
|
||||||
|
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
|
||||||
<div class="col column no-wrap">
|
<span
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.overtime') }} </span>
|
class="text-weight-bolder q-pa-none q-mb-xs"
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.overtime_hours }} </span>
|
style="font-size: 1.2em; line-height: 1em;"
|
||||||
|
> {{ hour_type }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator
|
<q-separator
|
||||||
vertical
|
vertical
|
||||||
class="q-mt-xs q-mb-none"
|
class="q-mt-xs q-mb-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Right portion of pay period card -->
|
<!-- Right portion of pay period card -->
|
||||||
<div class="col-auto column q-px-sm">
|
<div class="col-auto column q-px-sm">
|
||||||
<div class="col column no-wrap">
|
<div class="col column no-wrap">
|
||||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.expenses }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder text-h6 q-pa-none"
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
> {{ row.expenses }} </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column no-wrap">
|
<div class="col column no-wrap">
|
||||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.mileage }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder text-h6 q-pa-none"
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
> {{ row.mileage }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-separator color="primary" size="2px" />
|
<q-separator
|
||||||
|
color="primary"
|
||||||
|
size="2px"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Validate Pay Period section -->
|
<!-- Validate Pay Period section -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="justify-between items-center text-weight-bold q-px-sm"
|
class="justify-between items-center text-weight-bold q-px-sm"
|
||||||
:class="row.is_approved ? 'text-white bg-primary' : 'bg-dark'"
|
:class="row.is_approved ? 'text-white bg-primary' : 'bg-dark'"
|
||||||
>
|
>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span>
|
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span>
|
||||||
|
|
@ -109,15 +125,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="modelApproval"
|
v-model="modelApproval"
|
||||||
dense
|
dense
|
||||||
left-label
|
left-label
|
||||||
size="lg"
|
size="lg"
|
||||||
checked-icon="lock"
|
checked-icon="lock"
|
||||||
unchecked-icon="lock_open"
|
unchecked-icon="lock_open"
|
||||||
:color="row.is_approved ? 'white' : 'primary'"
|
:color="row.is_approved ? 'white' : 'primary'"
|
||||||
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
|
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
|
||||||
class="col-auto text-uppercase"
|
class="col-auto text-uppercase"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,29 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
import 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 timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
|
|
||||||
const filter = ref<string | number | null>('');
|
const filter = ref<string | number | null>('');
|
||||||
|
const is_grid_mode = ref(true);
|
||||||
|
const IS_ABNORMAL_SHIFT = ['OVERTIME', 'EMERGENCY'];
|
||||||
|
const IS_PTO = ['HOLIDAY', 'VACATION', 'SICK'];
|
||||||
|
|
||||||
const employeeEmail = defineModel();
|
const employeeEmail = defineModel();
|
||||||
|
|
||||||
|
const visible_columns = ref<string[]>(['REGULAR', 'email']);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clickedDetailsButton': [email: string];
|
'clickedDetailsButton': [email: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -23,28 +34,46 @@
|
||||||
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;
|
||||||
emit('clickedDetailsButton', employee_email);
|
|
||||||
timesheet_store.current_pay_period_overview = row;
|
timesheet_store.current_pay_period_overview = row;
|
||||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
emit('clickedDetailsButton', employee_email);
|
||||||
|
|
||||||
|
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
||||||
|
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getListModeTextColor = (type: string): string => {
|
||||||
|
console.log('type: ', type);
|
||||||
|
if (IS_ABNORMAL_SHIFT.includes(type)) {
|
||||||
|
return ' text-negative text-weight-bolder';
|
||||||
|
}
|
||||||
|
else if (IS_PTO.includes(type)) {
|
||||||
|
return ' text-warning text-weight-bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<q-table
|
<q-table
|
||||||
|
:visible-columns="visible_columns"
|
||||||
:rows="overview_rows"
|
:rows="overview_rows"
|
||||||
:columns="pay_period_overview_columns"
|
:columns="pay_period_overview_columns"
|
||||||
row-key="email"
|
row-key="email"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
grid
|
:grid="is_grid_mode"
|
||||||
dense
|
dense
|
||||||
hide-pagination
|
hide-pagination
|
||||||
color="primary"
|
color="primary"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
card-container-class="justify-center"
|
card-container-class="justify-center"
|
||||||
:loading="timesheet_store.is_loading"
|
:loading="timesheet_store.is_loading"
|
||||||
|
class="q-py-md bg-transparent"
|
||||||
|
:class="is_grid_mode ? '' : 'sticky-header-table no-shadow'"
|
||||||
|
table-class="q-pa-none q-py-none q-mx-md rounded-10 bg-dark shadow-4'"
|
||||||
:no-data-label="$t('shared.error.no_data_found')"
|
:no-data-label="$t('shared.error.no_data_found')"
|
||||||
:no-results-label="$t('shared.error.no_search_results')"
|
:no-results-label="$t('shared.error.no_search_results')"
|
||||||
:loading-label="$t('shared.label.loading')"
|
:loading-label="$t('shared.label.loading')"
|
||||||
|
|
@ -54,18 +83,69 @@
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
||||||
>
|
>
|
||||||
<PayPeriodNavigator />
|
<PayPeriodNavigator
|
||||||
|
@date-selected="timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<!-- Grid-or-List toggle goes here -->
|
<q-btn-toggle
|
||||||
|
v-model="is_grid_mode"
|
||||||
|
push
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
toggle-color="primary"
|
||||||
|
class="q-mr-md"
|
||||||
|
:options="[
|
||||||
|
{ icon: 'grid_view', value: true },
|
||||||
|
{ icon: 'view_list', value: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
<QTableFilters v-model="filter" />
|
<QTableFilters v-model="filter" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #header="props">
|
||||||
|
<q-tr
|
||||||
|
:props="props"
|
||||||
|
class="bg-primary"
|
||||||
|
>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="col.label !== 'timesheet_approvals.table.is_approved'"
|
||||||
|
class="text-uppercase text-weight-bolder text-white"
|
||||||
|
>
|
||||||
|
{{ $t(col.label) }}
|
||||||
|
</span>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
||||||
|
:class="getListModeTextColor(props.col.name)"
|
||||||
|
>{{ props.value }}</span>
|
||||||
|
<q-icon
|
||||||
|
v-if="typeof props.value === 'boolean'"
|
||||||
|
:name="props.value ? 'verified' : 'fiber_manual_record'"
|
||||||
|
:color="props.value ? 'primary' : 'grey-5'"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Template for individual employee cards -->
|
<!-- Template 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"
|
||||||
|
|
@ -77,8 +157,8 @@
|
||||||
<template #no-data="{ message, filter }">
|
<template #no-data="{ message, filter }">
|
||||||
<div class="full-width column items-center text-primary q-gutter-sm">
|
<div class="full-width column items-center text-primary q-gutter-sm">
|
||||||
<q-icon
|
<q-icon
|
||||||
size="4em"
|
size="4em"
|
||||||
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="text-h6">
|
<span class="text-h6">
|
||||||
|
|
@ -89,3 +169,22 @@
|
||||||
</q-table>
|
</q-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.sticky-header-table
|
||||||
|
thead tr:first-child th
|
||||||
|
background-color: var(--q-primary)
|
||||||
|
margin-top: none
|
||||||
|
|
||||||
|
thead tr th
|
||||||
|
position: sticky
|
||||||
|
z-index: 1
|
||||||
|
thead tr:first-child th
|
||||||
|
top: 0
|
||||||
|
|
||||||
|
&.q-table--loading thead tr:last-child th
|
||||||
|
top: 48px
|
||||||
|
|
||||||
|
tbody
|
||||||
|
scroll-margin-top: 48px
|
||||||
|
</style>
|
||||||
|
|
@ -1,24 +1,51 @@
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { 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/timesheet-overview.models";
|
||||||
|
|
||||||
export const useTimesheetApprovalApi = () => {
|
export const useTimesheetApprovalApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
|
||||||
const getPayPeriodOverviewsByDate = async (date_string: string): Promise<void> => {
|
const getPayPeriodOverviewsByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<void> => {
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
let success = false;
|
||||||
|
if (typeof date_or_year === 'string') success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year);
|
||||||
|
else if (typeof date_or_year === 'number' && period_number) success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year, period_number);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await timesheet_store.getPayPeriodOverviewsBySupervisorEmail(
|
await timesheet_store.getTimesheetOverviewsByPayPeriod(
|
||||||
timesheet_store.pay_period.pay_year,
|
timesheet_store.pay_period?.pay_year ?? 1,
|
||||||
timesheet_store.pay_period.pay_period_no,
|
timesheet_store.pay_period?.pay_period_no ?? 1,
|
||||||
auth_store.user.email
|
auth_store.user?.email
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNextOrPreviousPayPeriodOverview = async (direction: number) => {
|
||||||
|
if (timesheet_store.pay_period === undefined) return;
|
||||||
|
|
||||||
|
let new_period_number = (timesheet_store.pay_period.pay_period_no) + direction;
|
||||||
|
let new_year = timesheet_store.pay_period.pay_year;
|
||||||
|
|
||||||
|
if ( new_period_number > 26 || new_period_number < 1) {
|
||||||
|
new_period_number = 1;
|
||||||
|
new_year += direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getPayPeriodOverviewsByDateOrYearAndNumber(new_year, new_period_number);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPayPeriodOverview = async () => {
|
||||||
|
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.NEXT_PERIOD);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviousPayPeriodOverview = async () => {
|
||||||
|
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.PREVIOUS_PERIOD);
|
||||||
|
};
|
||||||
|
|
||||||
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number ) => {
|
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number ) => {
|
||||||
|
if (timesheet_store.pay_period === undefined) return;
|
||||||
|
|
||||||
const [ targo, solucom ] = report_filter_company;
|
const [ targo, solucom ] = report_filter_company;
|
||||||
const [ shifts, expenses, holiday, vacation ] = report_filter_type;
|
const [ shifts, expenses, holiday, vacation ] = report_filter_type;
|
||||||
const options = {
|
const options = {
|
||||||
|
|
@ -34,7 +61,9 @@ export const useTimesheetApprovalApi = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPayPeriodOverviewsByDate,
|
getPayPeriodOverviewsByDateOrYearAndNumber,
|
||||||
getTimesheetApprovalCSVReport,
|
getTimesheetApprovalCSVReport,
|
||||||
|
getNextPayPeriodOverview,
|
||||||
|
getPreviousPayPeriodOverview,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
export interface PayPeriodOverview {
|
|
||||||
email: string;
|
|
||||||
employee_name: string;
|
|
||||||
regular_hours: number;
|
|
||||||
evening_hours: number;
|
|
||||||
emergency_hours: number;
|
|
||||||
overtime_hours: number;
|
|
||||||
total_hours: number;
|
|
||||||
expenses: number;
|
|
||||||
mileage: number;
|
|
||||||
is_approved: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PayPeriodOverviewResponse {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
period_start: string;
|
|
||||||
period_end: string;
|
|
||||||
payday: string;
|
|
||||||
label: string;
|
|
||||||
employees_overview: PayPeriodOverview[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_pay_period_overview: PayPeriodOverview = {
|
|
||||||
email: '',
|
|
||||||
employee_name: '',
|
|
||||||
regular_hours: -1,
|
|
||||||
evening_hours: -1,
|
|
||||||
emergency_hours: -1,
|
|
||||||
overtime_hours: -1,
|
|
||||||
total_hours: -1,
|
|
||||||
expenses: -1,
|
|
||||||
mileage: -1,
|
|
||||||
is_approved: false
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pay_period_overview_columns = [
|
|
||||||
{
|
|
||||||
name: 'employee_name',
|
|
||||||
label: 'timesheet_approvals.table.full_name',
|
|
||||||
field: 'employee_name',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'email',
|
|
||||||
label: 'timesheet_approvals.table.email',
|
|
||||||
field: 'email',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'regular_hours',
|
|
||||||
label: 'shared.shift_type.regular',
|
|
||||||
field: 'regular_hours',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'evening_hours',
|
|
||||||
label: 'shared.shift_type.evening',
|
|
||||||
field: 'evening_hours',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'emergency_hours',
|
|
||||||
label: 'shared.shift_type.emergency',
|
|
||||||
field: 'emergency_hours',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'overtime_hours',
|
|
||||||
label: 'shared.shift_type.overtime',
|
|
||||||
field: 'overtime_hours',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'expenses',
|
|
||||||
label: 'timesheet_approvals.table.expenses',
|
|
||||||
field: 'expenses',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mileage',
|
|
||||||
label: 'timesheet_approvals.table.mileage',
|
|
||||||
field: 'mileage',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'is_approved',
|
|
||||||
label: 'timesheet_approvals.table.is_approved',
|
|
||||||
field: 'is_approved',
|
|
||||||
sortable: true,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type { QTableColumn } from "quasar";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export enum NavigatorConstants {
|
||||||
|
NEXT_PERIOD = 1,
|
||||||
|
PREVIOUS_PERIOD = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetOverview {
|
||||||
|
email: string;
|
||||||
|
employee_name: string;
|
||||||
|
regular_hours: number;
|
||||||
|
other_hours: {
|
||||||
|
evening_hours: number;
|
||||||
|
emergency_hours: number;
|
||||||
|
overtime_hours: number;
|
||||||
|
sick_hours: number;
|
||||||
|
holiday_hours: number;
|
||||||
|
vacation_hours: number;
|
||||||
|
};
|
||||||
|
total_hours: number;
|
||||||
|
expenses: number;
|
||||||
|
mileage: number;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayPeriodOverviewResponse {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
period_start: string;
|
||||||
|
period_end: string;
|
||||||
|
payday: string;
|
||||||
|
label: string;
|
||||||
|
employees_overview: TimesheetOverview[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const default_pay_period_overview: TimesheetOverview = {
|
||||||
|
email: '',
|
||||||
|
employee_name: '',
|
||||||
|
regular_hours: -1,
|
||||||
|
other_hours: {
|
||||||
|
evening_hours: -1,
|
||||||
|
emergency_hours: -1,
|
||||||
|
overtime_hours: -1,
|
||||||
|
sick_hours: -1,
|
||||||
|
holiday_hours: -1,
|
||||||
|
vacation_hours: -1,
|
||||||
|
},
|
||||||
|
total_hours: -1,
|
||||||
|
expenses: -1,
|
||||||
|
mileage: -1,
|
||||||
|
is_approved: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pay_period_overview_columns: QTableColumn[] = [
|
||||||
|
{
|
||||||
|
name: 'employee_name',
|
||||||
|
label: 'timesheet_approvals.table.full_name',
|
||||||
|
align: 'left',
|
||||||
|
field: 'employee_name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'timesheet_approvals.table.email',
|
||||||
|
field: 'email',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'REGULAR',
|
||||||
|
label: 'shared.shift_type.regular',
|
||||||
|
field: 'regular_hours',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EVENING',
|
||||||
|
label: 'shared.shift_type.evening',
|
||||||
|
field: row => row.other_hours.evening_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EMERGENCY',
|
||||||
|
label: 'shared.shift_type.emergency',
|
||||||
|
field: row => row.other_hours.emergency_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SICK',
|
||||||
|
label: 'shared.shift_type.sick',
|
||||||
|
field: row => row.other_hours.sick_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HOLIDAY',
|
||||||
|
label: 'shared.shift_type.holiday',
|
||||||
|
field: row => row.other_hours.holiday_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VACATION',
|
||||||
|
label: 'shared.shift_type.vacation',
|
||||||
|
field: row => row.other_hours.vacation_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OVERTIME',
|
||||||
|
label: 'shared.shift_type.overtime',
|
||||||
|
field: row => row.other_hours.overtime_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expenses',
|
||||||
|
label: 'timesheet_approvals.table.expenses',
|
||||||
|
field: 'expenses',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mileage',
|
||||||
|
label: 'timesheet_approvals.table.mileage',
|
||||||
|
field: 'mileage',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'is_approved',
|
||||||
|
label: 'timesheet_approvals.table.is_approved',
|
||||||
|
field: 'is_approved',
|
||||||
|
sortable: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { api } from "src/boot/axios";
|
import { 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> => {
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-item class="row justify-between">
|
|
||||||
<q-item-label
|
|
||||||
header
|
|
||||||
class="text-h6 col-auto"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.title') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-section class="items-center col-auto">
|
|
||||||
<q-badge
|
|
||||||
lines="1"
|
|
||||||
class="q-pa-sm q-px-md"
|
|
||||||
:label="$t('timesheet.expense.total_amount') + ': ' + expense_store.pay_period_expenses_totals.amount.toFixed(2)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
<q-badge
|
|
||||||
lines="2"
|
|
||||||
class="q-pa-sm q-px-md"
|
|
||||||
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses_totals.mileage.toFixed(1)"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
|
||||||
import { expenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
|
||||||
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const expenses_store = useExpensesStore();
|
|
||||||
const expenses_api = useExpensesApi();
|
|
||||||
|
|
||||||
const expenses_list = computed(() => timesheet_store.pay_period_details.weeks.flatMap(week =>
|
|
||||||
Object.values(week.expenses).flatMap(day => day.expenses)));
|
|
||||||
|
|
||||||
const employee_email = inject('employeeEmail', '');
|
|
||||||
|
|
||||||
const setExpenseToModify = (expense: Expense) => {
|
|
||||||
expenses_store.mode = 'update';
|
|
||||||
expenses_store.current_expense = expense;
|
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestExpenseDeletion = async (expense: Expense) => {
|
|
||||||
expenses_store.mode = 'delete';
|
|
||||||
expenses_store.initial_expense = expense;
|
|
||||||
expenses_store.current_expense = default_expense;
|
|
||||||
await expenses_api.deleteExpenseByEmployeeEmail(employee_email, expenses_store.initial_expense.date);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- liste des dépenses pré existantes -->
|
|
||||||
<q-list
|
|
||||||
padding
|
|
||||||
class="rounded-borders"
|
|
||||||
>
|
|
||||||
<q-item-label
|
|
||||||
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
|
||||||
class="text-italic q-px-sm"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.expense.empty_list') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item
|
|
||||||
style="border: solid 1px lightgrey; border-radius: 7px;"
|
|
||||||
v-for="(expense, index) in expenses_list"
|
|
||||||
:key="index"
|
|
||||||
class="q-my-xs shadow-1"
|
|
||||||
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
|
||||||
>
|
|
||||||
<!-- avatar type icon section -->
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon
|
|
||||||
:name="expenseTypeIcon(expense.type)"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
|
||||||
<template v-if="typeof expense.mileage === 'number'">
|
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ expense.amount.toFixed(2) }} $
|
|
||||||
</template>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label v-else>
|
|
||||||
{{ expense.amount.toFixed(2) }} $
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<!-- date label -->
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' })
|
|
||||||
}}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="q-mx-lg"
|
|
||||||
icon="attach_file"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ expense.comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
v-if="expense.supervisor_comment"
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ expense.supervisor_comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section
|
|
||||||
v-if="!expenses_store.pay_period_expenses.is_approved && !expense.is_approved"
|
|
||||||
side
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
color="primary"
|
|
||||||
icon="edit"
|
|
||||||
@click="setExpenseToModify(expense)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
color="negative"
|
|
||||||
icon="close"
|
|
||||||
@click="requestExpenseDeletion(expense)"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
@ -2,40 +2,43 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
/* eslint-disable */
|
||||||
import { inject, ref } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { empty_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
import { 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';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
|
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
const employee_email = inject<string>('employeeEmail');
|
const employee_email = inject<string>('employeeEmail');
|
||||||
const rules = makeExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
const cancelUpdateMode = () => {
|
const cancelUpdateMode = () => {
|
||||||
expenses_store.current_expense = default_expense;
|
expenses_store.current_expense = empty_expense;
|
||||||
expenses_store.initial_expense = default_expense;
|
expenses_store.initial_expense = empty_expense;
|
||||||
expenses_store.mode = 'create';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date);
|
if (mode.value === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||||
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense.date);
|
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
flat
|
flat
|
||||||
v-if="!expenses_store.pay_period_expenses.is_approved"
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
>
|
>
|
||||||
<div class="text-subtitle2 q-py-sm">
|
<div class="text-subtitle2 q-py-sm">
|
||||||
|
|
@ -43,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="row justify-between rounded-5"
|
class="row justify-between rounded-5"
|
||||||
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
:class="mode === 'update' ? 'bg-accent' : ''"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
|
|
@ -87,11 +90,11 @@
|
||||||
map-options
|
map-options
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
:option-label="label => $t(label)"
|
:option-label="label => $t(`timesheet.expense.types.${label}`)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- amount input -->
|
<!-- amount input -->
|
||||||
<template v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense.type)">
|
<template v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')">
|
||||||
<q-input
|
<q-input
|
||||||
key="amount"
|
key="amount"
|
||||||
v-model.number="expenses_store.current_expense.amount"
|
v-model.number="expenses_store.current_expense.amount"
|
||||||
|
|
@ -174,7 +177,7 @@
|
||||||
<!-- add btn section -->
|
<!-- add btn section -->
|
||||||
<div>
|
<div>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="expenses_store.mode === 'update'"
|
v-if="mode === 'update'"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
44
src/modules/timesheets/components/expense-dialog-header.vue
Normal file
44
src/modules/timesheets/components/expense-dialog-header.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
/* eslint-disable */
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
|
||||||
|
const expense_store = useExpensesStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-item class="row justify-between items-center q-pa-none">
|
||||||
|
<q-item-label
|
||||||
|
header
|
||||||
|
class="text-h6 col q-pa-none"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.title') }}
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- <q-item-section
|
||||||
|
no-wrap
|
||||||
|
class="col-auto items-center"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
outline
|
||||||
|
class="q-py-xs q-px-md"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.total_amount') + ': $' + expense_store.pay_period_expenses?.toFixed(2)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section
|
||||||
|
no-wrap
|
||||||
|
class="col-auto items-center"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
outline
|
||||||
|
class="q-py-xs q-px-md"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses?.total_mileage.toFixed(1) + ' km'"
|
||||||
|
/>
|
||||||
|
</q-item-section> -->
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
203
src/modules/timesheets/components/expense-dialog-list-item.vue
Normal file
203
src/modules/timesheets/components/expense-dialog-list-item.vue
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
/* eslint-disable */
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
|
import { empty_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
|
const { expense, horizontal = false } = defineProps<{
|
||||||
|
expense: Expense;
|
||||||
|
index: number;
|
||||||
|
horizontal?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
|
const is_approved = defineModel<boolean>({ required: true });
|
||||||
|
const is_selected = ref(false);
|
||||||
|
const refresh_key = ref(1);
|
||||||
|
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
||||||
|
|
||||||
|
const expenseItemStyle = computed(() => is_approved.value ? 'border: solid 2px var(--q-primary);' : 'border: solid 2px grey;');
|
||||||
|
// const highlightClass = computed(() => (expenses_store.mode === 'update' && is_selected) ? 'bg-accent' : '');
|
||||||
|
const approvedClass = computed(() => horizontal ? ' q-mx-xs q-pa-xs cursor-pointer' : '')
|
||||||
|
|
||||||
|
|
||||||
|
const employeeEmail = inject<string>('employeeEmail') ?? '';
|
||||||
|
|
||||||
|
|
||||||
|
const setExpenseToModify = () => {
|
||||||
|
// expenses_store.mode = 'update';
|
||||||
|
expenses_store.current_expense = expense;
|
||||||
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestExpenseDeletion = async () => {
|
||||||
|
// expenses_store.mode = 'delete';
|
||||||
|
expenses_store.initial_expense = expense;
|
||||||
|
expenses_store.current_expense = empty_expense;
|
||||||
|
await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpenseClicked() {
|
||||||
|
if (is_authorized_to_approve.value) {
|
||||||
|
is_approved.value = !is_approved.value;
|
||||||
|
refresh_key.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated pulse"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<q-item
|
||||||
|
:key="refresh_key"
|
||||||
|
:clickable="horizontal"
|
||||||
|
class="row col-4 q-ma-xs shadow-2"
|
||||||
|
:style="expenseItemStyle + approvedClass"
|
||||||
|
@click="onExpenseClicked"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="expense.is_approved"
|
||||||
|
class="absolute z-top rounded-20 bg-dark q-pa-none"
|
||||||
|
style="transform: translate(-15px, -15px);"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="verified"
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<!-- avatar type icon section -->
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon
|
||||||
|
:name="getExpenseIcon(expense.type)"
|
||||||
|
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="expense.type === 'ON_CALL'"
|
||||||
|
floating
|
||||||
|
class="q-pa-none rounded-50 bg-white z-top"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="shield"
|
||||||
|
size="xs"
|
||||||
|
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- amount or mileage section -->
|
||||||
|
<q-item-section class="col-auto">
|
||||||
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
${{ expense.amount.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
${{ expense.amount.toFixed(2) }}
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- date label -->
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
lines="1"
|
||||||
|
>
|
||||||
|
<!-- {{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' }) }} -->
|
||||||
|
{{ expense.date }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-space v-if="horizontal" />
|
||||||
|
|
||||||
|
<!-- attachment file icon -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
class="q-mx-lg"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- comment section -->
|
||||||
|
<q-item-section
|
||||||
|
v-if="!horizontal"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
lines="1"
|
||||||
|
>
|
||||||
|
{{ expense.comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- supervisor comment section -->
|
||||||
|
<q-item-section
|
||||||
|
v-if="expense.supervisor_comment && !horizontal"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
v-if="expense.supervisor_comment"
|
||||||
|
caption
|
||||||
|
lines="2"
|
||||||
|
>
|
||||||
|
{{ expense.supervisor_comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section
|
||||||
|
side
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
icon="edit"
|
||||||
|
class="q-mb-xs z-top"
|
||||||
|
@click.stop="setExpenseToModify"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="negative"
|
||||||
|
icon="close"
|
||||||
|
class="z-top"
|
||||||
|
@click.stop="requestExpenseDeletion"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
39
src/modules/timesheets/components/expense-dialog-list.vue
Normal file
39
src/modules/timesheets/components/expense-dialog-list.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
||||||
|
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
|
||||||
|
const { horizontal = false } = defineProps<{
|
||||||
|
horizontal?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- liste des dépenses pré existantes -->
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
class="rounded-borders"
|
||||||
|
:class="horizontal ? 'row flex-center' : ''"
|
||||||
|
>
|
||||||
|
<q-item-label
|
||||||
|
v-if="expenses_store.pay_period_expenses?.length === 0"
|
||||||
|
class="text-italic q-px-sm"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.empty_list') }}
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<ExpenseDialogListItem
|
||||||
|
v-for="(expense, index) in expenses_store.pay_period_expenses"
|
||||||
|
:key="index"
|
||||||
|
v-model="expense.is_approved"
|
||||||
|
:index="index"
|
||||||
|
:expense="expense"
|
||||||
|
:horizontal="horizontal"
|
||||||
|
/>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
import ExpenseCrudDialogForm from 'src/modules/timesheets/components/expense-crud-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
import ExpenseCrudDialogHeader from 'src/modules/timesheets/components/expense-crud-dialog-header.vue';
|
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
const expense_store = useExpensesStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -32,11 +32,17 @@
|
||||||
{{ expenses_error }}
|
{{ expenses_error }}
|
||||||
</q-banner> -->
|
</q-banner> -->
|
||||||
|
|
||||||
<ExpenseCrudDialogHeader />
|
<ExpenseDialogHeader />
|
||||||
|
|
||||||
<ExpenseCrudDialogList />
|
<ExpenseDialogList />
|
||||||
|
|
||||||
<ExpenseCrudDialogForm />
|
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved" />
|
||||||
|
<q-icon
|
||||||
|
v-else
|
||||||
|
name="block"
|
||||||
|
color="negative"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-separator spaced />
|
<q-separator spaced />
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const shift = defineModel<Shift>({ required: true });
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'onCommentBlur': [void];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row full-width justify-center">
|
||||||
|
<div class="col-sm-6 col-md-3 row q-mx-xs q-my-none">
|
||||||
|
<div class="col-auto column items-center">
|
||||||
|
<span
|
||||||
|
class="text-caption q-pa-none q-ma-none"
|
||||||
|
style="line-height: 0.7em; font-size: 0.7em;"
|
||||||
|
>{{ $t('timesheet.shift.types.REMOTE') }}</span>
|
||||||
|
<q-toggle
|
||||||
|
v-model="shift.is_remote"
|
||||||
|
class="q-pa-none q-ma-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="shift.type"
|
||||||
|
options-dense
|
||||||
|
:options="SHIFT_TYPES"
|
||||||
|
:label="$t('timesheet.shift.types.label')"
|
||||||
|
class="col q-pa-none"
|
||||||
|
color="primary"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
square
|
||||||
|
hide-dropdown-icon
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto row q-mx-xs">
|
||||||
|
<q-input
|
||||||
|
v-model="shift.start_time"
|
||||||
|
:label="$t('timesheet.shift.fields.start')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
square
|
||||||
|
inputmode="numeric"
|
||||||
|
mask="##:##"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="shift.end_time"
|
||||||
|
:label="$t('timesheet.shift.fields.end')"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
square
|
||||||
|
inputmode="numeric"
|
||||||
|
mask="##:##"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="shift.comment"
|
||||||
|
type="textarea"
|
||||||
|
autogrow
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
square
|
||||||
|
:label="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
:counter="true"
|
||||||
|
:maxlength="512"
|
||||||
|
class="col-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useShiftStore } from 'src/stores/shift-store';
|
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/api/use-shift-api';
|
|
||||||
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
|
|
||||||
const { date_iso, mode, current_shift, is_open, close } = useShiftStore();
|
|
||||||
const { upsertOrDeleteShiftByEmployeeEmail } = useShiftApi();
|
|
||||||
|
|
||||||
const { employeeEmail } = defineProps<{
|
|
||||||
employeeEmail: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isSubmitting = ref(false);
|
|
||||||
const errorBanner = ref<string | null>(null);
|
|
||||||
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
|
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
|
||||||
mode === 'delete' ||
|
|
||||||
(current_shift.start_time.trim().length === 5 &&
|
|
||||||
current_shift.end_time.trim().length === 5 &&
|
|
||||||
current_shift.type !== undefined)
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model=" is_open"
|
|
||||||
persistent
|
|
||||||
transition-show="fade"
|
|
||||||
transition-hide="fade"
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-card class="q-pa-md">
|
|
||||||
<div class="row items-center q-mb-sm">
|
|
||||||
<q-icon
|
|
||||||
name="schedule"
|
|
||||||
size="24px"
|
|
||||||
class="q-mr-sm"
|
|
||||||
/>
|
|
||||||
<div class="text-h6">
|
|
||||||
{{
|
|
||||||
mode === 'create'
|
|
||||||
? $t('timesheet.shift.actions.add')
|
|
||||||
: mode === 'update'
|
|
||||||
? $t('timesheet.shift.actions.edit')
|
|
||||||
: $t('timesheet.shift.actions.delete')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<q-space />
|
|
||||||
<q-badge
|
|
||||||
outline
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{{ date_iso }}
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="mode !== 'delete'"
|
|
||||||
class="column q-gutter-md"
|
|
||||||
>
|
|
||||||
<div class="row ">
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.start_time"
|
|
||||||
:label="$t('timesheet.shift.fields.start')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.end_time"
|
|
||||||
:label="$t('timesheet.shift.fields.end')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-select
|
|
||||||
v-model="current_shift.type"
|
|
||||||
options-dense
|
|
||||||
:options="SHIFT_TYPES"
|
|
||||||
:label="$t('timesheet.shift.types.label')"
|
|
||||||
class="col"
|
|
||||||
color="primary"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
hide-dropdown-icon
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
<q-toggle
|
|
||||||
v-model="current_shift.is_remote"
|
|
||||||
:label="$t('timesheet.shift.types.REMOTE')"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.comment"
|
|
||||||
type="textarea"
|
|
||||||
autogrow
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:label="$t('timesheet.shift.fields.header_comment')"
|
|
||||||
:counter="true"
|
|
||||||
:maxlength="512"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="q-pa-md"
|
|
||||||
>
|
|
||||||
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="errorBanner"
|
|
||||||
class="q-mt-md"
|
|
||||||
>
|
|
||||||
<q-banner
|
|
||||||
dense
|
|
||||||
class="bg-red-2 text-negative"
|
|
||||||
>{{ errorBanner }}</q-banner>
|
|
||||||
<div
|
|
||||||
v-if="conflicts.length"
|
|
||||||
class="q-mt-xs"
|
|
||||||
>
|
|
||||||
<div class="text-caption">Conflits :</div>
|
|
||||||
<ul class="q-pl-md q-mt-xs">
|
|
||||||
<li
|
|
||||||
v-for="(c, i) in conflicts"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
{{ c.start_time }}–{{ c.end_time }} ({{ c.type }})
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator spaced />
|
|
||||||
|
|
||||||
<div class="row justify-end q-gutter-sm">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
color="grey-8"
|
|
||||||
:label="$t('timesheet.cancel_button')"
|
|
||||||
@click="close"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
icon="save_alt"
|
|
||||||
:label="mode === 'delete' ? $t('timesheet.delete_button') : $t('timesheet.save_button')"
|
|
||||||
:loading="isSubmitting"
|
|
||||||
:disable="!canSubmit"
|
|
||||||
@click="upsertOrDeleteShiftByEmployeeEmail(employeeEmail)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card-section
|
|
||||||
horizontal
|
|
||||||
class="text-uppercase text-center items-center q-pa-none"
|
|
||||||
>
|
|
||||||
<!-- shift row itself -->
|
|
||||||
<q-card-section class="col q-pa-none">
|
|
||||||
<q-card-section horizontal class="col q-pa-none">
|
|
||||||
<!-- punch-in timestamps -->
|
|
||||||
<q-card-section class="col q-pa-none">
|
|
||||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
|
||||||
{{ $t('shared.misc.in') }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- arrows pointing to punch-out timestamps -->
|
|
||||||
<q-card-section class="col q-py-none q-px-sm">
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- punch-out timestamps -->
|
|
||||||
<q-card-section class="col q-pa-none">
|
|
||||||
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
|
|
||||||
{{ $t('shared.misc.out') }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- comment button -->
|
|
||||||
<q-card-section class="col column q-pa-none">
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</template>
|
|
||||||
|
|
@ -10,10 +10,9 @@
|
||||||
const is_showing_legend = ref(false);
|
const is_showing_legend = ref(false);
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
const legend: ShiftLegendItem[] = [
|
||||||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR', text_color: 'grey-8' },
|
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
|
||||||
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
||||||
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
||||||
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
|
|
||||||
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
|
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
|
||||||
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
|
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
|
||||||
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
|
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
|
||||||
|
|
@ -35,7 +34,7 @@
|
||||||
dense
|
dense
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col-auto q-ma-sm"
|
class="col-auto q-my-sm"
|
||||||
@click="is_showing_legend = !is_showing_legend"
|
@click="is_showing_legend = !is_showing_legend"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|
@ -55,7 +54,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="is_showing_legend"
|
v-if="is_showing_legend"
|
||||||
class="q-pa-xs bg-white rounded-5 shadow-2 text-center q-ma-xs"
|
class="q-py-xs bg-white rounded-5 shadow-2 text-center q-my-xs"
|
||||||
>
|
>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-for="shift_type in shift_type_legend"
|
v-for="shift_type in shift_type_legend"
|
||||||
|
|
@ -63,7 +62,7 @@
|
||||||
:color="shift_type.color"
|
:color="shift_type.color"
|
||||||
:label="shift_type.label"
|
:label="shift_type.label"
|
||||||
:text-color="shift_type.text_color || 'white'"
|
:text-color="shift_type.text_color || 'white'"
|
||||||
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
class="q-pa-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||||
style="font-size: 0.8em;"
|
style="font-size: 0.8em;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,197 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import { computed } from 'vue';
|
setup
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
lang="ts"
|
||||||
|
>
|
||||||
|
/* eslint-disable*/
|
||||||
|
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { QSelect } from 'quasar';
|
||||||
|
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const ui_store = useUiStore();
|
||||||
|
|
||||||
const { shift, dense = false } = defineProps<{
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
shift: Shift;
|
|
||||||
|
const { dense = false } = defineProps<{
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
'save-comment': [comment: string, shift: Shift];
|
'saveComment': [comment: string, shift_id: number];
|
||||||
'request-update': [shift: Shift];
|
'requestUpdate': [shift_id: number];
|
||||||
'request-delete': [shift: Shift];
|
'requestDelete': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const has_comment = computed(() => {
|
const time_picker_model = ref('');
|
||||||
const comment = shift.comment ?? '';
|
const is_showing_time_picker = ref(false);
|
||||||
return typeof comment === 'string' && comment.trim().length > 0;
|
const select_ref = useTemplateRef<QSelect>('select');
|
||||||
})
|
|
||||||
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
|
||||||
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
|
||||||
const hour_font_size = computed(() => dense ? '0.9em' : '1.5em' )
|
|
||||||
|
|
||||||
const get_shift_color = (type: string): string => {
|
|
||||||
switch (type) {
|
|
||||||
case 'REGULAR': return 'secondary';
|
const options: { label: string, value: ShiftType, icon: string, icon_color: string }[] = [
|
||||||
case 'EVENING': return 'warning';
|
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
||||||
case 'EMERGENCY': return 'amber-10';
|
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||||
case 'OVERTIME': return 'negative';
|
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||||
case 'VACATION': return 'purple-10';
|
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||||
case 'HOLIDAY': return 'purple-5';
|
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||||
case 'SICK': return 'grey-8';
|
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
||||||
default: return 'transparent';
|
];
|
||||||
}
|
|
||||||
|
const shift_type_selected = ref(options.find(option => option.value == shift.value.type));
|
||||||
|
|
||||||
|
const showTimePicker = (time: string) => {
|
||||||
|
is_showing_time_picker.value = true;
|
||||||
|
time_picker_model.value = time;
|
||||||
};
|
};
|
||||||
|
|
||||||
const get_text_color = (type: string): string => {
|
onMounted(() => {
|
||||||
switch (type) {
|
if (ui_store.focus_next_component) {
|
||||||
case 'REGULAR': return 'grey-8';
|
select_ref.value?.focus();
|
||||||
case '': return 'grey-5';
|
select_ref.value?.showPopup();
|
||||||
default: return 'white';
|
ui_store.focus_next_component = false;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
const onClickUpdate = (type: string) => {
|
|
||||||
if (type !== '') { emit('request-update', shift) };
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickDelete = () => emit('request-delete', shift);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card-section
|
<div
|
||||||
horizontal
|
v-if="shift.shift_id !== 0"
|
||||||
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
class="col row flex-center text-uppercase rounded-10"
|
||||||
:class="shift.type"
|
|
||||||
style="line-height: 1;"
|
|
||||||
@click.stop="onClickUpdate(shift.type)"
|
|
||||||
>
|
>
|
||||||
<!-- punch-in timestamps -->
|
<!-- shift type -->
|
||||||
<q-card-section class="q-pa-none col">
|
<q-select
|
||||||
<q-item-label
|
ref="select"
|
||||||
class="text-weight-bolder q-pa-xs rounded-5"
|
v-model="shift_type_selected"
|
||||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
standout="bg-blue-grey-9"
|
||||||
:style="'font-size: ' + hour_font_size + '; line-height: 80% !important;'"
|
dense
|
||||||
>
|
options-dense
|
||||||
{{ shift.start_time }}
|
hide-dropdown-icon
|
||||||
</q-item-label>
|
:menu-offset="[0, 10]"
|
||||||
</q-card-section>
|
:options="options"
|
||||||
|
class="rounded-5 q-mx-xs shadow-1"
|
||||||
<!-- arrows pointing to punch-out timestamps -->
|
:class="ui_store.is_mobile_mode ? 'col-auto' : 'col'"
|
||||||
<q-card-section
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
horizontal
|
popup-content-style="border: 2px solid var(--q-primary)"
|
||||||
class="items-center justify-center q-mx-sm col"
|
|
||||||
>
|
>
|
||||||
<div
|
<template #selected-item="scope">
|
||||||
v-for="icon_data, index in [
|
<div
|
||||||
{ transform: 'transform: translateX(5px);', color: 'accent' },
|
class="row text-weight-bold q-ma-none q-pa-none no-wrap ellipsis"
|
||||||
{ transform: 'transform: translateX(-5px);', color: 'primary' }]"
|
:class="ui_store.is_mobile_mode ? 'items-center' : 'flex-center'"
|
||||||
:key="index"
|
:tabindex="scope.tabindex"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="shift.type"
|
:name="scope.opt.icon"
|
||||||
name="double_arrow"
|
:color="scope.opt.icon_color"
|
||||||
:color="icon_data.color"
|
size="sm"
|
||||||
size="24px"
|
class="col-auto q-mx-xs"
|
||||||
:style="icon_data.transform"
|
/>
|
||||||
|
<span
|
||||||
|
v-if="$q.screen.gt.md"
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
class="col ellipsis"
|
||||||
|
>{{ scope.opt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<!-- punch-in timestamp -->
|
||||||
|
<q-input
|
||||||
|
v-model="shift.start_time"
|
||||||
|
dense
|
||||||
|
type="time"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
label-slot
|
||||||
|
label-color="primary"
|
||||||
|
input-class="text-weight-medium"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
class="col q-mx-xs"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.in') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<q-btn
|
||||||
|
v-if="ui_store.is_mobile_mode"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="access_time"
|
||||||
|
color="primary"
|
||||||
|
@click.stop="showTimePicker(shift.start_time)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</q-card-section>
|
</q-input>
|
||||||
|
|
||||||
<!-- punch-out timestamps -->
|
<!-- punch-out timestamps -->
|
||||||
<q-card-section class="q-pa-none col">
|
<q-input
|
||||||
<q-item-label
|
v-model="shift.end_time"
|
||||||
class="text-weight-bolder text-white q-pa-xs rounded-5"
|
dense
|
||||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
type="time"
|
||||||
style="font-size: 1.5em; line-height: 80% !important;"
|
standout="bg-blue-grey-9"
|
||||||
>
|
label-slot
|
||||||
{{ shift.end_time }}
|
label-color="primary"
|
||||||
</q-item-label>
|
input-class="text-weight-medium"
|
||||||
</q-card-section>
|
input-style="font-size: 1.2em;"
|
||||||
|
class="col q-mx-xs"
|
||||||
|
lazy-rules
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- comment and expenses buttons -->
|
<template #append>
|
||||||
<q-card-section class="col q-pa-none text-right">
|
<q-btn
|
||||||
<!-- comment btn -->
|
v-if="ui_store.is_mobile_mode"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="access_time"
|
||||||
|
color="primary"
|
||||||
|
@click="showTimePicker(shift.end_time)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- comment and delete buttons -->
|
||||||
|
<div
|
||||||
|
v-if="$q.screen.gt.sm"
|
||||||
|
class="col-auto"
|
||||||
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="shift.type"
|
v-if="shift.type && dense"
|
||||||
:name="comment_icon"
|
:name="shift.comment ? 'comment' : ''"
|
||||||
:color="comment_color"
|
color="primary"
|
||||||
class="q-pa-none q-mx-xs"
|
:size="dense ? 'xs' : 'sm'"
|
||||||
size="sm"
|
class="col-auto q-pa-none q-mr-xs"
|
||||||
/>
|
/>
|
||||||
<!-- expenses btn -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="shift.type"
|
v-else
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
color='grey-8'
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
icon="attach_money"
|
:text-color="shift.comment ? 'primary' : 'grey-8'"
|
||||||
class="q-pa-none q-mx-xs"
|
class="col-auto q-ma-none q-pl-md full-height"
|
||||||
/>
|
/>
|
||||||
<!-- delete btn -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="shift.type"
|
|
||||||
push
|
|
||||||
dense
|
dense
|
||||||
size="sm"
|
flat
|
||||||
color="red-6"
|
round
|
||||||
icon="close"
|
unelevated
|
||||||
class="q-ml-xs"
|
tabindex="-1"
|
||||||
@click.stop="onClickDelete"
|
icon="cancel"
|
||||||
|
color="negative"
|
||||||
|
class="q-pa-none q-mr-xs"
|
||||||
|
@click="$emit('requestDelete')"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</div>
|
||||||
</q-card-section>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -3,96 +3,122 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import ShiftListHeader from 'src/modules/timesheets/components/shift-list-header.vue';
|
import { computed } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
||||||
import { 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 { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { computed } from 'vue';
|
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
|
||||||
|
const q = useQuasar();
|
||||||
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const { openCreate, openDelete, openUpdate } = useShiftStore();
|
const shift_api = useShiftApi();
|
||||||
|
|
||||||
const { dense = false } = defineProps<{
|
const { dense = false } = defineProps<{
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const font_size = computed(() => dense ? '1.5em' : '2.5em')
|
const is_mobile = computed(() => q.screen.lt.md);
|
||||||
|
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||||
|
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||||
|
const date_box_size = computed(() => dense || is_mobile.value ? 'width: 40px; height: 75px;' : 'width: 75px; height: 75px;');
|
||||||
|
|
||||||
const get_date_from_short = (short_date: string): Date => {
|
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
ui_store.focus_next_component = true;
|
||||||
|
const new_shift = new Shift;
|
||||||
|
new_shift.date = date;
|
||||||
|
new_shift.timesheet_id = timesheet_id;
|
||||||
|
day_shifts.push(new_shift);
|
||||||
};
|
};
|
||||||
|
|
||||||
const to_iso_date = (short_date: string): string => {
|
const deleteCurrentShift = async (shift: Shift) => {
|
||||||
return date.formatDate(get_date_from_short(short_date), 'YYYY-MM-DD');
|
console.log('shift to delete: ', shift);
|
||||||
};
|
if (shift.shift_id < 0) {
|
||||||
|
shift.shift_id = 0;
|
||||||
const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
|
return;
|
||||||
return shifts.length > 0 ? shifts : [default_shift];
|
}
|
||||||
};
|
await shift_api.deleteShiftById(shift.shift_id);
|
||||||
|
}
|
||||||
const getDate = (shift_date: string): Date => {
|
|
||||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + shift_date);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||||
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
<div
|
||||||
:key="index"
|
v-for="timesheet in timesheet_store.timesheets"
|
||||||
class="col q-px-xs q-pt-xs q-mx-sm rounded-5"
|
:key="timesheet.timesheet_id"
|
||||||
>
|
class="col column"
|
||||||
<q-card
|
|
||||||
v-for="day, day_index in week.shifts"
|
|
||||||
:key="day_index + index"
|
|
||||||
class="row items-center rounded-10 q-mb-xs"
|
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
<!-- Dates column -->
|
v-for="day in timesheet.days"
|
||||||
<q-card-section class="col-auto q-pa-xs text-white q-mr-md">
|
:key="day.date"
|
||||||
|
class="col-auto row shadow-2 rounded-10 q-ma-xs"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-primary rounded-10 q-pa-xs text-center"
|
class="col row bg-dark"
|
||||||
:style="'width: ' + dense? '60px' : '75px;'"
|
style="border-radius: 10px 0 0 10px;"
|
||||||
>
|
>
|
||||||
<q-item-label
|
<!-- Dates column -->
|
||||||
style="font-size: 0.7em;"
|
<div
|
||||||
class="text-uppercase"
|
class="col-auto column flex-center bg-primary rounded-10 text-center q-ma-sm self-center"
|
||||||
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
:class="$q.screen.lt.md ? '' : ''"
|
||||||
<q-item-label
|
:style="date_box_size"
|
||||||
class="text-weight-bolder"
|
>
|
||||||
:style="'font-size: ' + font_size + '; line-height: 90% !important;'"
|
<span
|
||||||
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
v-if="!dense"
|
||||||
<q-item-label
|
class="col-auto text-uppercase text-white"
|
||||||
style="font-size: 0.7em;"
|
:style="'font-size: ' + weekday_font_size"
|
||||||
class="text-uppercase"
|
>
|
||||||
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||||
|
weekday: $q.screen.lt.md ? 'short' :
|
||||||
|
'long'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="col-auto text-weight-bolder text-grey-1"
|
||||||
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
|
>
|
||||||
|
{{ date.extractDate(day.date, 'YYYY-MM-DD').getDate() }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="col-auto text-uppercase text-white"
|
||||||
|
:style="'font-size: ' + weekday_font_size"
|
||||||
|
>
|
||||||
|
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
||||||
|
month: $q.screen.lt.md ? 'short' : 'long'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of shifts column -->
|
||||||
|
<div class="col column">
|
||||||
|
<ShiftListRow
|
||||||
|
v-for="shift, shift_index in day.shifts"
|
||||||
|
:key="shift_index"
|
||||||
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
|
:dense="dense"
|
||||||
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<div class="col-auto self-stretch">
|
||||||
<q-card-section class="col q-pa-none">
|
<q-btn
|
||||||
<ShiftListHeader v-if="day.shifts.length > 0"/>
|
unelevated
|
||||||
<div
|
icon="more_time"
|
||||||
v-if="day.shifts.length > 0"
|
:size="$q.screen.lt.md ? 'md' : 'lg'"
|
||||||
>
|
color="primary"
|
||||||
<ShiftListRow
|
text-color="white"
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
class="full-height"
|
||||||
:key="shift_index"
|
:class="$q.screen.lt.md ? 'q-px-xs' : ''"
|
||||||
:shift="shift"
|
style="border-radius: 0 10px 10px 0;"
|
||||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</div>
|
||||||
<!-- add shift btn column -->
|
</div>
|
||||||
<q-card-section class="q-pr-xs col-auto">
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
color="primary"
|
|
||||||
icon="more_time"
|
|
||||||
class="q-pa-sm"
|
|
||||||
@click="openCreate(to_iso_date(day.short_date))"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -3,86 +3,107 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
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 ExpenseDialog from 'src/modules/timesheets/components/expense-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';
|
||||||
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
|
||||||
const { open } = useExpensesStore();
|
const { open } = useExpensesStore();
|
||||||
|
const shift_api = useShiftApi();
|
||||||
|
|
||||||
const { employeeEmail, dense = false } = defineProps<{
|
const { employeeEmail, dense = false } = defineProps<{
|
||||||
employeeEmail: string;
|
employeeEmail: string;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { is_loading } = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const { getPayPeriodDetailsByDate, getPreviousPayPeriodDetails, getNextPayPeriodDetails } = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
provide('employeeEmail', employeeEmail);
|
provide('employeeEmail', employeeEmail);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card
|
<div class="column flex-center full-width">
|
||||||
flat
|
|
||||||
class="q-mt-md bg-secondary full-width"
|
|
||||||
>
|
|
||||||
<q-inner-loading
|
|
||||||
:showing="is_loading"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-card-section
|
<q-dialog
|
||||||
:horizontal="$q.screen.gt.sm"
|
v-model="timesheet_store.is_loading"
|
||||||
class="q-px-lg items-center"
|
transition-show="jump-down"
|
||||||
:class="$q.screen.lt.md ? 'column' : ''"
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
<!-- navigation btn -->
|
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
|
||||||
<PayPeriodNavigator
|
<q-spinner-radio
|
||||||
@date-selected="getPayPeriodDetailsByDate"
|
color="primary"
|
||||||
@pressed-previous-button="getPreviousPayPeriodDetails"
|
size="20vh"
|
||||||
@pressed-next-button="getNextPayPeriodDetails"
|
/>
|
||||||
/>
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<!-- mobile expenses button -->
|
<q-card
|
||||||
<q-btn
|
flat
|
||||||
v-if="$q.screen.lt.md"
|
class="transparent full-width"
|
||||||
push
|
>
|
||||||
rounded
|
<q-card-section
|
||||||
color="primary"
|
v-if="!dense"
|
||||||
icon="receipt_long"
|
:horizontal="$q.screen.gt.sm"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
class="q-px-md items-center q-mb-md"
|
||||||
class="q-mt-sm"
|
:class="$q.screen.lt.md ? 'column' : ''"
|
||||||
@click="open(employeeEmail)"
|
>
|
||||||
/>
|
<!-- navigation btn -->
|
||||||
|
<PayPeriodNavigator
|
||||||
|
v-if="!dense"
|
||||||
|
@date-selected="timesheet_api.getTimesheetsByDate(employeeEmail)"
|
||||||
|
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||||
|
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- shift's colored legend -->
|
<!-- mobile expenses button -->
|
||||||
<ShiftListLegend :is-loading="false" />
|
<q-btn
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
icon="receipt_long"
|
||||||
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
|
class="q-mt-sm"
|
||||||
|
@click="open"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-space />
|
<!-- shift's colored legend -->
|
||||||
|
<!-- <ShiftListLegend :is-loading="false" /> -->
|
||||||
|
|
||||||
<!-- desktop expenses button -->
|
<q-space />
|
||||||
<q-btn
|
<!-- save timesheet changes button -->
|
||||||
v-if="$q.screen.gt.sm"
|
<q-btn
|
||||||
push
|
v-if="$q.screen.gt.sm"
|
||||||
rounded
|
push
|
||||||
color="primary"
|
rounded
|
||||||
icon="receipt_long"
|
:disable="timesheet_store.is_loading"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
color="primary"
|
||||||
@click="open(employeeEmail)"
|
icon="upload"
|
||||||
/>
|
:label="$t('shared.label.save')"
|
||||||
|
class="q-mr-md"
|
||||||
|
@click="shift_api.saveShiftChanges"
|
||||||
|
/>
|
||||||
|
|
||||||
</q-card-section>
|
<!-- desktop expenses button -->
|
||||||
|
<q-btn
|
||||||
|
v-if="$q.screen.gt.sm"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
color="primary"
|
||||||
|
icon="receipt_long"
|
||||||
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
|
@click="open"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-card-section :horizontal="$q.screen.gt.sm">
|
</q-card-section>
|
||||||
<ShiftList :dense="dense"/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<ExpenseCrudDialog />
|
<ShiftList :dense="dense" />
|
||||||
|
</q-card>
|
||||||
<ShiftCrudDialog :employee-email="employeeEmail" />
|
<ExpenseDialog />
|
||||||
|
</div>
|
||||||
</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: false,
|
|
||||||
is_remote: shift.is_remote,
|
|
||||||
comment: comment,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseHHMM = (s: string): [number, number] => {
|
|
||||||
const m = /^(\d{2}):(\d{2})$/.exec(s);
|
|
||||||
|
|
||||||
if (!m) {
|
|
||||||
throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const h = Number(m[1]);
|
|
||||||
const min = Number(m[2]);
|
|
||||||
|
|
||||||
if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) {
|
|
||||||
throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` })
|
|
||||||
}
|
|
||||||
return [h, min];
|
|
||||||
};
|
|
||||||
|
|
||||||
const toMinutes = (hhmm: string): number => {
|
|
||||||
const [h, m] = parseHHMM(hhmm);
|
|
||||||
return h * 60 + m;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => {
|
|
||||||
if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) {
|
|
||||||
throw new GenericApiError({
|
|
||||||
status_code: 400,
|
|
||||||
message: `Invalid time format in ${label}. Expected HH:MM`,
|
|
||||||
context: { [label]: shift }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) {
|
|
||||||
throw new GenericApiError({
|
|
||||||
status_code: 400,
|
|
||||||
message: `Invalid time range in ${label}. The End time must be after the Start time`,
|
|
||||||
context: { [label]: shift }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
|
||||||
const flat_upsert_shift: UpsertShift = {
|
|
||||||
...(deepEqual(shift_store.initial_shift, default_shift) ? { old_shift: unwrapAndClone(shift_store.initial_shift) } : {}),
|
|
||||||
...(deepEqual(shift_store.current_shift, default_shift) ? { new_shift: unwrapAndClone(shift_store.current_shift) } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalized_upsert_shift: UpsertShift = {
|
|
||||||
...(flat_upsert_shift.old_shift ? { old_shift: normalizeShiftPayload(flat_upsert_shift.old_shift) } : {}),
|
|
||||||
...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift');
|
|
||||||
if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift');
|
|
||||||
|
|
||||||
await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
upsertOrDeleteShiftByEmployeeEmail,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { useAuthStore } from "src/stores/auth-store";
|
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
|
||||||
|
|
||||||
export const useTimesheetApi = () => {
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const auth_store = useAuthStore();
|
|
||||||
const NEXT = 1;
|
|
||||||
const PREVIOUS = -1;
|
|
||||||
|
|
||||||
const getPayPeriodDetailsByDate = async (date_string: string, employee_email?: string) => {
|
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNextOrPreviousPayPeriodDetails = async (direction: number, employee_email?: string) => {
|
|
||||||
const { pay_period } = timesheet_store;
|
|
||||||
let new_number = pay_period.pay_period_no + direction;
|
|
||||||
let new_year = pay_period.pay_year;
|
|
||||||
|
|
||||||
if (new_number > 26) {
|
|
||||||
new_number = 1;
|
|
||||||
new_year += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_number < 1) {
|
|
||||||
new_number = 26;
|
|
||||||
new_year -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(new_year, new_number);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextPayPeriodDetails = async (employee_email?: string) => {
|
|
||||||
await getNextOrPreviousPayPeriodDetails(NEXT, employee_email ?? auth_store.user.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPreviousPayPeriodDetails = async (employee_email?: string) => {
|
|
||||||
await getNextOrPreviousPayPeriodDetails(PREVIOUS, employee_email ?? auth_store.user.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getPayPeriodDetailsByDate,
|
|
||||||
getNextPayPeriodDetails,
|
|
||||||
getPreviousPayPeriodDetails,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
28
src/modules/timesheets/composables/use-expense-api.ts
Normal file
28
src/modules/timesheets/composables/use-expense-api.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import { normalizeObject } from "src/utils/normalize-object";
|
||||||
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
|
import { expense_validation_schema } from "src/modules/timesheets/models/expense-validation.models";
|
||||||
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
|
export const useExpensesApi = () => {
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
|
||||||
|
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
|
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
|
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
|
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createExpenseByEmployeeEmail,
|
||||||
|
updateExpenseByEmployeeEmail,
|
||||||
|
deleteExpenseByEmployeeEmail,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { ref, computed } from "vue";
|
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
|
||||||
import type { ExpenseType } from "src/modules/timesheets/models/expense.models";
|
|
||||||
|
|
||||||
export const useExpenseDraft = (initial?: Partial<Expense>) => {
|
|
||||||
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
|
||||||
|
|
||||||
const draft = ref<Partial<Expense>>({
|
|
||||||
date: '',
|
|
||||||
type: DEFAULT_TYPE,
|
|
||||||
comment: '',
|
|
||||||
...(initial ?? {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
draft.value = {
|
|
||||||
date: '',
|
|
||||||
type: DEFAULT_TYPE,
|
|
||||||
comment: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setType = (value: ExpenseType) => {
|
|
||||||
draft.value.type = value;
|
|
||||||
if(value === 'MILEAGE') {
|
|
||||||
delete draft.value.amount;
|
|
||||||
} else {
|
|
||||||
delete draft.value.mileage;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
|
||||||
const showAmount = computed(()=> !showMileage.value);
|
|
||||||
|
|
||||||
return { draft, setType, reset, showMileage, showAmount };
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { ref } from "vue";
|
|
||||||
import type { QForm } from "quasar"
|
|
||||||
|
|
||||||
|
|
||||||
export const useExpenseForm = () => {
|
|
||||||
const formRef = ref<QForm | null>(null);
|
|
||||||
const triedSubmit = ref(false);
|
|
||||||
|
|
||||||
const validateAnd = async (fn: ()=> void | Promise<void>) => {
|
|
||||||
triedSubmit.value = true;
|
|
||||||
const ok = await formRef.value?.validate(true);
|
|
||||||
if(!ok) return false;
|
|
||||||
await fn();
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
formRef,
|
|
||||||
validateAnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
// import { ref, type Ref } from "vue";
|
|
||||||
// import { normalizeObject } from "src/utils/normalize-object";
|
|
||||||
// import { normExpenseType } from "../utils/expense.util";
|
|
||||||
// import type { Expense, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
|
||||||
// import { useExpensesStore } from "src/stores/expense-store";
|
|
||||||
// import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
|
||||||
// import { expense_validation_schema } from "src/modules/timesheets/models/expense.validation";
|
|
||||||
|
|
||||||
// const expenses_store = useExpensesStore();
|
|
||||||
|
|
||||||
// export const useExpenseItems = () => {
|
|
||||||
// let expenses = unwrapAndClone(expenses_store.pay_period_expenses.expenses.map(normalizeExpense));
|
|
||||||
|
|
||||||
// const normalizePayload = (expense: Expense): Expense => {
|
|
||||||
// const exp = normalizeObject(expense, expense_validation_schema);
|
|
||||||
// const out: Expense = {
|
|
||||||
// date: exp.date,
|
|
||||||
// type: exp.type as ExpenseType,
|
|
||||||
// comment: exp.comment || '',
|
|
||||||
// };
|
|
||||||
// if(typeof exp.amount === 'number') out.amount = exp.amount;
|
|
||||||
// if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
|
|
||||||
// return out;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const addFromDraft = () => {
|
|
||||||
// const candidate: Expense = normalizeExpense({
|
|
||||||
// date: draft.date,
|
|
||||||
// type: normExpenseType(draft.type),
|
|
||||||
// ...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
|
|
||||||
// ...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
|
|
||||||
// comment: String(draft.comment ?? '').trim(),
|
|
||||||
// } as Expense);
|
|
||||||
|
|
||||||
// validateExpenseUI(candidate, 'expense_draft');
|
|
||||||
// expenses = [ ...expenses, candidate];
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const removeAt = (index: number) => {
|
|
||||||
// if(index < 0 || index >= expenses.length) return;
|
|
||||||
// expenses = expenses.filter((_,i)=> i !== index);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const validateAll = () => {
|
|
||||||
// for (const expense of expenses) {
|
|
||||||
// validateExpenseUI(expense, 'expense_item');
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const payload = () => expenses.map(normalizeExpense);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// expenses,
|
|
||||||
// addFromDraft,
|
|
||||||
// removeAt,
|
|
||||||
// validateAll,
|
|
||||||
// payload,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
39
src/modules/timesheets/composables/use-shift-api.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
|
import { useShiftStore } from "src/stores/shift-store";
|
||||||
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
|
|
||||||
|
export const useShiftApi = () => {
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const shift_store = useShiftStore();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
|
||||||
|
const deleteShiftById = async (shift_id: number) => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
const success = await shift_store.deleteShiftById(shift_id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveShiftChanges = async () => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
const create_success = await shift_store.createNewShifts();
|
||||||
|
|
||||||
|
if (create_success) {
|
||||||
|
const update_success = await shift_store.updateShifts();
|
||||||
|
|
||||||
|
if (update_success) {
|
||||||
|
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteShiftById,
|
||||||
|
saveShiftChanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
38
src/modules/timesheets/composables/use-timesheet-api.ts
Normal file
38
src/modules/timesheets/composables/use-timesheet-api.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
|
import { useTimesheetStore } from "src/stores/timesheet-store"
|
||||||
|
|
||||||
|
export const useTimesheetApi = () => {
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
|
||||||
|
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
|
||||||
|
if (timesheet_store.pay_period === undefined) return false;
|
||||||
|
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no );
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? '');
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getTimesheetsByDate,
|
||||||
|
getTimesheetsByCurrentPayPeriod,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
|
|
@ -43,6 +43,7 @@ export class ExpensesApiError extends ApiError {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expense_validation_schema: Normalizer<Expense> = {
|
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,37 +1,22 @@
|
||||||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'PRIME_GARDE';
|
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'ON_CALL';
|
||||||
|
|
||||||
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'PRIME_GARDE',];
|
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'ON_CALL',];
|
||||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE',];
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||||
|
|
||||||
export interface Expense {
|
export interface Expense {
|
||||||
date: string;
|
id: number;
|
||||||
type: ExpenseType;
|
date: string; //YYYY-MM-DD
|
||||||
amount: number;
|
type: ExpenseType;
|
||||||
mileage?: number;
|
amount: number;
|
||||||
comment: string;
|
mileage?: number;
|
||||||
|
comment: string;
|
||||||
supervisor_comment?: string;
|
supervisor_comment?: string;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
export type ExpenseTotals = {
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
reimburseable_total?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PayPeriodExpenses {
|
export const empty_expense: Expense = {
|
||||||
is_approved: boolean;
|
id: -1,
|
||||||
expenses: Expense[];
|
|
||||||
totals?: ExpenseTotals;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpense {
|
|
||||||
old_expense: Expense;
|
|
||||||
new_expense: Expense;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_expense: Expense = {
|
|
||||||
date: '',
|
date: '',
|
||||||
type: 'EXPENSES',
|
type: 'EXPENSES',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
|
@ -39,7 +24,24 @@ export const default_expense: Expense = {
|
||||||
is_approved: false,
|
is_approved: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const default_pay_period_expenses: PayPeriodExpenses = {
|
export const test_expenses: Expense[] = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
date: '2025-01-06',
|
||||||
|
type: 'EXPENSES',
|
||||||
|
amount: 15.5,
|
||||||
|
comment: 'Lunch receipt',
|
||||||
is_approved: false,
|
is_approved: false,
|
||||||
expenses: [],
|
},
|
||||||
}
|
{
|
||||||
|
id: 202,
|
||||||
|
date: '2025-01-07',
|
||||||
|
type: 'MILEAGE',
|
||||||
|
amount: 0,
|
||||||
|
mileage: 32.4,
|
||||||
|
comment: 'Travel to client site',
|
||||||
|
is_approved: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import type { Shift } from "./shift.models";
|
|
||||||
import { default_expense, type Expense } from "src/modules/timesheets/models/expense.models";
|
|
||||||
|
|
||||||
export type Week<T> = {
|
|
||||||
sun: T;
|
|
||||||
mon: T;
|
|
||||||
tue: T;
|
|
||||||
wed: T;
|
|
||||||
thu: T;
|
|
||||||
fri: T;
|
|
||||||
sat: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PayPeriodDetails {
|
|
||||||
weeks: PayPeriodDetailsWeek[];
|
|
||||||
employee_full_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayPeriodDetailsWeek {
|
|
||||||
is_approved: boolean;
|
|
||||||
shifts: Week<PayPeriodDetailsWeekDayShifts>
|
|
||||||
expenses: Week<PayPeriodDetailsWeekDayExpenses>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayPeriodDetailsWeekDayShifts {
|
|
||||||
shifts: Shift[];
|
|
||||||
regular_hours: number;
|
|
||||||
evening_hours: number;
|
|
||||||
emergency_hours: number;
|
|
||||||
overtime_hours: number;
|
|
||||||
total_hours: number;
|
|
||||||
short_date: string;
|
|
||||||
break_duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PayPeriodDetailsWeekDayExpenses {
|
|
||||||
expenses: Expense[];
|
|
||||||
total_expenses: number;
|
|
||||||
total_mileage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeWeek = <T>(factory: ()=> T): Week<T> => ({
|
|
||||||
sun: factory(),
|
|
||||||
mon: factory(),
|
|
||||||
tue: factory(),
|
|
||||||
wed: factory(),
|
|
||||||
thu: factory(),
|
|
||||||
fri: factory(),
|
|
||||||
sat: factory(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailySchedule = (): PayPeriodDetailsWeekDayShifts => ({
|
|
||||||
shifts: [],
|
|
||||||
regular_hours: 0,
|
|
||||||
evening_hours: 0,
|
|
||||||
emergency_hours: 0,
|
|
||||||
overtime_hours: 0,
|
|
||||||
total_hours: 0,
|
|
||||||
short_date: "",
|
|
||||||
break_duration: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailyExpenses = (): PayPeriodDetailsWeekDayExpenses => ({
|
|
||||||
expenses: [default_expense,],
|
|
||||||
total_expenses: -1,
|
|
||||||
total_mileage: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const defaultPayPeriodDetailsWeek = (): PayPeriodDetailsWeek => ({
|
|
||||||
is_approved: false,
|
|
||||||
shifts: makeWeek(emptyDailySchedule),
|
|
||||||
expenses: makeWeek(emptyDailyExpenses),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const default_pay_period_details: PayPeriodDetails = {
|
|
||||||
weeks: [ defaultPayPeriodDetailsWeek(), ],
|
|
||||||
employee_full_name: "",
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +1,46 @@
|
||||||
export const SHIFT_TYPES = [
|
export const SHIFT_TYPES: ShiftType[] = [
|
||||||
'REGULAR',
|
'REGULAR',
|
||||||
'EVENING',
|
'EVENING',
|
||||||
'EMERGENCY',
|
'EMERGENCY',
|
||||||
'OVERTIME',
|
|
||||||
'HOLIDAY',
|
'HOLIDAY',
|
||||||
'VACATION',
|
'VACATION',
|
||||||
'SICK'
|
'SICK'
|
||||||
];
|
];
|
||||||
|
|
||||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
|
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
|
||||||
|
|
||||||
export type UpsertAction = 'create' | 'update' | 'delete';
|
|
||||||
|
|
||||||
export type ShiftLegendItem = {
|
export type ShiftLegendItem = {
|
||||||
type: ShiftType;
|
type: ShiftType;
|
||||||
color: string;
|
color: string;
|
||||||
label_type: string;
|
label_type: string;
|
||||||
text_color?: string;
|
text_color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Shift {
|
export class Shift {
|
||||||
date: string;
|
shift_id: number;
|
||||||
type: ShiftType;
|
timesheet_id: number;
|
||||||
start_time: string;
|
date: string; //YYYY-MM-DD
|
||||||
end_time: string;
|
type: ShiftType;
|
||||||
comment: string | undefined;
|
start_time: string; //HH:mm:ss
|
||||||
|
end_time: string; //HH:mm:ss
|
||||||
|
comment: string | undefined;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
is_remote: boolean;
|
is_remote: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.shift_id = -1;
|
||||||
|
this.timesheet_id = -1;
|
||||||
|
this.date = '';
|
||||||
|
this.type = 'REGULAR';
|
||||||
|
this.start_time = '';
|
||||||
|
this.end_time = '';
|
||||||
|
this.comment = undefined;
|
||||||
|
this.is_approved = false;
|
||||||
|
this.is_remote = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpsertShiftsResponse {
|
export interface NewShift {
|
||||||
action: UpsertAction;
|
timesheet_id: number;
|
||||||
day: Shift[];
|
shifts: Shift[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpsertShift {
|
|
||||||
old_shift?: Shift | undefined;
|
|
||||||
new_shift?: Shift | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_shift: Readonly<Shift> = {
|
|
||||||
date: '',
|
|
||||||
start_time: '--:--',
|
|
||||||
end_time: '--:--',
|
|
||||||
type: 'REGULAR',
|
|
||||||
comment: '',
|
|
||||||
is_approved: false,
|
|
||||||
is_remote: false,
|
|
||||||
};
|
|
||||||
117
src/modules/timesheets/models/timesheet.models.ts
Normal file
117
src/modules/timesheets/models/timesheet.models.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import type { Shift } from "./shift.models";
|
||||||
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
|
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
|
||||||
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
|
export interface TimesheetResponse {
|
||||||
|
employee_full_name: string;
|
||||||
|
timesheets: Timesheet[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Timesheet {
|
||||||
|
timesheet_id: number;
|
||||||
|
is_approved: boolean;
|
||||||
|
weekly_hours: TotalHours;
|
||||||
|
weekly_expenses: TotalExpenses;
|
||||||
|
days: TimesheetDay[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDay {
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
daily_hours: TotalHours;
|
||||||
|
daily_expenses: TotalExpenses;
|
||||||
|
shifts: Shift[];
|
||||||
|
expenses: Expense[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotalHours {
|
||||||
|
regular: number;
|
||||||
|
evening: number;
|
||||||
|
emergency: number;
|
||||||
|
overtime: number;
|
||||||
|
vacation: number;
|
||||||
|
holiday: number;
|
||||||
|
sick: number;
|
||||||
|
absent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotalExpenses {
|
||||||
|
expenses: number;
|
||||||
|
mileage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const test_timesheets: Timesheet[] = [
|
||||||
|
// {
|
||||||
|
// timehsid: 1,
|
||||||
|
// is_approved: false,
|
||||||
|
// weekly_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||||
|
// weekly_expenses: { expenses: 15.5, mileage: 0 },
|
||||||
|
// days: [
|
||||||
|
// {
|
||||||
|
// date: '2025-10-18',
|
||||||
|
// daily_hours: { regular: 8, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, absent: 0 },
|
||||||
|
// daily_expenses: { expenses: 15.5, mileage: 0 },
|
||||||
|
// shifts: [
|
||||||
|
// { id: 101, date: '2025-01-06', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: 'blah', is_approved: false, is_remote: false, },
|
||||||
|
// { id: 102, date: '2025-01-06', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||||
|
// ],
|
||||||
|
// expenses: [
|
||||||
|
// { id: 201, date: '2025-01-06', type: 'EXPENSES', amount: 15.5, comment: 'Lunch receipt', is_approved: false, },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 2,
|
||||||
|
// is_approved: true,
|
||||||
|
// weekly_hours: {
|
||||||
|
// regular: 0,
|
||||||
|
// evening: 0,
|
||||||
|
// emergency: 0,
|
||||||
|
// overtime: 8,
|
||||||
|
// vacation: 0,
|
||||||
|
// holiday: 0,
|
||||||
|
// sick: 0,
|
||||||
|
// absent: 0,
|
||||||
|
// },
|
||||||
|
// weekly_expenses: {
|
||||||
|
// expenses: 0,
|
||||||
|
// mileage: 32.4,
|
||||||
|
// },
|
||||||
|
// days: [
|
||||||
|
// {
|
||||||
|
// date: '2025-10-27',
|
||||||
|
// daily_hours: {
|
||||||
|
// regular: 0,
|
||||||
|
// evening: 0,
|
||||||
|
// emergency: 0,
|
||||||
|
// overtime: 8,
|
||||||
|
// vacation: 0,
|
||||||
|
// holiday: 0,
|
||||||
|
// sick: 0,
|
||||||
|
// absent: 0,
|
||||||
|
// },
|
||||||
|
// daily_expenses: {
|
||||||
|
// expenses: 0,
|
||||||
|
// mileage: 32.4,
|
||||||
|
// },
|
||||||
|
// shifts: [
|
||||||
|
// { id: 101, date: '2025-10-27', type: 'REGULAR', start_time: '08:00', end_time: '12:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||||
|
// { id: 102, date: '2025-10-27', type: 'REGULAR', start_time: '13:00', end_time: '17:00', comment: undefined, is_approved: false, is_remote: false, },
|
||||||
|
// ],
|
||||||
|
// expenses: [
|
||||||
|
// {
|
||||||
|
// id: 202,
|
||||||
|
// date: '2025-10-27',
|
||||||
|
// type: 'MILEAGE',
|
||||||
|
// amount: 0,
|
||||||
|
// mileage: 32.4,
|
||||||
|
// comment: 'Travel to client site',
|
||||||
|
// is_approved: true,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export type PayPeriodLabel = {
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
|
||||||
13
src/modules/timesheets/services/expense-service.ts
Normal file
13
src/modules/timesheets/services/expense-service.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { api } from "src/boot/axios";
|
||||||
|
|
||||||
|
export const ExpenseService = {
|
||||||
|
getExpensesByTimesheetId: async (timesheet_id: number) => {
|
||||||
|
const response = await api.get(`timesheet/${timesheet_id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
upsertOrDeleteExpenseById: async (expense_id: number) => {
|
||||||
|
const response = await api.post(`epxense/${expense_id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
24
src/modules/timesheets/services/shift-service.ts
Normal file
24
src/modules/timesheets/services/shift-service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import { api } from "src/boot/axios";
|
||||||
|
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
|
export const ShiftService = {
|
||||||
|
deleteShiftById: async (shift_id: number) => {
|
||||||
|
const response = await api.delete(`/shift/${shift_id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createNewShifts: async (new_shifts: Shift[]) => {
|
||||||
|
// const response = await api.post(`/shift/`, { dtos: new_shifts });
|
||||||
|
// return response;
|
||||||
|
console.log('create shift payload: ', new_shifts);
|
||||||
|
return {status: 200};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateShifts: async (existing_shifts: Shift[]) => {
|
||||||
|
// const response = await api.patch(`/shift/`, { dtos: existing_shifts });
|
||||||
|
// return response;
|
||||||
|
console.log('update shift payload: ', existing_shifts);
|
||||||
|
return {status: 200};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { UpsertShift } from "src/modules/timesheets/models/shift.models";
|
|
||||||
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
|
||||||
import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
|
import type { TimesheetResponse } 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";
|
|
||||||
|
|
||||||
export const timesheetService = {
|
export const timesheetService = {
|
||||||
getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise<PayPeriodDetails> => {
|
|
||||||
const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
|
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
|
||||||
const response = await api.get(`pay-periods/date/${date_string}`);
|
const response = await api.get(`pay-periods/date/${date_string}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
@ -21,30 +14,13 @@ export const timesheetService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview[]> => {
|
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
|
||||||
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
|
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> => {
|
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<TimesheetResponse> => {
|
||||||
const response = await api.get('timesheets', { params: { year, period_no, email, } });
|
const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
|
||||||
const response = await api.get(`/expenses/${email}/${year}/${period_number}`);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
upsertOrDeleteShiftsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[], date: string): Promise<PayPeriodDetails> => {
|
|
||||||
const response = await api.put(`/shifts/upsert/${email}/${date}`, payload);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail: async (email: string, date: string, payload: UpsertExpense): Promise<Expense[]> => {
|
|
||||||
const headers = { 'Content-Type': 'application/json' }
|
|
||||||
|
|
||||||
const response = await api.put(`/expenses/upsert/${email}/${date}`, payload, { headers });
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
@ -1,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 getExpenseIcon = (type: ExpenseType) => {
|
||||||
export const normExpenseType = (type: unknown): string =>
|
switch (type) {
|
||||||
typeof type === 'string' ? type.trim().toUpperCase() : '';
|
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',
|
}
|
||||||
PRIME_GARDE: 'admin_panel_settings',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expenseTypeIcon = (type: unknown): string => {
|
export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
const t = normExpenseType(type);
|
|
||||||
return (
|
|
||||||
icon_map[t.toLowerCase()] ??
|
|
||||||
icon_map[t.replace('-','_').toLowerCase()] ??
|
|
||||||
'help_outline'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//------------------ totals ------------------
|
|
||||||
export const computeExpenseTotals = (items: readonly Expense[]): ExpenseTotals =>
|
|
||||||
items.reduce<ExpenseTotals>(
|
|
||||||
(acc, e) => ({
|
|
||||||
amount: acc.amount + (Number(e.amount) || 0),
|
|
||||||
mileage: acc.mileage + (Number(e.mileage) || 0),
|
|
||||||
}),
|
|
||||||
{ amount: 0, mileage: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
//------------------ Quasar :rules=[] ------------------
|
|
||||||
export const makeExpenseRules = (t: (_key: string) => string) => {
|
|
||||||
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 mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
const commentRequired = (val: unknown) => (typeof val === 'string' ? val.trim().length > 0 : false) || t('timesheet.expense.errors.comment_required');
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
typeRequired,
|
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 };
|
|
||||||
}
|
|
||||||
47
src/pages/dashboard-page.vue
Normal file
47
src/pages/dashboard-page.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { Notify } from 'quasar';
|
||||||
|
|
||||||
|
const clickNotify = () => {
|
||||||
|
Notify.create({
|
||||||
|
message: 'You clicked the little click button!',
|
||||||
|
color: 'info'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page
|
||||||
|
padding
|
||||||
|
class="q-pa-md row items-center justify-center"
|
||||||
|
>
|
||||||
|
<q-card class="shadow-2 col-9 dark-font">
|
||||||
|
<q-img src="src/assets/line-truck-1.jpg">
|
||||||
|
<div class="absolute-bottom text-h5">
|
||||||
|
Welcome to App Targo, !
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
|
||||||
|
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
|
||||||
|
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
||||||
|
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
||||||
|
deserunt mollit anim id est laborum.
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-actions align="center">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
label="Click Me"
|
||||||
|
@click="clickNotify"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
18
src/pages/employee-list-page.vue
Normal file
18
src/pages/employee-list-page.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
|
||||||
|
import EmployeeListAddModifyDialog from 'src/modules/employee-list/components/employee/employee-list-add-modify-dialog.vue';
|
||||||
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page class="column flex-center">
|
||||||
|
<EmployeeListAddModifyDialog />
|
||||||
|
|
||||||
|
<PageHeaderTemplate title="employee_list.page_header" />
|
||||||
|
|
||||||
|
<EmployeeListTable />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<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>
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import MenuEmployee from 'src/modules/profile/components/employee/menu-employee.vue';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
// import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
|
const employee_roles = ['SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING'];
|
||||||
|
|
||||||
const { employeeProfile } = defineProps<{
|
// const employee_profile = defineModel<EmployeeProfile>({ required: true });
|
||||||
employeeProfile?: EmployeeProfile | undefined;
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="bg-secondary column items-center justify-center">
|
<q-page class="bg-secondary column items-center justify-center">
|
||||||
<ProfileEmployee
|
<MenuEmployee
|
||||||
v-if="employee_roles.includes( auth_store.user.role.toUpperCase() )"
|
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
:employee-profile="employeeProfile"
|
|
||||||
/>
|
/>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import SupervisorCrewTable from '../components/supervisor/supervisor-crew-table.vue';
|
|
||||||
import EmployeeListAddModifyDialog from '../components/employee/employee-list-add-modify-dialog.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-page>
|
|
||||||
<EmployeeListAddModifyDialog />
|
|
||||||
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder">
|
|
||||||
{{ $t('employee_list.page_header') }}
|
|
||||||
</div>
|
|
||||||
<SupervisorCrewTable />
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
import { useQuasar } from 'quasar';
|
|
||||||
import type { QVueGlobals } from 'quasar';
|
|
||||||
|
|
||||||
const q: QVueGlobals = useQuasar();
|
|
||||||
|
|
||||||
const clickNotify = () => {
|
|
||||||
q.notify({
|
|
||||||
message: 'Nick pinged you.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-page
|
|
||||||
padding
|
|
||||||
class="q-pa-md row items-center justify-center"
|
|
||||||
>
|
|
||||||
<q-card class="shadow-2 col-9 dark-font">
|
|
||||||
<q-img src="src/assets/line-truck-1.jpg">
|
|
||||||
<div class="absolute-bottom text-h5">
|
|
||||||
Welcome to App Targo!
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
|
|
||||||
<q-card-section class="text-center">
|
|
||||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
|
|
||||||
totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
|
|
||||||
sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
|
|
||||||
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui
|
|
||||||
dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
|
|
||||||
incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
|
|
||||||
exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem
|
|
||||||
vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum
|
|
||||||
qui
|
|
||||||
dolorem eum fugiat quo voluptas nulla pariatur?
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="text-center">
|
|
||||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
|
|
||||||
deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
|
|
||||||
provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.
|
|
||||||
Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
|
|
||||||
eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas
|
|
||||||
assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum
|
|
||||||
necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
|
|
||||||
rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut
|
|
||||||
perferendis doloribus asperiores repellat.
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="text-center">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
|
|
||||||
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
|
|
||||||
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
|
||||||
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
|
||||||
deserunt mollit anim id est laborum.
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-card-actions align="center">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
label="Click Me"
|
|
||||||
@click="clickNotify"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
||||||
import DetailscrudDialog from 'src/modules/timesheet-approval/components/details-crud-dialog.vue';
|
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
|
||||||
|
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
await timesheet_approval_api.getPayPeriodOverviewsByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
await timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -29,16 +29,16 @@
|
||||||
>
|
>
|
||||||
<PageHeaderTemplate
|
<PageHeaderTemplate
|
||||||
title="timesheet_approvals.page_title"
|
title="timesheet_approvals.page_title"
|
||||||
:start-date="timesheet_store.pay_period.period_start"
|
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||||
:end-date="timesheet_store.pay_period.period_end"
|
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailscrudDialog
|
<DetailsDialog
|
||||||
v-model:dialog="is_details_dialog_open"
|
v-model:dialog="is_details_dialog_open"
|
||||||
:employee-email="employee_email"
|
:employee-email="employee_email"
|
||||||
:is-loading="timesheet_store.is_loading"
|
:is-loading="timesheet_store.is_loading"
|
||||||
:employee-overview="timesheet_store.current_pay_period_overview"
|
:employee-overview="timesheet_store.current_pay_period_overview"
|
||||||
:timesheet-details="timesheet_store.pay_period_details"
|
:timesheets="timesheet_store.timesheets"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<OverviewList @clickedDetailsButton="onDetailsClicked"/>
|
<OverviewList @clickedDetailsButton="onDetailsClicked"/>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await timesheet_api.getPayPeriodDetailsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -23,15 +23,21 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page
|
<q-page
|
||||||
padding
|
padding
|
||||||
class="q-pa-md bg-secondary"
|
class="column q-pa-md bg-secondary items-center"
|
||||||
>
|
>
|
||||||
<PageHeaderTemplate
|
<PageHeaderTemplate
|
||||||
:title="$t('timesheet.page_header')"
|
:title="'timesheet.page_header'"
|
||||||
:start-date="timesheet_store.pay_period.period_start"
|
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||||
:end-date="timesheet_store.pay_period.period_end"
|
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||||
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimesheetWrapper :employee-email="user.email" />
|
<div
|
||||||
|
class="col"
|
||||||
|
:style="$q.screen.gt.sm ? 'width: 90vw' : ''"
|
||||||
|
>
|
||||||
|
<TimesheetWrapper :employee-email="user?.email ?? ''" />
|
||||||
|
</div>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,6 +2,7 @@ import { defineRouter } from '#q-app/wrappers';
|
||||||
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router';
|
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory, } from 'vue-router';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If not building with SSR mode, you can
|
* If not building with SSR mode, you can
|
||||||
|
|
@ -13,27 +14,29 @@ import { useAuthStore } from 'src/stores/auth-store';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
|
||||||
|
|
||||||
const Router = createRouter({
|
const Router = createRouter({
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
routes,
|
routes,
|
||||||
|
|
||||||
// Leave this as is and make changes in quasar.conf.js instead!
|
// Leave this as is and make changes in quasar.conf.js instead!
|
||||||
// quasar.conf.js -> build -> vueRouterMode
|
// quasar.conf.js -> build -> vueRouterMode
|
||||||
// quasar.conf.js -> build -> publicPath
|
// quasar.conf.js -> build -> publicPath
|
||||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||||
});
|
});
|
||||||
|
|
||||||
Router.beforeEach((destinationPage) => {
|
Router.beforeEach(async (destinationPage) => {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||||
|
|
||||||
if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) {
|
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
|
||||||
return { name: 'login' };
|
console.log('no user account found');
|
||||||
}
|
return { name: 'login' };
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return Router;
|
return Router;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ export enum RouteNames {
|
||||||
TIMESHEET_APPROVALS = 'timesheet-approvals',
|
TIMESHEET_APPROVALS = 'timesheet-approvals',
|
||||||
EMPLOYEE_LIST = 'employee-list',
|
EMPLOYEE_LIST = 'employee-list',
|
||||||
PROFILE = 'user/profile',
|
PROFILE = 'user/profile',
|
||||||
TIMESHEET_TEMP = 'timesheet-temp'
|
TIMESHEET = 'timesheet'
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: RouteNames.DASHBOARD,
|
name: RouteNames.DASHBOARD,
|
||||||
component: () => import('src/pages/test-page.vue'),
|
component: () => import('src/pages/dashboard-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet-approvals',
|
path: 'timesheet-approvals',
|
||||||
|
|
@ -20,11 +20,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: 'employees',
|
path: 'employees',
|
||||||
name: RouteNames.EMPLOYEE_LIST,
|
name: RouteNames.EMPLOYEE_LIST,
|
||||||
component: () => import('src/pages/supervisor-crew-page.vue'),
|
component: () => import('src/pages/employee-list-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet-temp',
|
path: 'timesheet',
|
||||||
name: RouteNames.TIMESHEET_TEMP,
|
name: RouteNames.TIMESHEET,
|
||||||
component: () => import('src/pages/timesheet-page.vue')
|
component: () => import('src/pages/timesheet-page.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +38,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/v1/login',
|
path: '/v1/login',
|
||||||
name: RouteNames.LOGIN,
|
name: RouteNames.LOGIN,
|
||||||
component: () => import('src/modules/auth/pages/auth-login.vue'),
|
component: () => import('src/pages/login-page.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requiresAuth: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,67 @@
|
||||||
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 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";
|
||||||
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
const TestUsers: Record<CompanyRole, User> = {
|
|
||||||
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
|
|
||||||
supervisor: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: 'supervisor' },
|
|
||||||
accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
|
||||||
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
|
||||||
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => user.value.role !== 'guest');
|
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 = () => {
|
||||||
const oidcPopup = AuthService.oidcLogin();
|
window.addEventListener('message', (event) => {
|
||||||
if (!oidcPopup) {
|
void handleAuthMessage(event);
|
||||||
authError.value = "You have popups blocked on this website!";
|
});
|
||||||
|
|
||||||
|
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_AUTH_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
||||||
|
|
||||||
|
if (!oidc_popup)
|
||||||
|
Notify.create({
|
||||||
|
message: "You have popups blocked on this website!",
|
||||||
|
color: 'negative',
|
||||||
|
textColor: 'white',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
user.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthMessage = async (event: MessageEvent) => {
|
||||||
|
if (event.data.type === 'authSuccess') {
|
||||||
|
try {
|
||||||
|
await getProfile();
|
||||||
|
await router.push('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('failed to login: ', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notify.create({
|
||||||
|
message: "You have popups blocked on this website!",
|
||||||
|
color: 'negative',
|
||||||
|
textColor: 'white',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProfile = async (): Promise<{ status: number, message: string }> => {
|
||||||
|
try {
|
||||||
|
const new_user = await AuthService.getProfile();
|
||||||
|
user.value = new_user;
|
||||||
|
return { status: 200, message: 'profile retrieved successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error while retrieving profile: ', error);
|
||||||
|
}
|
||||||
|
return { status: 400, message: 'unknown error occured' };
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, getProfile };
|
||||||
user.value = TestUsers.guest;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUser = (bypassRole: string) => {
|
|
||||||
if (bypassRole in TestUsers) {
|
|
||||||
user.value = TestUsers[bypassRole as CompanyRole];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
user.value = TestUsers.guest;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, setUser };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list";
|
import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
|
||||||
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
|
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
|
||||||
import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface";
|
|
||||||
|
|
||||||
export const useEmployeeStore = defineStore('employee', () => {
|
export const useEmployeeStore = defineStore('employee', () => {
|
||||||
const employee = ref<EmployeeProfile>( default_employee_profile );
|
const employee = ref<EmployeeProfile>( default_employee_profile );
|
||||||
const employeeList = ref<EmployeeListTableItem[]>([]);
|
const employeeList = ref<EmployeeProfile[]>([]);
|
||||||
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
||||||
const isLoadingEmployeeProfile = ref(false);
|
const isLoadingEmployeeProfile = ref(false);
|
||||||
const isLoadingEmployeeList = ref(false);
|
const isLoadingEmployeeList = ref(false);
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,17 @@
|
||||||
import { computed, ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
|
||||||
import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||||
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
|
||||||
import { computeExpenseTotals } from "src/modules/timesheets/utils/expense.util";
|
|
||||||
import type { UpsertAction } from "src/modules/timesheets/models/shift.models";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const mode = ref<UpsertAction>('create');
|
const pay_period_expenses = ref<Expense[]>(test_expenses);
|
||||||
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
const current_expense = ref<Expense>(empty_expense);
|
||||||
const pay_period_expenses_totals = computed(() => computeExpenseTotals(pay_period_expenses.value.expenses))
|
const initial_expense = ref<Expense>(empty_expense);
|
||||||
const current_expense = ref<Expense>(default_expense);
|
|
||||||
const initial_expense = ref<Expense>(default_expense);
|
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
// const setErrorFrom = (err: unknown) => {
|
// const setErrorFrom = (err: unknown) => {
|
||||||
|
|
@ -25,14 +19,14 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
// error.value = e?.message || 'Unknown error';
|
// error.value = e?.message || 'Unknown error';
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const open = async (employee_email: string): Promise<void> => {
|
const open = (): void => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
current_expense.value = default_expense;
|
current_expense.value = empty_expense;
|
||||||
initial_expense.value = default_expense;
|
initial_expense.value = empty_expense;
|
||||||
|
|
||||||
await getPayPeriodExpensesByEmployeeEmail(employee_email);
|
// await getPayPeriodExpensesByTimesheetId(timesheet_id);
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,16 +35,12 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
const getPayPeriodExpensesByTimesheetId = async (timesheet_id: number): Promise<void> => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(
|
const expenses = await ExpenseService.getExpensesByTimesheetId(timesheet_id);
|
||||||
encodeURIComponent(employee_email),
|
|
||||||
encodeURIComponent(timesheet_store.pay_period.pay_year),
|
|
||||||
encodeURIComponent(timesheet_store.pay_period.pay_period_no),
|
|
||||||
);
|
|
||||||
pay_period_expenses.value = expenses;
|
pay_period_expenses.value = expenses;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (typeof err === 'object') {
|
if (typeof err === 'object') {
|
||||||
|
|
@ -72,21 +62,16 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, date: string, expense: UpsertExpense): Promise<void> => {
|
const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(
|
await ExpenseService.upsertOrDeleteExpenseById(expense_id);
|
||||||
encodeURIComponent(employee_email),
|
// TODO: Save response data into proper ref
|
||||||
encodeURIComponent(date),
|
|
||||||
expense,
|
|
||||||
);
|
|
||||||
console.log('updated expenses received: ', updated_expenses)
|
|
||||||
pay_period_expenses.value.expenses = updated_expenses;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// setErrorFrom(err);
|
// setErrorFrom(err);
|
||||||
console.log('error doing some expense thing: ', err)
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -95,15 +80,13 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
mode,
|
|
||||||
pay_period_expenses,
|
pay_period_expenses,
|
||||||
pay_period_expenses_totals,
|
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
error,
|
error,
|
||||||
open,
|
open,
|
||||||
getPayPeriodExpensesByEmployeeEmail,
|
getPayPeriodExpensesByTimesheetId,
|
||||||
upsertOrDeleteExpensesByEmployeeEmail,
|
upsertOrDeleteExpensesById,
|
||||||
close,
|
close,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -1,80 +1,71 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { default_shift, type UpsertAction, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
import { Notify } from "quasar";
|
||||||
|
|
||||||
export const useShiftStore = defineStore('shift', () => {
|
|
||||||
const is_open = ref(false);
|
|
||||||
const mode = ref<UpsertAction>('create');
|
|
||||||
const date_iso = ref<string>('');
|
|
||||||
const current_shift = ref<Shift>(default_shift);
|
|
||||||
const initial_shift = ref<Shift>(default_shift);
|
|
||||||
|
|
||||||
|
export const useShiftStore = defineStore('shift_store', () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const shift_error = ref();
|
||||||
|
|
||||||
const open = (next_mode: UpsertAction, date: string, current: Shift, initial: Shift) => {
|
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
||||||
mode.value = next_mode;
|
try {
|
||||||
date_iso.value = date;
|
await ShiftService.deleteShiftById(shift_id);
|
||||||
current_shift.value = current; // new shift
|
return true;
|
||||||
initial_shift.value = initial; // old shift
|
} catch (error) {
|
||||||
is_open.value = true;
|
console.error('DEV ERROR || error while deleting shift: ', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreate = (date: string) => {
|
const createNewShifts = async (): Promise<boolean> => {
|
||||||
open('create', date, default_shift, default_shift);
|
if (timesheet_store.timesheets === undefined) return false;
|
||||||
};
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
current_shift.value = default_shift;
|
|
||||||
initial_shift.value = default_shift;
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
|
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
|
||||||
const encoded_date = encodeURIComponent(current_shift.value.date);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, [ upsert_shift, ], encoded_date);
|
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id < 0);
|
||||||
timesheet_store.pay_period_details = result;
|
|
||||||
} catch (err) {
|
if (new_shifts?.length > 0) {
|
||||||
console.log('error doing thing: ', err)
|
const response = await ShiftService.createNewShifts(new_shifts);
|
||||||
// const status_code: number = err?.response?.status ?? 500;
|
if (response.status <= 200) {
|
||||||
// const data = err?.response?.data ?? {};
|
return true;
|
||||||
// throw new GenericApiError({
|
}
|
||||||
// status_code,
|
}
|
||||||
// error_code: data.error_code,
|
|
||||||
// message: data.message || data.error || err.message,
|
console.log('No new shifts to save');
|
||||||
// context: data.context,
|
Notify.create('no new shifts to save')
|
||||||
// });
|
return false;
|
||||||
} finally {
|
} catch (error) {
|
||||||
close();
|
console.error('Error creating new shifts: ', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateShifts = async (): Promise<boolean> => {
|
||||||
|
if (timesheet_store.timesheets === undefined) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id > 0);
|
||||||
|
|
||||||
|
if (existing_shifts?.length > 0) {
|
||||||
|
const response = await ShiftService.updateShifts(existing_shifts);
|
||||||
|
if (response.status <= 200) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('No shifts to update');
|
||||||
|
Notify.create('no shifts to update')
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating shifts: ', error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_open,
|
shift_error,
|
||||||
mode,
|
deleteShiftById,
|
||||||
date_iso,
|
createNewShifts,
|
||||||
current_shift,
|
updateShifts,
|
||||||
initial_shift,
|
}
|
||||||
openCreate,
|
|
||||||
openUpdate,
|
|
||||||
openDelete,
|
|
||||||
close,
|
|
||||||
upsertOrDeleteShiftByEmployeeEmail,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
|
|
@ -1,28 +1,24 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, ref } from 'vue';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
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 type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
import { default_pay_period, type PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
|
||||||
import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
|
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
|
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 auth_store = useAuthStore();
|
||||||
const is_loading = ref<boolean>(false);
|
const is_loading = ref<boolean>(false);
|
||||||
const pay_period = ref<PayPeriod>(default_pay_period);
|
const pay_period = ref<PayPeriod>();
|
||||||
const pay_period_overviews = ref<PayPeriodOverview[]>([default_pay_period_overview,]);
|
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
||||||
const current_pay_period_overview = ref<PayPeriodOverview>(default_pay_period_overview);
|
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||||
const pay_period_details = ref<PayPeriodDetails>(default_pay_period_details);
|
const timesheets = ref<Timesheet[]>();
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
const is_calendar_limit = computed(() =>
|
|
||||||
pay_period.value.pay_year === 2024 &&
|
|
||||||
pay_period.value.pay_period_no <= 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => {
|
||||||
is_loading.value = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof date_or_year === 'string') {
|
if (typeof date_or_year === 'string') {
|
||||||
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
|
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year);
|
||||||
|
|
@ -30,33 +26,31 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
else if (typeof date_or_year === 'number' && period_number) {
|
else if (typeof date_or_year === 'number' && period_number) {
|
||||||
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
|
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
|
||||||
}
|
}
|
||||||
else pay_period.value = default_pay_period;
|
else pay_period.value = undefined;
|
||||||
is_loading.value = false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not get current pay period: ', error);
|
console.error('Could not get current pay period: ', error);
|
||||||
pay_period.value = default_pay_period;
|
pay_period.value = undefined;
|
||||||
pay_period_overviews.value = [default_pay_period_overview,];
|
pay_period_overviews.value = [];
|
||||||
//TODO: More in-depth error-handling here
|
//TODO: More in-depth error-handling here
|
||||||
is_loading.value = false;
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise<boolean> => {
|
const getTimesheetOverviewsByPayPeriod = async (pay_year: number, period_number: number, supervisor_email?: string): Promise<boolean> => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email);
|
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email ?? auth_store.user?.email ?? '');
|
||||||
pay_period_overviews.value = response.employees_overview;
|
pay_period_overviews.value = response.employees_overview;
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
||||||
pay_period_overviews.value = [default_pay_period_overview,];
|
pay_period_overviews.value = [];
|
||||||
// TODO: More in-depth error-handling here
|
// TODO: More in-depth error-handling here
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
|
|
||||||
|
|
@ -64,55 +58,48 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => {
|
const getTimesheetsByEmployeeEmail = async (employee_email: string) => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
|
|
||||||
|
if (pay_period.value === undefined) return;
|
||||||
try {
|
try {
|
||||||
const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
|
const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||||
pay_period.value.pay_year,
|
timesheets.value = response.timesheets;
|
||||||
pay_period.value.pay_period_no,
|
|
||||||
employee_email
|
|
||||||
);
|
|
||||||
pay_period_details.value = response;
|
|
||||||
console.log('pay period details: ', response, pay_period_details.value.employee_full_name)
|
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
// TODO: More in-depth error-handling here
|
// TODO: More in-depth error-handling here
|
||||||
pay_period_details.value = default_pay_period_details;
|
timesheets.value = [];
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
|
||||||
return withLoading(is_loading.value, async () => {
|
try {
|
||||||
try {
|
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
||||||
const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
|
year,
|
||||||
year,
|
period_number,
|
||||||
period_number,
|
report_filters
|
||||||
report_filters
|
);
|
||||||
);
|
pay_period_report.value = response;
|
||||||
pay_period_report.value = response;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error retrieving the report CSV: ', error);
|
||||||
|
// TODO: More in-depth error-handling here
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
} catch (error) {
|
|
||||||
console.error('There was an error retrieving the report CSV: ', error);
|
|
||||||
// TODO: More in-depth error-handling here
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_loading,
|
is_loading,
|
||||||
is_calendar_limit,
|
|
||||||
pay_period,
|
pay_period,
|
||||||
pay_period_overviews,
|
pay_period_overviews,
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
pay_period_details,
|
timesheets,
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getPayPeriodOverviewsBySupervisorEmail,
|
getTimesheetOverviewsByPayPeriod,
|
||||||
getPayPeriodDetailsByEmployeeEmail,
|
getTimesheetsByEmployeeEmail,
|
||||||
getPayPeriodReportByYearAndPeriodNumber,
|
getPayPeriodReportByYearAndPeriodNumber,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { useQuasar } from 'quasar';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
export const useUiStore = defineStore('ui', () => {
|
export const useUiStore = defineStore('ui', () => {
|
||||||
const isRightDrawerOpen = ref(true);
|
const q = useQuasar();
|
||||||
|
const is_left_drawer_open = ref(true);
|
||||||
|
const focus_next_component = ref(false);
|
||||||
|
const is_mobile_mode = computed(() => q.screen.lt.md);
|
||||||
|
|
||||||
const toggleRightDrawer = () => {
|
const toggleRightDrawer = () => {
|
||||||
isRightDrawerOpen.value = !isRightDrawerOpen.value;
|
is_left_drawer_open.value = !is_left_drawer_open.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isRightDrawerOpen, toggleRightDrawer };
|
return {
|
||||||
|
is_mobile_mode,
|
||||||
|
focus_next_component,
|
||||||
|
is_left_drawer_open,
|
||||||
|
toggleRightDrawer
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user