feat(timesheet-approval): add toggle of approval for timesheets through overview card
This commit is contained in:
parent
9a70875f78
commit
ec0ea14a91
|
|
@ -3,14 +3,6 @@
|
|||
.rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.rounded-#{$size} > div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.rounded-#{$size} > div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-fb-blue {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
>
|
||||
/* eslint-disable */
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { colors } from 'quasar';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { Doughnut } from 'vue-chartjs';
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
|
||||
ChartJS.defaults.font.family = '"Roboto", sans-serif';
|
||||
|
|
@ -20,10 +22,10 @@
|
|||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const shift_type_labels = ref<string[]>([
|
||||
timesheet_store.current_pay_period_overview!.regular_hours.toString() + 'h',
|
||||
timesheet_store.current_pay_period_overview!.other_hours.evening_hours.toString() + 'h',
|
||||
timesheet_store.current_pay_period_overview!.other_hours.emergency_hours.toString() + 'h',
|
||||
timesheet_store.current_pay_period_overview!.other_hours.overtime_hours.toString() + 'h',
|
||||
t('shared.shift_type.regular'),
|
||||
t('shared.shift_type.evening'),
|
||||
t('shared.shift_type.emergency'),
|
||||
t('shared.shift_type.overtime'),
|
||||
]);
|
||||
|
||||
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]);
|
||||
|
|
|
|||
|
|
@ -25,36 +25,34 @@
|
|||
@show="is_dialog_open = true"
|
||||
@hide="is_dialog_open = false"
|
||||
>
|
||||
<q-card
|
||||
class="shadow-12 rounded-15 bg-secondary hide-scrollbar"
|
||||
<div
|
||||
class="column bg-secondary hide-scrollbar shadow-12 rounded-15 q-pa-sm no-wrap"
|
||||
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||
>
|
||||
|
||||
<!-- employee name -->
|
||||
<q-card-section class="text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
|
||||
<div class="col-auto text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
|
||||
<span>{{ timesheet_store.selected_employee_name }}</span>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<!-- employee pay period details using chart -->
|
||||
<q-card-section
|
||||
v-if="is_dialog_open"
|
||||
:horizontal="!$q.screen.lt.md"
|
||||
class="q-px-md rounded-10 no-wrap"
|
||||
<div
|
||||
v-if="is_dialog_open && !$q.platform.is.mobile"
|
||||
class="col-4 q-px-md no-wrap"
|
||||
:class="$q.platform.is.mobile ? 'column' : 'row'"
|
||||
>
|
||||
<DetailsDialogChartHoursWorked class="col" />
|
||||
<DetailsDialogChartShiftTypes class="col q-ma-lg" />
|
||||
<DetailsDialogChartShiftTypes class="col" />
|
||||
<DetailsDialogChartExpenses class="col" />
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-section>
|
||||
<div class="col-auto">
|
||||
<ExpenseDialogList />
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<!-- list of shifts -->
|
||||
<q-card-section class="col-auto">
|
||||
<TimesheetWrapper mode="approval" />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
</q-card>
|
||||
<div class="col column no-wrap">
|
||||
<TimesheetWrapper mode="approval" class="col"/>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -13,7 +13,20 @@
|
|||
|
||||
const emit = defineEmits<{
|
||||
'clickDetails': [overview: TimesheetOverview];
|
||||
'clickApprovalAll' : [is_approved: boolean];
|
||||
}>();
|
||||
|
||||
const getMinutes = (hours: number) => {
|
||||
const minutes_percent = hours - Math.floor(hours);
|
||||
const minutes = Math.round(minutes_percent * 60);
|
||||
return minutes > 1 ? minutes.toString() : '0';
|
||||
}
|
||||
|
||||
const getHoursMinutesString = (hours: number): string => {
|
||||
const flat_hours = Math.floor(hours);
|
||||
const minutes = Math.round((hours - flat_hours) * 60);
|
||||
return `${flat_hours}h ${minutes > 1 ? minutes : ''}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -60,7 +73,10 @@
|
|||
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
|
||||
</q-tooltip>
|
||||
|
||||
<q-icon name="las la-chart-bar" color="accent"/>
|
||||
<q-icon
|
||||
name="las la-chart-bar"
|
||||
color="accent"
|
||||
/>
|
||||
</q-btn>
|
||||
</q-card-section>
|
||||
|
||||
|
|
@ -79,7 +95,7 @@
|
|||
<span
|
||||
class="text-weight-bolder text-h3 q-py-none"
|
||||
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''"
|
||||
> {{ row.regular_hours }} </span>
|
||||
> {{ getHoursMinutesString(row.regular_hours) }} </span>
|
||||
<q-separator class="q-mr-sm" />
|
||||
</div>
|
||||
|
||||
|
|
@ -94,11 +110,13 @@
|
|||
<span
|
||||
class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
|
||||
style="font-size: 0.7em;"
|
||||
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
|
||||
>
|
||||
{{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }}
|
||||
</span>
|
||||
<span
|
||||
class="text-weight-bolder q-pa-none q-mb-xs"
|
||||
style="font-size: 1.2em; line-height: 1em;"
|
||||
> {{ hour_type }} </span>
|
||||
> {{ getHoursMinutesString(hour_type) }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -139,21 +157,24 @@
|
|||
<q-card-section
|
||||
horizontal
|
||||
class="justify-between items-center text-weight-bold q-pa-none"
|
||||
:class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark text-accent') : 'bg-transparent'"
|
||||
:class="row.is_active ? (row.is_approved ? 'text-white bg-accent' : 'bg-dark') : 'bg-transparent'"
|
||||
>
|
||||
<div
|
||||
v-if="row.is_active"
|
||||
class="col row full-width"
|
||||
>
|
||||
<div class="col">
|
||||
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }}
|
||||
</span>
|
||||
<span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span>
|
||||
<div
|
||||
class="col text-uppercase"
|
||||
:class="row.total_hours > 80 || !row.is_active ? 'text-negative' : ''"
|
||||
>
|
||||
<span class="text-h6 q-ml-sm text-weight-bolder">{{ 'Total : ' + Math.floor(row.total_hours)
|
||||
}}</span>
|
||||
<span class="text-uppercase text-weight-medium text-caption">H</span>
|
||||
<span class="text-h6 q-ml-sm text-weight-bolder">{{ getMinutes(row.total_hours) }}</span>
|
||||
<span class="text-uppercase text-weight-medium text-caption">M</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto q-py-xs q-px-md"
|
||||
>
|
||||
<div class="col-auto q-py-xs q-px-md">
|
||||
<q-checkbox
|
||||
v-model="modelApproval"
|
||||
dense
|
||||
|
|
@ -166,6 +187,7 @@
|
|||
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
|
||||
class="text-uppercase"
|
||||
:class="row.is_approved ? '' : 'text-accent'"
|
||||
@update:model-value="value => $emit('clickApprovalAll', value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@
|
|||
timesheet_store.is_details_dialog_open = true;
|
||||
};
|
||||
|
||||
const onClickApproveAll = async (email: string, is_approved: boolean) => {
|
||||
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
|
||||
}
|
||||
|
||||
const filterEmployeeRows = (rows: readonly TimesheetOverview[], terms: PayPeriodOverviewFilters): TimesheetOverview[] => {
|
||||
let result = [...rows];
|
||||
|
||||
|
|
@ -244,7 +248,8 @@
|
|||
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
|
||||
:index="props.rowIndex"
|
||||
:row="props.row"
|
||||
@click-details="overview => onClickedDetails(overview)"
|
||||
@click-details="onClickedDetails"
|
||||
@click-approval-all="is_approved => onClickApproveAll(props.row.email, is_approved)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,20 @@ export const useTimesheetApprovalApi = () => {
|
|||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
const toggleTimesheetsApprovalByEmployeeEmail = async (email: string, approval_status: boolean) => {
|
||||
timesheet_store.is_loading = true;
|
||||
|
||||
const success = await timesheet_store.getTimesheetsByOptionalEmployeeEmail(email);
|
||||
if (success) {
|
||||
const approval_success = await timesheet_store.toggleTimesheetsApprovalByEmployeeEmail(email, approval_status);
|
||||
const overview = timesheet_store.pay_period_overviews.find(overview => overview.email === email);
|
||||
|
||||
if (overview && approval_success) overview.is_approved = approval_status;
|
||||
}
|
||||
|
||||
timesheet_store.is_loading = false;
|
||||
};
|
||||
|
||||
const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[]) => {
|
||||
if (timesheet_store.pay_period === undefined) return;
|
||||
|
||||
|
|
@ -40,6 +54,7 @@ export const useTimesheetApprovalApi = () => {
|
|||
|
||||
return {
|
||||
getTimesheetOverviewsByDate,
|
||||
toggleTimesheetsApprovalByEmployeeEmail,
|
||||
getTimesheetApprovalCSVReport,
|
||||
getTimesheetOverviews,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
|
||||
|
||||
|
|
@ -12,4 +13,9 @@ export const timesheetApprovalService = {
|
|||
const response = await api.get(`exports/csv/${year}/${period_number}`, { params: filters, responseType: 'arraybuffer' });
|
||||
return response;
|
||||
},
|
||||
|
||||
updateTimesheetsApprovalStatus: async (email: string, timesheet_ids: number[], is_approved: boolean): Promise<BackendResponse<{shifts: number, expenses: number}>> => {
|
||||
const response = await api.patch<BackendResponse<{shifts: number, expenses: number}>>('pay-periods/pay-period-approval', { email, timesheet_ids, is_approved});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_api = useTimesheetApi();
|
||||
|
||||
const { mode = 'normal'} = defineProps<{
|
||||
mode: 'normal' | 'approval';
|
||||
}>();
|
||||
|
||||
const mobile_animation_direction = ref('fadeInLeft');
|
||||
const animation_style = computed(() => ui_store.is_mobile_mode ? mobile_animation_direction.value : 'fadeInDown');
|
||||
|
||||
|
|
@ -67,7 +71,6 @@
|
|||
class="absolute-full hide-scrollbar"
|
||||
:thumb-style="{ opacity: '0' }"
|
||||
:bar-style="{ opacity: '0' }"
|
||||
style="min-height: 50vh;"
|
||||
>
|
||||
<!-- Show if no timesheets found (further than one month from present) -->
|
||||
<div
|
||||
|
|
@ -103,13 +106,12 @@
|
|||
leave-active-class="animated fadeOutUp"
|
||||
>
|
||||
<q-btn
|
||||
v-if="!$q.platform.is.mobile"
|
||||
v-if="!$q.platform.is.mobile && timesheet.days.every(day => day.shifts.length < 1)"
|
||||
:disable="!timesheet.days.every(day => day.shifts.length < 1)"
|
||||
flat
|
||||
dense
|
||||
:label="$t('timesheet.apply_preset_week')"
|
||||
class="col-auto text-uppercase text-weight-bold text-accent q-mx-lg q-py-none rounded-5"
|
||||
:class="timesheet.days.every(day => day.shifts.length < 1) ? '' : 'invisible'"
|
||||
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
||||
>
|
||||
<q-icon
|
||||
|
|
@ -135,7 +137,7 @@
|
|||
class="col column full-width q-px-md q-py-sm"
|
||||
>
|
||||
<q-card
|
||||
class="rounded-10 shadow-12"
|
||||
class="mobile-rounded-10 shadow-12"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||
>
|
||||
|
||||
|
|
@ -192,13 +194,11 @@
|
|||
|
||||
<div
|
||||
v-else
|
||||
class="col row full-width shadow-8 rounded-10"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||
class="col row full-width rounded-10 ellipsis shadow-10"
|
||||
>
|
||||
<div
|
||||
class="col row bg-dark"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
||||
style="border-radius: 10px 0 0 10px;"
|
||||
class="col row"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||
>
|
||||
<!-- Date block -->
|
||||
<ShiftListDateWidget
|
||||
|
|
@ -225,17 +225,19 @@
|
|||
color="white"
|
||||
size="xl"
|
||||
class="full-height"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : ''"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
v-else
|
||||
:dense="!$q.platform.is.mobile"
|
||||
square
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
color="accent"
|
||||
text-color="white"
|
||||
class="full-height"
|
||||
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
||||
style="border-radius: 0 10px 10px 0;"
|
||||
:class="$q.platform.is.mobile ? 'q-px-xs' : ''"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -247,6 +249,7 @@
|
|||
</q-scroll-area>
|
||||
|
||||
<q-page-sticky
|
||||
v-if="mode === 'normal'"
|
||||
position="bottom-right"
|
||||
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
|
||||
class="z-top"
|
||||
|
|
@ -269,4 +272,21 @@
|
|||
</q-page-sticky>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100, 200) {
|
||||
.mobile-rounded-#{$size} {
|
||||
border-radius: #{$size}px !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size} > div:first-child {
|
||||
border-radius: #{$size}px #{$size}px 0 0 !important;
|
||||
}
|
||||
|
||||
.mobile-rounded-#{$size} > div:last-child {
|
||||
border-radius: 0 0 #{$size}px #{$size}px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -42,8 +42,8 @@
|
|||
|
||||
<!-- top menu -->
|
||||
<div
|
||||
class="col-auto row items-center full-width q-px-lg"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-mt-md q-pb-sm q-px-md'"
|
||||
class="col-auto row items-center full-width"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-pb-sm q-px-md'"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
<PayPeriodNavigator
|
||||
|
|
@ -66,6 +66,9 @@
|
|||
@click="expenses_store.open"
|
||||
/>
|
||||
|
||||
<!-- label for approval mode to delimit that this is the timesheet -->
|
||||
<span class="col-auto text-uppercase text-bold text-h5"> {{ $t('timesheet.page_header') }}</span>
|
||||
|
||||
<q-space v-if="$q.screen.width > $q.screen.height" />
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
|
|
@ -81,7 +84,7 @@
|
|||
|
||||
<!-- desktop save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="mode === 'normal' && !is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||
v-if="!is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||
|
|
@ -99,7 +102,7 @@
|
|||
<!-- mobile weekly overview widget -->
|
||||
<ShiftListWeeklyOverview />
|
||||
|
||||
<ShiftList />
|
||||
<ShiftList :mode="mode" />
|
||||
|
||||
<q-btn
|
||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
|
|
@ -114,6 +117,9 @@
|
|||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
||||
<ExpenseDialog :is-approved="is_timesheets_approved" class="z-top"/>
|
||||
<ExpenseDialog
|
||||
:is-approved="is_timesheets_approved"
|
||||
class="z-top"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -85,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<boolean> => {
|
||||
if (pay_period.value === undefined) return false;
|
||||
is_loading.value = true;
|
||||
let response;
|
||||
|
||||
|
|
@ -96,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;
|
||||
|
|
@ -106,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<boolean> => {
|
||||
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;
|
||||
|
|
@ -158,6 +181,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
|||
getPayPeriodByDateOrYearAndNumber,
|
||||
getTimesheetOverviews,
|
||||
getTimesheetsByOptionalEmployeeEmail,
|
||||
toggleTimesheetsApprovalByEmployeeEmail,
|
||||
getPayPeriodReport,
|
||||
openReportDialog,
|
||||
closeReportDialog,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user