Merge pull request 'dev/nicolas/timesheet-approval-staging-prep' (#35) from dev/nicolas/timesheet-approval-staging-prep into main
Reviewed-on: Targo/targo_frontend#35
This commit is contained in:
commit
ddd9dca8ba
|
|
@ -53,6 +53,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
error :{
|
||||||
|
not_found_header: "page not found",
|
||||||
|
not_found_description: "You may have entered the wrong URL, or you may not have access to this page",
|
||||||
|
go_back: "go back",
|
||||||
|
},
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
page_header: "account login",
|
page_header: "account login",
|
||||||
email: "e-mail",
|
email: "e-mail",
|
||||||
|
|
@ -176,6 +182,10 @@ export default {
|
||||||
|
|
||||||
timesheet: {
|
timesheet: {
|
||||||
page_header: "Timesheet",
|
page_header: "Timesheet",
|
||||||
|
week: "week",
|
||||||
|
total_hours: "total hours: ",
|
||||||
|
current_shifts: "shifts worked",
|
||||||
|
apply_preset: "auto-fill",
|
||||||
apply_preset_day: "Apply schedule to day",
|
apply_preset_day: "Apply schedule to day",
|
||||||
apply_preset_week: "Apply schedule to week",
|
apply_preset_week: "Apply schedule to week",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
|
|
@ -240,7 +250,7 @@ export default {
|
||||||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||||
SHIFT_OVERLAP_SHORT: "Overlap",
|
SHIFT_OVERLAP_SHORT: "Overlap",
|
||||||
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
||||||
SHIFT_TIME_REQUIRED: "Time required",
|
SHIFT_TIME_REQUIRED: "Time missing",
|
||||||
SHIFT_TYPE_REQUIRED: "Shift type required",
|
SHIFT_TYPE_REQUIRED: "Shift type required",
|
||||||
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
||||||
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||||
|
|
@ -254,15 +264,6 @@ export default {
|
||||||
|
|
||||||
timesheet_approvals: {
|
timesheet_approvals: {
|
||||||
page_title: "Validation cartes de temps",
|
page_title: "Validation cartes de temps",
|
||||||
table: {
|
|
||||||
full_name: "full name",
|
|
||||||
email: "email address",
|
|
||||||
is_approved: "approval",
|
|
||||||
expenses: "expenses",
|
|
||||||
mileage: "mileage",
|
|
||||||
verified: "approved",
|
|
||||||
unverified: "pending",
|
|
||||||
},
|
|
||||||
chart: {
|
chart: {
|
||||||
hours_worked_title: "hours worked",
|
hours_worked_title: "hours worked",
|
||||||
expenses_title: "expenses accrued",
|
expenses_title: "expenses accrued",
|
||||||
|
|
@ -275,6 +276,16 @@ export default {
|
||||||
expenses: "expenses",
|
expenses: "expenses",
|
||||||
options: "options",
|
options: "options",
|
||||||
},
|
},
|
||||||
|
table: {
|
||||||
|
full_name: "full name",
|
||||||
|
email: "email address",
|
||||||
|
is_approved: "approval",
|
||||||
|
expenses: "expenses",
|
||||||
|
mileage: "mileage",
|
||||||
|
verified: "approved",
|
||||||
|
unverified: "pending",
|
||||||
|
inactive: "inactive",
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
button_detailed_view: "detailed view",
|
button_detailed_view: "detailed view",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
error :{
|
||||||
|
not_found_header: "page introuvable",
|
||||||
|
not_found_description: "Vous avez possiblement entré une mauvaise addresse URL, ou vous n'avez pas accès à cette section du site",
|
||||||
|
go_back: "retour en arrière",
|
||||||
|
},
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
page_header: "connexion au compte",
|
page_header: "connexion au compte",
|
||||||
email: "courriel",
|
email: "courriel",
|
||||||
|
|
@ -177,6 +183,10 @@ export default {
|
||||||
|
|
||||||
timesheet: {
|
timesheet: {
|
||||||
page_header: "Carte de temps",
|
page_header: "Carte de temps",
|
||||||
|
week: "semaine",
|
||||||
|
total_hours: "heures totales: ",
|
||||||
|
current_shifts: "quarts entrées",
|
||||||
|
apply_preset: "auto-remplir",
|
||||||
apply_preset_day: "Appliquer horaire pour la journée",
|
apply_preset_day: "Appliquer horaire pour la journée",
|
||||||
apply_preset_week: "Appliquer horaire pour la semaine",
|
apply_preset_week: "Appliquer horaire pour la semaine",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
|
|
@ -241,7 +251,7 @@ export default {
|
||||||
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||||
SHIFT_OVERLAP_SHORT: "Chevauchement",
|
SHIFT_OVERLAP_SHORT: "Chevauchement",
|
||||||
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
||||||
SHIFT_TIME_REQUIRED: "Heure requise",
|
SHIFT_TIME_REQUIRED: "Heures manquantes",
|
||||||
SHIFT_TYPE_REQUIRED: "Type requis",
|
SHIFT_TYPE_REQUIRED: "Type requis",
|
||||||
SHIFT_NOT_FOUND: "Quart de travail manquant ou supprimé",
|
SHIFT_NOT_FOUND: "Quart de travail manquant ou supprimé",
|
||||||
PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies",
|
PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies",
|
||||||
|
|
@ -255,15 +265,6 @@ export default {
|
||||||
|
|
||||||
timesheet_approvals: {
|
timesheet_approvals: {
|
||||||
page_title: "Validation cartes de temps",
|
page_title: "Validation cartes de temps",
|
||||||
table: {
|
|
||||||
full_name: "nom complet",
|
|
||||||
email: "courriel",
|
|
||||||
is_approved: "approuvé",
|
|
||||||
expenses: "dépenses",
|
|
||||||
mileage: "kilométrage",
|
|
||||||
verified: "approuvé",
|
|
||||||
unverified: "à vérifier",
|
|
||||||
},
|
|
||||||
chart: {
|
chart: {
|
||||||
hours_worked_title: "heures travaillées",
|
hours_worked_title: "heures travaillées",
|
||||||
expenses_title: "dépenses encourues"
|
expenses_title: "dépenses encourues"
|
||||||
|
|
@ -276,6 +277,16 @@ export default {
|
||||||
expenses: "dépenses",
|
expenses: "dépenses",
|
||||||
options: "options",
|
options: "options",
|
||||||
},
|
},
|
||||||
|
table: {
|
||||||
|
full_name: "nom complet",
|
||||||
|
email: "courriel",
|
||||||
|
is_approved: "approuvé",
|
||||||
|
expenses: "dépenses",
|
||||||
|
mileage: "kilométrage",
|
||||||
|
verified: "approuvé",
|
||||||
|
unverified: "à vérifier",
|
||||||
|
inactive: "inactif",
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
button_detailed_view: "vue détaillée",
|
button_detailed_view: "vue détaillée",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
setup
|
setup
|
||||||
>
|
>
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
// import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
<q-item class="q-pa-none">
|
<q-item class="q-pa-none">
|
||||||
<HeaderBarNotification />
|
<!-- <HeaderBarNotification /> -->
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
import { ModuleNames } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
|
|
@ -32,155 +32,109 @@
|
||||||
<template>
|
<template>
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="ui_store.is_left_drawer_open"
|
v-model="ui_store.is_left_drawer_open"
|
||||||
:persistent="!ui_store.is_mobile_mode"
|
:persistent="!$q.platform.is.mobile"
|
||||||
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 z-max"
|
class="bg-dark z-max column no-wrap"
|
||||||
|
:class="!$q.platform.is.mobile && is_mini ? 'items-center' : 'items-start'"
|
||||||
>
|
>
|
||||||
<q-scroll-area class="fit">
|
<!-- Home -->
|
||||||
<q-list>
|
<q-btn
|
||||||
<!-- Home -->
|
flat
|
||||||
<q-item
|
dense
|
||||||
v-ripple
|
no-wrap
|
||||||
clickable
|
size="lg"
|
||||||
side
|
icon="home"
|
||||||
@click="goToPageName(RouteNames.DASHBOARD)"
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.home')"
|
||||||
>
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
<q-item-section avatar>
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
<q-icon
|
@click="goToPageName(RouteNames.DASHBOARD)"
|
||||||
name="home"
|
/>
|
||||||
color="accent"
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
<!-- Timesheet Validation -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS_APPROVAL)"
|
||||||
clickable
|
flat
|
||||||
side
|
dense
|
||||||
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
no-wrap
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
size="lg"
|
||||||
>
|
icon="event_available"
|
||||||
<q-item-section avatar>
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet_approvals')"
|
||||||
<q-icon
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
name="event_available"
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
color="accent"
|
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||||
size="lg"
|
/>
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals')
|
|
||||||
}}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
<!-- Employee List -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
v-if="auth_store.user?.user_module_access.includes(ModuleNames.EMPLOYEE_LIST)"
|
||||||
clickable
|
flat
|
||||||
side
|
dense
|
||||||
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
no-wrap
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
size="lg"
|
||||||
>
|
icon="groups"
|
||||||
<q-item-section avatar>
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.employee_list')"
|
||||||
<q-icon
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
name="groups"
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
color="accent"
|
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||||
size="lg"
|
/>
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list')
|
|
||||||
}}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
<!-- Employee Timesheet -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS)"
|
||||||
clickable
|
flat
|
||||||
side
|
dense
|
||||||
@click="goToPageName(RouteNames.TIMESHEET)"
|
no-wrap
|
||||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
size="lg"
|
||||||
>
|
icon="punch_clock"
|
||||||
<q-item-section avatar>
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet')"
|
||||||
<q-icon
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
name="punch_clock"
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
color="accent"
|
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||||
size="lg"
|
/>
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet')
|
|
||||||
}}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- Profile -->
|
<!-- Profile -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
v-if="auth_store.user?.user_module_access.includes(ModuleNames.PERSONAL_PROFILE)"
|
||||||
clickable
|
flat
|
||||||
side
|
dense
|
||||||
@click="goToPageName(RouteNames.PROFILE)"
|
no-wrap
|
||||||
>
|
size="lg"
|
||||||
<q-item-section avatar>
|
icon="account_box"
|
||||||
<q-icon
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.profile')"
|
||||||
name="account_box"
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
color="accent"
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
size="lg"
|
@click="goToPageName(RouteNames.PROFILE)"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<!-- Help -->
|
<!-- Help -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
flat
|
||||||
clickable
|
dense
|
||||||
@click="goToPageName('help')"
|
no-wrap
|
||||||
>
|
size="lg"
|
||||||
<q-item-section avatar>
|
icon="contact_support"
|
||||||
<q-icon
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.help')"
|
||||||
name="contact_support"
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
color="accent"
|
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||||
size="lg"
|
@click="goToPageName(RouteNames.HELP)"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<!-- Logout -->
|
<!-- Logout -->
|
||||||
<q-item
|
<q-btn
|
||||||
v-ripple
|
flat
|
||||||
clickable
|
dense
|
||||||
@click="handleLogout"
|
no-wrap
|
||||||
class="absolute-bottom"
|
size="lg"
|
||||||
>
|
icon="exit_to_app"
|
||||||
<q-item-section avatar>
|
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.logout')"
|
||||||
<q-icon
|
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||||
name="exit_to_app"
|
:class="!$q.platform.is.mobile && is_mini ? 'absolute-bottom': 'absolute-bottom-left'"
|
||||||
color="accent"
|
@click="handleLogout"
|
||||||
size="lg"
|
/>
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-scroll-area>
|
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,15 +2,14 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
setup
|
setup
|
||||||
>
|
>
|
||||||
import { RouterView } from 'vue-router';
|
|
||||||
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
|
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
|
||||||
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
|
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
|
||||||
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
|
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
|
||||||
import { onMounted, watch, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
import { onMounted, watch, ref } from 'vue';
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
|
||||||
|
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const user_preferences = ref(ui_store.user_preferences);
|
const user_preferences = ref(ui_store.user_preferences);
|
||||||
|
|
@ -23,7 +22,7 @@
|
||||||
|
|
||||||
watch(user_preferences, async () => {
|
watch(user_preferences, async () => {
|
||||||
if (ui_store.user_preferences.id !== -1) {
|
if (ui_store.user_preferences.id !== -1) {
|
||||||
await ui_store.updateUserPreferences(t);
|
await ui_store.updateUserPreferences();
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await ui_store.getUserPreferences();
|
await ui_store.getUserPreferences();
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||||
import { employee_access_options, type ModuleAccessPreset, type ModuleAccessName, employee_access_presets, getEmployeeAccessOptionIcon } from 'src/modules/employee-list/models/employee-profile.models';
|
import { employee_access_options, type ModuleAccessPreset, employee_access_presets, getEmployeeAccessOptionIcon } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
const employee_store = useEmployeeStore();
|
const employee_store = useEmployeeStore();
|
||||||
const preset_preview = ref<ModuleAccessPreset>();
|
const preset_preview = ref<ModuleAccessPreset>();
|
||||||
|
|
||||||
const toggleInSelected = (value: ModuleAccessName) => {
|
const toggleInSelected = (value: UserModuleAccess) => {
|
||||||
const i = employee_store.employee.user_module_access.indexOf(value);
|
const i = employee_store.employee.user_module_access.indexOf(value);
|
||||||
if (i === -1) employee_store.employee.user_module_access.push(value);
|
if (i === -1) employee_store.employee.user_module_access.push(value);
|
||||||
else employee_store.employee.user_module_access.splice(i, 1);
|
else employee_store.employee.user_module_access.splice(i, 1);
|
||||||
|
|
@ -21,7 +22,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const getPreviewBackgroundColor = (name: ModuleAccessName) => {
|
const getPreviewBackgroundColor = (name: UserModuleAccess) => {
|
||||||
if (employee_access_presets[preset_preview.value!].includes(name)) {
|
if (employee_access_presets[preset_preview.value!].includes(name)) {
|
||||||
if (!employee_store.employee.user_module_access.includes(name)) return 'bg-info text-white';
|
if (!employee_store.employee.user_module_access.includes(name)) return 'bg-info text-white';
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@
|
||||||
return 'bg-dark';
|
return 'bg-dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBackgroundColor = (name: ModuleAccessName) => {
|
const getBackgroundColor = (name: UserModuleAccess) => {
|
||||||
if (employee_store.employee.user_module_access.includes(name)) return 'bg-accent text-white';
|
if (employee_store.employee.user_module_access.includes(name)) return 'bg-accent text-white';
|
||||||
|
|
||||||
return 'bg-dark';
|
return 'bg-dark';
|
||||||
|
|
@ -143,7 +144,11 @@
|
||||||
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)"
|
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)"
|
||||||
@click="toggleInSelected(option.value)"
|
@click="toggleInSelected(option.value)"
|
||||||
>
|
>
|
||||||
<q-icon :name="getEmployeeAccessOptionIcon(option.value)" size="sm" class="q-mr-sm"/>
|
<q-icon
|
||||||
|
:name="getEmployeeAccessOptionIcon(option.value)"
|
||||||
|
size="sm"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
<span class="text-uppercase text-weight-bold non-selectable">
|
<span class="text-uppercase text-weight-bold non-selectable">
|
||||||
{{ $t('employee_management.module_access.' + option.value) }}
|
{{ $t('employee_management.module_access.' + option.value) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { QSelectOption, QTableColumn } from "quasar";
|
import type { QSelectOption, QTableColumn } from "quasar";
|
||||||
|
import type { UserModuleAccess } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export type ModuleAccessName = 'dashboard' | 'employee_list' | 'employee_management' | 'personal_profile' | 'timesheets' | 'timesheets_approval';
|
|
||||||
export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none';
|
export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none';
|
||||||
export type CompanyNames = 'Targo' | 'Solucom';
|
export type CompanyNames = 'Targo' | 'Solucom';
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class EmployeeProfile {
|
||||||
residence: string;
|
residence: string;
|
||||||
birth_date: string;
|
birth_date: string;
|
||||||
is_supervisor: boolean;
|
is_supervisor: boolean;
|
||||||
user_module_access: ModuleAccessName[];
|
user_module_access: UserModuleAccess[];
|
||||||
preset_id?: number | null;
|
preset_id?: number | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -100,7 +100,7 @@ export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
|
export const employee_access_options: QSelectOption<UserModuleAccess>[] = [
|
||||||
{ label: 'dashboard', value: 'dashboard' },
|
{ label: 'dashboard', value: 'dashboard' },
|
||||||
{ label: 'employee_list', value: 'employee_list' },
|
{ label: 'employee_list', value: 'employee_list' },
|
||||||
{ label: 'personal_profile', value: 'personal_profile' },
|
{ label: 'personal_profile', value: 'personal_profile' },
|
||||||
|
|
@ -109,14 +109,14 @@ export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
|
||||||
{ label: 'timesheets_approval', value: 'timesheets_approval' },
|
{ label: 'timesheets_approval', value: 'timesheets_approval' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const employee_access_presets: Record<ModuleAccessPreset, ModuleAccessName[]> = {
|
export const employee_access_presets: Record<ModuleAccessPreset, UserModuleAccess[]> = {
|
||||||
'admin' : ['dashboard', 'employee_list', 'employee_management', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
'admin' : ['dashboard', 'employee_list', 'employee_management', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
||||||
'supervisor' : ['dashboard', 'employee_list', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
'supervisor' : ['dashboard', 'employee_list', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
||||||
'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'],
|
'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'],
|
||||||
'none' : [],
|
'none' : [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEmployeeAccessOptionIcon = (module: ModuleAccessName): string => {
|
export const getEmployeeAccessOptionIcon = (module: UserModuleAccess): string => {
|
||||||
switch (module) {
|
switch (module) {
|
||||||
case 'dashboard': return 'home';
|
case 'dashboard': return 'home';
|
||||||
case 'employee_list' : return 'groups';
|
case 'employee_list' : return 'groups';
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<div>
|
<div>
|
||||||
<q-card
|
<q-card
|
||||||
flat
|
flat
|
||||||
class="rounded-5 bg-transparent q-pa-none"
|
class="rounded-5 bg-transparent q-pa-none fit"
|
||||||
>
|
>
|
||||||
<MenuTemplate
|
<MenuTemplate
|
||||||
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md column fit">
|
<div class="q-px-md column no-wrap">
|
||||||
<div
|
<div style="transform: translate(10px, 12px);">
|
||||||
class="col-auto"
|
|
||||||
style="transform: translate(10px, 12px);"
|
|
||||||
>
|
|
||||||
<span class="text-uppercase text-weight-bold text-accent bg-dark q-px-sm">
|
<span class="text-uppercase text-weight-bold text-accent bg-dark q-px-sm">
|
||||||
{{ $t('profile.preferences.display_options') }}
|
{{ $t('profile.preferences.display_options') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-auto justify-center content-center q-mb-sm q-pa-sm rounded-5"
|
class="col-auto justify-center content-center q-mb-sm q-pa-md rounded-5"
|
||||||
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
||||||
style="border: 1px solid var(--q-accent);"
|
style="border: 1px solid var(--q-accent);"
|
||||||
>
|
>
|
||||||
|
|
@ -32,8 +29,8 @@
|
||||||
clickable
|
clickable
|
||||||
dense
|
dense
|
||||||
v-ripple
|
v-ripple
|
||||||
class="col rounded-5 q-ma-sm shadow-4"
|
class="col row rounded-5 q-ma-sm shadow-4"
|
||||||
:class="mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : ''"
|
:class="(mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : '') + ($q.platform.is.mobile ? ' full-width q-py-xs' : '')"
|
||||||
@click="ui_store.user_preferences.is_dark_mode = mode.value"
|
@click="ui_store.user_preferences.is_dark_mode = mode.value"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="column flex-center"
|
class="column flex-center fit"
|
||||||
>
|
>
|
||||||
<MenuHeader
|
<MenuHeader
|
||||||
:user-first-name="firstName"
|
:user-first-name="firstName"
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
class="col"
|
class="col"
|
||||||
:class="$q.screen.lt.sm ? 'full-width' : 'q-ml-sm'"
|
:class="$q.platform.is.mobile ? 'fit' : 'q-ml-sm'"
|
||||||
>
|
>
|
||||||
<q-tab-panels
|
<q-tab-panels
|
||||||
v-model="current_menu"
|
v-model="current_menu"
|
||||||
|
|
@ -49,8 +49,8 @@
|
||||||
vertical
|
vertical
|
||||||
transition-prev="jump-up"
|
transition-prev="jump-up"
|
||||||
transition-next="jump-up"
|
transition-next="jump-up"
|
||||||
class="rounded-5"
|
class="rounded-5 q-py-sm"
|
||||||
style="height: 50vh;"
|
:style="$q.platform.is.mobile ? '' : 'height: 50vh;'"
|
||||||
>
|
>
|
||||||
<slot name="panels"></slot>
|
<slot name="panels"></slot>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@
|
||||||
dense
|
dense
|
||||||
rounded
|
rounded
|
||||||
debounce="300"
|
debounce="300"
|
||||||
class="right-rounded"
|
|
||||||
:label="$t('shared.label.search')"
|
:label="$t('shared.label.search')"
|
||||||
label-color="accent"
|
|
||||||
bg-color="white"
|
|
||||||
color="accent"
|
color="accent"
|
||||||
|
bg-color="white"
|
||||||
|
label-color="accent"
|
||||||
|
class="text-primary"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,18 @@ export interface User {
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
|
user_module_access: UserModuleAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserRole = 'ADMIN' |'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
export type UserRole = 'ADMIN' | 'SUPERVISOR' | 'HR' | 'ACCOUNTING' | 'EMPLOYEE' | 'DEALER' | 'CUSTOMER' | 'GUEST';
|
||||||
|
|
||||||
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
export const ModuleNames = {
|
||||||
'ADMIN',
|
DASHBOARD: 'dashboard',
|
||||||
'SUPERVISOR',
|
EMPLOYEE_LIST: 'employee_list',
|
||||||
'HR',
|
EMPLOYEE_MANAGEMENT: 'employee_management',
|
||||||
'ACCOUNTING',
|
PERSONAL_PROFILE: 'personal_profile',
|
||||||
]
|
TIMESHEETS: 'timesheets',
|
||||||
|
TIMESHEETS_APPROVAL: 'timesheets_approval',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type UserModuleAccess = typeof ModuleNames[keyof typeof ModuleNames];
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
>
|
>
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { colors } from 'quasar';
|
import { colors } from 'quasar';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { Doughnut } from 'vue-chartjs';
|
import { Doughnut } from 'vue-chartjs';
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
|
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
|
||||||
ChartJS.defaults.font.family = '"Roboto", sans-serif';
|
ChartJS.defaults.font.family = '"Roboto", sans-serif';
|
||||||
|
|
@ -20,10 +22,10 @@
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const shift_type_labels = ref<string[]>([
|
const shift_type_labels = ref<string[]>([
|
||||||
timesheet_store.current_pay_period_overview!.regular_hours.toString() + 'h',
|
t('shared.shift_type.regular'),
|
||||||
timesheet_store.current_pay_period_overview!.other_hours.evening_hours.toString() + 'h',
|
t('shared.shift_type.evening'),
|
||||||
timesheet_store.current_pay_period_overview!.other_hours.emergency_hours.toString() + 'h',
|
t('shared.shift_type.emergency'),
|
||||||
timesheet_store.current_pay_period_overview!.other_hours.overtime_hours.toString() + 'h',
|
t('shared.shift_type.overtime'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]);
|
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]);
|
||||||
|
|
|
||||||
|
|
@ -25,36 +25,34 @@
|
||||||
@show="is_dialog_open = true"
|
@show="is_dialog_open = true"
|
||||||
@hide="is_dialog_open = false"
|
@hide="is_dialog_open = false"
|
||||||
>
|
>
|
||||||
<q-card
|
<div
|
||||||
class="shadow-12 rounded-15 bg-secondary hide-scrollbar"
|
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pa-sm no-wrap"
|
||||||
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- employee name -->
|
<!-- employee name -->
|
||||||
<q-card-section class="text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
|
<div class="col-auto text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
|
||||||
<span>{{ timesheet_store.selected_employee_name }}</span>
|
<span>{{ timesheet_store.selected_employee_name }}</span>
|
||||||
</q-card-section>
|
</div>
|
||||||
|
|
||||||
<!-- employee pay period details using chart -->
|
<!-- employee pay period details using chart -->
|
||||||
<q-card-section
|
<div
|
||||||
v-if="is_dialog_open"
|
v-if="is_dialog_open && !$q.platform.is.mobile"
|
||||||
:horizontal="!$q.screen.lt.md"
|
class="col-4 q-px-md no-wrap"
|
||||||
class="q-px-md rounded-10 no-wrap"
|
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||||
>
|
>
|
||||||
<DetailsDialogChartHoursWorked class="col" />
|
<DetailsDialogChartHoursWorked class="col" />
|
||||||
<DetailsDialogChartShiftTypes class="col q-ma-lg" />
|
<DetailsDialogChartShiftTypes class="col" />
|
||||||
<DetailsDialogChartExpenses class="col" />
|
<DetailsDialogChartExpenses class="col" />
|
||||||
</q-card-section>
|
</div>
|
||||||
|
|
||||||
<q-card-section>
|
<div class="col-auto">
|
||||||
<ExpenseDialogList />
|
<ExpenseDialogList />
|
||||||
</q-card-section>
|
</div>
|
||||||
|
|
||||||
<!-- list of shifts -->
|
<!-- list of shifts -->
|
||||||
<q-card-section class="col-auto">
|
<div class="col column no-wrap">
|
||||||
<TimesheetWrapper mode="approval" />
|
<TimesheetWrapper mode="approval" class="col"/>
|
||||||
</q-card-section>
|
</div>
|
||||||
<q-separator />
|
</div>
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import type { PayPeriodOverviewFilters } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||||
|
|
||||||
|
const filters = defineModel<PayPeriodOverviewFilters>('filters', { required: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="column text-weight-medium">
|
||||||
|
<q-separator
|
||||||
|
color="accent"
|
||||||
|
size="5px"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.is_showing_inactive"
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
label="show inactive"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.is_showing_team_only"
|
||||||
|
size="lg"
|
||||||
|
val="team"
|
||||||
|
color="accent"
|
||||||
|
label="show team only"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -13,7 +13,20 @@
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clickDetails': [overview: TimesheetOverview];
|
'clickDetails': [overview: TimesheetOverview];
|
||||||
|
'clickApprovalAll' : [is_approved: boolean];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const getMinutes = (hours: number) => {
|
||||||
|
const minutes_percent = hours - Math.floor(hours);
|
||||||
|
const minutes = Math.round(minutes_percent * 60);
|
||||||
|
return minutes > 1 ? minutes.toString() : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHoursMinutesString = (hours: number): string => {
|
||||||
|
const flat_hours = Math.floor(hours);
|
||||||
|
const minutes = Math.round((hours - flat_hours) * 60);
|
||||||
|
return `${flat_hours}h ${minutes > 1 ? minutes : ''}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -24,16 +37,20 @@
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="rounded-10 shadow-5"
|
class="rounded-10 shadow-5"
|
||||||
:style="`animation-delay: ${index / 15}s;`"
|
:style="`animation-delay: ${index / 15}s; opacity: ${row.is_active ? '1' : '0.5'}; transform: scale(${row.is_active ? '1' : '0.9'})`"
|
||||||
>
|
>
|
||||||
<!-- Card header with employee name and details button-->
|
<!-- Card header with employee name and details button-->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="q-py-none q-px-sm q-ma-none justify-between items-center bg-primary text-white"
|
class="q-py-xs q-px-sm q-ma-none justify-between items-center bg-primary text-white"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-h5 text-uppercase text-weight-medium text-accent q-mr-xs">{{ row.employee_name.split(' ')[0]
|
<span
|
||||||
}}</span>
|
class="text-h5 text-uppercase text-weight-medium q-mr-xs"
|
||||||
|
:class="row.is_active ? 'text-accent' : 'text-negative'"
|
||||||
|
>
|
||||||
|
{{ row.employee_name.split(' ')[0] }}
|
||||||
|
</span>
|
||||||
<span class="text-uppercase text-weight-light">{{ row.employee_name.split(' ')[1] }}</span>
|
<span class="text-uppercase text-weight-light">{{ row.employee_name.split(' ')[1] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -45,7 +62,7 @@
|
||||||
unelevated
|
unelevated
|
||||||
class="col-auto q-pa-none q-ma-none"
|
class="col-auto q-pa-none q-ma-none"
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="work_history"
|
icon="las la-chart-pie"
|
||||||
@click="emit('clickDetails', row)"
|
@click="emit('clickDetails', row)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|
@ -55,6 +72,11 @@
|
||||||
>
|
>
|
||||||
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
|
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
name="las la-chart-bar"
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
|
|
@ -67,13 +89,13 @@
|
||||||
<div class="col column">
|
<div class="col column">
|
||||||
<span
|
<span
|
||||||
class="text-weight-bold text-uppercase q-pa-none q-my-none"
|
class="text-weight-bold text-uppercase q-pa-none q-my-none"
|
||||||
:class="row.regular_hours > 80 ? 'text-negative' : 'text-accent'"
|
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : 'text-accent'"
|
||||||
> {{
|
> {{
|
||||||
$t('shared.shift_type.regular') }} </span>
|
$t('shared.shift_type.regular') }} </span>
|
||||||
<span
|
<span
|
||||||
class="text-weight-bolder text-h3 q-py-none"
|
class="text-weight-bolder text-h3 q-py-none"
|
||||||
:class="row.regular_hours > 80 ? 'text-negative' : ''"
|
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''"
|
||||||
> {{ row.regular_hours }} </span>
|
> {{ getHoursMinutesString(row.regular_hours) }} </span>
|
||||||
<q-separator class="q-mr-sm" />
|
<q-separator class="q-mr-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,11 +110,13 @@
|
||||||
<span
|
<span
|
||||||
class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
|
class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
|
||||||
style="font-size: 0.7em;"
|
style="font-size: 0.7em;"
|
||||||
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
|
>
|
||||||
|
{{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
class="text-weight-bolder q-pa-none q-mb-xs"
|
class="text-weight-bolder q-pa-none q-mb-xs"
|
||||||
style="font-size: 1.2em; line-height: 1em;"
|
style="font-size: 1.2em; line-height: 1em;"
|
||||||
> {{ hour_type }} </span>
|
> {{ getHoursMinutesString(hour_type) }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,30 +157,53 @@
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="justify-between items-center text-weight-bold q-pa-none"
|
class="justify-between items-center text-weight-bold q-pa-none"
|
||||||
:class="row.is_approved ? 'text-white bg-accent' : 'bg-dark text-accent'"
|
:class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark') : 'bg-transparent'"
|
||||||
>
|
>
|
||||||
<div class="col-auto">
|
<div
|
||||||
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span>
|
v-if="row.is_active"
|
||||||
<span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span>
|
class="col row full-width"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col text-uppercase"
|
||||||
|
:class="row.total_hours > 80 || !row.is_active ? 'text-negative' : ''"
|
||||||
|
>
|
||||||
|
<span class="text-h6 q-ml-sm text-weight-bolder">{{ 'Total : ' + Math.floor(row.total_hours)
|
||||||
|
}}</span>
|
||||||
|
<span class="text-uppercase text-weight-medium text-caption">H</span>
|
||||||
|
<span class="text-h6 q-ml-sm text-weight-bolder">{{ getMinutes(row.total_hours) }}</span>
|
||||||
|
<span class="text-uppercase text-weight-medium text-caption">M</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto q-py-xs q-px-md">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="modelApproval"
|
||||||
|
dense
|
||||||
|
left-label
|
||||||
|
keep-color
|
||||||
|
size="lg"
|
||||||
|
checked-icon="lock"
|
||||||
|
unchecked-icon="lock_open"
|
||||||
|
:color="row.is_approved ? 'white' : 'accent'"
|
||||||
|
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
|
||||||
|
class="text-uppercase"
|
||||||
|
:class="row.is_approved ? '' : 'text-accent'"
|
||||||
|
@update:model-value="value => $emit('clickApprovalAll', value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-auto q-py-xs q-px-md"
|
v-else
|
||||||
style="border: 1px solid var(--q-accent);"
|
class="col row flex-center q-px-sm full-width"
|
||||||
>
|
>
|
||||||
<q-checkbox
|
<q-icon
|
||||||
v-model="modelApproval"
|
name="block"
|
||||||
dense
|
color="negative"
|
||||||
left-label
|
class="col-auto"
|
||||||
keep-color
|
|
||||||
size="lg"
|
size="lg"
|
||||||
checked-icon="lock"
|
|
||||||
unchecked-icon="lock_open"
|
|
||||||
:color="row.is_approved ? 'white' : 'accent'"
|
|
||||||
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
|
|
||||||
class="text-uppercase"
|
|
||||||
:class="row.is_approved ? '' : 'text-accent'"
|
|
||||||
/>
|
/>
|
||||||
|
<span class="col q-pl-sm text-uppercase text-weight-bold text-h5">{{
|
||||||
|
$t('timesheet_approvals.table.inactive') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,68 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
/* eslint-disable */
|
setup
|
||||||
import { computed, ref } from 'vue';
|
lang="ts"
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
>
|
||||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
/* eslint-disable */
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||||
import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
||||||
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||||
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
|
import OverviewListFilters from 'src/modules/timesheet-approval/components/overview-list-filters.vue';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
import { computed, ref } from 'vue';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
const visible_columns = ref<string[]>([
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
overview_column_names.REGULAR,
|
import { overview_column_names, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
|
||||||
overview_column_names.EVENING,
|
|
||||||
overview_column_names.EMERGENCY,
|
|
||||||
overview_column_names.SICK,
|
|
||||||
overview_column_names.VACATION,
|
|
||||||
overview_column_names.HOLIDAY,
|
|
||||||
overview_column_names.OVERTIME,
|
|
||||||
overview_column_names.IS_APPROVED,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ?
|
|
||||||
[] :
|
|
||||||
timesheet_store.pay_period_overviews
|
|
||||||
)
|
|
||||||
|
|
||||||
const onClickedDetails = async (row: TimesheetOverview) => {
|
|
||||||
timesheet_store.current_pay_period_overview = row;
|
|
||||||
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
|
||||||
|
|
||||||
timesheet_store.is_details_dialog_open = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
|
|
||||||
|
const visible_columns = ref<string[]>([
|
||||||
|
overview_column_names.REGULAR,
|
||||||
|
overview_column_names.EVENING,
|
||||||
|
overview_column_names.EMERGENCY,
|
||||||
|
overview_column_names.SICK,
|
||||||
|
overview_column_names.VACATION,
|
||||||
|
overview_column_names.HOLIDAY,
|
||||||
|
overview_column_names.OVERTIME,
|
||||||
|
overview_column_names.IS_APPROVED,
|
||||||
|
]);
|
||||||
|
const is_showing_filters = ref(false);
|
||||||
|
const search_string = ref('');
|
||||||
|
|
||||||
|
const overview_rows = computed(() => timesheet_store.pay_period_overviews.filter(overview => overview));
|
||||||
|
const overview_filters = ref<PayPeriodOverviewFilters>({
|
||||||
|
is_showing_inactive: false,
|
||||||
|
is_showing_team_only: false,
|
||||||
|
supervisors: [],
|
||||||
|
name_search_string: search_string.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
|
timesheet_store.current_pay_period_overview = row;
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
|
||||||
|
|
||||||
|
timesheet_store.is_details_dialog_open = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
||||||
|
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterEmployeeRows = (rows: readonly TimesheetOverview[], terms: PayPeriodOverviewFilters): TimesheetOverview[] => {
|
||||||
|
let result = [...rows];
|
||||||
|
|
||||||
|
if (!terms.is_showing_inactive) {
|
||||||
|
result = result.filter(row => row.is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (terms.name_search_string) {
|
||||||
|
// result = result.filter(row => row.employee_name.includes(terms.name_search_string ?? ''));
|
||||||
|
// }
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -49,10 +80,12 @@ const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
:rows="overview_rows"
|
:rows="overview_rows"
|
||||||
:columns="pay_period_overview_columns"
|
:columns="pay_period_overview_columns"
|
||||||
row-key="email"
|
row-key="email"
|
||||||
:filter="timesheet_store.search_filter"
|
|
||||||
:grid="timesheet_store.is_approval_grid_mode"
|
:grid="timesheet_store.is_approval_grid_mode"
|
||||||
:dense="timesheet_store.is_approval_grid_mode"
|
:dense="timesheet_store.is_approval_grid_mode"
|
||||||
hide-pagination
|
hide-pagination
|
||||||
|
:pagination="{ sortBy: 'is_active' }"
|
||||||
|
:filter="overview_filters"
|
||||||
|
:filter-method="filterEmployeeRows"
|
||||||
color="accent"
|
color="accent"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
card-container-class="justify-center"
|
card-container-class="justify-center"
|
||||||
|
|
@ -63,6 +96,87 @@ const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
:no-results-label="$t('shared.error.no_search_results')"
|
:no-results-label="$t('shared.error.no_search_results')"
|
||||||
:loading-label="$t('shared.label.loading')"
|
:loading-label="$t('shared.label.loading')"
|
||||||
>
|
>
|
||||||
|
<template #top>
|
||||||
|
<div class="column full-width">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-auto row items-start full-width q-px-lg"
|
||||||
|
:class="($q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md') + (timesheet_store.is_approval_grid_mode ? '' : ' q-mb-md')"
|
||||||
|
>
|
||||||
|
<PayPeriodNavigator
|
||||||
|
@date-selected="timesheet_approval_api.getTimesheetOverviews"
|
||||||
|
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
|
||||||
|
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
|
||||||
|
:class="$q.platform.is.mobile ? 'q-mb-sm' : ''"
|
||||||
|
style="height: 40px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-space />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-auto row no-wrap items-start"
|
||||||
|
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||||
|
>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="timesheet_store.is_approval_grid_mode"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
color="white"
|
||||||
|
text-color="accent"
|
||||||
|
toggle-color="accent"
|
||||||
|
class="col-auto"
|
||||||
|
:class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-sm'"
|
||||||
|
:options="[
|
||||||
|
{ icon: 'grid_view', value: true },
|
||||||
|
{ icon: 'view_list', value: false },
|
||||||
|
]"
|
||||||
|
style="height: 40px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
icon="download"
|
||||||
|
color="accent"
|
||||||
|
:label="$q.screen.lt.md ? '' : $t('shared.label.download')"
|
||||||
|
class="col-auto q-mr-sm"
|
||||||
|
style="height: 40px;"
|
||||||
|
@click="timesheet_store.is_report_dialog_open = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QTableFilters
|
||||||
|
v-model:search="search_string"
|
||||||
|
class="col-auto q-mb-xs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
icon="filter_alt"
|
||||||
|
color="white"
|
||||||
|
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
|
||||||
|
class="col q-ml-sm self-stretch bg-accent"
|
||||||
|
style="border-radius: 5px 5px 0 0;"
|
||||||
|
@click="is_showing_filters = !is_showing_filters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-slide-transition>
|
||||||
|
<OverviewListFilters
|
||||||
|
v-if="is_showing_filters"
|
||||||
|
v-model:filters="overview_filters"
|
||||||
|
class="q-mx-lg col-auto"
|
||||||
|
/>
|
||||||
|
</q-slide-transition>
|
||||||
|
|
||||||
|
<q-separator
|
||||||
|
color="accent"
|
||||||
|
size="5px"
|
||||||
|
class="q-mx-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #header="props">
|
<template #header="props">
|
||||||
<q-tr
|
<q-tr
|
||||||
:props="props"
|
:props="props"
|
||||||
|
|
@ -119,7 +233,7 @@ const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
{{ props.value.split(' ')[0] }}
|
{{ props.value.split(' ')[0] }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1]
|
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1]
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ props.value }}</span>
|
<span v-else>{{ props.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,7 +248,8 @@ const onClickedDetails = async (row: TimesheetOverview) => {
|
||||||
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
|
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
|
||||||
:index="props.rowIndex"
|
:index="props.rowIndex"
|
||||||
:row="props.row"
|
:row="props.row"
|
||||||
@click-details="overview => onClickedDetails(overview)"
|
@click-details="onClickedDetails"
|
||||||
|
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,20 @@ export const useTimesheetApprovalApi = () => {
|
||||||
timesheet_store.is_loading = false;
|
timesheet_store.is_loading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean) => {
|
||||||
|
timesheet_store.is_loading = true;
|
||||||
|
|
||||||
|
const success = await timesheet_store.getTimesheetsByOptionalEmployeeEmail(email);
|
||||||
|
if (success) {
|
||||||
|
const approval_success = await timesheet_store.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
|
||||||
|
const overview = timesheet_store.pay_period_overviews.find(overview => overview.email === email);
|
||||||
|
|
||||||
|
if (overview && approval_success) overview.is_approved = approval_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_store.is_loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[]) => {
|
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[]) => {
|
||||||
if (timesheet_store.pay_period === undefined) return;
|
if (timesheet_store.pay_period === undefined) return;
|
||||||
|
|
||||||
|
|
@ -40,6 +54,7 @@ export const useTimesheetApprovalApi = () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTimesheetOverviewsByDate,
|
getTimesheetOverviewsByDate,
|
||||||
|
toggleTimesheetsApprovalByEmployeeEmail,
|
||||||
getTimesheetApprovalCSVReport,
|
getTimesheetApprovalCSVReport,
|
||||||
getTimesheetOverviews,
|
getTimesheetOverviews,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import type { QTableColumn } from "quasar";
|
import type { QTableColumn } from "quasar";
|
||||||
|
|
||||||
export interface TimesheetOverview {
|
export class TimesheetOverview {
|
||||||
email: string;
|
email: string;
|
||||||
employee_name: string;
|
employee_name: string;
|
||||||
|
is_active: boolean;
|
||||||
regular_hours: number;
|
regular_hours: number;
|
||||||
other_hours: {
|
other_hours: {
|
||||||
evening_hours: number;
|
evening_hours: number;
|
||||||
|
|
@ -16,6 +17,25 @@ export interface TimesheetOverview {
|
||||||
expenses: number;
|
expenses: number;
|
||||||
mileage: number;
|
mileage: number;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.email = '';
|
||||||
|
this.employee_name = 'John Doe';
|
||||||
|
this.is_active = true;
|
||||||
|
this.regular_hours = 0;
|
||||||
|
this.other_hours = {
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
sick_hours: 0,
|
||||||
|
holiday_hours: 0,
|
||||||
|
vacation_hours: 0,
|
||||||
|
}
|
||||||
|
this.total_hours = 0;
|
||||||
|
this.expenses = 0;
|
||||||
|
this.mileage = 0;
|
||||||
|
this.is_approved = false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayPeriodOverviewResponse {
|
export interface PayPeriodOverviewResponse {
|
||||||
|
|
@ -28,22 +48,11 @@ export interface PayPeriodOverviewResponse {
|
||||||
employees_overview: TimesheetOverview[];
|
employees_overview: TimesheetOverview[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const default_pay_period_overview: TimesheetOverview = {
|
export interface PayPeriodOverviewFilters {
|
||||||
email: '',
|
is_showing_inactive: boolean;
|
||||||
employee_name: '',
|
is_showing_team_only: boolean;
|
||||||
regular_hours: -1,
|
supervisors: string[];
|
||||||
other_hours: {
|
name_search_string: string | number | null;
|
||||||
evening_hours: -1,
|
|
||||||
emergency_hours: -1,
|
|
||||||
overtime_hours: -1,
|
|
||||||
sick_hours: -1,
|
|
||||||
holiday_hours: -1,
|
|
||||||
vacation_hours: -1,
|
|
||||||
},
|
|
||||||
total_hours: -1,
|
|
||||||
expenses: -1,
|
|
||||||
mileage: -1,
|
|
||||||
is_approved: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const overview_column_names = {
|
export const overview_column_names = {
|
||||||
|
|
@ -59,6 +68,7 @@ export const overview_column_names = {
|
||||||
EXPENSES: 'expenses',
|
EXPENSES: 'expenses',
|
||||||
MILEAGE: 'mileage',
|
MILEAGE: 'mileage',
|
||||||
IS_APPROVED: 'is_approved',
|
IS_APPROVED: 'is_approved',
|
||||||
|
IS_ACTIVE: 'is_active',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pay_period_overview_columns: QTableColumn[] = [
|
export const pay_period_overview_columns: QTableColumn[] = [
|
||||||
|
|
@ -145,5 +155,11 @@ export const pay_period_overview_columns: QTableColumn[] = [
|
||||||
label: 'timesheet_approvals.table.is_approved',
|
label: 'timesheet_approvals.table.is_approved',
|
||||||
field: 'is_approved',
|
field: 'is_approved',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: overview_column_names.IS_ACTIVE,
|
||||||
|
label: 'timesheet_approvals.table.is_active',
|
||||||
|
field: 'is_active',
|
||||||
|
sortable: true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
|
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||||
|
|
||||||
|
|
@ -12,4 +13,9 @@ export const timesheetApprovalService = {
|
||||||
const response = await api.get(`exports/csv/${year}/${period_number}`, { params: filters, responseType: 'arraybuffer' });
|
const response = await api.get(`exports/csv/${year}/${period_number}`, { params: filters, responseType: 'arraybuffer' });
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateTimesheetsApprovalStatus: async (email: string, timesheet_ids: number[], is_approved: boolean): Promise<BackendResponse<{shifts: number, expenses: number}>> => {
|
||||||
|
const response = await api.patch<BackendResponse<{shifts: number, expenses: number}>>('pay-periods/pay-period-approval', { email, timesheet_ids, is_approved});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -101,6 +101,7 @@
|
||||||
v-model="is_navigator_open"
|
v-model="is_navigator_open"
|
||||||
transition-show="jump-right"
|
transition-show="jump-right"
|
||||||
transition-hide="jump-right"
|
transition-hide="jump-right"
|
||||||
|
class="z-top"
|
||||||
>
|
>
|
||||||
<q-date
|
<q-date
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
|
|
|
||||||
|
|
@ -56,15 +56,16 @@
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="row items-center">
|
<div class="row items-center">
|
||||||
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-lg q-mr-sm">
|
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
|
||||||
</span>
|
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="expense_store.mode !== 'create'"
|
name="add_circle_outline"
|
||||||
name="las la-plus-square"
|
|
||||||
size="md"
|
size="md"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
|
:class="expense_store.is_showing_create_form ? 'invisible' : ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-xs q-mr-sm">
|
||||||
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@
|
||||||
v-model="is_navigator_open"
|
v-model="is_navigator_open"
|
||||||
transition-show="jump-right"
|
transition-show="jump-right"
|
||||||
transition-hide="jump-right"
|
transition-hide="jump-right"
|
||||||
|
class="z-top"
|
||||||
>
|
>
|
||||||
<q-date
|
<q-date
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
|
|
@ -217,12 +218,12 @@
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-dialog v-model="is_showing_comment_dialog_mobile">
|
<q-dialog v-model="is_showing_comment_dialog_mobile" class="z-top">
|
||||||
<q-card class="full-width bg-secondary rounded-10">
|
<q-card class="full-width bg-primary rounded-10">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<span
|
<span
|
||||||
class="text-weight-bold text-accent text-uppercase text-caption"
|
class="text-weight-bold text-accent text-uppercase text-caption q-mx-md"
|
||||||
style="font-size: 1.5em;"
|
style="font-size: 1.2em;"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -231,14 +232,13 @@
|
||||||
<q-card-section class="q-pa-none bg-primary rounded-10">
|
<q-card-section class="q-pa-none bg-primary rounded-10">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.comment"
|
v-model="expenses_store.current_expense.comment"
|
||||||
standout="bg-blue-grey-9"
|
filled
|
||||||
dense
|
dense
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
color="primary"
|
color="accent"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
lazy-rules="ondemand"
|
class="bg-white no-border"
|
||||||
:rules="[rules.commentRequired]"
|
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { QSelect, QInput } from 'quasar';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const ui_store = useUiStore();
|
||||||
|
|
||||||
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
|
const select_ref = ref<QSelect | null>(null);
|
||||||
|
const is_showing_comment_popup = ref(false);
|
||||||
|
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||||
|
const error_message = ref('');
|
||||||
|
|
||||||
|
const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||||
|
dense?: boolean;
|
||||||
|
hasShiftAfter?: boolean;
|
||||||
|
isTimesheetApproved?: boolean;
|
||||||
|
errorMessage?: string | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'requestDelete': [void];
|
||||||
|
'onTimeFieldBlur': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const onBlurShiftTypeSelect = () => {
|
||||||
|
if (shift_type_selected.value === undefined) {
|
||||||
|
shift.value.type = 'REGULAR';
|
||||||
|
shift.value.id = 0;
|
||||||
|
emit('requestDelete');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeFieldBlur = (time_string: string) => {
|
||||||
|
if (time_string.length < 1 || !time_string) {
|
||||||
|
shift.value.has_error = true;
|
||||||
|
error_message.value = 'timesheet.errors.SHIFT_TIME_REQUIRED';
|
||||||
|
} else {
|
||||||
|
shift.value.has_error = false;
|
||||||
|
error_message.value = '';
|
||||||
|
emit('onTimeFieldBlur');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage)
|
||||||
|
error_message.value = errorMessage;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row q-px-xs">
|
||||||
|
<div class="col column">
|
||||||
|
<div class="col row items-center text-uppercase q-px-xs rounded-5">
|
||||||
|
<!-- comment button -->
|
||||||
|
<q-btn
|
||||||
|
v-if="ui_store.is_mobile_mode && !dense"
|
||||||
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
|
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
||||||
|
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
||||||
|
@click="is_showing_comment_popup = true"
|
||||||
|
>
|
||||||
|
<q-dialog v-model="is_showing_comment_popup">
|
||||||
|
<q-input
|
||||||
|
color="white"
|
||||||
|
v-model="shift.comment"
|
||||||
|
dense
|
||||||
|
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||||
|
autofocus
|
||||||
|
counter
|
||||||
|
bottom-slots
|
||||||
|
stack-label
|
||||||
|
:label="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
:maxlength="COMMENT_LENGTH_MAX"
|
||||||
|
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon name="edit" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #counter>
|
||||||
|
<div class="row flex-center">
|
||||||
|
<q-space />
|
||||||
|
<q-knob
|
||||||
|
v-model="comment_length"
|
||||||
|
readonly
|
||||||
|
:max="COMMENT_LENGTH_MAX"
|
||||||
|
size="1.6em"
|
||||||
|
:thickness="0.4"
|
||||||
|
:color="getCommentCounterColor(comment_length)"
|
||||||
|
track-color="grey-4"
|
||||||
|
class="col-auto q-mr-xs"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(comment_length)"
|
||||||
|
>{{ 280 - comment_length }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-dialog>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<!-- shift type -->
|
||||||
|
<q-select
|
||||||
|
ref="select"
|
||||||
|
v-model="shift_type_selected"
|
||||||
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
|
dense
|
||||||
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
options-dense
|
||||||
|
hide-dropdown-icon
|
||||||
|
:menu-offset="[0, 10]"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
:options="SHIFT_OPTIONS"
|
||||||
|
class="col rounded-5 bg-dark"
|
||||||
|
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
||||||
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
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 items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
:color="scope.opt.icon_color"
|
||||||
|
size="sm"
|
||||||
|
class="col-auto"
|
||||||
|
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style="line-height: 1.2em;"
|
||||||
|
class="col-auto ellipsis"
|
||||||
|
:class="!shift.is_approved ? '' : 'text-white'"
|
||||||
|
>
|
||||||
|
{{ $t(scope.opt.label) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #after>
|
||||||
|
<q-icon
|
||||||
|
v-if="shift.is_approved"
|
||||||
|
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||||
|
size="1.2em"
|
||||||
|
color="white"
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
:hide-delay="1000"
|
||||||
|
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
v-else
|
||||||
|
v-model="shift.is_remote"
|
||||||
|
:disable="shift.is_approved"
|
||||||
|
dense
|
||||||
|
keep-color
|
||||||
|
size="3em"
|
||||||
|
color="accent"
|
||||||
|
icon="las la-building"
|
||||||
|
checked-icon="las la-laptop"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
:hide-delay="1000"
|
||||||
|
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-toggle>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||||
|
<!-- punch in field -->
|
||||||
|
<div class="col q-pr-xs">
|
||||||
|
<q-input
|
||||||
|
v-model="shift.start_time"
|
||||||
|
dense
|
||||||
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
type="time"
|
||||||
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
|
label-slot
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:error="shift.has_error"
|
||||||
|
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
|
||||||
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
|
class="rounded-5 bg-dark"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="onTimeFieldBlur(shift.start_time)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.in') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- punch out field -->
|
||||||
|
<div class="col">
|
||||||
|
<q-input
|
||||||
|
v-model="shift.end_time"
|
||||||
|
standout
|
||||||
|
dense
|
||||||
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
type="time"
|
||||||
|
label-slot
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:error="shift.has_error"
|
||||||
|
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
|
||||||
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
class="rounded-5 bg-dark"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="onTimeFieldBlur(shift.end_time)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn
|
||||||
|
v-if="!shift.is_approved"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
color="negative"
|
||||||
|
icon="las la-trash"
|
||||||
|
size="lg"
|
||||||
|
class="full-height"
|
||||||
|
@click="$emit('requestDelete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-separator
|
||||||
|
v-if="hasShiftAfter"
|
||||||
|
spaced
|
||||||
|
class="q-mx-md col-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-field--error) {
|
||||||
|
background-color: var(--q-negative) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-field--error .q-field__bottom) {
|
||||||
|
color: white;
|
||||||
|
font-weight: 900;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
padding-top: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_api = useTimesheetApi();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
|
class="col-auto row items-start q-px-sm q-pt-sm full-width"
|
||||||
|
>
|
||||||
|
<!-- per timesheet -->
|
||||||
|
<div
|
||||||
|
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
||||||
|
:key="timesheet_index"
|
||||||
|
class="col column flex-center q-pa-sm"
|
||||||
|
>
|
||||||
|
<!-- container -->
|
||||||
|
<div
|
||||||
|
class="rounded-5 relative-position q-px-sm q-pt-sm q-pb-xs full-width shadow-4"
|
||||||
|
style="border: 1px solid var(--q-accent);"
|
||||||
|
>
|
||||||
|
<!-- label for week number -->
|
||||||
|
<div
|
||||||
|
class="self-start text-uppercase text-weight-bolder text-accent bg-secondary absolute-top-left q-px-xs"
|
||||||
|
style="font-size: 0.8em; top: -7px; left: 10px; line-height: 1em;"
|
||||||
|
>{{ $t('timesheet.week') + ` ${timesheet_index + 1}` }}</div>
|
||||||
|
|
||||||
|
<!-- hours worked in the week -->
|
||||||
|
<div class="col-auto row">
|
||||||
|
<span class="text-weight-bolder text-uppercase text-accent text-caption q-mr-sm">{{
|
||||||
|
$t('timesheet.total_hours') }}</span>
|
||||||
|
<span>{{
|
||||||
|
(timesheet.weekly_hours.regular +
|
||||||
|
timesheet.weekly_hours.evening +
|
||||||
|
timesheet.weekly_hours.emergency +
|
||||||
|
timesheet.weekly_hours.overtime).toFixed(2)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- label for current shifts preview -->
|
||||||
|
<div
|
||||||
|
class="col-auto full-width text-center text-weight-medium text-caption text-uppercase q-mt-xs"
|
||||||
|
style="font-size: 0.65em; line-height: 1.2em;"
|
||||||
|
> {{ $t('timesheet.current_shifts') }}</div>
|
||||||
|
|
||||||
|
<!-- preview of current number of shifts -->
|
||||||
|
<div
|
||||||
|
class="col row flex-center"
|
||||||
|
style="height: 20px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="day, day_index in timesheet.days"
|
||||||
|
:key="day_index"
|
||||||
|
class="col row flex-center"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
:color="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'accent shadow-2' : 'dark shadow-2') : 'blue-grey-5'"
|
||||||
|
:class="day.shifts.length > 0 ? (day.shifts.every(shift => shift.is_approved) ? 'q-px-xs' : 'q-pa-sm') : ''"
|
||||||
|
:style="day.shifts.length > 0 ? '' : 'opacity: 0.5'"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="day.shifts.every(shift => shift.is_approved) && day.shifts.length > 0"
|
||||||
|
name="check"
|
||||||
|
class="q-pa-none"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- button to apply weekly schedule preset -->
|
||||||
|
<div class="col-auto flex-center row q-pt-xs full-width">
|
||||||
|
<q-btn
|
||||||
|
v-if="timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
color="accent"
|
||||||
|
:label="$t('timesheet.apply_preset')"
|
||||||
|
class="full-width"
|
||||||
|
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -2,55 +2,42 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable*/
|
import { onMounted, ref } from 'vue';
|
||||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, nextTick } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { QSelect, QInput } from 'quasar';
|
import { QSelect, QInput } from 'quasar';
|
||||||
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useShiftRules, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
const { t } = useI18n();
|
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
const select_ref = useTemplateRef<QSelect>('select');
|
const select_ref = ref<QSelect | null>(null);
|
||||||
const start_time_ref = useTemplateRef<QInput>('start_time');
|
const error_message = ref('');
|
||||||
const end_time_ref = useTemplateRef<QInput>('end_time');
|
|
||||||
|
|
||||||
const { dayShifts = [], dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
const { errorMessage = undefined, isTimesheetApproved = false } = defineProps<{
|
||||||
dayShifts: Shift[];
|
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
hasShiftAfter?: boolean;
|
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
|
errorMessage?: string | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'), t('timesheet.errors.SHIFT_OVERLAP_SHORT'), dayShifts);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'saveComment': [comment: string, shift_id: number];
|
|
||||||
'requestDelete': [void];
|
'requestDelete': [void];
|
||||||
'onTimeFieldBlur': [void];
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const onBlurShiftTypeSelect = () => {
|
const onTimeFieldBlur = (time_string: string) => {
|
||||||
if (shift_type_selected.value === undefined) {
|
if (time_string.length < 1 || !time_string) {
|
||||||
shift.value.type = 'REGULAR';
|
shift.value.has_error = true;
|
||||||
shift.value.id = 0;
|
error_message.value = 'timesheet.errors.SHIFT_TIME_REQUIRED'
|
||||||
emit('requestDelete');
|
} else {
|
||||||
|
shift.value.has_error = false;
|
||||||
|
error_message.value = '';
|
||||||
|
emit('onTimeFieldBlur');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const slideDeleteShift = async (reset: () => void) => {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
reset();
|
|
||||||
emit('requestDelete');
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCommentCounterColor = (comment_length: number) => {
|
const getCommentCounterColor = (comment_length: number) => {
|
||||||
if (comment_length < 200) return 'primary';
|
if (comment_length < 200) return 'primary';
|
||||||
|
|
@ -65,43 +52,193 @@
|
||||||
shift_type_selected.value = undefined;
|
shift_type_selected.value = undefined;
|
||||||
ui_store.focus_next_component = false;
|
ui_store.focus_next_component = false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
if (errorMessage)
|
||||||
clearTimeout(timer);
|
error_message.value = errorMessage;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => [start_time_ref.value?.hasError, end_time_ref.value?.hasError], ([start_error, end_error]) => {
|
|
||||||
shift.value.has_error = (start_error || end_error) ?? false;
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-slide-item
|
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
||||||
right-color="negative"
|
<div
|
||||||
class="rounded-5 transparent"
|
class="row items-center text-uppercase rounded-5"
|
||||||
:class="ui_store.is_mobile_mode ? 'q-my-md' : 'q-mr-xs'"
|
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
||||||
@right="details => slideDeleteShift(details.reset)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#right
|
|
||||||
v-if="ui_store.is_mobile_mode"
|
|
||||||
>
|
>
|
||||||
<q-icon name="delete" />
|
<!-- shift type -->
|
||||||
</template>
|
<q-select
|
||||||
|
ref="select"
|
||||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
v-model="shift_type_selected"
|
||||||
<div
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
class="row items-center text-uppercase rounded-5"
|
dense
|
||||||
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
: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="col rounded-5 q-mx-xs bg-dark"
|
||||||
|
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
||||||
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
|
@update:model-value="option => shift.type = option.value"
|
||||||
>
|
>
|
||||||
<!-- mobile comment button -->
|
<template #selected-item="scope">
|
||||||
|
<div
|
||||||
|
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
:color="scope.opt.icon_color"
|
||||||
|
size="sm"
|
||||||
|
class="col-auto"
|
||||||
|
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style="line-height: 1.2em;"
|
||||||
|
class="col-auto ellipsis"
|
||||||
|
:class="!shift.is_approved ? '' : 'text-white'"
|
||||||
|
>
|
||||||
|
{{ $t(scope.opt.label) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #after>
|
||||||
|
<q-icon
|
||||||
|
v-if="shift.is_approved"
|
||||||
|
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||||
|
size="1.2em"
|
||||||
|
color="white"
|
||||||
|
class="q-mr-sm"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
|
||||||
|
<q-toggle
|
||||||
|
v-else
|
||||||
|
v-model="shift.is_remote"
|
||||||
|
:disable="shift.is_approved"
|
||||||
|
dense
|
||||||
|
keep-color
|
||||||
|
size="3em"
|
||||||
|
color="accent"
|
||||||
|
icon="las la-building"
|
||||||
|
checked-icon="las la-laptop"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
anchor="top middle"
|
||||||
|
self="bottom middle"
|
||||||
|
:offset="[0, 10]"
|
||||||
|
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||||
|
>
|
||||||
|
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||||
|
$t('timesheet.shift.types.OFFICE') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</q-toggle>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||||
|
<!-- punch in field -->
|
||||||
|
<q-input
|
||||||
|
ref="start_time"
|
||||||
|
v-model="shift.start_time"
|
||||||
|
dense
|
||||||
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
type="time"
|
||||||
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
|
label-slot
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:error="shift.has_error"
|
||||||
|
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
|
||||||
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
|
class="col rounded-5 bg-dark q-mx-xs"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="onTimeFieldBlur(shift.start_time)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.in') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- punch out field -->
|
||||||
|
<q-input
|
||||||
|
ref="end_time"
|
||||||
|
v-model="shift.end_time"
|
||||||
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
|
dense
|
||||||
|
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||||
|
type="time"
|
||||||
|
label-slot
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:error="shift.has_error"
|
||||||
|
:error-message="errorMessage || error_message !== '' ? $t(errorMessage ?? error_message) : ''"
|
||||||
|
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||||
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
|
class="col rounded-5 bg-dark q-mx-xs"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||||
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
|
@blur="onTimeFieldBlur(shift.end_time)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder"
|
||||||
|
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||||
|
style="font-size: 0.95em;"
|
||||||
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- comment and delete buttons -->
|
||||||
|
<div
|
||||||
|
class="row full-height"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'"
|
||||||
|
>
|
||||||
|
<!-- desktop comment button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="ui_store.is_mobile_mode && !dense"
|
v-if="!ui_store.is_mobile_mode"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
:color="shift.is_approved ? 'white' : 'accent'"
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
:text-color="shift.is_approved ? 'accent' : 'white'"
|
||||||
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
class="col"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
||||||
>
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="shift.comment"
|
||||||
|
floating
|
||||||
|
rounded
|
||||||
|
color="negative"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-popup-edit
|
<q-popup-edit
|
||||||
v-model="shift.comment"
|
v-model="shift.comment"
|
||||||
:title="$t('timesheet.shift.fields.header_comment')"
|
:title="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
|
@ -113,13 +250,13 @@
|
||||||
color="white"
|
color="white"
|
||||||
v-model="scope.value"
|
v-model="scope.value"
|
||||||
dense
|
dense
|
||||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
:readonly="shift.is_approved"
|
||||||
autofocus
|
autofocus
|
||||||
counter
|
counter
|
||||||
bottom-slots
|
bottom-slots
|
||||||
:maxlength="COMMENT_LENGTH_MAX"
|
:maxlength="COMMENT_LENGTH_MAX"
|
||||||
class="q-pb-lg"
|
class="q-pb-lg"
|
||||||
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
||||||
@keyup.enter="scope.set"
|
@keyup.enter="scope.set"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
|
|
@ -148,248 +285,23 @@
|
||||||
</q-popup-edit>
|
</q-popup-edit>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<!-- shift type -->
|
<q-btn
|
||||||
<q-select
|
v-if="!shift.is_approved"
|
||||||
ref="select"
|
flat
|
||||||
v-model="shift_type_selected"
|
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
|
||||||
dense
|
dense
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
:disable="shift.is_approved"
|
||||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
tabindex="-1"
|
||||||
:options-dense="!ui_store.is_mobile_mode"
|
icon="las la-trash"
|
||||||
hide-dropdown-icon
|
text-color="negative"
|
||||||
:menu-offset="[0, 10]"
|
class="col"
|
||||||
menu-anchor="bottom middle"
|
size="1.2em"
|
||||||
menu-self="top middle"
|
:class="shift.is_approved ? 'invisible' : ''"
|
||||||
:options="SHIFT_OPTIONS"
|
@click="$emit('requestDelete')"
|
||||||
class="col rounded-5 q-mx-xs bg-dark"
|
|
||||||
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
|
||||||
@blur="onBlurShiftTypeSelect"
|
|
||||||
@update:model-value="option => shift.type = option.value"
|
|
||||||
>
|
>
|
||||||
<template #selected-item="scope">
|
</q-btn>
|
||||||
<div
|
|
||||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
|
||||||
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
|
||||||
:tabindex="scope.tabindex"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
:name="scope.opt.icon"
|
|
||||||
:color="scope.opt.icon_color"
|
|
||||||
size="sm"
|
|
||||||
class="col-auto"
|
|
||||||
:class="shift.is_approved ? 'q-mx-xs': 'q-mr-xs'"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
style="line-height: 1.2em;"
|
|
||||||
class="col-auto ellipsis"
|
|
||||||
:class="!shift.is_approved ? '' : 'text-white'"
|
|
||||||
>
|
|
||||||
{{ $t(scope.opt.label) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #after>
|
|
||||||
<q-icon
|
|
||||||
v-if="shift.is_approved"
|
|
||||||
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
|
||||||
size="1.2em"
|
|
||||||
color="white"
|
|
||||||
class="q-mr-sm"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
anchor="top middle"
|
|
||||||
self="bottom middle"
|
|
||||||
:offset="[0, 10]"
|
|
||||||
class="text-uppercase text-weight-bold text-white bg-primary"
|
|
||||||
>
|
|
||||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
|
||||||
$t('timesheet.shift.types.OFFICE') }}
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-else
|
|
||||||
v-model="shift.is_remote"
|
|
||||||
:disable="shift.is_approved"
|
|
||||||
dense
|
|
||||||
keep-color
|
|
||||||
size="3em"
|
|
||||||
color="accent"
|
|
||||||
icon="las la-building"
|
|
||||||
checked-icon="las la-laptop"
|
|
||||||
>
|
|
||||||
<q-tooltip
|
|
||||||
anchor="top middle"
|
|
||||||
self="bottom middle"
|
|
||||||
:offset="[0, 10]"
|
|
||||||
class="text-uppercase text-weight-medium text-white bg-accent"
|
|
||||||
>
|
|
||||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
|
||||||
$t('timesheet.shift.types.OFFICE') }}
|
|
||||||
</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
|
||||||
<!-- punch in field -->
|
|
||||||
<q-input
|
|
||||||
ref="start_time"
|
|
||||||
v-model="shift.start_time"
|
|
||||||
dense
|
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
|
||||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
|
||||||
type="time"
|
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
|
||||||
label-slot
|
|
||||||
lazy-rules
|
|
||||||
no-error-icon
|
|
||||||
hide-bottom-space
|
|
||||||
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
|
||||||
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 && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
|
||||||
input-style="font-size: 1.2em;"
|
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
|
||||||
@blur="emit('onTimeFieldBlur')"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span
|
|
||||||
class="text-weight-bolder"
|
|
||||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
|
||||||
style="font-size: 0.95em;"
|
|
||||||
>{{ $t('shared.misc.in') }}</span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<!-- punch out field -->
|
|
||||||
<q-input
|
|
||||||
ref="end_time"
|
|
||||||
v-model="shift.end_time"
|
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
|
||||||
dense
|
|
||||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
|
||||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
|
||||||
type="time"
|
|
||||||
label-slot
|
|
||||||
lazy-rules
|
|
||||||
no-error-icon
|
|
||||||
hide-bottom-space
|
|
||||||
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
|
||||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
|
||||||
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 q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
|
||||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
|
||||||
@blur="emit('onTimeFieldBlur')"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span
|
|
||||||
class="text-weight-bolder"
|
|
||||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
|
||||||
style="font-size: 0.95em;"
|
|
||||||
>{{ $t('shared.misc.out') }}</span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<!-- comment and delete buttons -->
|
|
||||||
<div class="row full-height" :class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'">
|
|
||||||
<!-- desktop comment button -->
|
|
||||||
<q-btn
|
|
||||||
v-if="!ui_store.is_mobile_mode"
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
:color="shift.is_approved ? 'white' : 'accent'"
|
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
|
||||||
:text-color="shift.is_approved ? 'accent' : 'white'"
|
|
||||||
class="col"
|
|
||||||
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
|
||||||
>
|
|
||||||
<q-badge
|
|
||||||
v-if="shift.comment"
|
|
||||||
floating
|
|
||||||
rounded
|
|
||||||
color="negative"
|
|
||||||
/>
|
|
||||||
<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 && !shift.is_approved"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
:disable="shift.is_approved"
|
|
||||||
tabindex="-1"
|
|
||||||
icon="las la-trash"
|
|
||||||
text-color="negative"
|
|
||||||
class="col"
|
|
||||||
size="1.2em"
|
|
||||||
:class="shift.is_approved ? 'invisible' : ''"
|
|
||||||
@click="$emit('requestDelete')"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-slide-item>
|
</div>
|
||||||
|
|
||||||
<q-separator
|
|
||||||
v-if="hasShiftAfter && ui_store.is_mobile_mode"
|
|
||||||
spaced
|
|
||||||
color="accent"
|
|
||||||
class="q-mx-md"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,18 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
||||||
|
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
import type { TimesheetDay } from 'src/modules/timesheets/models/timesheet.models';
|
||||||
|
import { isShiftOverlap } from 'src/modules/timesheets/utils/shift.util';
|
||||||
|
|
||||||
const shift_api = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
const shift_error_message = ref<string | undefined>();
|
||||||
|
|
||||||
const { day, dense = false, approved = false } = defineProps<{
|
const { day, dense = false, approved = false } = defineProps<{
|
||||||
timesheetId: number;
|
timesheetId: number;
|
||||||
|
|
@ -35,6 +38,15 @@
|
||||||
}
|
}
|
||||||
await shift_api.deleteShiftById(shift.id);
|
await shift_api.deleteShiftById(shift.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onTimeFieldBlur = () => {
|
||||||
|
const is_error = isShiftOverlap(day.shifts);
|
||||||
|
day.shifts.map(shift => shift.has_error = is_error);
|
||||||
|
if (is_error)
|
||||||
|
shift_error_message.value = 'timesheet.errors.SHIFT_OVERLAP_SHORT';
|
||||||
|
else
|
||||||
|
shift_error_message.value = undefined;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -69,15 +81,31 @@
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<ShiftListDayRow
|
<div
|
||||||
v-for="shift, shift_index in day.shifts"
|
v-for="shift, shift_index in day.shifts"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
class="col-auto"
|
||||||
:day-shifts="day.shifts"
|
>
|
||||||
:is-timesheet-approved="approved"
|
<ShiftListDayRowMobile
|
||||||
:dense="dense"
|
v-if="$q.platform.is.mobile"
|
||||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
:is-timesheet-approved="approved"
|
||||||
/>
|
:error-message="shift_error_message"
|
||||||
|
:dense="dense"
|
||||||
|
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||||
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
@on-time-field-blur="onTimeFieldBlur()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShiftListDayRow
|
||||||
|
v-else
|
||||||
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
|
:is-timesheet-approved="approved"
|
||||||
|
:error-message="shift_error_message"
|
||||||
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
@on-time-field-blur="onTimeFieldBlur()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -20,6 +20,10 @@
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const timesheet_api = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
|
const { mode = 'normal'} = defineProps<{
|
||||||
|
mode: 'normal' | 'approval';
|
||||||
|
}>();
|
||||||
|
|
||||||
const mobile_animation_direction = ref('fadeInLeft');
|
const mobile_animation_direction = ref('fadeInLeft');
|
||||||
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
||||||
|
|
||||||
|
|
@ -47,23 +51,26 @@
|
||||||
return day.shifts.every(shift => shift.is_approved === true);
|
return day.shifts.every(shift => shift.is_approved === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: {x?: number, y?: number}) => {
|
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: { x?: number, y?: number }) => {
|
||||||
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||||
if (distance.x && Math.abs(distance.x) > 10 ) {
|
if (distance.x && Math.abs(distance.x) > 10) {
|
||||||
await timesheet_api.getTimesheetsBySwiping( direction === 'left' ? 1 : -1 )
|
await timesheet_api.getTimesheetsBySwiping(direction === 'left' ? 1 : -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="col column fit relative-position" v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? {x: 0, y: 0})">
|
<div
|
||||||
|
class="col column fit relative-position"
|
||||||
|
:style="$q.platform.is.mobile ? 'margin-bottom: 40px' : ''"
|
||||||
|
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })"
|
||||||
|
>
|
||||||
<q-scroll-area
|
<q-scroll-area
|
||||||
ref="timesheet_page"
|
ref="timesheet_page"
|
||||||
:horizontal-offset="[0, 3]"
|
:horizontal-offset="[0, 3]"
|
||||||
class="absolute-full hide-scrollbar q-mt-sm"
|
class="absolute-full hide-scrollbar"
|
||||||
:thumb-style="{ opacity: '0' }"
|
:thumb-style="{ opacity: '0' }"
|
||||||
:bar-style="{ opacity: '0' }"
|
:bar-style="{ opacity: '0' }"
|
||||||
style="min-height: 50vh;"
|
|
||||||
>
|
>
|
||||||
<!-- Show if no timesheets found (further than one month from present) -->
|
<!-- Show if no timesheets found (further than one month from present) -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -91,7 +98,7 @@
|
||||||
<div
|
<div
|
||||||
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
|
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
|
||||||
:key="timesheet.timesheet_id"
|
:key="timesheet.timesheet_id"
|
||||||
class="col fit"
|
class="col column fit flex-center"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
|
|
@ -99,13 +106,12 @@
|
||||||
leave-active-class="animated fadeOutUp"
|
leave-active-class="animated fadeOutUp"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!$q.platform.is.mobile"
|
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
:label="$t('timesheet.apply_preset_week')"
|
:label="$t('timesheet.apply_preset_week')"
|
||||||
class="text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||||
:class="timesheet.days.every(day => day.shifts.length < 1) ? '' : 'invisible'"
|
|
||||||
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
|
|
@ -123,21 +129,20 @@
|
||||||
<div
|
<div
|
||||||
v-for="day, day_index in timesheet.days"
|
v-for="day, day_index in timesheet.days"
|
||||||
:key="day.date"
|
:key="day.date"
|
||||||
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
class="col-auto row q-pa-sm fit"
|
||||||
:style="`animation-delay: ${day_index / 15}s;`"
|
:style="`animation-delay: ${day_index / 15}s;`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||||
class="col column full-width"
|
class="col column full-width q-px-md q-py-sm"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="rounded-10"
|
class="mobile-rounded-10 shadow-12"
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||||
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-card-section
|
<q-card-section
|
||||||
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
||||||
style="line-height: 1em;"
|
style="line-height: 1em;"
|
||||||
>
|
>
|
||||||
|
|
@ -145,6 +150,15 @@
|
||||||
weekday: 'long', day: 'numeric', month:
|
weekday: 'long', day: 'numeric', month:
|
||||||
'long'
|
'long'
|
||||||
}) }}</span>
|
}) }}</span>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
name="verified"
|
||||||
|
size="3em"
|
||||||
|
color="white"
|
||||||
|
class="absolute-top-left z-top"
|
||||||
|
style="top: -0.2em; left: 0px;"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section
|
<q-card-section
|
||||||
|
|
@ -162,49 +176,29 @@
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||||
square
|
square
|
||||||
|
dense
|
||||||
|
size="xl"
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="more_time"
|
icon="more_time"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
style="border-radius: 0 0 5px 5px;"
|
style="border-radius: 0 0 10px 10px;"
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-section>
|
||||||
|
|
||||||
<q-badge
|
|
||||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
|
||||||
floating
|
|
||||||
class="transparent q-pa-none rounded-50"
|
|
||||||
style="transform: translate(15px, -5px);"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="verified"
|
|
||||||
size="5em"
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
</q-badge>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="col row full-width"
|
class="col row full-width rounded-10 ellipsis shadow-10"
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
|
||||||
>
|
>
|
||||||
<transition
|
|
||||||
appear
|
|
||||||
enter-active-class="animated fadeInDown"
|
|
||||||
leave-active-class="animated fadeOutUp"
|
|
||||||
>
|
|
||||||
</transition>
|
|
||||||
<!-- List of shifts -->
|
|
||||||
<div
|
<div
|
||||||
class="col row bg-dark"
|
class="col row"
|
||||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||||
style="border-radius: 10px 0 0 10px;"
|
|
||||||
>
|
>
|
||||||
<!-- Date block -->
|
<!-- Date block -->
|
||||||
<ShiftListDateWidget
|
<ShiftListDateWidget
|
||||||
|
|
@ -231,17 +225,19 @@
|
||||||
color="white"
|
color="white"
|
||||||
size="xl"
|
size="xl"
|
||||||
class="full-height"
|
class="full-height"
|
||||||
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else
|
v-else
|
||||||
:dense="!ui_store.is_mobile_mode"
|
:dense="!$q.platform.is.mobile"
|
||||||
|
square
|
||||||
icon="more_time"
|
icon="more_time"
|
||||||
size="lg"
|
size="lg"
|
||||||
color="accent"
|
color="accent"
|
||||||
text-color="white"
|
text-color="white"
|
||||||
class="full-height"
|
class="full-height"
|
||||||
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"
|
||||||
style="border-radius: 0 10px 10px 0;"
|
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,29 +246,47 @@
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-page-sticky
|
|
||||||
position="bottom-right"
|
|
||||||
:offset="[0, -35]"
|
|
||||||
class="z-top"
|
|
||||||
>
|
|
||||||
<transition
|
|
||||||
appear
|
|
||||||
enter-active-class="animated zoomIn"
|
|
||||||
leave-active-class="animated zoomOut"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-if="scroll_y > 400"
|
|
||||||
fab
|
|
||||||
icon="las la-chevron-up"
|
|
||||||
color="white"
|
|
||||||
text-color="accent"
|
|
||||||
class="shadow-12"
|
|
||||||
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
</q-page-sticky>
|
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
|
|
||||||
|
<q-page-sticky
|
||||||
|
v-if="mode === 'normal'"
|
||||||
|
position="bottom-right"
|
||||||
|
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
|
||||||
|
class="z-top"
|
||||||
|
>
|
||||||
|
<transition
|
||||||
|
appear
|
||||||
|
enter-active-class="animated zoomIn"
|
||||||
|
leave-active-class="animated zoomOut"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="scroll_y > 400"
|
||||||
|
fab
|
||||||
|
icon="las la-chevron-up"
|
||||||
|
color="white"
|
||||||
|
text-color="accent"
|
||||||
|
class="shadow-12"
|
||||||
|
@click="timesheet_page!.setScrollPosition('vertical', 0, 300)"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</q-page-sticky>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||||
|
.mobile-rounded-#{$size} {
|
||||||
|
border-radius: #{$size}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-rounded-#{$size} > div:first-child {
|
||||||
|
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-rounded-#{$size} > div:last-child {
|
||||||
|
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
||||||
|
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview.vue';
|
||||||
|
|
||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
|
|
@ -30,7 +31,8 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
if (mode === 'normal')
|
||||||
|
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -38,9 +40,10 @@
|
||||||
<div class="column items-center full-height">
|
<div class="column items-center full-height">
|
||||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||||
|
|
||||||
|
<!-- top menu -->
|
||||||
<div
|
<div
|
||||||
class="col-auto row items-center full-width"
|
class="col-auto row items-center full-width"
|
||||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-mt-md q-px-md'"
|
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-pb-sm q-px-md'"
|
||||||
>
|
>
|
||||||
<!-- navigation btn -->
|
<!-- navigation btn -->
|
||||||
<PayPeriodNavigator
|
<PayPeriodNavigator
|
||||||
|
|
@ -63,20 +66,10 @@
|
||||||
@click="expenses_store.open"
|
@click="expenses_store.open"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-space v-if="!$q.platform.is.mobile" />
|
<!-- label for approval mode to delimit that this is the timesheet -->
|
||||||
|
<span class="col-auto text-uppercase text-bold text-h5"> {{ $t('timesheet.page_header') }}</span>
|
||||||
|
|
||||||
<!-- desktop save timesheet changes button -->
|
<q-space v-if="$q.screen.width > $q.screen.height" />
|
||||||
<q-btn
|
|
||||||
v-if="mode === 'normal' && !is_timesheets_approved && !$q.platform.is.mobile"
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
|
||||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
|
||||||
icon="upload"
|
|
||||||
:label="$t('shared.label.save')"
|
|
||||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-mr-md'"
|
|
||||||
@click="shift_api.saveShiftChanges"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- desktop expenses button -->
|
<!-- desktop expenses button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -88,25 +81,45 @@
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="expenses_store.open"
|
@click="expenses_store.open"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- desktop save timesheet changes button -->
|
||||||
|
<q-btn
|
||||||
|
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||||
|
push
|
||||||
|
rounded
|
||||||
|
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||||
|
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||||
|
icon="upload"
|
||||||
|
:label="$t('shared.label.save')"
|
||||||
|
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||||
|
@click="shift_api.saveShiftChanges"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TimesheetErrorWidget class="col-auto"/>
|
<!-- error message widget for potential backend-provided errors -->
|
||||||
|
<TimesheetErrorWidget class="col-auto" />
|
||||||
|
|
||||||
<ShiftList />
|
<!-- mobile weekly overview widget -->
|
||||||
|
<ShiftListWeeklyOverview />
|
||||||
|
|
||||||
|
<ShiftList :mode="mode" />
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="mode === 'approval' || $q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||||
push
|
square
|
||||||
rounded
|
|
||||||
:disable="timesheet_store.is_loading"
|
:disable="timesheet_store.is_loading"
|
||||||
|
size="lg"
|
||||||
color="accent"
|
color="accent"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
:label="$t('shared.label.save')"
|
:label="$t('shared.label.save')"
|
||||||
class="col-auto"
|
class="col-auto absolute-bottom shadow-up-10 z-top"
|
||||||
:class="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'full-width q-mt-sm' : 'q-mr-md'"
|
style="height: 50px;"
|
||||||
@click="shift_api.saveShiftChanges"
|
@click="shift_api.saveShiftChanges"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExpenseDialog :is-approved="is_timesheets_approved" />
|
<ExpenseDialog
|
||||||
|
:is-approved="is_timesheets_approved"
|
||||||
|
class="z-top"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { date, patterns, type ValidationRule } from "quasar";
|
import { date } from "quasar";
|
||||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||||
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
|
|
@ -26,16 +26,6 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useShiftRules = (time_required_error: string, overlap_error_string: string, day_shifts: Shift[]) => {
|
|
||||||
const isTimeRequiredRule: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
|
||||||
const isShiftOverlapRule: ValidationRule<string> = (_time_string: string) => !isShiftOverlap(day_shifts) || overlap_error_string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isTimeRequiredRule,
|
|
||||||
isShiftOverlapRule
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SHIFT_OPTIONS: ShiftOption[] = [
|
export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
||||||
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,43 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh lpR fFf">
|
<q-layout view="hHh lpR fFf">
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<q-page padding class="column justify-center items-center bg-secondary">
|
<q-page class="row justify-center bg-secondary">
|
||||||
<q-card class="col-shrink rounded-20">
|
<div class=" column justify-center" :class="$q.platform.is.mobile ? 'col-11' : 'col-8'">
|
||||||
<q-img src="src/assets/line-truck-1.jpg" height="20vh">
|
<div class="column rounded-20 q-pa-xs bg-accent" :class="$q.platform.is.mobile ? 'col-5' : 'col-4'">
|
||||||
<div
|
<div class="col">
|
||||||
class="absolute-bottom text-h4 text-center text-weight-bolder justify-center items-center row">
|
<q-img src="src/assets/line-truck-1.jpg" fit="cover" class="relative-position fit border-radius-inherit">
|
||||||
<div class="q-pr-md text-primary text-h3 text-weight-bolder">404</div>
|
<div class="absolute-bottom text-center column flex-center">
|
||||||
PAGE NOT FOUND
|
<div class="q-pr-md text-white text-h2 text-weight-bolder">404</div>
|
||||||
|
<div class="q-pr-md text-white text-h5 text-weight-bold">{{
|
||||||
|
$t('error.not_found_header')
|
||||||
|
}}</div>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
</div>
|
</div>
|
||||||
</q-img>
|
|
||||||
<q-card-section class="text-center text-h5 text-primary">
|
<div class="col-auto text-center text-h6 text-weight-light bg-dark q-pa-md">
|
||||||
{{ $t('notFoundPage.pageText') }}
|
<div>{{ $t('error.not_found_description') }}</div>
|
||||||
</q-card-section>
|
</div>
|
||||||
</q-card>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto row self-center q-py-md">
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
color="accent"
|
||||||
|
:label="$t('error.go_back')"
|
||||||
|
class="col-auto q-px-md q-py-xs"
|
||||||
|
@click="router.go(-2)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ import { onMounted } from 'vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-page class="bg-secondary row items-center justify-center">
|
<q-page class="bg-secondary row items-center justify-center fit">
|
||||||
<MenuEmployee
|
<MenuEmployee
|
||||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||||
class="col-xs-12 col-md-10 col-lg-7 col-xl-5"
|
class="col-xs-12 col-md-10 col-lg-7 col-xl-5"
|
||||||
|
:class="$q.platform.is.mobile ? 'self-stretch' : ''"
|
||||||
/>
|
/>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
/* eslint-disable */
|
setup
|
||||||
import { onMounted } from 'vue';
|
lang="ts"
|
||||||
import { date } from 'quasar';
|
>
|
||||||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
/* eslint-disable */
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
||||||
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
|
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
|
||||||
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
|
import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
|
||||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
|
||||||
import OverviewReport from 'src/modules/timesheet-approval/components/overview-report.vue';
|
|
||||||
|
|
||||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
import { date } from 'quasar';
|
||||||
const timesheet_store = useTimesheetStore();
|
import { onMounted } from 'vue';
|
||||||
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
onMounted(async () => {
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
const timesheet_store = useTimesheetStore();
|
||||||
});
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -37,50 +39,8 @@ onMounted(async () => {
|
||||||
:timesheets="timesheet_store.timesheets"
|
:timesheets="timesheet_store.timesheets"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<OverviewReport />
|
||||||
class="col-auto full-width q-px-lg"
|
|
||||||
:class="($q.screen.lt.md ? 'column flex-center' : 'row') + (timesheet_store.is_approval_grid_mode ? ' q-mb-sm' : ' q-mb-md')"
|
|
||||||
>
|
|
||||||
<PayPeriodNavigator
|
|
||||||
@date-selected="timesheet_approval_api.getTimesheetOverviews"
|
|
||||||
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
|
|
||||||
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
|
|
||||||
:class="$q.screen.lt.md ? 'q-mb-sm' : ''"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-space />
|
|
||||||
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="timesheet_store.is_approval_grid_mode"
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
color="white"
|
|
||||||
text-color="accent"
|
|
||||||
toggle-color="accent"
|
|
||||||
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-md'"
|
|
||||||
:options="[
|
|
||||||
{ icon: 'grid_view', value: true },
|
|
||||||
{ icon: 'view_list', value: false },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="col-auto row no-wrap flex-center"
|
|
||||||
:class="$q.screen.lt.md ? 'q-mb-md' : ''"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
rounded
|
|
||||||
icon="download"
|
|
||||||
color="accent"
|
|
||||||
:label="$q.screen.lt.md ? '' : $t('shared.label.download')"
|
|
||||||
class="col-auto q-mr-sm"
|
|
||||||
@click="timesheet_store.is_report_dialog_open = true"
|
|
||||||
/>
|
|
||||||
<QTableFilters v-model:search="timesheet_store.search_filter" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<OverviewReport/>
|
|
||||||
<OverviewList class="col" />
|
<OverviewList class="col" />
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page
|
<q-page
|
||||||
padding
|
padding
|
||||||
class="column q-pa-md bg-secondary items-center"
|
class="column bg-secondary items-center"
|
||||||
>
|
>
|
||||||
<PageHeaderTemplate
|
<PageHeaderTemplate
|
||||||
:title="'timesheet.page_header'"
|
:title="'timesheet.page_header'"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { createMemoryHistory, createRouter, createWebHashHistory, createWebHisto
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
|
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If not building with SSR mode, you can
|
* If not building with SSR mode, you can
|
||||||
|
|
@ -28,14 +29,19 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||||
});
|
});
|
||||||
|
|
||||||
Router.beforeEach(async (destinationPage) => {
|
Router.beforeEach(async (destination_page) => {
|
||||||
const authStore = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||||
|
|
||||||
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
|
if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
|
||||||
console.error('no user account found');
|
console.error('no user account found');
|
||||||
return { name: 'login' };
|
return { name: 'login' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (destination_page.meta.required_module && auth_store.user) {
|
||||||
|
if (!auth_store.user.user_module_access.includes(destination_page.meta.required_module as UserModuleAccess))
|
||||||
|
return {name: 'error'};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Router;
|
return Router;
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,8 @@ export enum RouteNames {
|
||||||
EMPLOYEE_LIST = 'employee_list',
|
EMPLOYEE_LIST = 'employee_list',
|
||||||
EMPLOYEE_MANAGEMENT = 'employee_management',
|
EMPLOYEE_MANAGEMENT = 'employee_management',
|
||||||
PROFILE = 'personal_profile',
|
PROFILE = 'personal_profile',
|
||||||
TIMESHEET = 'timesheets'
|
TIMESHEET = 'timesheets',
|
||||||
|
HELP = 'help',
|
||||||
|
|
||||||
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
|
|
@ -1,36 +1,42 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import { RouteNames } from './router-constants';
|
import { RouteNames } from './router-constants';
|
||||||
|
import { ModuleNames } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('src/layouts/main-layout.vue'),
|
component: () => import('src/layouts/main-layout.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requires_auth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: RouteNames.DASHBOARD,
|
name: RouteNames.DASHBOARD,
|
||||||
component: () => import('src/pages/dashboard-page.vue'),
|
component: () => import('src/pages/dashboard-page.vue'),
|
||||||
|
meta: { required_module: ModuleNames.DASHBOARD }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet-approvals',
|
path: 'timesheet-approvals',
|
||||||
name: RouteNames.TIMESHEET_APPROVALS,
|
name: RouteNames.TIMESHEET_APPROVALS,
|
||||||
component: () => import('src/pages/timesheet-approval-page.vue'),
|
component: () => import('src/pages/timesheet-approval-page.vue'),
|
||||||
|
meta: { required_module: ModuleNames.TIMESHEETS_APPROVAL }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'employees',
|
path: 'employees',
|
||||||
name: RouteNames.EMPLOYEE_LIST,
|
name: RouteNames.EMPLOYEE_LIST,
|
||||||
component: () => import('src/pages/employee-list-page.vue'),
|
component: () => import('src/pages/employee-list-page.vue'),
|
||||||
|
meta: { required_module: ModuleNames.EMPLOYEE_LIST }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet',
|
path: 'timesheet',
|
||||||
name: RouteNames.TIMESHEET,
|
name: RouteNames.TIMESHEET,
|
||||||
component: () => import('src/pages/timesheet-page.vue')
|
component: () => import('src/pages/timesheet-page.vue'),
|
||||||
|
meta: { required_module: ModuleNames.TIMESHEETS },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/profile',
|
path: 'user/profile',
|
||||||
name: RouteNames.PROFILE,
|
name: RouteNames.PROFILE,
|
||||||
component: () => import('src/pages/profile-page.vue'),
|
component: () => import('src/pages/profile-page.vue'),
|
||||||
|
meta: { required_module: ModuleNames.PERSONAL_PROFILE },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -39,22 +45,23 @@ const routes: RouteRecordRaw[] = [
|
||||||
path: '/v1/login',
|
path: '/v1/login',
|
||||||
name: RouteNames.LOGIN,
|
name: RouteNames.LOGIN,
|
||||||
component: () => import('src/pages/login-page.vue'),
|
component: () => import('src/pages/login-page.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requires_auth: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/login-success',
|
path: '/login-success',
|
||||||
name: RouteNames.LOGIN_SUCCESS,
|
name: RouteNames.LOGIN_SUCCESS,
|
||||||
component: () => import('src/modules/auth/pages/auth-login-popup-success.vue'),
|
component: () => import('src/modules/auth/pages/auth-login-popup-success.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requires_auth: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)*',
|
path: '/:catchAll(.*)*',
|
||||||
|
name: RouteNames.ERROR,
|
||||||
component: () => import('src/pages/error-page.vue'),
|
component: () => import('src/pages/error-page.vue'),
|
||||||
meta: { requiresAuth: false },
|
meta: { requires_auth: false },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { computed, ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { Notify } from "quasar";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { AuthService } from "../modules/auth/services/services-auth";
|
import { AuthService } from "../modules/auth/services/services-auth";
|
||||||
import { CAN_APPROVE_PAY_PERIODS, type User } from "src/modules/shared/models/user.models";
|
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { Notify } from "quasar";
|
import type { User } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref<User>();
|
const user = ref<User>();
|
||||||
const authError = ref("");
|
const authError = ref("");
|
||||||
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value?.role ?? 'GUEST'));
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
|
|
@ -62,6 +61,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
return { status: 400, message: 'unknown error occured' };
|
return { status: 400, message: 'unknown error occured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, getProfile };
|
return {
|
||||||
|
user,
|
||||||
|
authError,
|
||||||
|
login,
|
||||||
|
oidcLogin,
|
||||||
|
logout,
|
||||||
|
getProfile
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
const is_details_dialog_open = ref(false);
|
const is_details_dialog_open = ref(false);
|
||||||
const selected_employee_name = ref<string>();
|
const selected_employee_name = ref<string>();
|
||||||
const current_pay_period_overview = ref<TimesheetOverview>();
|
const current_pay_period_overview = ref<TimesheetOverview>();
|
||||||
const search_filter = ref<string | number | null>('');
|
|
||||||
const is_approval_grid_mode = ref<boolean>(true);
|
const is_approval_grid_mode = ref<boolean>(true);
|
||||||
const pay_period_report = ref();
|
const pay_period_report = ref();
|
||||||
|
|
||||||
|
|
@ -86,8 +85,8 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
|
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string): Promise<boolean> => {
|
||||||
if (pay_period.value === undefined) return;
|
if (pay_period.value === undefined) return false;
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
|
|
@ -107,15 +106,38 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
timesheets.value = [];
|
timesheets.value = [];
|
||||||
initial_timesheets.value = [];
|
initial_timesheets.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
|
return response.success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
// TODO: More in-depth error-handling here
|
// TODO: More in-depth error-handling here
|
||||||
timesheets.value = [];
|
timesheets.value = [];
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
|
||||||
|
|
||||||
|
// Backend returns the amount of shifts and expenses successfully updated, could be useful for error handling???
|
||||||
|
// const shift_expense_count = timesheets.value.reduce((timesheets_sum, timesheet) => {
|
||||||
|
// return timesheets_sum + timesheet.days.reduce((day_sum, day) => {
|
||||||
|
// return day_sum + day.shifts.length + day.expenses.length
|
||||||
|
// }, 0);
|
||||||
|
// }, 0);
|
||||||
|
|
||||||
|
const response = await timesheetApprovalService.updateTimesheetsApprovalStatus(email, timesheet_ids, approval_status);
|
||||||
|
return response.success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("couldn't approve timesheets for employee: ", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
|
||||||
try {
|
try {
|
||||||
if (!pay_period.value) return false;
|
if (!pay_period.value) return false;
|
||||||
|
|
@ -147,7 +169,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
is_report_dialog_open,
|
is_report_dialog_open,
|
||||||
is_approval_grid_mode,
|
is_approval_grid_mode,
|
||||||
is_details_dialog_open,
|
is_details_dialog_open,
|
||||||
search_filter,
|
|
||||||
pay_period,
|
pay_period,
|
||||||
pay_period_overviews,
|
pay_period_overviews,
|
||||||
current_pay_period_overview,
|
current_pay_period_overview,
|
||||||
|
|
@ -160,6 +181,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
getPayPeriodByDateOrYearAndNumber,
|
getPayPeriodByDateOrYearAndNumber,
|
||||||
getTimesheetOverviews,
|
getTimesheetOverviews,
|
||||||
getTimesheetsByOptionalEmployeeEmail,
|
getTimesheetsByOptionalEmployeeEmail,
|
||||||
|
toggleTimesheetsApprovalByEmployeeEmail,
|
||||||
getPayPeriodReport,
|
getPayPeriodReport,
|
||||||
openReportDialog,
|
openReportDialog,
|
||||||
closeReportDialog,
|
closeReportDialog,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { Notify, LocalStorage, useQuasar, Dark } from 'quasar';
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { LocalStorage, useQuasar, Dark } from 'quasar';
|
||||||
import { Preferences } from 'src/modules/profile/models/preferences.models';
|
import { Preferences } from 'src/modules/profile/models/preferences.models';
|
||||||
import { ProfileService } from 'src/modules/profile/services/profile-service';
|
import { ProfileService } from 'src/modules/profile/services/profile-service';
|
||||||
import { useI18n, type ComposerTranslation } from 'vue-i18n';
|
|
||||||
|
|
||||||
|
|
||||||
export const useUiStore = defineStore('ui', () => {
|
export const useUiStore = defineStore('ui', () => {
|
||||||
|
|
@ -44,7 +44,7 @@ export const useUiStore = defineStore('ui', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUserPreferences = async (t: ComposerTranslation) => {
|
const updateUserPreferences = async () => {
|
||||||
try {
|
try {
|
||||||
if (user_preferences.value.id === -1) return;
|
if (user_preferences.value.id === -1) return;
|
||||||
|
|
||||||
|
|
@ -53,13 +53,11 @@ export const useUiStore = defineStore('ui', () => {
|
||||||
Object.assign(user_preferences.value, response.data);
|
Object.assign(user_preferences.value, response.data);
|
||||||
LocalStorage.setItem('user_preferences', response.data);
|
LocalStorage.setItem('user_preferences', response.data);
|
||||||
setPreferences();
|
setPreferences();
|
||||||
Notify.create({ message: t('profile.preferences.update_successful'), color: 'accent' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Could not update user preferences: ', error);
|
console.error('Could not update user preferences: ', error);
|
||||||
}
|
}
|
||||||
Notify.create({ message: t('profile.preferences.update_failed'), color: 'negative' })
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPreferences = () => {
|
const setPreferences = () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user