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:
parent
505fdf0e62
commit
b09057a6be
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal file
74
src/modules/timesheets/components/unsaved-changes-dialog.vue
Normal 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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user