feat(approvals): add more functionalities to details window, see notes
Add ability to approve whole time from details dialog directly, in a sticky bar at top with employee name Add possibility of right-clicking on individual shifts to approve or unapprove
This commit is contained in:
parent
6dc1804918
commit
db6ec4bc90
|
|
@ -45,7 +45,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="chatbot_store.is_showing_chatbot"
|
v-if="chatbot_store.is_showing_chatbot"
|
||||||
class="col column"
|
class="col column"
|
||||||
style="background: rgba(0, 0, 0, 0.7); overflow: hidden;"
|
style="background: rgba(0, 0, 0, 0.7); overflow: hidden; pointer-events: auto;"
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
dense
|
dense
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
dark
|
dark
|
||||||
label-color="white"
|
label-color="white"
|
||||||
class="col q-px-md"
|
class="col q-px-md"
|
||||||
style="background: rgba(0, 0, 0, 0.3);"
|
style="background: rgba(0, 0, 0, 0.3); pointer-events: all;"
|
||||||
@keydown.enter="handleSend"
|
@keydown.enter="handleSend"
|
||||||
/>
|
/>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
color="accent"
|
color="accent"
|
||||||
size="2em"
|
size="2em"
|
||||||
class="shadow-5"
|
class="shadow-5"
|
||||||
|
style="pointer-events: auto;"
|
||||||
@click="chatbot_store.is_showing_chatbot = true"
|
@click="chatbot_store.is_showing_chatbot = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,5 +104,6 @@
|
||||||
:deep(.q-drawer) {
|
:deep(.q-drawer) {
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -2,17 +2,39 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable */
|
import { useI18n } from 'vue-i18n';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
||||||
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
||||||
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
|
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||||
const is_dialog_open = ref(false);
|
const is_dialog_open = ref(false);
|
||||||
|
|
||||||
|
const isApproved = computed(() => timesheet_store.timesheets.every(timesheet => timesheet.is_approved));
|
||||||
|
const approveButtonLabel = computed(() => isApproved.value ?
|
||||||
|
t('timesheet_approvals.table.verified') :
|
||||||
|
t('timesheet_approvals.table.unverified')
|
||||||
|
);
|
||||||
|
const approveButtonIcon = computed(() => isApproved.value ? 'lock' : 'lock_open');
|
||||||
|
|
||||||
|
const onClickApproveAll = async () => {
|
||||||
|
const employeeEmail = timesheet_store.current_pay_period_overview?.email;
|
||||||
|
const isApproved = timesheet_store.timesheets.every(timesheet => timesheet.is_approved);
|
||||||
|
|
||||||
|
if (employeeEmail !== undefined && isApproved !== undefined) {
|
||||||
|
await timesheetApprovalApi.toggleTimesheetsApprovalByEmployeeEmail(
|
||||||
|
employeeEmail,
|
||||||
|
!isApproved
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -28,12 +50,34 @@
|
||||||
@before-hide="timesheet_store.getTimesheetOverviews"
|
@before-hide="timesheet_store.getTimesheetOverviews"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pa-sm no-wrap"
|
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pb-sm no-wrap"
|
||||||
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||||
>
|
>
|
||||||
<!-- employee name -->
|
<!-- employee name -->
|
||||||
<div class="col-auto text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
|
<div class="col-auto row flex-center q-px-none q-py-sm sticky-top bg-secondary full-width shadow-4">
|
||||||
<span>{{ timesheet_store.selected_employee_name }}</span>
|
<span class="col text-h4 text-weight-bolder text-uppercase q-px-lg">
|
||||||
|
{{ timesheet_store.selected_employee_name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="col-auto q-px-lg">
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
:label="approveButtonLabel"
|
||||||
|
class="q-px-xl"
|
||||||
|
@click="onClickApproveAll"
|
||||||
|
>
|
||||||
|
<transition enter-active-class="animated swing" mode="out-in">
|
||||||
|
<q-icon
|
||||||
|
:key="isApproved ? '1' : '2'"
|
||||||
|
:name="approveButtonIcon"
|
||||||
|
class="q-pl-md"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- employee pay period details using chart -->
|
<!-- employee pay period details using chart -->
|
||||||
|
|
@ -61,4 +105,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sticky-top {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 5;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column bg-primary text-uppercase q-px-sm text-white">
|
<div class="column bg-primary text-uppercase q-px-sm text-white no-wrap">
|
||||||
<div class="col row">
|
<div class="col row no-wrap">
|
||||||
<q-checkbox
|
<q-checkbox
|
||||||
v-model="filters.is_showing_inactive"
|
v-model="filters.is_showing_inactive"
|
||||||
keep-color
|
keep-color
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="col-auto q-px-md q-pt-sm text-h6 text-bold">
|
<span class="col q-px-md q-pt-sm text-h6 text-bold ellipsis">
|
||||||
{{ $t('timesheet_approvals.table.filter_columns') }}
|
{{ $t('timesheet_approvals.table.filter_columns') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ export const useTimesheetApprovalApi = () => {
|
||||||
const approval_success = await timesheet_store.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
|
const approval_success = await timesheet_store.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
|
||||||
const overview = timesheet_store.pay_period_overviews.find(overview => overview.email === email);
|
const overview = timesheet_store.pay_period_overviews.find(overview => overview.email === email);
|
||||||
|
|
||||||
if (overview && approval_success) overview.is_approved = approval_status;
|
if (overview && approval_success) {
|
||||||
|
overview.is_approved = approval_status;
|
||||||
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timesheet_store.is_loading = false;
|
timesheet_store.is_loading = false;
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, inject, onMounted, ref } from 'vue';
|
||||||
import { QSelect, QInput, useQuasar, type QSelectProps } from 'quasar';
|
import { QSelect, QInput, useQuasar, type QSelectProps, QPopupProxy } from 'quasar';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
|
||||||
// ================== State ==================
|
// ================== State ==================
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
|
|
||||||
const { errorMessage = undefined, isTimesheetApproved = false, holiday = false } = defineProps<{
|
const { errorMessage = undefined, isTimesheetApproved = false, holiday = false } = defineProps<{
|
||||||
dense?: boolean;
|
|
||||||
isTimesheetApproved?: boolean;
|
isTimesheetApproved?: boolean;
|
||||||
errorMessage?: string | undefined;
|
errorMessage?: string | undefined;
|
||||||
holiday?: boolean | undefined;
|
holiday?: boolean | undefined;
|
||||||
|
|
@ -25,19 +25,30 @@
|
||||||
'onTimeFieldBlur': [void];
|
'onTimeFieldBlur': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
|
||||||
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
|
|
||||||
const q = useQuasar();
|
const q = useQuasar();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const mode = inject<'normal' | 'approval'>('mode');
|
||||||
|
|
||||||
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shiftTypeSelected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
const selectRef = ref<QSelect | null>(null);
|
const selectRef = ref<QSelect | null>(null);
|
||||||
const shiftErrorMessage = ref<string | undefined>();
|
const shiftErrorMessage = ref<string | undefined>();
|
||||||
const is_showing_delete_confirm = ref(false);
|
const is_showing_delete_confirm = ref(false);
|
||||||
|
const popupProxyRef = ref<QPopupProxy | null>(null);
|
||||||
|
|
||||||
// ================== Computed ==================
|
// ================== Computed ==================
|
||||||
|
|
||||||
|
const rightClickMenuIcon = computed(() => shift.value.is_approved ? 'lock_open' : 'lock');
|
||||||
|
|
||||||
|
const rightClickMenuLabel = computed(() => shift.value.is_approved ?
|
||||||
|
t('timesheet_approvals.tooltip.unapprove') :
|
||||||
|
t('timesheet_approvals.tooltip.approve'));
|
||||||
|
|
||||||
const timeInputProps = computed(() => ({
|
const timeInputProps = computed(() => ({
|
||||||
dense: true,
|
dense: true,
|
||||||
borderless: shift.value.is_approved && isTimesheetApproved,
|
borderless: shift.value.is_approved && isTimesheetApproved,
|
||||||
|
|
@ -109,6 +120,14 @@
|
||||||
is_showing_delete_confirm.value = state;
|
is_showing_delete_confirm.value = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onRightClickApprove = () => {
|
||||||
|
if (authStore.user?.user_module_access.includes('timesheets_approval'))
|
||||||
|
shift.value.is_approved = !shift.value.is_approved;
|
||||||
|
|
||||||
|
if (popupProxyRef.value)
|
||||||
|
popupProxyRef.value.hide();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (ui_store.focus_next_component) {
|
if (ui_store.focus_next_component) {
|
||||||
selectRef.value?.focus();
|
selectRef.value?.focus();
|
||||||
|
|
@ -121,6 +140,31 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<!-- right-click to approve shift only (if in approval mode) -->
|
||||||
|
<q-popup-proxy
|
||||||
|
v-if="mode === 'approval'"
|
||||||
|
ref="popupProxyRef"
|
||||||
|
context-menu
|
||||||
|
class="rounded-5 q-px-md shadow-24 cursor-pointer"
|
||||||
|
style="border: 3px solid var(--q-primary);"
|
||||||
|
>
|
||||||
|
<q-banner
|
||||||
|
dense
|
||||||
|
class="cursor-pointer q-px-lg"
|
||||||
|
@click="onRightClickApprove"
|
||||||
|
>
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon
|
||||||
|
:name="rightClickMenuIcon"
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase">
|
||||||
|
{{ rightClickMenuLabel }}
|
||||||
|
</span>
|
||||||
|
</q-banner>
|
||||||
|
</q-popup-proxy>
|
||||||
|
|
||||||
<!-- delete shift confirmation dialog -->
|
<!-- delete shift confirmation dialog -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="is_showing_delete_confirm"
|
v-model="is_showing_delete_confirm"
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@
|
||||||
shift.id = 0;
|
shift.id = 0;
|
||||||
emit('deleteUnsavedShift');
|
emit('deleteUnsavedShift');
|
||||||
} else {
|
} else {
|
||||||
console.log('email: ', employeeEmail);
|
|
||||||
await shift_api.deleteShiftById(shift.id, employeeEmail);
|
await shift_api.deleteShiftById(shift.id, employeeEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,9 @@
|
||||||
));
|
));
|
||||||
|
|
||||||
// =================== methods ==========================
|
// =================== methods ==========================
|
||||||
|
|
||||||
provide('employeeEmail', employeeEmail);
|
provide('employeeEmail', employeeEmail);
|
||||||
|
provide('mode', mode);
|
||||||
|
|
||||||
const onClickSaveTimesheets = async () => {
|
const onClickSaveTimesheets = async () => {
|
||||||
if (mode === 'normal') {
|
if (mode === 'normal') {
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
|
||||||
pay_period_overviews.value = [];
|
pay_period_overviews.value = [];
|
||||||
// TODO: More in-depth error-handling here
|
|
||||||
is_loading.value = false;
|
is_loading.value = false;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -107,7 +106,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
|
|
||||||
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string): Promise<boolean> => {
|
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string): Promise<boolean> => {
|
||||||
if (pay_period.value === undefined) return false;
|
if (pay_period.value === undefined) return false;
|
||||||
is_loading.value = true;
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -128,13 +126,11 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
initial_timesheets.value = [];
|
initial_timesheets.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
is_loading.value = false;
|
|
||||||
return response.success;
|
return response.success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
console.error('There was an error retrieving timesheet details for this employee: ', error);
|
||||||
// TODO: More in-depth error-handling here
|
|
||||||
timesheets.value = [];
|
timesheets.value = [];
|
||||||
is_loading.value = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user