fix(timesheet): more refactors and fixes to timesheet, mostly error handling, mobile UI/UX adjustments
This commit is contained in:
parent
4231b51c11
commit
b28f8768d2
|
|
@ -3,6 +3,14 @@
|
|||
.rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.rounded-#{$size} > div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.rounded-#{$size} > div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-fb-blue {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
page_header: "account login",
|
||||
email: "e-mail",
|
||||
|
|
@ -244,7 +250,7 @@ export default {
|
|||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||
SHIFT_OVERLAP_SHORT: "Overlap",
|
||||
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_NOT_FOUND: "Shift missing or deleted",
|
||||
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
page_header: "connexion au compte",
|
||||
email: "courriel",
|
||||
|
|
@ -245,7 +251,7 @@ export default {
|
|||
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||
SHIFT_OVERLAP_SHORT: "Chevauchement",
|
||||
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_NOT_FOUND: "Quart de travail manquant ou supprimé",
|
||||
PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { ref } from 'vue';
|
||||
import { RouteNames } from 'src/router/router-constants';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
import { ModuleNames } from 'src/modules/shared/models/user.models';
|
||||
|
||||
const auth_store = useAuthStore();
|
||||
const ui_store = useUiStore();
|
||||
|
|
@ -32,155 +32,109 @@
|
|||
<template>
|
||||
<q-drawer
|
||||
v-model="ui_store.is_left_drawer_open"
|
||||
:persistent="!ui_store.is_mobile_mode"
|
||||
:persistent="!$q.platform.is.mobile"
|
||||
mini-to-overlay
|
||||
elevated
|
||||
side="left"
|
||||
:mini="is_mini"
|
||||
@mouseenter="is_mini = false"
|
||||
@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">
|
||||
<q-list>
|
||||
<!-- Home -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="home"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.home')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@click="goToPageName(RouteNames.DASHBOARD)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
<!-- Timesheet Validation -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS_APPROVAL)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="event_available"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet_approvals')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="event_available"
|
||||
color="accent"
|
||||
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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
<!-- Employee List -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.EMPLOYEE_LIST)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="groups"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.employee_list')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="groups"
|
||||
color="accent"
|
||||
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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="punch_clock"
|
||||
color="accent"
|
||||
<!-- Employee Timesheet -->
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.TIMESHEETS)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="punch_clock"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.timesheet')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@click="goToPageName(RouteNames.TIMESHEET)"
|
||||
/>
|
||||
</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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
side
|
||||
@click="goToPageName(RouteNames.PROFILE)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="account_box"
|
||||
color="accent"
|
||||
<q-btn
|
||||
v-if="auth_store.user?.user_module_access.includes(ModuleNames.PERSONAL_PROFILE)"
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="account_box"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.profile')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="goToPageName('help')"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="contact_support"
|
||||
color="accent"
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="contact_support"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.help')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? '': 'q-px-sm'"
|
||||
@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 -->
|
||||
<q-item
|
||||
v-ripple
|
||||
clickable
|
||||
@click="handleLogout"
|
||||
class="absolute-bottom"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="exit_to_app"
|
||||
color="accent"
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
no-wrap
|
||||
size="lg"
|
||||
icon="exit_to_app"
|
||||
:label="!$q.platform.is.mobile && is_mini ? '' : $t('nav_bar.logout')"
|
||||
class="col-auto text-uppercase text-weight-bold q-my-xs"
|
||||
:class="!$q.platform.is.mobile && is_mini ? 'absolute-bottom': 'absolute-bottom-left'"
|
||||
@click="handleLogout"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
|
@ -5,12 +5,13 @@
|
|||
import { ref } from 'vue';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
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 preset_preview = ref<ModuleAccessPreset>();
|
||||
|
||||
const toggleInSelected = (value: ModuleAccessName) => {
|
||||
const toggleInSelected = (value: UserModuleAccess) => {
|
||||
const i = employee_store.employee.user_module_access.indexOf(value);
|
||||
if (i === -1) employee_store.employee.user_module_access.push(value);
|
||||
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_store.employee.user_module_access.includes(name)) return 'bg-info text-white';
|
||||
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
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';
|
||||
|
||||
return 'bg-dark';
|
||||
|
|
@ -143,7 +144,11 @@
|
|||
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(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">
|
||||
{{ $t('employee_management.module_access.' + option.value) }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 CompanyNames = 'Targo' | 'Solucom';
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ export class EmployeeProfile {
|
|||
residence: string;
|
||||
birth_date: string;
|
||||
is_supervisor: boolean;
|
||||
user_module_access: ModuleAccessName[];
|
||||
user_module_access: UserModuleAccess[];
|
||||
preset_id?: number | null;
|
||||
|
||||
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: 'employee_list', value: 'employee_list' },
|
||||
{ label: 'personal_profile', value: 'personal_profile' },
|
||||
|
|
@ -109,14 +109,14 @@ export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
|
|||
{ 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'],
|
||||
'supervisor' : ['dashboard', 'employee_list', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
||||
'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'],
|
||||
'none' : [],
|
||||
}
|
||||
|
||||
export const getEmployeeAccessOptionIcon = (module: ModuleAccessName): string => {
|
||||
export const getEmployeeAccessOptionIcon = (module: UserModuleAccess): string => {
|
||||
switch (module) {
|
||||
case 'dashboard': return 'home';
|
||||
case 'employee_list' : return 'groups';
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ export interface User {
|
|||
last_name: string;
|
||||
email: string;
|
||||
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[] = [
|
||||
'ADMIN',
|
||||
'SUPERVISOR',
|
||||
'HR',
|
||||
'ACCOUNTING',
|
||||
]
|
||||
export const ModuleNames = {
|
||||
DASHBOARD: 'dashboard',
|
||||
EMPLOYEE_LIST: 'employee_list',
|
||||
EMPLOYEE_MANAGEMENT: 'employee_management',
|
||||
PERSONAL_PROFILE: 'personal_profile',
|
||||
TIMESHEETS: 'timesheets',
|
||||
TIMESHEETS_APPROVAL: 'timesheets_approval',
|
||||
} as const;
|
||||
|
||||
export type UserModuleAccess = typeof ModuleNames[keyof typeof ModuleNames];
|
||||
|
|
@ -101,6 +101,7 @@
|
|||
v-model="is_navigator_open"
|
||||
transition-show="jump-right"
|
||||
transition-hide="jump-right"
|
||||
class="z-top"
|
||||
>
|
||||
<q-date
|
||||
v-model="expenses_store.current_expense.date"
|
||||
|
|
|
|||
|
|
@ -56,15 +56,16 @@
|
|||
>
|
||||
<template #header>
|
||||
<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
|
||||
v-if="expense_store.mode !== 'create'"
|
||||
name="las la-plus-square"
|
||||
name="add_circle_outline"
|
||||
size="md"
|
||||
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>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
v-model="is_navigator_open"
|
||||
transition-show="jump-right"
|
||||
transition-hide="jump-right"
|
||||
class="z-top"
|
||||
>
|
||||
<q-date
|
||||
v-model="expenses_store.current_expense.date"
|
||||
|
|
@ -217,12 +218,12 @@
|
|||
class="col-auto"
|
||||
/>
|
||||
|
||||
<q-dialog v-model="is_showing_comment_dialog_mobile">
|
||||
<q-card class="full-width bg-secondary rounded-10">
|
||||
<q-dialog v-model="is_showing_comment_dialog_mobile" class="z-top">
|
||||
<q-card class="full-width bg-primary rounded-10">
|
||||
<q-card-section class="q-pa-none">
|
||||
<span
|
||||
class="text-weight-bold text-accent text-uppercase text-caption"
|
||||
style="font-size: 1.5em;"
|
||||
class="text-weight-bold text-accent text-uppercase text-caption q-mx-md"
|
||||
style="font-size: 1.2em;"
|
||||
>
|
||||
{{ $t('timesheet.expense.employee_comment') }}
|
||||
</span>
|
||||
|
|
@ -231,14 +232,13 @@
|
|||
<q-card-section class="q-pa-none bg-primary rounded-10">
|
||||
<q-input
|
||||
v-model="expenses_store.current_expense.comment"
|
||||
standout="bg-blue-grey-9"
|
||||
filled
|
||||
dense
|
||||
hide-bottom-space
|
||||
color="primary"
|
||||
color="accent"
|
||||
type="textarea"
|
||||
:maxlength="COMMENT_MAX_LENGTH"
|
||||
lazy-rules="ondemand"
|
||||
:rules="[rules.commentRequired]"
|
||||
class="bg-white no-border"
|
||||
>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@
|
|||
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 { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -37,6 +39,17 @@
|
|||
}
|
||||
};
|
||||
|
||||
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';
|
||||
|
|
@ -50,6 +63,9 @@
|
|||
shift_type_selected.value = undefined;
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
|
||||
if (errorMessage)
|
||||
error_message.value = errorMessage;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -162,6 +178,7 @@
|
|||
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') :
|
||||
|
|
@ -184,6 +201,7 @@
|
|||
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') :
|
||||
|
|
@ -208,13 +226,15 @@
|
|||
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="emit('onTimeFieldBlur')"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
@ -239,13 +259,15 @@
|
|||
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="emit('onTimeFieldBlur')"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -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,52 +2,42 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
/* eslint-disable*/
|
||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput } from 'quasar';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
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';
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
const { t } = useI18n();
|
||||
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 = useTemplateRef<QSelect>('select');
|
||||
const start_time_ref = useTemplateRef<QInput>('start_time');
|
||||
const end_time_ref = useTemplateRef<QInput>('end_time');
|
||||
const select_ref = ref<QSelect | null>(null);
|
||||
const error_message = ref('');
|
||||
|
||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
const { errorMessage = undefined, isTimesheetApproved = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
errorMessage?: string | undefined;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'saveComment': [comment: string, shift_id: number];
|
||||
'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 slideDeleteShift = async (reset: () => void) => {
|
||||
timer = setTimeout(() => {
|
||||
reset();
|
||||
emit('requestDelete');
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const getCommentCounterColor = (comment_length: number) => {
|
||||
if (comment_length < 200) return 'primary';
|
||||
|
|
@ -62,89 +52,18 @@
|
|||
shift_type_selected.value = undefined;
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(timer);
|
||||
if (errorMessage)
|
||||
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>
|
||||
|
||||
<template>
|
||||
<q-slide-item
|
||||
right-color="negative"
|
||||
class="rounded-5 transparent"
|
||||
:class="ui_store.is_mobile_mode ? 'q-my-md' : 'q-mr-xs'"
|
||||
@right="details => slideDeleteShift(details.reset)"
|
||||
>
|
||||
<template
|
||||
#right
|
||||
v-if="ui_store.is_mobile_mode"
|
||||
>
|
||||
<q-icon name="delete" />
|
||||
</template>
|
||||
|
||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
||||
<div
|
||||
class="row items-center text-uppercase rounded-5"
|
||||
:class="ui_store.is_mobile_mode ? 'col q-mb-xs q-px-xs' : 'col-4'"
|
||||
>
|
||||
<!-- mobile 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"
|
||||
>
|
||||
<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 || isTimesheetApproved)"
|
||||
autofocus
|
||||
counter
|
||||
bottom-slots
|
||||
:maxlength="COMMENT_LENGTH_MAX"
|
||||
class="q-pb-lg"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? '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>
|
||||
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
|
|
@ -164,7 +83,6 @@
|
|||
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">
|
||||
|
|
@ -178,7 +96,7 @@
|
|||
:color="scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto"
|
||||
:class="shift.is_approved ? 'q-mx-xs': 'q-mr-xs'"
|
||||
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||
/>
|
||||
<span
|
||||
style="line-height: 1.2em;"
|
||||
|
|
@ -248,13 +166,15 @@
|
|||
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"
|
||||
: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')"
|
||||
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="emit('onTimeFieldBlur')"
|
||||
@blur="onTimeFieldBlur(shift.start_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
@ -277,13 +197,15 @@
|
|||
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"
|
||||
: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' : ''))"
|
||||
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="emit('onTimeFieldBlur')"
|
||||
@blur="onTimeFieldBlur(shift.end_time)"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
|
|
@ -295,7 +217,10 @@
|
|||
</q-input>
|
||||
|
||||
<!-- comment and delete buttons -->
|
||||
<div class="row full-height" :class="ui_store.is_mobile_mode ? 'col-12' : 'col-auto flex-center'">
|
||||
<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"
|
||||
|
|
@ -313,6 +238,7 @@
|
|||
rounded
|
||||
color="negative"
|
||||
/>
|
||||
|
||||
<q-popup-edit
|
||||
v-model="shift.comment"
|
||||
:title="$t('timesheet.shift.fields.header_comment')"
|
||||
|
|
@ -360,7 +286,7 @@
|
|||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="!ui_store.is_mobile_mode && !shift.is_approved"
|
||||
v-if="!shift.is_approved"
|
||||
flat
|
||||
dense
|
||||
:disable="shift.is_approved"
|
||||
|
|
@ -376,14 +302,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-slide-item>
|
||||
|
||||
<q-separator
|
||||
v-if="hasShiftAfter && ui_store.is_mobile_mode"
|
||||
spaced
|
||||
color="accent"
|
||||
class="q-mx-md"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.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 timesheet_api = useTimesheetApi();
|
||||
const shift_error_message = ref<string | undefined>();
|
||||
|
||||
const { day, dense = false, approved = false } = defineProps<{
|
||||
timesheetId: number;
|
||||
|
|
@ -36,6 +38,15 @@
|
|||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -73,24 +84,26 @@
|
|||
<div
|
||||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
class="col"
|
||||
class="col-auto"
|
||||
>
|
||||
<ShiftListDayRowMobile
|
||||
v-if="$q.platform.is.mobile"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
: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"
|
||||
:dense="dense"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
:error-message="shift_error_message"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
@on-time-field-blur="onTimeFieldBlur()"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@
|
|||
<div
|
||||
v-for="timesheet, timesheet_index of timesheet_store.timesheets"
|
||||
:key="timesheet.timesheet_id"
|
||||
class="col fit"
|
||||
class="col column fit flex-center"
|
||||
>
|
||||
<transition
|
||||
appear
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
flat
|
||||
dense
|
||||
: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)"
|
||||
>
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day.date"
|
||||
class="col-auto row q-ma-sm"
|
||||
class="col-auto row q-pa-sm fit"
|
||||
:style="`animation-delay: ${day_index / 15}s;`"
|
||||
>
|
||||
<div
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
class="col column full-width q-px-md q-py-sm"
|
||||
>
|
||||
<q-card
|
||||
class="rounded-5 shadow-12"
|
||||
class="rounded-10 shadow-12"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||
>
|
||||
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions class="q-pa-none">
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-btn
|
||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||
square
|
||||
|
|
@ -183,15 +183,16 @@
|
|||
color="accent"
|
||||
icon="more_time"
|
||||
class="full-width"
|
||||
style="border-radius: 0 0 10px 10px;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="col row full-width"
|
||||
class="col row full-width shadow-8 rounded-10"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.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 { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
|
@ -38,9 +39,10 @@
|
|||
<div class="column items-center full-height">
|
||||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||
|
||||
<!-- top menu -->
|
||||
<div
|
||||
class="col-auto row items-center full-width q-px-lg"
|
||||
: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-mt-md q-pb-sm q-px-md'"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
|
|
@ -90,80 +92,11 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- error message widget for potential backend-provided errors -->
|
||||
<TimesheetErrorWidget class="col-auto" />
|
||||
|
||||
<!-- mobile weekly overview widget -->
|
||||
<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 ? 'accent' : 'blue-grey-4'">
|
||||
<span class="text-weight-bolder">{{ day.shifts.length > 0 ? day.shifts.length : ''
|
||||
}}</span>
|
||||
</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>
|
||||
<ShiftListWeeklyOverview />
|
||||
|
||||
<ShiftList />
|
||||
|
||||
|
|
@ -180,6 +113,6 @@
|
|||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
||||
<ExpenseDialog :is-approved="is_timesheets_approved" />
|
||||
<ExpenseDialog :is-approved="is_timesheets_approved" class="z-top"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,19 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout view="hHh lpR fFf">
|
||||
<q-page-container>
|
||||
<q-page padding class="column justify-center items-center bg-secondary">
|
||||
<q-card class="col-shrink rounded-20">
|
||||
<q-img src="src/assets/line-truck-1.jpg" height="20vh">
|
||||
<div
|
||||
class="absolute-bottom text-h4 text-center text-weight-bolder justify-center items-center row">
|
||||
<div class="q-pr-md text-primary text-h3 text-weight-bolder">404</div>
|
||||
PAGE NOT FOUND
|
||||
<q-page class="row justify-center bg-secondary">
|
||||
<div class=" column justify-center" :class="$q.platform.is.mobile ? 'col-11' : 'col-8'">
|
||||
<div class="column rounded-20 q-pa-xs bg-accent" :class="$q.platform.is.mobile ? 'col-5' : 'col-4'">
|
||||
<div class="col">
|
||||
<q-img src="src/assets/line-truck-1.jpg" fit="cover" class="relative-position fit border-radius-inherit">
|
||||
<div class="absolute-bottom text-center column flex-center">
|
||||
<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>
|
||||
<q-card-section class="text-center text-h5 text-primary">
|
||||
{{ $t('notFoundPage.pageText') }}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-auto text-center text-h6 text-weight-light bg-dark q-pa-md">
|
||||
<div>{{ $t('error.not_found_description') }}</div>
|
||||
</div>
|
||||
</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-container>
|
||||
</q-layout>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createMemoryHistory, createRouter, createWebHashHistory, createWebHisto
|
|||
import routes from './routes';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
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
|
||||
|
|
@ -28,14 +29,19 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
Router.beforeEach(async (destinationPage) => {
|
||||
const authStore = useAuthStore();
|
||||
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
|
||||
Router.beforeEach(async (destination_page) => {
|
||||
const auth_store = useAuthStore();
|
||||
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');
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ export enum RouteNames {
|
|||
EMPLOYEE_LIST = 'employee_list',
|
||||
EMPLOYEE_MANAGEMENT = 'employee_management',
|
||||
PROFILE = 'personal_profile',
|
||||
TIMESHEET = 'timesheets'
|
||||
TIMESHEET = 'timesheets',
|
||||
HELP = 'help',
|
||||
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
|
@ -1,36 +1,42 @@
|
|||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { RouteNames } from './router-constants';
|
||||
import { ModuleNames } from 'src/modules/shared/models/user.models';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('src/layouts/main-layout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
meta: { requires_auth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: RouteNames.DASHBOARD,
|
||||
component: () => import('src/pages/dashboard-page.vue'),
|
||||
meta: { required_module: ModuleNames.DASHBOARD }
|
||||
},
|
||||
{
|
||||
path: 'timesheet-approvals',
|
||||
name: RouteNames.TIMESHEET_APPROVALS,
|
||||
component: () => import('src/pages/timesheet-approval-page.vue'),
|
||||
meta: { required_module: ModuleNames.TIMESHEETS_APPROVAL }
|
||||
},
|
||||
{
|
||||
path: 'employees',
|
||||
name: RouteNames.EMPLOYEE_LIST,
|
||||
component: () => import('src/pages/employee-list-page.vue'),
|
||||
meta: { required_module: ModuleNames.EMPLOYEE_LIST }
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: RouteNames.PROFILE,
|
||||
component: () => import('src/pages/profile-page.vue'),
|
||||
meta: { required_module: ModuleNames.PERSONAL_PROFILE },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -39,22 +45,23 @@ const routes: RouteRecordRaw[] = [
|
|||
path: '/v1/login',
|
||||
name: RouteNames.LOGIN,
|
||||
component: () => import('src/pages/login-page.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
meta: { requires_auth: false },
|
||||
},
|
||||
|
||||
{
|
||||
path: '/login-success',
|
||||
name: RouteNames.LOGIN_SUCCESS,
|
||||
component: () => import('src/modules/auth/pages/auth-login-popup-success.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
meta: { requires_auth: false },
|
||||
},
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
name: RouteNames.ERROR,
|
||||
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 { 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 { Notify } from "quasar";
|
||||
import type { User } from "src/modules/shared/models/user.models";
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User>();
|
||||
const authError = ref("");
|
||||
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value?.role ?? 'GUEST'));
|
||||
const router = useRouter();
|
||||
|
||||
const login = () => {
|
||||
|
|
@ -62,6 +61,13 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
return { status: 400, message: 'unknown error occured' };
|
||||
}
|
||||
|
||||
return { user, authError, isAuthorizedUser, login, oidcLogin, logout, getProfile };
|
||||
return {
|
||||
user,
|
||||
authError,
|
||||
login,
|
||||
oidcLogin,
|
||||
logout,
|
||||
getProfile
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user