-
-
-
-
-
+
shift.type = option.value"
>
-
+
+
+
+
+ {{ $t(scope.opt.label) }}
+
+
+
+
+
+
+
+ {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
+ $t('timesheet.shift.types.OFFICE') }}
+
+
+
+
+
+ {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
+ $t('timesheet.shift.types.OFFICE') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('shared.misc.in') }}
+
+
+
+
+
+
+ {{ $t('shared.misc.out') }}
+
+
+
+
+
+
+
+
@@ -148,248 +285,23 @@
-
-
shift.type = option.value"
+ :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')"
>
-
-
-
-
- {{ $t(scope.opt.label) }}
-
-
-
-
-
-
-
- {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
- $t('timesheet.shift.types.OFFICE') }}
-
-
-
-
-
- {{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
- $t('timesheet.shift.types.OFFICE') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('shared.misc.in') }}
-
-
-
-
-
-
- {{ $t('shared.misc.out') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ 280 - (scope.value?.length ?? 0) }}
-
-
-
-
-
-
-
-
-
+
-
-
-
+
\ No newline at end of file
diff --git a/src/modules/timesheets/components/timesheet-wrapper.vue b/src/modules/timesheets/components/timesheet-wrapper.vue
index ad8cdee..2bc0ec8 100644
--- a/src/modules/timesheets/components/timesheet-wrapper.vue
+++ b/src/modules/timesheets/components/timesheet-wrapper.vue
@@ -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';
@@ -30,7 +31,8 @@
}>();
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'));
});
@@ -38,9 +40,10 @@
+
-
+
+
{{ $t('timesheet.page_header') }}
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/src/modules/timesheets/utils/shift.util.ts b/src/modules/timesheets/utils/shift.util.ts
index 00fb6ef..0dc2c5e 100644
--- a/src/modules/timesheets/utils/shift.util.ts
+++ b/src/modules/timesheets/utils/shift.util.ts
@@ -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 { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
@@ -26,16 +26,6 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
return false;
};
-export const useShiftRules = (time_required_error: string, overlap_error_string: string, day_shifts: Shift[]) => {
- const isTimeRequiredRule: ValidationRule
= (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
- const isShiftOverlapRule: ValidationRule = (_time_string: string) => !isShiftOverlap(day_shifts) || overlap_error_string;
-
- return {
- isTimeRequiredRule,
- isShiftOverlapRule
- };
-};
-
export const SHIFT_OPTIONS: ShiftOption[] = [
{ 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' },
diff --git a/src/pages/error-page.vue b/src/pages/error-page.vue
index 8bf297d..dae929a 100644
--- a/src/pages/error-page.vue
+++ b/src/pages/error-page.vue
@@ -1,19 +1,43 @@
+
+
-
-
-
-
-
404
- PAGE NOT FOUND
+
+
+
+
+
+
+
404
+
{{
+ $t('error.not_found_header')
+ }}
+
+
-
-
- {{ $t('notFoundPage.pageText') }}
-
-
+
+
+
{{ $t('error.not_found_description') }}
+
+
+
+
+
+
+
diff --git a/src/pages/profile-page.vue b/src/pages/profile-page.vue
index e010de1..cfb2309 100644
--- a/src/pages/profile-page.vue
+++ b/src/pages/profile-page.vue
@@ -21,10 +21,11 @@ import { onMounted } from 'vue';
-
+
\ No newline at end of file
diff --git a/src/pages/timesheet-approval-page.vue b/src/pages/timesheet-approval-page.vue
index 2dc7a08..72a6def 100644
--- a/src/pages/timesheet-approval-page.vue
+++ b/src/pages/timesheet-approval-page.vue
@@ -1,22 +1,24 @@
-
@@ -37,50 +39,8 @@ onMounted(async () => {
:timesheets="timesheet_store.timesheets"
/>
-
-
\ No newline at end of file
diff --git a/src/pages/timesheet-page.vue b/src/pages/timesheet-page.vue
index fd440f9..f632dab 100644
--- a/src/pages/timesheet-page.vue
+++ b/src/pages/timesheet-page.vue
@@ -16,7 +16,7 @@
{
- 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;
diff --git a/src/router/router-constants.ts b/src/router/router-constants.ts
index eff5290..3297a47 100644
--- a/src/router/router-constants.ts
+++ b/src/router/router-constants.ts
@@ -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',
}
\ No newline at end of file
diff --git a/src/router/routes.ts b/src/router/routes.ts
index faf1ecb..b217d9e 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -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 },
},
];
diff --git a/src/stores/auth-store.ts b/src/stores/auth-store.ts
index 17b54e7..3eecc6f 100644
--- a/src/stores/auth-store.ts
+++ b/src/stores/auth-store.ts
@@ -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();
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
+ };
});
diff --git a/src/stores/timesheet-store.ts b/src/stores/timesheet-store.ts
index 742b207..2ac9f4e 100644
--- a/src/stores/timesheet-store.ts
+++ b/src/stores/timesheet-store.ts
@@ -23,7 +23,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const is_details_dialog_open = ref(false);
const selected_employee_name = ref();
const current_pay_period_overview = ref();
- const search_filter = ref('');
const is_approval_grid_mode = ref(true);
const pay_period_report = ref();
@@ -86,8 +85,8 @@ export const useTimesheetStore = defineStore('timesheet', () => {
}
};
- const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
- if (pay_period.value === undefined) return;
+ const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string): Promise => {
+ if (pay_period.value === undefined) return false;
is_loading.value = true;
let response;
@@ -97,7 +96,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} else {
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
}
-
+
if (response.success && response.data) {
selected_employee_name.value = response.data.employee_fullname;
timesheets.value = response.data.timesheets;
@@ -107,15 +106,38 @@ export const useTimesheetStore = defineStore('timesheet', () => {
timesheets.value = [];
initial_timesheets.value = [];
}
+
is_loading.value = false;
+ return response.success;
} catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error);
// TODO: More in-depth error-handling here
timesheets.value = [];
is_loading.value = false;
+ return false;
}
};
+ const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean): Promise => {
+ 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) => {
try {
if (!pay_period.value) return false;
@@ -147,7 +169,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
is_report_dialog_open,
is_approval_grid_mode,
is_details_dialog_open,
- search_filter,
pay_period,
pay_period_overviews,
current_pay_period_overview,
@@ -160,6 +181,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
getPayPeriodByDateOrYearAndNumber,
getTimesheetOverviews,
getTimesheetsByOptionalEmployeeEmail,
+ toggleTimesheetsApprovalByEmployeeEmail,
getPayPeriodReport,
openReportDialog,
closeReportDialog,
diff --git a/src/stores/ui-store.ts b/src/stores/ui-store.ts
index 791067e..e4adde1 100644
--- a/src/stores/ui-store.ts
+++ b/src/stores/ui-store.ts
@@ -1,9 +1,9 @@
+import { useI18n } from 'vue-i18n';
import { defineStore } from 'pinia';
-import { Notify, LocalStorage, useQuasar, Dark } from 'quasar';
import { computed, ref } from 'vue';
+import { LocalStorage, useQuasar, Dark } from 'quasar';
import { Preferences } from 'src/modules/profile/models/preferences.models';
import { ProfileService } from 'src/modules/profile/services/profile-service';
-import { useI18n, type ComposerTranslation } from 'vue-i18n';
export const useUiStore = defineStore('ui', () => {
@@ -44,7 +44,7 @@ export const useUiStore = defineStore('ui', () => {
}
};
- const updateUserPreferences = async (t: ComposerTranslation) => {
+ const updateUserPreferences = async () => {
try {
if (user_preferences.value.id === -1) return;
@@ -53,13 +53,11 @@ export const useUiStore = defineStore('ui', () => {
Object.assign(user_preferences.value, response.data);
LocalStorage.setItem('user_preferences', response.data);
setPreferences();
- Notify.create({ message: t('profile.preferences.update_successful'), color: 'accent' });
return;
}
} catch (error) {
console.error('Could not update user preferences: ', error);
}
- Notify.create({ message: t('profile.preferences.update_failed'), color: 'negative' })
};
const setPreferences = () => {