Merge pull request 'dev/nicolas/timesheet-gui-refactor' (#23) from dev/nicolas/timesheet-gui-refactor into main
Reviewed-on: Targo/targo_frontend#23
This commit is contained in:
commit
dccf5a8d82
|
|
@ -7,15 +7,12 @@ WORKDIR /app
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
|
ENV VITE_TARGO_BACKEND_URL="http://targo-backend:3000"
|
||||||
|
|
||||||
# Copy package.json & package-lock.json first (for caching)
|
# Copy the code
|
||||||
COPY package*.json ./
|
COPY . .
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy the rest of the code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Expose Quasar dev port
|
# Expose Quasar dev port
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,6 @@ export default defineConfig((ctx) => {
|
||||||
config: {
|
config: {
|
||||||
notify: {
|
notify: {
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
|
|
||||||
},
|
},
|
||||||
dark: false,
|
dark: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body.body--dark {
|
body.body--dark {
|
||||||
--q-secondary: #2b2f34;
|
--q-secondary: #151520;
|
||||||
color: $grey-2;
|
color: $grey-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,4 +41,16 @@ body.body--dark {
|
||||||
.frosted-glass {
|
.frosted-glass {
|
||||||
background-color: #FFFA !important;
|
background-color: #FFFA !important;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-btn--push::before {
|
||||||
|
border-bottom: 4px solid rgba(0,0,0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-btn--push:active {
|
||||||
|
transform: translateY(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-btn--push:active::before {
|
||||||
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
// to match your app's branding.
|
// to match your app's branding.
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||||
|
|
||||||
$primary : #019547;
|
$primary : #30303A;
|
||||||
$secondary : #DAE0E7;
|
$secondary : #DAE0E7;
|
||||||
$accent : #AAD5C4;
|
$accent : #0c9a3b;
|
||||||
|
|
||||||
$dark-shadow-color : #00220f;
|
$dark-shadow-color : #00220f;
|
||||||
|
|
||||||
|
|
@ -30,9 +30,10 @@ $input-autofill-color : #AAD5C4;
|
||||||
$field-dense-label-top : 5px !default;
|
$field-dense-label-top : 5px !default;
|
||||||
$field-dense-label-font-size : 16px !default;
|
$field-dense-label-font-size : 16px !default;
|
||||||
|
|
||||||
|
$button-shadow : 0 0 0 transparent;
|
||||||
|
|
||||||
$dark : #42444b;
|
$dark : #40404C;
|
||||||
$dark-page : #343434;
|
$dark-page : #343444;
|
||||||
|
|
||||||
$positive : #21ba45;
|
$positive : #21ba45;
|
||||||
$negative : #e6364b;
|
$negative : #e6364b;
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ export default {
|
||||||
dark_mode: "dark",
|
dark_mode: "dark",
|
||||||
light_mode: "light",
|
light_mode: "light",
|
||||||
},
|
},
|
||||||
|
schedule_presets: {
|
||||||
|
tab_title: "Schedule",
|
||||||
|
selected_schedule: "Selected Schedule Preset",
|
||||||
|
new_preset: "Build a new preset",
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
must_enter_birthdate: "You must enter a valid birthdate",
|
must_enter_birthdate: "You must enter a valid birthdate",
|
||||||
}
|
}
|
||||||
|
|
@ -151,12 +156,12 @@ export default {
|
||||||
REMOTE: "Remote work",
|
REMOTE: "Remote work",
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
not_found:"Shift not found",
|
not_found: "Shift not found",
|
||||||
overlap:"An overlaps occured between 2 or more shifts",
|
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||||
invalid:"Invalid shift`s entry",
|
invalid: "Invalid shift`s entry",
|
||||||
unknown:"Unknown error",
|
unknown: "Unknown error",
|
||||||
comment_required:"A comment is required",
|
comment_required: "A comment is required",
|
||||||
comment_too_long:"Your comment is too long",
|
comment_too_long: "Your comment is too long",
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start:"Start (HH:mm)",
|
start:"Start (HH:mm)",
|
||||||
|
|
@ -197,7 +202,7 @@ export default {
|
||||||
PER_DIEM:"Per Diem",
|
PER_DIEM:"Per Diem",
|
||||||
EXPENSES:"expense",
|
EXPENSES:"expense",
|
||||||
MILEAGE:"mileage",
|
MILEAGE:"mileage",
|
||||||
PRIME_GARDE:"on-call allowance",
|
ON_CALL:"on-call allowance",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@ export default {
|
||||||
dark_mode: "sombre",
|
dark_mode: "sombre",
|
||||||
light_mode: "clair",
|
light_mode: "clair",
|
||||||
},
|
},
|
||||||
|
schedule_presets: {
|
||||||
|
tab_title: "horaire",
|
||||||
|
selected_schedule: "Horaire Sélectionné",
|
||||||
|
new_preset: "Construire un nouvel horaire",
|
||||||
|
|
||||||
|
},
|
||||||
errors: {
|
errors: {
|
||||||
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
|
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
|
||||||
}
|
}
|
||||||
|
|
@ -151,12 +157,12 @@ export default {
|
||||||
REMOTE: "Télétravail",
|
REMOTE: "Télétravail",
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
not_found:"Aucun quart trouvé",
|
not_found: "Aucun quart trouvé",
|
||||||
overlap:"Il y a un chevauchement entre deux ou plusieurs quarts",
|
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||||
invalid:"Entrée du quart invalide",
|
invalid: "Entrée du quart invalide",
|
||||||
unknown:"Erreur inconnue",
|
unknown: "Erreur inconnue",
|
||||||
comment_required:"un commentaire est requis",
|
comment_required: "un commentaire est requis",
|
||||||
comment_too_long:"votre commentaire est trop long",
|
comment_too_long: "votre commentaire est trop long",
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
start:"Début (HH:mm)",
|
start:"Début (HH:mm)",
|
||||||
|
|
@ -197,7 +203,7 @@ export default {
|
||||||
PER_DIEM:"Per diem",
|
PER_DIEM:"Per diem",
|
||||||
EXPENSES:"dépense",
|
EXPENSES:"dépense",
|
||||||
MILEAGE:"kilométrage",
|
MILEAGE:"kilométrage",
|
||||||
PRIME_GARDE:"Prime de garde",
|
ON_CALL:"Prime de garde",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
<script lang="ts" setup>
|
<script
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
lang="ts"
|
||||||
import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
setup
|
||||||
|
>
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-header elevated>
|
<q-header elevated>
|
||||||
<q-toolbar>
|
<q-toolbar class="q-px-sm">
|
||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
<q-btn flat dense color="white" icon="menu" @click="uiStore.toggleRightDrawer">
|
<q-btn
|
||||||
<q-img src="src/assets/logo-targo-white.svg" fit="contain" width="150px" height="30px"/>
|
flat
|
||||||
</q-btn>
|
dense
|
||||||
</q-toolbar-title>
|
color="white"
|
||||||
<q-item class="q-pa-none">
|
@click="uiStore.toggleRightDrawer"
|
||||||
<HeaderBarNotification />
|
class="q-px-none"
|
||||||
</q-item>
|
>
|
||||||
</q-toolbar>
|
<q-icon name="menu" size="lg" class="q-mr-lg"/>
|
||||||
</q-header>
|
<q-img
|
||||||
|
src="src/assets/logo-targo-white.svg"
|
||||||
|
fit="contain"
|
||||||
|
width="150px"
|
||||||
|
height="30px"
|
||||||
|
/>
|
||||||
|
</q-btn>
|
||||||
|
</q-toolbar-title>
|
||||||
|
<q-item class="q-pa-none">
|
||||||
|
<HeaderBarNotification />
|
||||||
|
</q-item>
|
||||||
|
</q-toolbar>
|
||||||
|
</q-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@
|
||||||
<template>
|
<template>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="ui_store.is_left_drawer_open"
|
v-model="ui_store.is_left_drawer_open"
|
||||||
persistent
|
:persistent="!ui_store.is_mobile_mode"
|
||||||
mini-to-overlay
|
mini-to-overlay
|
||||||
elevated
|
elevated
|
||||||
side="left"
|
side="left"
|
||||||
:mini="is_mini"
|
:mini="is_mini"
|
||||||
@mouseenter="is_mini = false"
|
@mouseenter="is_mini = false"
|
||||||
@mouseleave="is_mini = true"
|
@mouseleave="is_mini = true"
|
||||||
class="bg-dark"
|
class="bg-dark z-max"
|
||||||
>
|
>
|
||||||
<q-scroll-area class="fit">
|
<q-scroll-area class="fit">
|
||||||
<q-list>
|
<q-list>
|
||||||
|
|
@ -53,7 +53,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="home"
|
name="home"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -72,7 +73,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="event_available"
|
name="event_available"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -92,7 +94,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="view_list"
|
name="view_list"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -112,7 +115,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="punch_clock"
|
name="punch_clock"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -131,7 +135,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="account_box"
|
name="account_box"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -148,7 +153,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="contact_support"
|
name="contact_support"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
@ -167,7 +173,8 @@
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="exit_to_app"
|
name="exit_to_app"
|
||||||
color="primary"
|
color="accent"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
v-model="email"
|
v-model="email"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
label-color="primary"
|
label-color="accent"
|
||||||
class="rounded-5 inset-shadow bg-blue-grey-1"
|
class="rounded-5 inset-shadow bg-blue-grey-1"
|
||||||
label-slot
|
label-slot
|
||||||
input-class="text-weight-medium text-h6"
|
input-class="text-weight-medium text-h6"
|
||||||
|
|
@ -44,12 +44,28 @@
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
|
<q-card-section
|
||||||
|
horizontal
|
||||||
|
class="q-mb-md q-pa-none text-uppercase text-caption text-weight-medium"
|
||||||
|
>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="is_remembered"
|
v-model="is_remembered"
|
||||||
color="primary"
|
size="sm"
|
||||||
:label="$t('login.button.remember_me')"
|
color="accent"
|
||||||
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated rubberBand fast"
|
||||||
|
leave-active-class=""
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:key="is_remembered ? 'yep' : 'nope'"
|
||||||
|
class="col-auto text-weight-bold self-center q-ml-sm"
|
||||||
|
:class="is_remembered ? 'text-accent' : ''"
|
||||||
|
>{{ $t('login.button.remember_me') }}</span>
|
||||||
|
</transition>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
|
|
@ -58,7 +74,7 @@
|
||||||
rounded
|
rounded
|
||||||
disabled
|
disabled
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="accent"
|
||||||
:label="$t('login.button.connect')"
|
:label="$t('login.button.connect')"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
/>
|
/>
|
||||||
|
|
@ -72,12 +88,14 @@
|
||||||
|
|
||||||
<q-card-section class="row q-pt-sm">
|
<q-card-section class="row q-pt-sm">
|
||||||
<q-separator
|
<q-separator
|
||||||
|
size="2px"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col self-center"
|
class="col self-center"
|
||||||
/>
|
/>
|
||||||
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
|
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{
|
||||||
$t('shared.misc.or') }}</span>
|
$t('shared.misc.or') }}</span>
|
||||||
<q-separator
|
<q-separator
|
||||||
|
size="2px"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col self-center"
|
class="col self-center"
|
||||||
/>
|
/>
|
||||||
|
|
@ -108,7 +126,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="accent"
|
||||||
icon="img:src/assets/logo-targo-simple.svg"
|
icon="img:src/assets/logo-targo-simple.svg"
|
||||||
:label="$t('login.button.employee')"
|
:label="$t('login.button.employee')"
|
||||||
class="full-width row"
|
class="full-width row"
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,18 @@
|
||||||
import MenuPanelPersonal from 'src/modules/profile/components/employee/menu-panel-personal.vue';
|
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 MenuPanelEmployee from 'src/modules/profile/components/employee/menu-panel-employee.vue';
|
||||||
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
|
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
|
||||||
|
import MenuPanelSchedulePresets from 'src/modules/profile/components/shared/menu-panel-schedule-presets.vue';
|
||||||
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.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';
|
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
|
||||||
const PanelNames = {
|
const PanelNames = {
|
||||||
PERSONAL_INFO: 'personal_info',
|
PERSONAL_INFO: 'personal_info',
|
||||||
EMPLOYEE_INFO: 'employee_info',
|
EMPLOYEE_INFO: 'employee_info',
|
||||||
PREFERENCES: 'references',
|
PREFERENCES: 'references',
|
||||||
|
SCHEDULE_PRESETS: 'schedule_presets',
|
||||||
};
|
};
|
||||||
|
|
||||||
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
|
const employee_profile = defineModel<EmployeeProfile>({ default: default_employee_profile });
|
||||||
|
|
@ -24,8 +29,8 @@
|
||||||
class="rounded-5 bg-transparent q-pa-none"
|
class="rounded-5 bg-transparent q-pa-none"
|
||||||
>
|
>
|
||||||
<MenuTemplate
|
<MenuTemplate
|
||||||
:first-name="employee_profile.first_name"
|
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
||||||
:last-name="employee_profile.last_name"
|
:last-name="employee_profile.last_name === '' ? auth_store.user?.last_name ?? '' : employee_profile.last_name"
|
||||||
:initial-menu="PanelNames.PERSONAL_INFO"
|
:initial-menu="PanelNames.PERSONAL_INFO"
|
||||||
>
|
>
|
||||||
<template #tabs>
|
<template #tabs>
|
||||||
|
|
@ -44,6 +49,11 @@
|
||||||
icon="display_settings"
|
icon="display_settings"
|
||||||
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
||||||
/>
|
/>
|
||||||
|
<q-tab
|
||||||
|
:name="PanelNames.SCHEDULE_PRESETS"
|
||||||
|
icon="list_alt"
|
||||||
|
:label="$q.screen.lt.md ? '' : $t('profile.schedule_presets.tab_title')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #panels>
|
<template #panels>
|
||||||
|
|
@ -67,6 +77,12 @@
|
||||||
>
|
>
|
||||||
<MenuPanelPreferences />
|
<MenuPanelPreferences />
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
<q-tab-panel
|
||||||
|
:name="PanelNames.SCHEDULE_PRESETS"
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<MenuPanelSchedulePresets />
|
||||||
|
</q-tab-panel>
|
||||||
</template>
|
</template>
|
||||||
</MenuTemplate>
|
</MenuTemplate>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
const { userFirstName = '', userLastName = '' } = defineProps<{
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
defineProps<{
|
||||||
userFirstName: string;
|
userFirstName: string;
|
||||||
userLastName: string;
|
userLastName: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-img
|
<q-img
|
||||||
src="src/assets/profile_header_default.png"
|
src="src/assets/profile_header_default.png"
|
||||||
height="15vh"
|
height="15vh"
|
||||||
:width="$q.screen.lt.md ? '80vw' : '40vw'"
|
:width="$q.screen.lt.md ? '80vw' : '40vw'"
|
||||||
class="rounded-5 q-mb-md shadow-2 col-auto"
|
class="rounded-5 q-mb-md shadow-2 col-auto"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
>
|
>
|
||||||
<div class="absolute-bottom text-h5 text-uppercase text-weight-bolder" style="line-height: 0.8em;">{{ userFirstName }} {{ userLastName }}</div>
|
<div
|
||||||
|
class="absolute-bottom text-h5 text-uppercase text-weight-bolder"
|
||||||
|
style="line-height: 0.8em;"
|
||||||
|
>{{ userFirstName }} {{ userLastName }}</div>
|
||||||
</q-img>
|
</q-img>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
autogrow
|
autogrow
|
||||||
filled
|
filled
|
||||||
debounce="500"
|
debounce="500"
|
||||||
label-color="primary"
|
label-color="accent"
|
||||||
class="q-ma-xs text-uppercase"
|
class="q-ma-xs text-uppercase"
|
||||||
input-class="text-weight-medium text-h6"
|
input-class="text-weight-medium text-h6"
|
||||||
:hide-hint="hint === ''"
|
:hide-hint="hint === ''"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
const model = defineModel<string>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-form class="q-pa-md column fit">
|
||||||
|
<div
|
||||||
|
class="col-auto text-uppercase rounded-5"
|
||||||
|
style="line-height: 1em;"
|
||||||
|
>{{ $t('profile.schedule_presets.selected_schedule') }}</div>
|
||||||
|
<!-- where the selected preset schedule will be displayed-->
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
|
||||||
|
style="border: solid #AAA 1px;"
|
||||||
|
>
|
||||||
|
<!-- will display the list of schedules by name -->
|
||||||
|
<q-select v-model="model" />
|
||||||
|
<!-- will display a list with details about the selected schedule -->
|
||||||
|
<q-card-section>
|
||||||
|
<q-list>
|
||||||
|
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-auto text-uppercase rounded-5"
|
||||||
|
style="line-height: 1em;"
|
||||||
|
>{{ $t('profile.schedule_presets.new_preset') }}</div>
|
||||||
|
<!-- where the user will be able to create a new preset schedule -->
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
|
||||||
|
style="border: solid #AAA 1px;"
|
||||||
|
>
|
||||||
|
<q-form>
|
||||||
|
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
dense
|
dense
|
||||||
:stack-label="!isEditing"
|
:stack-label="!isEditing"
|
||||||
filled
|
filled
|
||||||
label-color="primary"
|
label-color="accent"
|
||||||
class="q-ma-xs text-h6 text-uppercase"
|
class="q-ma-xs text-h6 text-uppercase"
|
||||||
popup-content-class="text-weight-medium text-h6"
|
popup-content-class="text-weight-medium text-h6"
|
||||||
input-class="text-weight-medium"
|
input-class="text-weight-medium"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MenuHeader from 'src/modules/profile/components/shared/menu-header.vue';
|
import MenuHeader from 'src/modules/profile/components/shared/menu-header.vue';
|
||||||
|
|
||||||
const { firstName, lastName, initialMenu } = defineProps<{
|
const { initialMenu } = defineProps<{
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
initialMenu: string;
|
initialMenu: string;
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
v-model="current_menu"
|
v-model="current_menu"
|
||||||
:vertical="$q.screen.gt.sm"
|
:vertical="$q.screen.gt.sm"
|
||||||
dense
|
dense
|
||||||
active-color="primary"
|
active-color="accent"
|
||||||
indicator-color="primary"
|
indicator-color="accent"
|
||||||
>
|
>
|
||||||
<slot name="tabs"></slot>
|
<slot name="tabs"></slot>
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
|
|
|
||||||
27
src/modules/profile/models/schedule-presets.models.ts
Normal file
27
src/modules/profile/models/schedule-presets.models.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export interface SchedulePreset {
|
||||||
|
name: string;
|
||||||
|
is_default: boolean;
|
||||||
|
presets_shifts: ShiftPreset[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShiftPreset {
|
||||||
|
week_day: Weekday;
|
||||||
|
preset_id: number;
|
||||||
|
sort_order: number;
|
||||||
|
type: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
is_remote: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.week_day = '';
|
||||||
|
this.preset_id = -1;
|
||||||
|
this.sort_order = -1;
|
||||||
|
this.type = '';
|
||||||
|
this.start_time = '';
|
||||||
|
this.end_time = '';
|
||||||
|
this.is_remote = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT' | '';
|
||||||
29
src/modules/profile/services/schedule-presets-service.ts
Normal file
29
src/modules/profile/services/schedule-presets-service.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { api } from "src/boot/axios";
|
||||||
|
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||||
|
|
||||||
|
export const SchedulePresetsService = {
|
||||||
|
createSchedulePresets: async (new_schedule: SchedulePreset) => {
|
||||||
|
const response = await api.post(`/schedule-presets/create/`, new_schedule);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSchedulePresets: async (preset_id: number, dto: Partial<SchedulePreset>) => {
|
||||||
|
const response = await api.patch(`/schedule-presets/update/${preset_id}`, dto);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSchedulePresets: async (preset_id: number) => {
|
||||||
|
const response = await api.delete(`/schedule-presets/delete/${preset_id}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
findListOfSchedulePresets: async () => {
|
||||||
|
const response = await api.get(`/schedule-presets/find-list`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyPresets: async (preset_name: string, start_date: string) => {
|
||||||
|
const response = await api.post(`/schedule-presets/apply-presets/`, { preset: preset_name, start: start_date });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -21,13 +21,13 @@
|
||||||
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-accent text-weight-bold text-h6">
|
||||||
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), 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-accent text-weight-bold text-h6">
|
||||||
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
const NEXT = 1;
|
const NEXT = 1;
|
||||||
const PREVIOUS = -1;
|
const PREVIOUS = -1;
|
||||||
|
const PAY_PERIOD_DATE_LIMIT = '2023/12/17';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
push rounded
|
push rounded
|
||||||
icon="keyboard_arrow_left"
|
icon="keyboard_arrow_left"
|
||||||
color="primary"
|
color="accent"
|
||||||
@click="getPreviousPayPeriod"
|
@click="getPreviousPayPeriod"
|
||||||
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
|
: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"
|
||||||
|
|
@ -81,7 +82,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
push rounded
|
push rounded
|
||||||
icon="calendar_month"
|
icon="calendar_month"
|
||||||
color="primary"
|
color="accent"
|
||||||
@click="is_showing_calendar_picker = true"
|
@click="is_showing_calendar_picker = true"
|
||||||
:disable="timesheet_store.is_loading || is_disabled"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
class="q-px-xl"
|
class="q-px-xl"
|
||||||
|
|
@ -99,7 +100,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
push rounded
|
push rounded
|
||||||
icon="keyboard_arrow_right"
|
icon="keyboard_arrow_right"
|
||||||
color="primary"
|
color="accent"
|
||||||
@click="getNextPayPeriod"
|
@click="getNextPayPeriod"
|
||||||
:disable="timesheet_store.is_loading || is_disabled"
|
:disable="timesheet_store.is_loading || is_disabled"
|
||||||
class="q-ml-sm q-px-sm"
|
class="q-ml-sm q-px-sm"
|
||||||
|
|
@ -125,7 +126,7 @@
|
||||||
class="q-mt-xl"
|
class="q-mt-xl"
|
||||||
today-btn
|
today-btn
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
:options="date => date > '2023/12/16'"
|
:options="date => date >= PAY_PERIOD_DATE_LIMIT"
|
||||||
@update:model-value="onDateSelected"
|
@update:model-value="onDateSelected"
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
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 { empty_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { Expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -18,61 +18,79 @@
|
||||||
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 = useExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
|
const openDatePicker = () => {
|
||||||
|
is_navigator_open.value = true;
|
||||||
|
if (expenses_store.current_expense.date === '') {
|
||||||
|
expenses_store.current_expense.date = date.formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancelUpdateMode = () => {
|
const cancelUpdateMode = () => {
|
||||||
expenses_store.current_expense = empty_expense;
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
expenses_store.initial_expense = empty_expense;
|
expenses_store.initial_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
}
|
expenses_store.mode = 'create';
|
||||||
|
};
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
if (mode.value === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||||
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExpenseCalendarRange = (current_date: string) => {
|
||||||
|
const period = timesheet_store.pay_period;
|
||||||
|
if (period !== undefined) return current_date >= period.period_start && current_date <= period.period_end;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
flat
|
|
||||||
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||||
|
:key="expenses_store.current_expense.id"
|
||||||
|
flat
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
>
|
>
|
||||||
<div class="text-subtitle2 q-py-sm">
|
<div class="text-uppercase text-weight-medium q-pt-sm q-px-lg">
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="row justify-between items-start rounded-5 q-px-lg q-pb-sm">
|
||||||
class="row justify-between rounded-5"
|
|
||||||
:class="mode === 'update' ? 'bg-accent' : ''"
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
filled
|
outlined
|
||||||
readonly
|
readonly
|
||||||
stack-label
|
stack-label
|
||||||
class="col q-px-xs"
|
class="col q-px-xs"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('timesheet.expense.date')"
|
:label="$t('timesheet.expense.date')"
|
||||||
>
|
>
|
||||||
<template #before>
|
<template #prepend>
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
icon="event"
|
icon="event"
|
||||||
color="primary"
|
color="accent"
|
||||||
@click="is_navigator_open = true"
|
class="q-mr-sm"
|
||||||
|
@click="openDatePicker"
|
||||||
/>
|
/>
|
||||||
<q-dialog v-model="is_navigator_open">
|
|
||||||
|
<q-dialog
|
||||||
|
v-model="is_navigator_open"
|
||||||
|
transition-show="jump-right"
|
||||||
|
transition-hide="jump-right"
|
||||||
|
>
|
||||||
<q-date
|
<q-date
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
@update:model-value="is_navigator_open = false"
|
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
|
event-color="accent"
|
||||||
|
:options="getExpenseCalendarRange"
|
||||||
|
@update:model-value="is_navigator_open = false"
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -82,19 +100,25 @@
|
||||||
<q-select
|
<q-select
|
||||||
v-model="expenses_store.current_expense.type"
|
v-model="expenses_store.current_expense.type"
|
||||||
:options="EXPENSE_TYPE"
|
:options="EXPENSE_TYPE"
|
||||||
filled
|
standout="bg-blue-grey-9"
|
||||||
dense
|
dense
|
||||||
class="col q-px-xs"
|
|
||||||
color="primary"
|
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
|
hide-dropdown-icon
|
||||||
|
class="col q-px-xs"
|
||||||
|
color="primary"
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
|
:menu-offset="[0, 10]"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
:option-label="label => $t(`timesheet.expense.types.${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 ?? 'EXPENSES')">
|
<div 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"
|
||||||
|
|
@ -102,18 +126,23 @@
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
stack-label
|
stack-label
|
||||||
clearable
|
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col q-px-xs"
|
class="col q-px-xs"
|
||||||
:label="$t('timesheet.expense.amount')"
|
label-slot
|
||||||
suffix="$"
|
suffix="$"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.amountRequired]"
|
:rules="[rules.amountRequired]"
|
||||||
/>
|
>
|
||||||
</template>
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.amount') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- mileage input -->
|
<!-- mileage input -->
|
||||||
<template v-else>
|
<div v-else>
|
||||||
<q-input
|
<q-input
|
||||||
key="mileage"
|
key="mileage"
|
||||||
v-model.number="expenses_store.current_expense.mileage"
|
v-model.number="expenses_store.current_expense.mileage"
|
||||||
|
|
@ -124,31 +153,37 @@
|
||||||
clearable
|
clearable
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col q-px-xs"
|
class="col q-px-xs"
|
||||||
:label="$t('timesheet.expense.mileage')"
|
label-slot
|
||||||
suffix="km"
|
suffix="km"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.mileageRequired]"
|
:rules="[rules.mileageRequired]"
|
||||||
/>
|
>
|
||||||
</template>
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.mileage') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- employee comment input -->
|
<!-- employee comment input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.comment"
|
v-model="expenses_store.current_expense.comment"
|
||||||
filled
|
filled
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
color="primary"
|
color="primary"
|
||||||
type="text"
|
type="text"
|
||||||
class="col q-px-sm"
|
class="col q-px-sm"
|
||||||
dense
|
|
||||||
stack-label
|
|
||||||
clearable
|
|
||||||
:counter="true"
|
:counter="true"
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.commentRequired]"
|
:rules="[rules.commentRequired]"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold ">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
{{ $t('timesheet.expense.comment') }}
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
@ -156,14 +191,14 @@
|
||||||
<!-- import attach file section -->
|
<!-- import attach file section -->
|
||||||
<q-file
|
<q-file
|
||||||
v-model="files"
|
v-model="files"
|
||||||
:label="$t('timesheet.expense.hints.attach_file')"
|
dense
|
||||||
filled
|
filled
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
multiple
|
||||||
stack-label
|
stack-label
|
||||||
|
:label="$t('timesheet.expense.hints.attach_file')"
|
||||||
class="col"
|
class="col"
|
||||||
style="max-width: 300px;"
|
style="max-width: 300px;"
|
||||||
dense
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
@ -173,28 +208,29 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-file>
|
</q-file>
|
||||||
|
</div>
|
||||||
|
<div class="col row full-width items-center">
|
||||||
|
<q-space />
|
||||||
|
|
||||||
<!-- add btn section -->
|
<q-btn
|
||||||
<div>
|
v-if="expenses_store.mode === 'update'"
|
||||||
<q-btn
|
flat
|
||||||
v-if="mode === 'update'"
|
dense
|
||||||
flat
|
class="col-auto q-ml-sm"
|
||||||
dense
|
icon="clear"
|
||||||
size="sm"
|
color="negative"
|
||||||
class="q-mt-sm q-ml-sm"
|
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
||||||
@click="cancelUpdateMode"
|
@click="cancelUpdateMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
dense
|
color="accent"
|
||||||
color="primary"
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||||
icon="add"
|
:label="$q.screen.gt.sm ? (expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')) : ''"
|
||||||
size="sm"
|
class="q-px-sm q-mb-sm q-mx-lg"
|
||||||
class="q-mt-sm q-ml-sm"
|
type="submit"
|
||||||
type="submit"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,43 +2,91 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable */
|
import { computed } from 'vue';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
const expense_store = useExpensesStore();
|
const expense_store = useExpensesStore();
|
||||||
|
const weekly_totals = computed(() => {
|
||||||
|
let expenses = 0;
|
||||||
|
let mileage = 0;
|
||||||
|
timesheet_store.timesheets.forEach(timesheet => {
|
||||||
|
expenses += timesheet.weekly_expenses.expenses ?? 0;
|
||||||
|
mileage += timesheet.weekly_expenses.mileage ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { expenses, mileage };
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-item class="row justify-between items-center q-pa-none">
|
<div class="column items-center q-pa-none">
|
||||||
<q-item-label
|
<div class="col row full-width">
|
||||||
header
|
<q-item-label class="col text-h6 text-weight-bolder text-uppercase q-py-sm q-px-md">
|
||||||
class="text-h6 col q-pa-none"
|
{{ $t('timesheet.expense.title') }}
|
||||||
>
|
</q-item-label>
|
||||||
{{ $t('timesheet.expense.title') }}
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<!-- <q-item-section
|
<q-space />
|
||||||
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
|
<q-btn
|
||||||
no-wrap
|
square
|
||||||
class="col-auto items-center"
|
icon="clear"
|
||||||
>
|
color="negative"
|
||||||
<q-badge
|
class="col-auto"
|
||||||
outline
|
style="border-radius: 0 0 0 5px;"
|
||||||
class="q-py-xs q-px-md"
|
@click="expense_store.close"
|
||||||
color="primary"
|
|
||||||
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses?.total_mileage.toFixed(1) + ' km'"
|
|
||||||
/>
|
/>
|
||||||
</q-item-section> -->
|
</div>
|
||||||
</q-item>
|
|
||||||
|
<div class="col column items-end full-width q-pt-sm q-px-md">
|
||||||
|
<div class="col-auto row items-center q-px-sm">
|
||||||
|
<span
|
||||||
|
v-if="$q.screen.gt.sm"
|
||||||
|
class="col-auto text-uppercase text-weight-light text-accent q-mr-xs"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.total_amount') }} :
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
v-else
|
||||||
|
name="payments"
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="col-auto text-weight-light"
|
||||||
|
style="font-size: 2.5em; line-height: 1em;"
|
||||||
|
>
|
||||||
|
{{ weekly_totals.expenses.toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto row items-center q-px-sm">
|
||||||
|
<span
|
||||||
|
v-if="$q.screen.gt.sm"
|
||||||
|
class="col text-uppercase text-weight-light text-accent q-mr-xs"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.total_mileage') }} :
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
v-else
|
||||||
|
name="drive_eta"
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="col-auto text-weight-light"
|
||||||
|
style="font-size: 2.5em; line-height: 1em;"
|
||||||
|
>
|
||||||
|
{{ weekly_totals.mileage.toFixed(1) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,60 +2,56 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable */
|
import { date } from 'quasar';
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
import { empty_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
const { expense, horizontal = false } = defineProps<{
|
const { expense, horizontal = false } = defineProps<{
|
||||||
expense: Expense;
|
expense: Expense;
|
||||||
index: number;
|
index: number;
|
||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
const is_approved = defineModel<boolean>({ required: true });
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
const is_approved = defineModel<boolean>({ required: true });
|
|
||||||
const is_selected = ref(false);
|
|
||||||
const refresh_key = ref(1);
|
const refresh_key = ref(1);
|
||||||
|
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
||||||
|
const background_style = computed(() => deepEqual(expense, expenses_store.current_expense) ? 'border: 3px solid var(--q-accent);' : '');
|
||||||
|
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
||||||
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
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 () => {
|
const requestExpenseDeletion = async () => {
|
||||||
// expenses_store.mode = 'delete';
|
await expenses_api.deleteExpenseById(expense.id);
|
||||||
expenses_store.initial_expense = expense;
|
|
||||||
expenses_store.current_expense = empty_expense;
|
|
||||||
await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExpenseClicked() {
|
const onExpenseClicked = () => {
|
||||||
if (is_authorized_to_approve.value) {
|
if (is_authorized_to_approve.value) {
|
||||||
is_approved.value = !is_approved.value;
|
is_approved.value = !is_approved.value;
|
||||||
refresh_key.value += 1;
|
refresh_key.value += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onUpdateClicked = () => {
|
||||||
|
if (deepEqual(expense, expenses_store.current_expense)) {
|
||||||
|
expenses_store.mode = 'create';
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expenses_store.mode = 'update';
|
||||||
|
expenses_store.current_expense = expense;
|
||||||
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -66,27 +62,16 @@
|
||||||
<q-item
|
<q-item
|
||||||
:key="refresh_key"
|
:key="refresh_key"
|
||||||
:clickable="horizontal"
|
:clickable="horizontal"
|
||||||
class="row col-4 q-ma-xs shadow-2"
|
class="row col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
|
||||||
:style="expenseItemStyle + approvedClass"
|
:class="background_class + approved_class"
|
||||||
|
:style="background_style"
|
||||||
@click="onExpenseClicked"
|
@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 -->
|
<!-- avatar type icon section -->
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<q-badge
|
<q-badge
|
||||||
|
|
@ -97,14 +82,14 @@
|
||||||
<q-icon
|
<q-icon
|
||||||
name="shield"
|
name="shield"
|
||||||
size="xs"
|
size="xs"
|
||||||
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')"
|
||||||
/>
|
/>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<q-item-section class="col-auto">
|
<q-item-section class="col col-md-2 text-weight-bold">
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
<template v-if="typeof expense.mileage === 'number'">
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
|
@ -121,37 +106,45 @@
|
||||||
<q-item-label
|
<q-item-label
|
||||||
caption
|
caption
|
||||||
lines="1"
|
lines="1"
|
||||||
|
class="text-uppercase text-weight-light"
|
||||||
|
:class="approved_class"
|
||||||
>
|
>
|
||||||
<!-- {{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' }) }} -->
|
{{ $d(new Date(expense.date), { month: 'short', day: 'numeric', weekday: 'long' }) }}
|
||||||
{{ expense.date }}
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-space v-if="horizontal" />
|
<q-space v-if="horizontal" />
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<!-- attachment file icon -->
|
||||||
<q-item-section side>
|
<q-item-section avatar>
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
dense
|
:color="expense.is_approved ? 'white' : 'accent'"
|
||||||
size="md"
|
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||||
color="primary"
|
class="col-auto q-mx-sm q-px-sm q-pb-sm"
|
||||||
class="q-mx-lg"
|
|
||||||
icon="attach_file"
|
icon="attach_file"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-label class="col text-weight-light text-caption">
|
||||||
|
<span>attachment_goes_here.jpg</span>
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
<!-- comment section -->
|
<!-- comment section -->
|
||||||
<q-item-section
|
<q-item-section
|
||||||
v-if="!horizontal"
|
v-if="!horizontal"
|
||||||
top
|
top
|
||||||
>
|
>
|
||||||
<q-item-label lines="1">
|
<q-item-label
|
||||||
|
lines="1"
|
||||||
|
class="text-weight-medium text-uppercase"
|
||||||
|
>
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
caption
|
caption
|
||||||
lines="1"
|
lines="1"
|
||||||
|
:class="approved_class"
|
||||||
>
|
>
|
||||||
{{ expense.comment }}
|
{{ expense.comment }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
@ -159,10 +152,13 @@
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
<!-- supervisor comment section -->
|
||||||
<q-item-section
|
<q-item-section
|
||||||
v-if="expense.supervisor_comment && !horizontal"
|
v-if="is_authorized_to_approve"
|
||||||
top
|
top
|
||||||
>
|
>
|
||||||
<q-item-label lines="1">
|
<q-item-label
|
||||||
|
lines="1"
|
||||||
|
class="text-weight-medium text-uppercase"
|
||||||
|
>
|
||||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
|
|
@ -174,27 +170,27 @@
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section
|
<q-item-section :side="$q.screen.gt.sm">
|
||||||
side
|
|
||||||
class="q-pa-none"
|
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
flat
|
||||||
dense
|
size="lg"
|
||||||
size="xs"
|
|
||||||
color="primary"
|
|
||||||
icon="edit"
|
icon="edit"
|
||||||
class="q-mb-xs z-top"
|
color="accent"
|
||||||
@click.stop="setExpenseToModify"
|
:disable="expense.is_approved"
|
||||||
|
class="q-pa-none z-top"
|
||||||
|
:class="expense.is_approved ? 'invisible no-pointer' : ''"
|
||||||
|
@click.stop="onUpdateClicked"
|
||||||
/>
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section :side="$q.screen.gt.sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
flat
|
||||||
dense
|
size="lg"
|
||||||
size="xs"
|
:icon="expense.is_approved ? 'verified' : 'close'"
|
||||||
color="negative"
|
:color="expense.is_approved ? 'white' : 'negative'"
|
||||||
icon="close"
|
class="q-pa-none z-top"
|
||||||
class="z-top"
|
:class="expense.is_approved ? 'no-pointer' : ''"
|
||||||
@click.stop="requestExpenseDeletion"
|
@click.stop="requestExpenseDeletion"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,41 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { computed } from 'vue';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
||||||
|
|
||||||
const expenses_store = useExpensesStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const { horizontal = false } = defineProps<{
|
const { horizontal = false } = defineProps<{
|
||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const expenses_list = computed(() => {
|
||||||
|
if (timesheet_store.timesheets !== undefined) {
|
||||||
|
return timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- liste des dépenses pré existantes -->
|
<!-- liste des dépenses pré existantes -->
|
||||||
<q-list
|
<q-list
|
||||||
padding
|
padding
|
||||||
class="rounded-borders"
|
class="q-px-lg"
|
||||||
:class="horizontal ? 'row flex-center' : ''"
|
:class="horizontal ? 'row flex-center' : ''"
|
||||||
>
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
v-if="expenses_store.pay_period_expenses?.length === 0"
|
v-if="expenses_list.length > 0"
|
||||||
class="text-italic q-px-sm"
|
class="text-italic q-px-sm"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.empty_list') }}
|
{{ $t('timesheet.expense.empty_list') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
<ExpenseDialogListItem
|
<ExpenseDialogListItem
|
||||||
v-for="(expense, index) in expenses_store.pay_period_expenses"
|
v-for="(expense, index) in expenses_list"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-model="expense.is_approved"
|
v-model="expense.is_approved"
|
||||||
:index="index"
|
:index="index"
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,18 @@
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="expense_store.is_open"
|
v-model="expense_store.is_open"
|
||||||
persistent
|
persistent
|
||||||
|
transition-show="jump-down"
|
||||||
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa-md"
|
class="q-pa-none rounded-10 shadow-10 bg-secondary"
|
||||||
style=" min-width: 70vw;"
|
style=" min-width: 70vw;"
|
||||||
>
|
>
|
||||||
<q-inner-loading :showing="expense_store.is_loading">
|
<q-inner-loading :showing="expense_store.is_loading">
|
||||||
<q-spinner size="32px" />
|
<q-spinner size="32px" />
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section class="q-pa-none">
|
||||||
<!-- <q-banner
|
<!-- <q-banner
|
||||||
v-if="expenses_error"
|
v-if="expenses_error"
|
||||||
dense
|
dense
|
||||||
|
|
@ -36,29 +38,21 @@
|
||||||
|
|
||||||
<ExpenseDialogList />
|
<ExpenseDialogList />
|
||||||
|
|
||||||
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved" />
|
<transition
|
||||||
<q-icon
|
appear
|
||||||
v-else
|
enter-active-class="animated fadeInDown faster"
|
||||||
name="block"
|
leave-active-class="animated fadeOutDown faster"
|
||||||
color="negative"
|
mode="out-in"
|
||||||
size="lg"
|
>
|
||||||
/>
|
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved" />
|
||||||
|
<q-icon
|
||||||
<q-separator spaced />
|
v-else
|
||||||
|
name="block"
|
||||||
|
color="negative"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<!-- close btn -->
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
class="col-auto q-mr-sm"
|
|
||||||
color="primary"
|
|
||||||
:label="$t('shared.label.close')"
|
|
||||||
@click="expense_store.close"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
53
src/modules/timesheets/components/shift-list-date-widget.vue
Normal file
53
src/modules/timesheets/components/shift-list-date-widget.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { date, useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const q = useQuasar();
|
||||||
|
const { extractDate } = date;
|
||||||
|
|
||||||
|
const { displayDate, dense = false, approved = false} = defineProps<{
|
||||||
|
displayDate: string;
|
||||||
|
dense?: boolean;
|
||||||
|
approved?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
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 display_date = extractDate(displayDate, 'YYYY-MM-DD');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="column flex-center rounded-10 text-center self-center bg-transparent"
|
||||||
|
:style="date_box_size"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!dense"
|
||||||
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
|
:class="approved ? 'text-white' : ''"
|
||||||
|
:style="'font-size: ' + weekday_font_size"
|
||||||
|
>
|
||||||
|
{{ $d(display_date, { weekday: $q.screen.lt.md ? 'short' : 'long'}) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="col-auto text-weight-bolder"
|
||||||
|
:class="approved ? 'text-white' : ''"
|
||||||
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
|
>
|
||||||
|
{{ display_date.getDate() }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="col-auto text-uppercase text-weight-bold"
|
||||||
|
:class="approved ? 'text-white' : ''"
|
||||||
|
:style="'font-size: ' + weekday_font_size"
|
||||||
|
>
|
||||||
|
{{ $d(display_date, { month: $q.screen.lt.md ? 'short' : 'long' }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
265
src/modules/timesheets/components/shift-list-day-row.vue
Normal file
265
src/modules/timesheets/components/shift-list-day-row.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
/* eslint-disable*/
|
||||||
|
import { onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef } from 'vue';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
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();
|
||||||
|
|
||||||
|
interface ShiftOption {
|
||||||
|
label: string;
|
||||||
|
value: ShiftType;
|
||||||
|
icon: string;
|
||||||
|
icon_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
|
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
||||||
|
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||||
|
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||||
|
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||||
|
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||||
|
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
const { dense = false, outlined = false } = defineProps<{
|
||||||
|
dense?: boolean;
|
||||||
|
outlined?: boolean;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'saveComment': [comment: string, shift_id: number];
|
||||||
|
'requestDelete': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const is_showing_time_picker = ref(false);
|
||||||
|
const select_ref = useTemplateRef<QSelect>('select');
|
||||||
|
const initial_shift = ref<Shift>(unwrapAndClone(toRaw(shift.value)))
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
|
|
||||||
|
const onBlurShiftTypeSelect = () => {
|
||||||
|
if (shift_type_selected.value === undefined) {
|
||||||
|
shift.value.type = 'REGULAR';
|
||||||
|
shift.value.id = 0;
|
||||||
|
emit('requestDelete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const slideDeleteShift = async (reset: () => void) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reset();
|
||||||
|
emit('requestDelete');
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommentCounterColor = (comment_length: number) => {
|
||||||
|
if (comment_length < 200) return 'primary';
|
||||||
|
if (comment_length < 250) return 'warning';
|
||||||
|
return 'negative';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (ui_store.focus_next_component) {
|
||||||
|
select_ref.value?.focus();
|
||||||
|
select_ref.value?.showPopup();
|
||||||
|
shift_type_selected.value = undefined;
|
||||||
|
ui_store.focus_next_component = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-slide-item
|
||||||
|
right-color="negative"
|
||||||
|
class="q-my-xs rounded-5 bg-transparent"
|
||||||
|
@right="details => slideDeleteShift(details.reset)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
#right
|
||||||
|
v-if="ui_store.is_mobile_mode"
|
||||||
|
>
|
||||||
|
<q-icon name="delete" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row flex-center text-uppercase rounded-5 bg-transparent"
|
||||||
|
>
|
||||||
|
<!-- shift type -->
|
||||||
|
<q-select
|
||||||
|
ref="select"
|
||||||
|
v-model="shift_type_selected"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
dense
|
||||||
|
:readonly="shift.is_approved"
|
||||||
|
:options-dense="!ui_store.is_mobile_mode"
|
||||||
|
hide-dropdown-icon
|
||||||
|
:menu-offset="[0, 10]"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
:options="SHIFT_OPTIONS"
|
||||||
|
class="rounded-5 q-mx-xs bg-dark"
|
||||||
|
:class="(ui_store.is_mobile_mode ? 'col-12 q-mb-xs ' : 'col ')"
|
||||||
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
|
@blur="onBlurShiftTypeSelect"
|
||||||
|
@update:model-value="option => shift.type = option.value"
|
||||||
|
>
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<div
|
||||||
|
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
:color="scope.opt.icon_color"
|
||||||
|
size="sm"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
class="col-auto ellipsis"
|
||||||
|
>{{ scope.opt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<!-- punch in field -->
|
||||||
|
<q-input
|
||||||
|
v-model="shift.start_time"
|
||||||
|
dense
|
||||||
|
:readonly="shift.is_approved"
|
||||||
|
type="time"
|
||||||
|
:standout="$q.dark.isActive ? 'bg-blue-grey-9' : 'bg-blue-grey-1 text-white'"
|
||||||
|
label-slot
|
||||||
|
label-color="accent"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
class="col rounded-5 bg-dark"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.in') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- punch out field -->
|
||||||
|
<q-input
|
||||||
|
v-model="shift.end_time"
|
||||||
|
dense
|
||||||
|
:readonly="shift.is_approved"
|
||||||
|
type="time"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
label-slot
|
||||||
|
label-color="accent"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
class="col rounded-5 bg-dark"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- comment and delete buttons -->
|
||||||
|
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto'">
|
||||||
|
<q-icon
|
||||||
|
v-if="shift.type && dense"
|
||||||
|
:name="shift.comment ? 'comment' : ''"
|
||||||
|
color="primary"
|
||||||
|
:size="dense ? 'xs' : 'sm'"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
|
:text-color="shift.comment ? 'accent' : 'grey-5'"
|
||||||
|
class="col"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
||||||
|
>
|
||||||
|
<q-popup-edit
|
||||||
|
v-model="shift.comment"
|
||||||
|
:title="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
auto-save
|
||||||
|
v-slot="scope"
|
||||||
|
class="bg-dark"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
color="white"
|
||||||
|
v-model="scope.value"
|
||||||
|
dense
|
||||||
|
:readonly="shift.is_approved"
|
||||||
|
autofocus
|
||||||
|
counter
|
||||||
|
bottom-slots
|
||||||
|
:maxlength="COMMENT_LENGTH_MAX"
|
||||||
|
class="q-pb-lg"
|
||||||
|
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
||||||
|
@keyup.enter="scope.set"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon name="edit" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #counter>
|
||||||
|
<div class="row flex-center">
|
||||||
|
<q-space />
|
||||||
|
<q-knob
|
||||||
|
:model-value="scope.value?.length"
|
||||||
|
readonly
|
||||||
|
:max="COMMENT_LENGTH_MAX"
|
||||||
|
size="1.6em"
|
||||||
|
:thickness="0.4"
|
||||||
|
:color="getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
|
track-color="grey-4"
|
||||||
|
class="col-auto q-mr-xs"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
|
>{{ 280 - (scope.value?.length ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-popup-edit>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="!ui_store.is_mobile_mode"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:disable="shift.is_approved"
|
||||||
|
tabindex="-1"
|
||||||
|
icon="cancel"
|
||||||
|
text-color="negative"
|
||||||
|
class="col"
|
||||||
|
:class="shift.is_approved ? 'invisible' : ''"
|
||||||
|
@click="$emit('requestDelete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-slide-item>
|
||||||
|
</template>
|
||||||
44
src/modules/timesheets/components/shift-list-day.vue
Normal file
44
src/modules/timesheets/components/shift-list-day.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
||||||
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
|
||||||
|
const shift_api = useShiftApi();
|
||||||
|
|
||||||
|
const { day, dense = false, outlined = false, approved = false } = defineProps<{
|
||||||
|
day: TimesheetDay;
|
||||||
|
dense?: boolean;
|
||||||
|
outlined?: boolean;
|
||||||
|
approved?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'deleteUnsavedShift': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const deleteCurrentShift = async (shift: Shift) => {
|
||||||
|
if (shift.id <= 0) {
|
||||||
|
shift.id = 0;
|
||||||
|
emit('deleteUnsavedShift');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await shift_api.deleteShiftById(shift.id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="column justify-center q-py-xs" :class="approved ? 'bg-dark' : ''">
|
||||||
|
<ShiftListDayRow
|
||||||
|
v-for="shift, shift_index in day.shifts"
|
||||||
|
:key="shift_index"
|
||||||
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
|
:outlined="outlined"
|
||||||
|
:dense="dense"
|
||||||
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
<script
|
|
||||||
setup
|
|
||||||
lang="ts"
|
|
||||||
>
|
|
||||||
/* eslint-disable*/
|
|
||||||
import { onMounted, ref, useTemplateRef } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { QSelect } from 'quasar';
|
|
||||||
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const ui_store = useUiStore();
|
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
|
||||||
|
|
||||||
const { dense = false } = defineProps<{
|
|
||||||
dense?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
'saveComment': [comment: string, shift_id: number];
|
|
||||||
'requestUpdate': [shift_id: number];
|
|
||||||
'requestDelete': [void];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const time_picker_model = ref('');
|
|
||||||
const is_showing_time_picker = ref(false);
|
|
||||||
const select_ref = useTemplateRef<QSelect>('select');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const options: { label: string, value: ShiftType, icon: string, icon_color: string }[] = [
|
|
||||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
|
||||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
|
||||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
|
||||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
|
||||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
|
||||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const shift_type_selected = ref(options.find(option => option.value == shift.value.type));
|
|
||||||
|
|
||||||
const showTimePicker = (time: string) => {
|
|
||||||
is_showing_time_picker.value = true;
|
|
||||||
time_picker_model.value = time;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (ui_store.focus_next_component) {
|
|
||||||
select_ref.value?.focus();
|
|
||||||
select_ref.value?.showPopup();
|
|
||||||
ui_store.focus_next_component = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="shift.shift_id !== 0"
|
|
||||||
class="col row flex-center text-uppercase rounded-10"
|
|
||||||
>
|
|
||||||
<!-- shift type -->
|
|
||||||
<q-select
|
|
||||||
ref="select"
|
|
||||||
v-model="shift_type_selected"
|
|
||||||
standout="bg-blue-grey-9"
|
|
||||||
dense
|
|
||||||
options-dense
|
|
||||||
hide-dropdown-icon
|
|
||||||
:menu-offset="[0, 10]"
|
|
||||||
:options="options"
|
|
||||||
class="rounded-5 q-mx-xs shadow-1"
|
|
||||||
:class="ui_store.is_mobile_mode ? 'col-auto' : 'col'"
|
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
|
||||||
popup-content-style="border: 2px solid var(--q-primary)"
|
|
||||||
>
|
|
||||||
<template #selected-item="scope">
|
|
||||||
<div
|
|
||||||
class="row text-weight-bold q-ma-none q-pa-none no-wrap ellipsis"
|
|
||||||
:class="ui_store.is_mobile_mode ? 'items-center' : 'flex-center'"
|
|
||||||
:tabindex="scope.tabindex"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
:name="scope.opt.icon"
|
|
||||||
:color="scope.opt.icon_color"
|
|
||||||
size="sm"
|
|
||||||
class="col-auto q-mx-xs"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="$q.screen.gt.md"
|
|
||||||
style="line-height: 0.9em;"
|
|
||||||
class="col ellipsis"
|
|
||||||
>{{ scope.opt.label }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
|
|
||||||
<!-- punch-in timestamp -->
|
|
||||||
<q-input
|
|
||||||
v-model="shift.start_time"
|
|
||||||
dense
|
|
||||||
type="time"
|
|
||||||
standout="bg-blue-grey-9"
|
|
||||||
label-slot
|
|
||||||
label-color="primary"
|
|
||||||
input-class="text-weight-medium"
|
|
||||||
input-style="font-size: 1.2em;"
|
|
||||||
class="col q-mx-xs"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span
|
|
||||||
class="text-weight-bolder"
|
|
||||||
style="font-size: 0.95em;"
|
|
||||||
>{{ $t('shared.misc.in') }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<q-btn
|
|
||||||
v-if="ui_store.is_mobile_mode"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="access_time"
|
|
||||||
color="primary"
|
|
||||||
@click.stop="showTimePicker(shift.start_time)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<!-- punch-out timestamps -->
|
|
||||||
<q-input
|
|
||||||
v-model="shift.end_time"
|
|
||||||
dense
|
|
||||||
type="time"
|
|
||||||
standout="bg-blue-grey-9"
|
|
||||||
label-slot
|
|
||||||
label-color="primary"
|
|
||||||
input-class="text-weight-medium"
|
|
||||||
input-style="font-size: 1.2em;"
|
|
||||||
class="col q-mx-xs"
|
|
||||||
lazy-rules
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span
|
|
||||||
class="text-weight-bolder"
|
|
||||||
style="font-size: 0.95em;"
|
|
||||||
>{{ $t('shared.misc.out') }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<q-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
|
|
||||||
v-if="shift.type && dense"
|
|
||||||
:name="shift.comment ? 'comment' : ''"
|
|
||||||
color="primary"
|
|
||||||
:size="dense ? 'xs' : 'sm'"
|
|
||||||
class="col-auto q-pa-none q-mr-xs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-else
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
|
||||||
:text-color="shift.comment ? 'primary' : 'grey-8'"
|
|
||||||
class="col-auto q-ma-none q-pl-md full-height"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
unelevated
|
|
||||||
tabindex="-1"
|
|
||||||
icon="cancel"
|
|
||||||
color="negative"
|
|
||||||
class="q-pa-none q-mr-xs"
|
|
||||||
@click="$emit('requestDelete')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -3,27 +3,17 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useQuasar } from 'quasar';
|
|
||||||
import ShiftListRow from 'src/modules/timesheets/components/shift-list-row.vue';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
|
||||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import ShiftListDay from 'src/modules/timesheets/components/shift-list-day.vue';
|
||||||
|
import ShiftListDateWidget from 'src/modules/timesheets/components/shift-list-date-widget.vue';
|
||||||
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
|
||||||
|
const { extractDate } = date;
|
||||||
|
|
||||||
const q = useQuasar();
|
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const shift_api = useShiftApi();
|
|
||||||
|
|
||||||
const { dense = false } = defineProps<{
|
|
||||||
dense?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
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 addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||||
ui_store.focus_next_component = true;
|
ui_store.focus_next_component = true;
|
||||||
|
|
@ -33,90 +23,139 @@
|
||||||
day_shifts.push(new_shift);
|
day_shifts.push(new_shift);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCurrentShift = async (shift: Shift) => {
|
const deleteUnsavedShift = (timesheet_index: number, day_index: number) => {
|
||||||
console.log('shift to delete: ', shift);
|
if (timesheet_store.timesheets !== undefined) {
|
||||||
if (shift.shift_id < 0) {
|
const day = timesheet_store.timesheets[timesheet_index]!.days[day_index]!;
|
||||||
shift.shift_id = 0;
|
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
|
||||||
return;
|
day.shifts = shifts_without_deleted_shift;
|
||||||
|
console.log("day's shifts after cleanup: ", day.shifts);
|
||||||
}
|
}
|
||||||
await shift_api.deleteShiftById(shift.shift_id);
|
}
|
||||||
|
|
||||||
|
const getDayApproval = (day: TimesheetDay) => {
|
||||||
|
if (day.shifts.length < 1) return false;
|
||||||
|
return day.shifts.every(shift => shift.is_approved === true);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
<div
|
||||||
|
:class="$q.screen.lt.md ? 'column full-width' : 'row'"
|
||||||
|
:style="$q.screen.lt.md ? 'width: 90vw !important;' : ''"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="timesheet in timesheet_store.timesheets"
|
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
||||||
:key="timesheet.timesheet_id"
|
:key="timesheet.timesheet_id"
|
||||||
class="col column"
|
class="col column"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="day in timesheet.days"
|
v-for="day, day_index in timesheet.days"
|
||||||
:key="day.date"
|
:key="day.date"
|
||||||
class="col-auto row shadow-2 rounded-10 q-ma-xs"
|
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="col row bg-dark"
|
v-if="ui_store.is_mobile_mode"
|
||||||
style="border-radius: 10px 0 0 10px;"
|
class="col column full-width"
|
||||||
>
|
>
|
||||||
<!-- Dates column -->
|
<q-card class="rounded-5 q-my-md" :class="getDayApproval(day) ? 'bg-accent' : 'bg-dark'">
|
||||||
<div
|
|
||||||
class="col-auto column flex-center bg-primary rounded-10 text-center q-ma-sm self-center"
|
<q-card-section
|
||||||
:class="$q.screen.lt.md ? '' : ''"
|
class="text-white text-weight-bolder text-uppercase text-h6 q-py-xs"
|
||||||
:style="date_box_size"
|
:class="getDayApproval(day) ? 'bg-dark' : 'bg-accent'"
|
||||||
>
|
style="line-height: 1em;"
|
||||||
<span
|
|
||||||
v-if="!dense"
|
|
||||||
class="col-auto text-uppercase text-white"
|
|
||||||
:style="'font-size: ' + weekday_font_size"
|
|
||||||
>
|
>
|
||||||
{{ $d(date.extractDate(day.date, 'YYYY-MM-DD'), {
|
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||||
weekday: $q.screen.lt.md ? 'short' :
|
weekday: 'long', day: 'numeric', month:
|
||||||
'long'
|
'long'
|
||||||
})
|
}) }}</span>
|
||||||
}}
|
</q-card-section>
|
||||||
</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 -->
|
<q-card-section
|
||||||
<div class="col column">
|
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||||
<ShiftListRow
|
class="q-pa-none transparent"
|
||||||
v-for="shift, shift_index in day.shifts"
|
>
|
||||||
:key="shift_index"
|
<ShiftListDay
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
outlined
|
||||||
:dense="dense"
|
:approved="getDayApproval(day)"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
:day="day"
|
||||||
/>
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
</div>
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions class="q-pa-none">
|
||||||
|
<q-btn
|
||||||
|
v-if="!getDayApproval(day)"
|
||||||
|
push
|
||||||
|
square
|
||||||
|
color="accent"
|
||||||
|
icon="more_time"
|
||||||
|
class="full-width"
|
||||||
|
style="border-radius: 0 0 5px 5px;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-if="getDayApproval(day)"
|
||||||
|
floating
|
||||||
|
class="bg-secondary q-pa-none rounded-50"
|
||||||
|
style="transform: translate(15px, -5px);"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="verified"
|
||||||
|
size="5em"
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto self-stretch">
|
<div
|
||||||
<q-btn
|
v-else
|
||||||
unelevated
|
class="col row full-width"
|
||||||
icon="more_time"
|
:class="getDayApproval(day) ? 'rounded-10 bg-accent' : ''"
|
||||||
:size="$q.screen.lt.md ? 'md' : 'lg'"
|
>
|
||||||
color="primary"
|
<div
|
||||||
text-color="white"
|
class="col row bg-dark"
|
||||||
class="full-height"
|
:class="getDayApproval(day) ? 'bg-transparent' : ''"
|
||||||
:class="$q.screen.lt.md ? 'q-px-xs' : ''"
|
style="border-radius: 10px 0 0 10px;"
|
||||||
style="border-radius: 0 10px 10px 0;"
|
>
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
<!-- Date block -->
|
||||||
/>
|
<ShiftListDateWidget
|
||||||
|
:display-date="day.date"
|
||||||
|
:approved="getDayApproval(day)"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- List of shifts -->
|
||||||
|
<ShiftListDay
|
||||||
|
:day="day"
|
||||||
|
class="col"
|
||||||
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto self-stretch">
|
||||||
|
<q-icon
|
||||||
|
v-if="getDayApproval(day)"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
|
size="xl"
|
||||||
|
class="full-height"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
:dense="!ui_store.is_mobile_mode"
|
||||||
|
icon="more_time"
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
text-color="white"
|
||||||
|
class="full-height"
|
||||||
|
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
||||||
|
style="border-radius: 0 10px 10px 0;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
56
src/modules/timesheets/components/timesheet-error-widget.vue
Normal file
56
src/modules/timesheets/components/timesheet-error-widget.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const shift_store = useShiftStore();
|
||||||
|
|
||||||
|
const is_showing_errors = computed(() => shift_store.shift_errors.length > 0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-slide-transition>
|
||||||
|
<div
|
||||||
|
clickable
|
||||||
|
v-if="is_showing_errors"
|
||||||
|
class="full-width q-px-md q-mb-sm"
|
||||||
|
@click="shift_store.shift_errors = []"
|
||||||
|
>
|
||||||
|
<q-list>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
dense
|
||||||
|
v-for="error, index in shift_store.shift_errors"
|
||||||
|
:key="index"
|
||||||
|
class="row items-center full-width bg-dark shadow-2 rounded-5 q-my-xs"
|
||||||
|
style="border: 2px solid var(--q-negative)"
|
||||||
|
>
|
||||||
|
<q-item-section class="col-auto">
|
||||||
|
<q-badge
|
||||||
|
outline
|
||||||
|
color="negative"
|
||||||
|
class="bg-dark text-weight-bolder"
|
||||||
|
>{{ error.conflicts.date }}</q-badge>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section class="col-auto">
|
||||||
|
<q-badge
|
||||||
|
outline
|
||||||
|
color="negative"
|
||||||
|
class="bg-dark text-weight-bolder"
|
||||||
|
>
|
||||||
|
{{ error.conflicts.start_time }} - {{ error.conflicts.end_time }}
|
||||||
|
</q-badge>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-label class="text-weight-medium text-caption q-ml-md">
|
||||||
|
{{ $t('timesheet.shift.errors.' + error.error_code) }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</q-slide-transition>
|
||||||
|
</template>
|
||||||
|
|
@ -5,12 +5,12 @@
|
||||||
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
import ShiftList from 'src/modules/timesheets/components/shift-list.vue';
|
||||||
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
|
import ExpenseDialog from 'src/modules/timesheets/components/expense-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 TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/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';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
|
||||||
const { open } = useExpensesStore();
|
const { open } = useExpensesStore();
|
||||||
const shift_api = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
|
|
@ -77,12 +77,12 @@ import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
<!-- save timesheet changes button -->
|
<!-- save timesheet changes button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.screen.gt.sm"
|
v-if="$q.screen.gt.sm"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheet_store.is_loading"
|
||||||
color="primary"
|
color="accent"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
:label="$t('shared.label.save')"
|
:label="$t('shared.label.save')"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
|
|
@ -94,7 +94,7 @@ import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
v-if="$q.screen.gt.sm"
|
v-if="$q.screen.gt.sm"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="accent"
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="open"
|
@click="open"
|
||||||
|
|
@ -102,6 +102,10 @@ import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<TimesheetErrorWidget />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
<ShiftList :dense="dense" />
|
<ShiftList :dense="dense" />
|
||||||
</q-card>
|
</q-card>
|
||||||
<ExpenseDialog />
|
<ExpenseDialog />
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const useExpensesApi = () => {
|
export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
|
|
||||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
||||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
};
|
};
|
||||||
|
|
@ -16,13 +16,13 @@ export const useExpensesApi = () => {
|
||||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
const deleteExpenseById = async (expense_id: number): Promise<void> => {
|
||||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
await expenses_store.deleteExpenseById(expense_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createExpenseByEmployeeEmail,
|
createExpenseByEmployeeEmail,
|
||||||
updateExpenseByEmployeeEmail,
|
updateExpenseByEmployeeEmail,
|
||||||
deleteExpenseByEmployeeEmail,
|
deleteExpenseById,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -20,15 +20,14 @@ export const useShiftApi = () => {
|
||||||
|
|
||||||
const saveShiftChanges = async () => {
|
const saveShiftChanges = async () => {
|
||||||
timesheet_store.is_loading = true;
|
timesheet_store.is_loading = true;
|
||||||
|
|
||||||
const create_success = await shift_store.createNewShifts();
|
const create_success = await shift_store.createNewShifts();
|
||||||
|
const update_success = await shift_store.updateShifts();
|
||||||
|
|
||||||
if (create_success) {
|
if (create_success || update_success){
|
||||||
const update_success = await shift_store.updateShifts();
|
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '');
|
||||||
|
|
||||||
if (update_success) {
|
|
||||||
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheet_store.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,44 +4,22 @@ export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', '
|
||||||
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', 'ON_CALL',];
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||||
|
|
||||||
export interface Expense {
|
export class Expense {
|
||||||
id: number;
|
id: number;
|
||||||
date: string; //YYYY-MM-DD
|
date: string; //YYYY-MM-DD
|
||||||
type: ExpenseType;
|
type: ExpenseType;
|
||||||
amount: number;
|
amount: number;
|
||||||
mileage?: number;
|
mileage?: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
supervisor_comment?: string;
|
supervisor_comment?: string;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
export const empty_expense: Expense = {
|
|
||||||
id: -1,
|
|
||||||
date: '',
|
|
||||||
type: 'EXPENSES',
|
|
||||||
amount: 0,
|
|
||||||
comment: '',
|
|
||||||
is_approved: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const test_expenses: Expense[] = [
|
|
||||||
{
|
|
||||||
id: 201,
|
|
||||||
date: '2025-01-06',
|
|
||||||
type: 'EXPENSES',
|
|
||||||
amount: 15.5,
|
|
||||||
comment: 'Lunch receipt',
|
|
||||||
is_approved: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 202,
|
|
||||||
date: '2025-01-07',
|
|
||||||
type: 'MILEAGE',
|
|
||||||
amount: 0,
|
|
||||||
mileage: 32.4,
|
|
||||||
comment: 'Travel to client site',
|
|
||||||
is_approved: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(date: string) {
|
||||||
|
this.id = -1;
|
||||||
|
this.date = date;
|
||||||
|
this.type = 'EXPENSES';
|
||||||
|
this.amount = 0;
|
||||||
|
this.comment = '';
|
||||||
|
this.is_approved = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,8 @@ export const SHIFT_TYPES: ShiftType[] = [
|
||||||
|
|
||||||
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
|
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
|
||||||
|
|
||||||
|
export type ShiftErrorCode = 'SHIFT_OVERLAP' | 'MISSING_START_TIME' | 'MISSING_END_TIME' | 'COMMENT_LENGTH_EXCEEDED' | 'APPROVAL_LOCK' | 'INVALID_DATE' | 'INVALID TYPE' | 'INVALID_TIMESHEET';
|
||||||
|
|
||||||
export type ShiftLegendItem = {
|
export type ShiftLegendItem = {
|
||||||
type: ShiftType;
|
type: ShiftType;
|
||||||
color: string;
|
color: string;
|
||||||
|
|
@ -17,7 +19,7 @@ export type ShiftLegendItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Shift {
|
export class Shift {
|
||||||
shift_id: number;
|
id: number;
|
||||||
timesheet_id: number;
|
timesheet_id: number;
|
||||||
date: string; //YYYY-MM-DD
|
date: string; //YYYY-MM-DD
|
||||||
type: ShiftType;
|
type: ShiftType;
|
||||||
|
|
@ -28,7 +30,7 @@ export class Shift {
|
||||||
is_remote: boolean;
|
is_remote: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.shift_id = -1;
|
this.id = -1;
|
||||||
this.timesheet_id = -1;
|
this.timesheet_id = -1;
|
||||||
this.date = '';
|
this.date = '';
|
||||||
this.type = 'REGULAR';
|
this.type = 'REGULAR';
|
||||||
|
|
@ -40,7 +42,21 @@ export class Shift {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NewShift {
|
export interface ShiftAPIResponse {
|
||||||
timesheet_id: number;
|
ok: boolean;
|
||||||
shifts: Shift[];
|
data?: {
|
||||||
|
shift: Shift;
|
||||||
|
overtime: unknown;
|
||||||
|
}
|
||||||
|
error?: ShiftAPIError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShiftAPIError {
|
||||||
|
error_code: ShiftErrorCode;
|
||||||
|
conflicts:
|
||||||
|
{
|
||||||
|
date: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const ExpenseService = {
|
export const ExpenseService = {
|
||||||
getExpensesByTimesheetId: async (timesheet_id: number) => {
|
createExpense: async (expense: Expense) => {
|
||||||
const response = await api.get(`timesheet/${timesheet_id}`);
|
const response = await api.post('expense/create', expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
upsertOrDeleteExpenseById: async (expense_id: number) => {
|
updateExpenseById: async (expense: Expense) => {
|
||||||
const response = await api.post(`epxense/${expense_id}`);
|
const response = await api.patch(`expense/update`, expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteExpenseById: async (expense_id: number): Promise<{ok: boolean, id: number, error?: unknown}> => {
|
||||||
|
const response = await api.delete(`expense/delete/${expense_id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/* eslint-disable */
|
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
import type { Shift, ShiftAPIResponse } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
export const ShiftService = {
|
export const ShiftService = {
|
||||||
deleteShiftById: async (shift_id: number) => {
|
deleteShiftById: async (shift_id: number) => {
|
||||||
|
|
@ -8,17 +7,15 @@ export const ShiftService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
createNewShifts: async (new_shifts: Shift[]) => {
|
createNewShifts: async (new_shifts: Shift[]):Promise<ShiftAPIResponse[]> => {
|
||||||
// const response = await api.post(`/shift/`, { dtos: new_shifts });
|
const response = await api.post(`/shift/create`, new_shifts);
|
||||||
// return response;
|
return response.data;
|
||||||
console.log('create shift payload: ', new_shifts);
|
|
||||||
return {status: 200};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateShifts: async (existing_shifts: Shift[]) => {
|
updateShifts: async (existing_shifts: Shift[]) => {
|
||||||
// const response = await api.patch(`/shift/`, { dtos: existing_shifts });
|
console.log('sent shifts: ', existing_shifts)
|
||||||
// return response;
|
const response = await api.patch(`/shift/update`, existing_shifts);
|
||||||
console.log('update shift payload: ', existing_shifts);
|
console.log('API response to existing shifts: ', response.data);
|
||||||
return {status: 200};
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -19,8 +19,8 @@ export const timesheetService = {
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimesheetsByPayPeriodAndEmployeeEmail: async (employee_email: string, year: number, period_number: number): Promise<TimesheetResponse> => {
|
getTimesheetsByPayPeriod: async (year: number, period_number: number): Promise<TimesheetResponse> => {
|
||||||
const response = await api.get('timesheets', { params: { employee_email, year, period_number } });
|
const response = await api.get('timesheets', { params: { year, period_number } });
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -2,7 +2,16 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { Notify } from 'quasar';
|
import { ref } from 'vue';
|
||||||
|
import { Notify } from 'quasar';
|
||||||
|
|
||||||
|
const LOREM_IPSUM = "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."
|
||||||
|
|
||||||
|
const slide = ref<string>('welcome');
|
||||||
|
|
||||||
const clickNotify = () => {
|
const clickNotify = () => {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
|
|
@ -10,38 +19,84 @@ import { Notify } from 'quasar';
|
||||||
color: 'info'
|
color: 'info'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page
|
<q-page
|
||||||
padding
|
padding
|
||||||
class="q-pa-md row items-center justify-center"
|
class="q-pa-md row justify-center"
|
||||||
>
|
>
|
||||||
<q-card class="shadow-2 col-9 dark-font">
|
<q-card flat class="column col-9 transparent ">
|
||||||
<q-img src="src/assets/line-truck-1.jpg">
|
<div class="col-1"></div>
|
||||||
<div class="absolute-bottom text-h5">
|
<q-carousel
|
||||||
Welcome to App Targo, !
|
v-model="slide"
|
||||||
</div>
|
transition-prev="jump-right"
|
||||||
</q-img>
|
transition-next="jump-left"
|
||||||
|
swipeable
|
||||||
<q-card-section class="text-center">
|
animated
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
|
control-color="accent"
|
||||||
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
|
navigation-icon="radio_button_unchecked"
|
||||||
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
navigation
|
||||||
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
class="col-5 bg-dark rounded-15 shadow-2"
|
||||||
deserunt mollit anim id est laborum.
|
>
|
||||||
</q-card-section>
|
<q-carousel-slide
|
||||||
|
name="welcome"
|
||||||
<q-separator />
|
class="column no-wrap flex-center q-pa-none q-pb-xl"
|
||||||
|
>
|
||||||
<q-card-actions align="center">
|
<q-img src="src/assets/line-truck-1.jpg" class="full-height">
|
||||||
|
<div class="absolute-bottom text-h5">
|
||||||
|
Welcome to App Targo!
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
<div class="q-mt-md text-center">
|
||||||
|
{{ LOREM_IPSUM }}
|
||||||
|
</div>
|
||||||
|
</q-carousel-slide>
|
||||||
|
<q-carousel-slide
|
||||||
|
name="tv"
|
||||||
|
class="column no-wrap flex-center q-pa-none q-pb-xl"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="live_tv"
|
||||||
|
size="56px"
|
||||||
|
/>
|
||||||
|
<div class="q-mt-md text-center">
|
||||||
|
{{ LOREM_IPSUM }}
|
||||||
|
</div>
|
||||||
|
</q-carousel-slide>
|
||||||
|
<q-carousel-slide
|
||||||
|
name="layers"
|
||||||
|
class="column no-wrap flex-center q-pa-none q-pb-xl"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="layers"
|
||||||
|
size="56px"
|
||||||
|
/>
|
||||||
|
<div class="q-mt-md text-center">
|
||||||
|
{{ LOREM_IPSUM }}
|
||||||
|
</div>
|
||||||
|
</q-carousel-slide>
|
||||||
|
<q-carousel-slide
|
||||||
|
name="map"
|
||||||
|
class="column no-wrap flex-center q-pa-none q-pb-xl"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="terrain"
|
||||||
|
size="56px"
|
||||||
|
/>
|
||||||
|
<div class="q-mt-md text-center">
|
||||||
|
{{ LOREM_IPSUM }}
|
||||||
|
</div>
|
||||||
|
</q-carousel-slide>
|
||||||
|
</q-carousel>
|
||||||
|
<div class="col column flex-center">
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
push
|
||||||
|
color="accent"
|
||||||
label="Click Me"
|
label="Click Me"
|
||||||
@click="clickNotify"
|
@click="clickNotify"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -20,7 +20,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
void handleAuthMessage(event);
|
void handleAuthMessage(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_AUTH_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
const oidc_popup = window.open(`${import.meta.env.VITE_TARGO_BACKEND_URL}auth/v1/login`, 'authPopup', 'width=600,height=800');
|
||||||
|
|
||||||
if (!oidc_popup)
|
if (!oidc_popup)
|
||||||
Notify.create({
|
Notify.create({
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,56 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
|
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
import { empty_expense, test_expenses, type Expense } from "src/modules/timesheets/models/expense.models";
|
|
||||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||||
|
import { date } from "quasar";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const pay_period_expenses = ref<Expense[]>(test_expenses);
|
const mode = ref<'create' | 'update' | 'delete'>('create');
|
||||||
const current_expense = ref<Expense>(empty_expense);
|
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
const initial_expense = ref<Expense>(empty_expense);
|
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
|
||||||
const error = ref<string | null>(null);
|
|
||||||
|
|
||||||
// const setErrorFrom = (err: unknown) => {
|
|
||||||
// const e = err as any;
|
|
||||||
// error.value = e?.message || 'Unknown error';
|
|
||||||
// };
|
|
||||||
|
|
||||||
const open = (): void => {
|
const open = (): void => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
is_loading.value = true;
|
current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
error.value = null;
|
initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
current_expense.value = empty_expense;
|
mode.value = 'create';
|
||||||
initial_expense.value = empty_expense;
|
|
||||||
|
|
||||||
// await getPayPeriodExpensesByTimesheetId(timesheet_id);
|
|
||||||
is_loading.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
error.value = null;
|
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPayPeriodExpensesByTimesheetId = async (timesheet_id: number): Promise<void> => {
|
const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => {
|
||||||
is_loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const expenses = await ExpenseService.getExpensesByTimesheetId(timesheet_id);
|
if (expense_id < 0) {
|
||||||
pay_period_expenses.value = expenses;
|
const data = await ExpenseService.createExpense(expense);
|
||||||
} catch (err: unknown) {
|
return data;
|
||||||
if (typeof err === 'object') {
|
|
||||||
const error = err as GenericApiError;
|
|
||||||
const status_code: number = error.status_code ?? 500;
|
|
||||||
// const data = error.context ?? '';
|
|
||||||
// error.value = data.message || data.error || err.message;
|
|
||||||
|
|
||||||
throw new ExpensesApiError({
|
|
||||||
status_code,
|
|
||||||
// error_code: data.error_code,
|
|
||||||
// message: data.message || data.error || err.message,
|
|
||||||
// context: data.context,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
|
||||||
is_loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
|
|
||||||
is_loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ExpenseService.upsertOrDeleteExpenseById(expense_id);
|
|
||||||
// TODO: Save response data into proper ref
|
// TODO: Save response data into proper ref
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// setErrorFrom(err);
|
// setErrorFrom(err);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
|
||||||
is_loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
|
||||||
|
const data = await ExpenseService.deleteExpenseById(expense_id);
|
||||||
|
return data.ok;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
pay_period_expenses,
|
mode,
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
error,
|
|
||||||
open,
|
open,
|
||||||
getPayPeriodExpensesByTimesheetId,
|
upsertExpensesById,
|
||||||
upsertOrDeleteExpensesById,
|
deleteExpenseById,
|
||||||
close,
|
close,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
68
src/stores/schedule-presets.store.ts
Normal file
68
src/stores/schedule-presets.store.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { SchedulePresetsService } from "src/modules/profile/services/schedule-presets-service";
|
||||||
|
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||||
|
|
||||||
|
|
||||||
|
export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
|
||||||
|
const schedule_presets = ref<SchedulePreset>();
|
||||||
|
|
||||||
|
const createSchedulePreset = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
// const new_preset: SchedulePreset = ??
|
||||||
|
// await SchedulePresetsService.createSchedulePresets(new_preset);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DEV ERROR || error while creating schedule preset: ', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSchedulePreset = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DEV ERROR || error while updating schedule preset: ', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await SchedulePresetsService.deleteSchedulePresets(preset_id);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DEV ERROR || error while deleting schedule preset: ', error);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findSchedulePresetList = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DEV ERROR || error while searching for schedule presets: ', error);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applySchedulePreset = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DEV ERROR || error while building schedule: ', error);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
schedule_presets,
|
||||||
|
createSchedulePreset,
|
||||||
|
updateSchedulePreset,
|
||||||
|
deleteSchedulePreset,
|
||||||
|
findSchedulePresetList,
|
||||||
|
applySchedulePreset,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { Notify } from "quasar";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { Notify } from "quasar";
|
import type { ShiftAPIError } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
export const useShiftStore = defineStore('shift_store', () => {
|
export const useShiftStore = defineStore('shift_store', () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const shift_error = ref();
|
const shift_errors = ref<ShiftAPIError[]>([]);
|
||||||
|
|
||||||
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -19,20 +20,25 @@ export const useShiftStore = defineStore('shift_store', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewShifts = async (): Promise<boolean> => {
|
const createNewShifts = async (): Promise<boolean> => {
|
||||||
if (timesheet_store.timesheets === undefined) return false;
|
if (timesheet_store.timesheets === undefined) {
|
||||||
|
console.log('no changes in existing shifts detected');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id < 0);
|
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.id < 0);
|
||||||
|
|
||||||
if (new_shifts?.length > 0) {
|
if (new_shifts?.length > 0) {
|
||||||
const response = await ShiftService.createNewShifts(new_shifts);
|
const response = await ShiftService.createNewShifts(new_shifts);
|
||||||
if (response.status <= 200) {
|
if (response.every(res => res.ok)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
response.forEach(res => {
|
||||||
|
shift_errors.value.push(res.error!);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('No new shifts to save');
|
|
||||||
Notify.create('no new shifts to save')
|
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating new shifts: ', error);
|
console.error('Error creating new shifts: ', error);
|
||||||
|
|
@ -44,11 +50,12 @@ export const useShiftStore = defineStore('shift_store', () => {
|
||||||
if (timesheet_store.timesheets === undefined) return false;
|
if (timesheet_store.timesheets === undefined) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.shift_id > 0);
|
const existing_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.id > 0);
|
||||||
|
|
||||||
if (existing_shifts?.length > 0) {
|
if (existing_shifts?.length > 0) {
|
||||||
const response = await ShiftService.updateShifts(existing_shifts);
|
const response = await ShiftService.updateShifts(existing_shifts);
|
||||||
if (response.status <= 200) {
|
|
||||||
|
if (response.status < 400) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +70,7 @@ export const useShiftStore = defineStore('shift_store', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shift_error,
|
shift_errors,
|
||||||
deleteShiftById,
|
deleteShiftById,
|
||||||
createNewShifts,
|
createNewShifts,
|
||||||
updateShifts,
|
updateShifts,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
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 type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
|
|
@ -15,7 +16,8 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const pay_period = ref<PayPeriod>();
|
const pay_period = ref<PayPeriod>();
|
||||||
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
const pay_period_overviews = ref<TimesheetOverview[]>([]);
|
||||||
const current_pay_period_overview = ref<TimesheetOverview>();
|
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||||
const timesheets = ref<Timesheet[]>();
|
const timesheets = ref<Timesheet[]>([]);
|
||||||
|
const initial_timesheets = ref<Timesheet[]>([]);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
|
|
||||||
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> => {
|
||||||
|
|
@ -58,13 +60,17 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTimesheetsByEmployeeEmail = async (employee_email: string) => {
|
const getTimesheetsByEmployeeEmail = async (employee_email?: string) => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
|
|
||||||
if (pay_period.value === undefined) return;
|
if (pay_period.value === undefined) return;
|
||||||
try {
|
try {
|
||||||
const response = await timesheetService.getTimesheetsByPayPeriodAndEmployeeEmail(employee_email, pay_period.value.pay_year, pay_period.value.pay_period_no);
|
if (employee_email) {
|
||||||
|
console.log('email: ', employee_email);
|
||||||
|
}
|
||||||
|
const response = await timesheetService.getTimesheetsByPayPeriod(pay_period.value.pay_year, pay_period.value.pay_period_no);
|
||||||
timesheets.value = response.timesheets;
|
timesheets.value = response.timesheets;
|
||||||
|
initial_timesheets.value = unwrapAndClone(timesheets.value);
|
||||||
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);
|
||||||
|
|
@ -97,6 +103,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
pay_period_overviews,
|
pay_period_overviews,
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
timesheets,
|
timesheets,
|
||||||
|
initial_timesheets,
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getTimesheetOverviewsByPayPeriod,
|
getTimesheetOverviewsByPayPeriod,
|
||||||
getTimesheetsByEmployeeEmail,
|
getTimesheetsByEmployeeEmail,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user