From ec0ea14a918e8f85cc9aab5795e37b4ca3907aa8 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Mon, 22 Dec 2025 15:51:11 -0500 Subject: [PATCH] feat(timesheet-approval): add toggle of approval for timesheets through overview card --- src/css/app.scss | 8 ---- .../details-dialog-chart-shift-types.vue | 10 ++-- .../components/details-dialog.vue | 34 +++++++------- .../components/overview-list-item.vue | 46 ++++++++++++++----- .../components/overview-list.vue | 7 ++- .../composables/use-timesheet-approval-api.ts | 15 ++++++ .../services/timesheet-approval-service.ts | 6 +++ .../timesheets/components/shift-list.vue | 44 +++++++++++++----- .../components/timesheet-wrapper.vue | 16 +++++-- src/stores/timesheet-store.ts | 30 ++++++++++-- 10 files changed, 153 insertions(+), 63 deletions(-) diff --git a/src/css/app.scss b/src/css/app.scss index d16f13e..ab0be1e 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -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 { diff --git a/src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue b/src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue index 91da8cf..74d3d65 100644 --- a/src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue +++ b/src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue @@ -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([ - 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[]>([]); diff --git a/src/modules/timesheet-approval/components/details-dialog.vue b/src/modules/timesheet-approval/components/details-dialog.vue index abfb4e4..5dadf21 100644 --- a/src/modules/timesheet-approval/components/details-dialog.vue +++ b/src/modules/timesheet-approval/components/details-dialog.vue @@ -25,36 +25,34 @@ @show="is_dialog_open = true" @hide="is_dialog_open = false" > - - - +
{{ timesheet_store.selected_employee_name }} - +
- - + - + - +
- +
- - - - -
+
+ +
+ \ No newline at end of file diff --git a/src/modules/timesheet-approval/components/overview-list-item.vue b/src/modules/timesheet-approval/components/overview-list-item.vue index 4d7abaf..55599e5 100644 --- a/src/modules/timesheet-approval/components/overview-list-item.vue +++ b/src/modules/timesheet-approval/components/overview-list-item.vue @@ -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 : ''}` + } diff --git a/src/modules/timesheet-approval/composables/use-timesheet-approval-api.ts b/src/modules/timesheet-approval/composables/use-timesheet-approval-api.ts index ffb74a9..c623cc6 100644 --- a/src/modules/timesheet-approval/composables/use-timesheet-approval-api.ts +++ b/src/modules/timesheet-approval/composables/use-timesheet-approval-api.ts @@ -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, } diff --git a/src/modules/timesheet-approval/services/timesheet-approval-service.ts b/src/modules/timesheet-approval/services/timesheet-approval-service.ts index a72c561..383dde2 100644 --- a/src/modules/timesheet-approval/services/timesheet-approval-service.ts +++ b/src/modules/timesheet-approval/services/timesheet-approval-service.ts @@ -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> => { + const response = await api.patch>('pay-periods/pay-period-approval', { email, timesheet_ids, is_approved}); + return response.data; + }, }; \ No newline at end of file diff --git a/src/modules/timesheets/components/shift-list.vue b/src/modules/timesheets/components/shift-list.vue index 382f808..ca95b19 100644 --- a/src/modules/timesheets/components/shift-list.vue +++ b/src/modules/timesheets/components/shift-list.vue @@ -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;" >
@@ -192,13 +194,11 @@
+
@@ -247,6 +249,7 @@
- \ No newline at end of file + + + \ No newline at end of file diff --git a/src/modules/timesheets/components/timesheet-wrapper.vue b/src/modules/timesheets/components/timesheet-wrapper.vue index bf0ed13..2bc0ec8 100644 --- a/src/modules/timesheets/components/timesheet-wrapper.vue +++ b/src/modules/timesheets/components/timesheet-wrapper.vue @@ -42,8 +42,8 @@
+ + {{ $t('timesheet.page_header') }} + @@ -81,7 +84,7 @@ - + - +
\ No newline at end of file diff --git a/src/stores/timesheet-store.ts b/src/stores/timesheet-store.ts index c93081e..2ac9f4e 100644 --- a/src/stores/timesheet-store.ts +++ b/src/stores/timesheet-store.ts @@ -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 => { + 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 => { + 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,