fix(details): complete chart implementation for worked hours, stacking bars, proper mobile scaling

This commit is contained in:
Nicolas Drolet 2025-08-29 12:54:50 -04:00
parent 58f6e808d0
commit 24a700d6f6
5 changed files with 86 additions and 58 deletions

View File

@ -24,7 +24,7 @@ $dark: #000;
$dark-page: #323232; $dark-page: #323232;
$positive: #21ba45; $positive: #21ba45;
$negative: #df6674; $negative: #ff576ba9;
$info: #54bbdd; $info: #54bbdd;
$warning: #eec964; $warning: #eec964;
$white: white; $white: white;

View File

@ -1,11 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin } from 'chart.js'; import { useI18n } from 'vue-i18n';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
const { t } = useI18n();
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale); ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale);
ChartJS.defaults.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false;
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
data: ChartData<"bar">; rawData: PayPeriodEmployeeDetails | undefined;
options?: ChartOptions<"bar"> | undefined; options?: ChartOptions<"bar"> | undefined;
plugins?: Plugin<"bar">[] | undefined; plugins?: Plugin<"bar">[] | undefined;
}>(), { }>(), {
@ -13,11 +20,73 @@
plugins: () => [], plugins: () => [],
}); });
const hours_worked_labels = ref<string[]>([]);
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
const getHoursWorkedData = (): ChartData<'bar'> => {
if (props.rawData) {
const all_weeks = [props.rawData.week1, props.rawData.week2];
const all_days = all_weeks.flatMap( week => Object.values(week.shifts));
const regular_hours = all_days.map( day => day.regular_hours);
const evening_hours = all_days.map( day => day.evening_hours);
const emergency_hours = all_days.map( day => day.emergency_hours);
const overtime_hours = all_days.map( day => day.overtime_hours);
hours_worked_dataset.value = [
{
label: t('timeSheetValidations.hoursWorkedRegular'),
data: regular_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedEvening'),
data: evening_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedEmergency'),
data: emergency_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedOvertime'),
data: overtime_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
},
]
hours_worked_labels.value = all_days.map( day => day.short_date);
}
return {
labels: hours_worked_labels.value,
datasets: hours_worked_dataset.value,
};
};
</script> </script>
<template> <template>
<Bar <Bar
:data="props.data" :data="getHoursWorkedData()"
:options="({
indexAxis: $q.screen.lt.md? 'y' : 'x',
plugins: {
title: {
display: true,
text: t('timeSheetValidations.hoursWorkedChartTitle'),
color: '#616161'
}
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
}
}
})"
/> />
</template> </template>

View File

@ -1,11 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface';
import type { ChartData, ChartDataset } from 'chart.js';
import TimesheetApprovalEmployeeDetailsHoursWorkedChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-details-hours-worked-chart.vue'; import TimesheetApprovalEmployeeDetailsHoursWorkedChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-details-hours-worked-chart.vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
isLoading: boolean; isLoading: boolean;
@ -13,50 +8,6 @@
employeeDetails: PayPeriodEmployeeDetails | undefined; employeeDetails: PayPeriodEmployeeDetails | undefined;
updateKey: number; updateKey: number;
}>(); }>();
const hours_worked_labels = ref<string[]>([]);
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
const getHoursWorkedData = (): ChartData<'bar'> => {
if (props.employeeDetails) {
const all_weeks = [props.employeeDetails.week1, props.employeeDetails.week2];
const all_days = all_weeks.flatMap( week => Object.values(week.shifts));
const regular_hours = all_days.map( day => day.regular_hours);
const evening_hours = all_days.map( day => day.evening_hours);
const emergency_hours = all_days.map( day => day.emergency_hours);
const overtime_hours = all_days.map( day => day.overtime_hours);
hours_worked_dataset.value = [
{
label: t('timeSheetValidations.hoursWorkedRegular'),
data: regular_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedEvening'),
data: evening_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedEmergency'),
data: emergency_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
},
{
label: t('timeSheetValidations.hoursWorkedOvertime'),
data: overtime_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
},
]
hours_worked_labels.value = all_days.map( day => day.short_date);
}
return {
labels: hours_worked_labels.value,
datasets: hours_worked_dataset.value,
};
};
</script> </script>
<template> <template>
@ -82,8 +33,16 @@
</q-card-section> </q-card-section>
<!-- employee timesheet details --> <!-- employee timesheet details -->
<q-card-section class="q-pa-none justify-center" :class="$q.screen.lt.lg? 'column': 'row'"> <q-card-section
<TimesheetApprovalEmployeeDetailsHoursWorkedChart :key="props.updateKey" :data="getHoursWorkedData()" /> v-if="!props.isLoading"
class="q-pa-none justify-center"
:class="$q.screen.lt.lg? 'column': 'row'"
>
<TimesheetApprovalEmployeeDetailsHoursWorkedChart
:key="props.updateKey"
:raw-data="props.employeeDetails"
style="min-height: 300px;"
/>
</q-card-section> </q-card-section>
</q-card> </q-card>
</template> </template>

View File

@ -122,12 +122,10 @@
clicked_employee_name.value = name; clicked_employee_name.value = name;
is_showing_details.value = true; is_showing_details.value = true;
await timesheet_approval_api.getTimesheetsByPayPeriodAndEmail(email); await timesheet_approval_api.getTimesheetsByPayPeriodAndEmail(email);
console.log('current employee details: ', timesheet_store.pay_period_employee_details);
}; };
const onClickPrintReport = async () => { const onClickPrintReport = async () => {
await timesheet_approval_api.getTimesheetApprovalCSVReport(report_filter_company.value, report_filter_type.value); await timesheet_approval_api.getTimesheetApprovalCSVReport(report_filter_company.value, report_filter_type.value);
console.log('current filter selections: [', report_filter_company.value.toLocaleString(),'], [', report_filter_type.value.toLocaleString(), ']');
}; };
onMounted( async () => { onMounted( async () => {
@ -144,7 +142,7 @@
v-model="is_showing_details" v-model="is_showing_details"
transition-show="jump-down" transition-show="jump-down"
transition-hide="jump-down" transition-hide="jump-down"
@show="() => update_key += 1" @before-show="() => update_key += 1"
> >
<TimesheetApprovalEmployeeDetails <TimesheetApprovalEmployeeDetails
:is-loading="timesheet_store.is_loading" :is-loading="timesheet_store.is_loading"

View File

@ -19,11 +19,13 @@ export const timesheetApprovalService = {
getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => { getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD // TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
console.log('all employee data: ', response.data);
return response.data; return response.data;
}, },
getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => { getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => {
const response = await api.get('timesheets', { params: { year, period_no, email, }}); const response = await api.get('timesheets', { params: { year, period_no, email, }});
console.log('employee details: ', response.data);
return response.data; return response.data;
}, },