refactor(timesheet): Add warning for unsaved changes when navigating away from timesheet.

refactor(approvals): Move save timesheets button to top bar in details dialog. Force timesheet reload when saving any modifications to timesheets.
This commit is contained in:
Nic D 2026-02-18 11:00:23 -05:00
parent 505fdf0e62
commit b09057a6be
11 changed files with 316 additions and 128 deletions

View File

@ -245,6 +245,8 @@ export default {
day: "day",
empty: "empty",
name: "name",
lock: "",
unlock: "",
},
misc: {
or: "or",
@ -290,6 +292,8 @@ export default {
apply_preset_day: "Apply schedule to day",
apply_preset_week: "Apply schedule to week",
save_successful: "timesheets saved",
unsaved_changes_title: "You have unsaved changes",
unsaved_changes_caption: "Save before leaving?",
nav_button: {
calendar_date_picker: "Calendar",
current_week: "This week",
@ -349,9 +353,10 @@ export default {
type: "Type",
types: {
PER_DIEM: "Per Diem",
EXPENSES: "expense",
EXPENSES: "reimbursement",
MILEAGE: "mileage",
ON_CALL: "on-call allowance",
COMMISSION: "Commission",
},
},
errors: {

View File

@ -245,6 +245,8 @@ export default {
day: "jour",
empty: "vide",
name: "nom",
lock: "verrouiller",
unlock: "déverrouiller",
},
misc: {
or: "ou",
@ -290,6 +292,8 @@ export default {
apply_preset_day: "Appliquer horaire pour la journée",
apply_preset_week: "Appliquer horaire pour la semaine",
save_successful: "feuilles de temps enregistrées",
unsaved_changes_title: "Vous avez des changements non-enregistrés",
unsaved_changes_caption: "Sauvegardez avant de quitter?",
nav_button: {
calendar_date_picker: "Calendrier",
current_week: "Semaine actuelle",
@ -349,9 +353,10 @@ export default {
type: "Type",
types: {
PER_DIEM: "Per diem",
EXPENSES: "dépense",
EXPENSES: "remboursement",
MILEAGE: "kilométrage",
ON_CALL: "Prime de garde",
COMMISSION: "Commission",
},
},
errors: {

View File

@ -11,25 +11,33 @@
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
// ========== state ========================================
const { t } = useI18n();
const timesheet_store = useTimesheetStore();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const is_dialog_open = ref(false);
const shiftApi = useShiftApi();
const isDialogOpen = ref(false);
const isApproved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved));
// ========== computed ========================================
const isApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
const approveButtonLabel = computed(() => isApproved.value ?
t('timesheet_approvals.table.verified') :
t('timesheet_approvals.table.unverified')
t('shared.label.unlock') :
t('shared.label.lock')
);
const approveButtonIcon = computed(() => isApproved.value ? 'lock' : 'lock_open');
const hasExpenses = computed(() => timesheet_store.timesheets.some(timesheet =>
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
);
// ========== methods ========================================
const onClickApproveAll = async () => {
const employeeEmail = timesheet_store.current_pay_period_overview?.email;
const isApproved = timesheet_store.timesheets.every(timesheet => timesheet.is_approved);
const employeeEmail = timesheetStore.current_pay_period_overview?.email;
const isApproved = timesheetStore.timesheets.every(timesheet => timesheet.is_approved);
if (employeeEmail !== undefined && isApproved !== undefined) {
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(
@ -38,19 +46,23 @@
);
}
}
const onClickSaveTimesheets = async () => {
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
}
</script>
<template>
<q-dialog
v-model="timesheet_store.is_details_dialog_open"
v-model="timesheetStore.is_details_dialog_open"
full-width
full-height
transition-show="jump-down"
transition-hide="jump-down"
backdrop-filter="blur(6px)"
@show="is_dialog_open = true"
@hide="is_dialog_open = false"
@before-hide="timesheet_store.getTimesheetOverviews"
@show="isDialogOpen = true"
@hide="isDialogOpen = false"
@before-hide="timesheetStore.getTimesheetOverviews"
>
<div
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pb-sm no-wrap"
@ -58,13 +70,14 @@
>
<!-- employee name -->
<div class="col-auto row flex-center q-px-none q-py-sm sticky-top bg-secondary full-width shadow-4">
<span class="col text-h4 text-weight-bolder text-uppercase q-px-lg">
{{ timesheet_store.selected_employee_name }}
<span class="col-auto text-h4 text-weight-bolder text-uppercase q-px-lg">
{{ timesheetStore.selected_employee_name }}
</span>
<div class="col-auto q-px-lg">
<q-btn
push
:push="isApproved"
:outline="!isApproved"
dense
size="lg"
color="accent"
@ -84,11 +97,27 @@
</transition>
</q-btn>
</div>
<q-space />
<div class="col-auto q-px-md">
<q-btn
push
dense
size="lg"
:disable="timesheetStore.is_loading || !timesheetStore.canSaveTimesheets"
:color="timesheetStore.is_loading || !timesheetStore.canSaveTimesheets ? '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="onClickSaveTimesheets"
/>
</div>
</div>
<!-- employee pay period details using chart -->
<div
v-if="is_dialog_open"
v-if="isDialogOpen"
class="col-auto q-px-md no-wrap"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
@ -108,7 +137,7 @@
<div class="col-auto">
<TimesheetWrapper
mode="approval"
:employee-email="timesheet_store.current_pay_period_overview?.email"
:employee-email="timesheetStore.current_pay_period_overview?.email"
class="col-auto"
/>
</div>

View File

@ -16,6 +16,7 @@
import { type OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
// ========== constants ========================================
@ -46,11 +47,12 @@
const q = useQuasar();
const uiStore = useUiStore();
const auth_store = useAuthStore();
const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi();
const authStore = useAuthStore();
const timesheetApi = useTimesheetApi();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const overview_filters = ref<PayPeriodOverviewFilters>({
const overviewFilters = ref<PayPeriodOverviewFilters>({
is_showing_inactive: false,
is_showing_team_only: true,
supervisors: [],
@ -63,21 +65,21 @@
uiStore.user_preferences.is_timesheet_approval_grid
);
const overview_rows = computed(() =>
timesheet_store.pay_period_overviews.filter(overview => overview)
const overviewRows = computed(() =>
timesheetStore.pay_period_overviews.filter(overview => overview)
);
// ========== methods ========================================
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
timesheet_store.current_pay_period_overview = row;
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
timesheetStore.current_pay_period_overview = row;
await timesheetApi.getTimesheetsByCurrentPayPeriod(row.email);
timesheet_store.is_details_dialog_open = true;
timesheetStore.is_details_dialog_open = true;
};
const onClickApproveAll = async (email: string, is_approved: boolean) => {
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
}
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
@ -88,7 +90,7 @@
}
if (terms.is_showing_team_only) {
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (auth_store.user ? auth_store.user.email : ''));
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (authStore.user ? authStore.user.email : ''));
}
if (terms.name_search_string.length > 0) {
@ -114,20 +116,20 @@
<template>
<div class="full-width">
<LoadingOverlay v-model="timesheet_store.is_loading" />
<LoadingOverlay v-model="timesheetStore.is_loading" />
<q-table
dense
row-key="email"
color="accent"
separator="none"
hide-pagination
:rows="overview_rows"
:rows="overviewRows"
:columns="pay_period_overview_columns"
:table-colspan="pay_period_overview_columns.length"
:visible-columns="VISIBLE_COLUMNS"
:grid="isGridMode"
:pagination="{ sortBy: 'is_active' }"
:filter="overview_filters"
:filter="overviewFilters"
:filter-method="filterEmployeeRows"
:rows-per-page-options="[0]"
class="bg-transparent"
@ -138,20 +140,20 @@
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
table-header-style="min-width: 80xp; max-width: 80px;"
:style="overview_rows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
:style="overviewRows.length > 0 ? `max-height: ${maxHeight}px;` : ''"
:table-style="{ tableLayout: 'fixed' }"
@row-click="(_evt, row: TimesheetApprovalOverview) => onClickedDetails(row)"
>
<template #top>
<OverviewListTopMobile
v-if="$q.platform.is.mobile"
v-model:filters="overview_filters"
v-model:filters="overviewFilters"
v-model:visible-columns="VISIBLE_COLUMNS"
/>
<OverviewListTop
v-else
v-model:filters="overview_filters"
v-model:filters="overviewFilters"
v-model:visible-columns="VISIBLE_COLUMNS"
/>
</template>
@ -192,7 +194,7 @@
mode="out-in"
>
<div
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
:key="props.rowIndex + (timesheetStore.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
:style="`animation-delay: ${props.rowIndex / 15}s; opacity: ${props.row.is_active ? '1' : '0.5'};`"
>
@ -265,7 +267,7 @@
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
<OverviewListItem
v-model="props.row.is_approved"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
:key="props.row.email + timesheetStore.pay_period?.pay_period_no"
:row="props.row"
@click-details="onClickedDetails"
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
@ -275,7 +277,7 @@
<!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }">
<div
v-if="!timesheet_store.is_loading"
v-if="!timesheetStore.is_loading"
class="full-width column items-center text-accent"
>
<q-icon

View File

@ -23,7 +23,8 @@
const bankedHours = computed(() => timesheetStore.paid_time_off_totals.banked_hours);
onMounted(async () => {
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
if (timesheetMode === 'normal')
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
})
</script>

View File

@ -10,14 +10,17 @@
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
import ShiftListWeeklyOverviewMobile from 'src/modules/timesheets/components/mobile/shift-list-weekly-overview-mobile.vue';
import UnsavedChangesDialog from 'src/modules/timesheets/components/unsaved-changes-dialog.vue';
import { date, Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { computed, onMounted, provide } from 'vue';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { RouteNames } from 'src/router/router-constants';
// ================= state ====================
@ -27,18 +30,25 @@
}>();
const { t } = useI18n();
const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi();
const shift_api = useShiftApi();
const router = useRouter();
const expenseStore = useExpensesStore();
const timesheetStore = useTimesheetStore();
const timesheetApi = useTimesheetApi();
const shiftApi = useShiftApi();
// ================== computed ====================
const has_shift_errors = computed(() => timesheet_store.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
const hasShiftErrors = computed(() => timesheetStore.all_current_shifts.filter(shift => shift.has_error === true).length > 0);
const isTimesheetsApproved = computed(() => timesheetStore.timesheets.every(timesheet => timesheet.is_approved));
// const timesheetStore.canSaveTimesheets = computed(() => {
// /* eslint-disable-next-line */
// const currentShifts = timesheetStore.timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
// const initialShifts = timesheetStore.initial_timesheets.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
const is_timesheets_approved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved))
// return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
// });
const total_hours = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
const totalHours = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum += timesheet.weekly_hours.regular
+ timesheet.weekly_hours.evening
+ timesheet.weekly_hours.emergency
@ -46,7 +56,7 @@
0) //initial value
);
const total_expenses = computed(() => timesheet_store.timesheets.reduce((sum, timesheet) =>
const totalExpenses = computed(() => timesheetStore.timesheets.reduce((sum, timesheet) =>
sum + timesheet.weekly_expenses.expenses
+ timesheet.weekly_expenses.on_call
+ timesheet.weekly_expenses.per_diem,
@ -60,25 +70,43 @@
const onClickSaveTimesheets = async () => {
if (mode === 'normal') {
await shift_api.saveShiftChanges();
await shiftApi.saveShiftChanges();
Notify.create({
message: t('timesheet.save_successful'),
color: 'accent',
});
} else {
await shift_api.saveShiftChanges(timesheet_store.current_pay_period_overview?.email);
await shiftApi.saveShiftChanges(timesheetStore.current_pay_period_overview?.email);
}
}
const onClickLeave = async () => {
timesheetStore.isShowingUnsavedWarning = false;
timesheetStore.timesheets = [];
timesheetStore.initial_timesheets = [];
await router.push({ name: timesheetStore.nextPageNameAfterUnsaveWarning ?? RouteNames.DASHBOARD});
}
const onClickSaveBeforeLeaving = async () => {
if (mode === 'approval') return;
timesheetStore.isShowingUnsavedWarning = false;
await onClickSaveTimesheets();
await onClickLeave();
}
onMounted(async () => {
if (mode === 'normal')
await timesheet_api.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
await timesheetApi.getTimesheetsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
});
</script>
<template>
<div class="column items-center full-height" :class="mode === 'normal' ? 'relative-position' : ' no-wrap'">
<LoadingOverlay v-model="timesheet_store.is_loading" />
<div
class="column items-center full-height"
:class="mode === 'normal' ? 'relative-position' : ' no-wrap'"
>
<LoadingOverlay v-model="timesheetStore.is_loading" />
<!-- label for approval mode to delimit that this is the timesheet -->
<div
@ -90,19 +118,6 @@
</span>
<q-space />
<!-- 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="onClickSaveTimesheets"
/>
</div>
@ -116,8 +131,8 @@
<ShiftListWeeklyOverview
mode="total-hours"
:timesheet-mode="mode"
:total-hours="total_hours"
:total-expenses="total_expenses"
:total-hours="totalHours"
:total-expenses="totalExpenses"
/>
</div>
@ -144,9 +159,9 @@
<!-- navigation btn -->
<PayPeriodNavigator
class="col-auto"
@date-selected="timesheet_api.getTimesheetsByDate"
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
@date-selected="timesheetApi.getTimesheetsByDate"
@pressed-previous-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
@pressed-next-button="timesheetApi.getTimesheetsByCurrentPayPeriod"
/>
<!-- mobile expenses button -->
@ -160,7 +175,7 @@
color="accent"
icon="receipt_long"
class="full-width"
@click="expenses_store.open"
@click="expenseStore.open"
/>
</div>
@ -174,16 +189,16 @@
color="accent"
icon="receipt_long"
:label="$t('timesheet.expense.open_btn')"
@click="expenses_store.open"
@click="expenseStore.open"
/>
<!-- desktop save timesheet changes button -->
<q-btn
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
v-if="!isTimesheetsApproved && $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'"
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
:color="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets ? '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'"
@ -211,14 +226,14 @@
:class="$q.platform.is.mobile ? 'fit no-wrap' : 'full-width'"
:style="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'margin-bottom: 40px' : ''"
>
<!-- Show if no timesheets found (further than one month from present) -->
<!-- If no timesheets found -->
<div
v-if="timesheet_store.timesheets.length < 1 && !timesheet_store.is_loading"
v-if="timesheetStore.timesheets.length < 1 && !timesheetStore.is_loading"
class="col-auto column flex-center fit q-py-lg"
style="min-height: 20vh;"
>
<span class="text-uppercase text-weight-bolder text-center">{{ $t('shared.error.no_data_found')
}}</span>
}}</span>
<q-icon
name="las la-calendar"
color="accent"
@ -228,7 +243,7 @@
/>
</div>
<!-- Else show timesheets if found -->
<!-- Else show timesheets -->
<ShiftList
v-else
class="col-auto"
@ -238,7 +253,7 @@
<q-btn
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
square
:disable="timesheet_store.is_loading"
:disable="timesheetStore.is_loading || hasShiftErrors || !timesheetStore.canSaveTimesheets"
size="lg"
color="accent"
icon="upload"
@ -249,8 +264,13 @@
/>
<ExpenseDialog
:is-approved="is_timesheets_approved"
:is-approved="isTimesheetsApproved"
class="z-top"
/>
<UnsavedChangesDialog
@click-save-no="onClickLeave"
@click-save-yes="onClickSaveBeforeLeaving"
/>
</div>
</template>

View File

@ -0,0 +1,74 @@
<script
setup
lang="ts"
>
import { useTimesheetStore } from 'src/stores/timesheet-store';
// ========== state ========================================
const emit = defineEmits<{
clickSaveNo: [];
clickSaveYes: [];
}>();
const timesheetStore = useTimesheetStore();
// ========== methods ========================================
const onClickSaveOptionButton = (option: 'cancel' | 'no' | 'yes') => {
switch(option) {
case 'cancel':
timesheetStore.isShowingUnsavedWarning = false;
break;
case 'no':
emit('clickSaveNo');
break;
case 'yes':
emit('clickSaveYes');
break;
}
}
</script>
<template>
<q-dialog
v-model="timesheetStore.isShowingUnsavedWarning"
backdrop-filter="blur(4px)"
>
<q-card
class="bg-dark shadow-12 flex-center"
style="border: 2px solid var(--q-accent);"
>
<div class="column flex-center q-py-sm q-px-md text-center">
<span class="col-auto text-bold text-uppercase" style="font-size: 1.2em;">
{{ $t('timesheet.unsaved_changes_title') }}
</span>
<span class="">{{ $t('timesheet.unsaved_changes_caption') }}</span>
</div>
<div class="col-auto row full-width">
<q-btn
flat
:label="$t('shared.label.cancel')"
class="col"
@click="onClickSaveOptionButton('cancel')"
/>
<q-btn
flat
color="negative"
:label="$t('shared.misc.no')"
class="col"
@click="onClickSaveOptionButton('no')"
/>
<q-btn
flat
color="accent"
:label="$t('shared.misc.yes')"
class="col"
@click="onClickSaveOptionButton('yes')"
/>
</div>
</q-card>
</q-dialog>
</template>

View File

@ -10,7 +10,9 @@ export const useShiftApi = () => {
const success = await shift_store.deleteShiftById(shift_id, employee_email);
if (success) {
timesheetStore.timesheets = [];
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
}
timesheetStore.is_loading = false;
@ -23,8 +25,9 @@ export const useShiftApi = () => {
const create_success = await shift_store.createNewShifts(employee_email);
if (create_success || update_success){
timesheetStore.timesheets = [];
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employee_email);
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail();
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employee_email);
}
timesheetStore.is_loading = false;

View File

@ -1,65 +1,70 @@
import { useShiftStore } from "src/stores/shift-store";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
export const useTimesheetApi = () => {
const timesheet_store = useTimesheetStore();
const timesheetStore = useTimesheetStore();
const shiftStore = useShiftStore();
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
timesheet_store.timesheets = [];
timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
const getTimesheetsByDate = async (date_string: string, employeeEmail?: string) => {
timesheetStore.timesheets = [];
timesheetStore.is_loading = true;
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber(date_string);
if (success) {
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
timesheet_store.is_loading = false;
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
timesheetStore.is_loading = false;
}
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
}
const getTimesheetsByCurrentPayPeriod = async (employee_email?: string) => {
if (timesheet_store.pay_period === undefined) return false;
const getTimesheetsByCurrentPayPeriod = async (employeeEmail?: string) => {
if (timesheetStore.pay_period === undefined) return false;
timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
timesheetStore.is_loading = true;
const success = await timesheetStore.getPayPeriodByDateOrYearAndNumber();
if (success) {
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
timesheet_store.is_loading = false;
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
timesheetStore.is_loading = false;
}
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
};
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string) => {
if (timesheet_store.timesheets.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
timesheet_store.is_loading = true;
try {
let response;
timesheetStore.is_loading = true;
if (week_day_index && date)
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
else
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
const success = await timesheetStore.applyPreset(timesheet_id, week_day_index, date, employeeEmail);
if (response.success)
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
} catch (error) {
console.error('Error applying weekly timesheet: ', error);
}
timesheet_store.is_loading = false;
if (!success) {
timesheetStore.is_loading = false;
return;
}
const timesheets = JSON.stringify(timesheetStore.timesheets);
const initialTimesheets = JSON.stringify(timesheetStore.initial_timesheets);
if (timesheets !== initialTimesheets) {
await shiftStore.updateShifts(employeeEmail);
await shiftStore.createNewShifts(employeeEmail);
await timesheetStore.getTimesheetsByOptionalEmployeeEmail(employeeEmail);
await timesheetStore.getPaidTimeOffTotalsWithOptionalEmployeeEmail(employeeEmail);
}
timesheetStore.is_loading = false;
}
const getTimesheetsBySwiping = async( direction: number ) => {
timesheet_store.is_loading = true;
const getTimesheetsBySwiping = async (direction: number) => {
timesheetStore.is_loading = true;
timesheet_store.getNextOrPreviousPayPeriod(direction);
await timesheet_store.getPayPeriodByDateOrYearAndNumber();
await timesheet_store.getTimesheetsByOptionalEmployeeEmail();
timesheetStore.getNextOrPreviousPayPeriod(direction);
await timesheetStore.getPayPeriodByDateOrYearAndNumber();
await timesheetStore.getTimesheetsByOptionalEmployeeEmail();
timesheet_store.is_loading = false;
timesheetStore.is_loading = false;
}
return {

View File

@ -5,6 +5,7 @@ import { useAuthStore } from 'src/stores/auth-store';
import { RouteNames } from 'src/router/router-constants';
import { useChatbotStore } from 'src/stores/chatbot-store';
import type { UserModuleAccess } from 'src/modules/shared/models/user.models';
import { useTimesheetStore } from 'src/stores/timesheet-store';
/*
* If not building with SSR mode, you can
@ -30,19 +31,29 @@ export default defineRouter(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE),
});
Router.beforeEach(async (destination_page) => {
Router.beforeEach(async (to, from) => {
const auth_store = useAuthStore();
const result = await auth_store.getProfile() ?? { status: 400, message: 'unknown error occured' };
if (destination_page.meta.requires_auth && !auth_store.user || (result.status >= 400 && destination_page.name !== RouteNames.LOGIN)) {
if (to.meta.requires_auth && !auth_store.user || (result.status >= 400 && to.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))
if (to.meta.required_module && auth_store.user) {
if (!auth_store.user.user_module_access.includes(to.meta.required_module as UserModuleAccess))
return {name: 'error'};
}
if (from.name === RouteNames.TIMESHEET) {
const timesheetStore = useTimesheetStore();
if(timesheetStore.canSaveTimesheets) {
timesheetStore.nextPageNameAfterUnsaveWarning = to.name;
timesheetStore.isShowingUnsavedWarning = true;
return false;
}
}
})
Router.afterEach( (destination_page) => {

View File

@ -12,6 +12,8 @@ import type { PaidTimeOff } from 'src/modules/employee-list/models/employee-prof
import type { PayPeriodEvent } from 'src/modules/timesheet-approval/models/pay-period-event.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
import type { RouteNames } from 'src/router/router-constants';
import type { RouteRecordNameGeneric } from 'vue-router';
export const useTimesheetStore = defineStore('timesheet', () => {
@ -21,7 +23,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const timesheets = ref<Timesheet[]>([]);
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
const initial_timesheets = ref<Timesheet[]>([]);
const canSaveTimesheets = computed(() => {
/* eslint-disable-next-line */
const currentShifts = timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts.map(shift => { const { has_error, ...shft } = shift; return shft; })));
const initialShifts = initial_timesheets.value.flatMap(timesheet => timesheet.days.flatMap(day => day.shifts));
return JSON.stringify(currentShifts) !== JSON.stringify(initialShifts);
});
const paid_time_off_totals = ref<PaidTimeOff>({ sick_hours: 0, vacation_hours: 0, banked_hours: 0 });
const isShowingUnsavedWarning = ref(false);
const nextPageNameAfterUnsaveWarning = ref<RouteNames | RouteRecordNameGeneric>();
const pay_period_overviews = ref<TimesheetApprovalOverview[]>([]);
const pay_period_infos = ref<PayPeriodOverviewResponse>();
@ -121,7 +132,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
has_timesheet_preset.value = response.data.has_preset_schedule;
selected_employee_name.value = response.data.employee_fullname;
timesheets.value = response.data.timesheets;
initial_timesheets.value = unwrapAndClone(timesheets.value);
initial_timesheets.value = unwrapAndClone(response.data.timesheets);
} else {
selected_employee_name.value = '';
timesheets.value = [];
@ -169,6 +180,24 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return false;
};
const applyPreset = async (timesheet_id: number, week_day_index?: number, date?: string, employeeEmail?: string): Promise<boolean> => {
if (timesheets.value.map(timesheet => timesheet.timesheet_id).includes(timesheet_id)) {
try {
let response;
if (week_day_index && date)
response = await timesheetService.applyPresetToDay(timesheet_id, week_day_index, date, employeeEmail);
else
response = await timesheetService.applyPresetToWeek(timesheet_id, employeeEmail);
return response.success ?? false;
} catch (error) {
console.error('Error applying weekly timesheet: ', error);
}
}
return false;
}
const getPayPeriodReport = async (report_filters: TimesheetApprovalCSVReportFilters) => {
try {
if (!pay_period.value) return false;
@ -239,18 +268,22 @@ export const useTimesheetStore = defineStore('timesheet', () => {
current_pay_period_overview,
pay_period_infos,
selected_employee_name,
canSaveTimesheets,
has_timesheet_preset,
timesheets,
all_current_shifts,
initial_timesheets,
federal_holidays,
paid_time_off_totals,
isShowingUnsavedWarning,
nextPageNameAfterUnsaveWarning,
getCurrentFederalHolidays,
getNextOrPreviousPayPeriod,
getPayPeriodByDateOrYearAndNumber,
getTimesheetOverviews,
getTimesheetsByOptionalEmployeeEmail,
toggleTimesheetsApprovalByEmployeeEmail,
applyPreset,
getPayPeriodReport,
openReportDialog,
closeReportDialog,