Merge pull request 'fix(details): complete chart implementation for worked hours, stacking bars, proper mobile scaling' (#11) from dev/nicolas/approvals-overview-details into main

Reviewed-on: Targo/targo_frontend#11
This commit is contained in:
Nicolas 2025-08-29 12:55:24 -04:00
commit 61d2b96c32
5 changed files with 86 additions and 58 deletions

View File

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

View File

@ -1,11 +1,18 @@
<script setup lang="ts">
import { ref } from 'vue';
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.defaults.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false;
const props = withDefaults(defineProps<{
data: ChartData<"bar">;
rawData: PayPeriodEmployeeDetails | undefined;
options?: ChartOptions<"bar"> | undefined;
plugins?: Plugin<"bar">[] | undefined;
}>(), {
@ -13,11 +20,73 @@
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>
<template>
<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>

View File

@ -1,11 +1,6 @@
<script setup lang="ts">
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 { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps<{
isLoading: boolean;
@ -13,50 +8,6 @@
employeeDetails: PayPeriodEmployeeDetails | undefined;
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>
<template>
@ -82,8 +33,16 @@
</q-card-section>
<!-- employee timesheet details -->
<q-card-section class="q-pa-none justify-center" :class="$q.screen.lt.lg? 'column': 'row'">
<TimesheetApprovalEmployeeDetailsHoursWorkedChart :key="props.updateKey" :data="getHoursWorkedData()" />
<q-card-section
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>
</template>

View File

@ -122,12 +122,10 @@
clicked_employee_name.value = name;
is_showing_details.value = true;
await timesheet_approval_api.getTimesheetsByPayPeriodAndEmail(email);
console.log('current employee details: ', timesheet_store.pay_period_employee_details);
};
const onClickPrintReport = async () => {
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 () => {
@ -144,7 +142,7 @@
v-model="is_showing_details"
transition-show="jump-down"
transition-hide="jump-down"
@show="() => update_key += 1"
@before-show="() => update_key += 1"
>
<TimesheetApprovalEmployeeDetails
: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> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
console.log('all employee data: ', response.data);
return response.data;
},
getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => {
const response = await api.get('timesheets', { params: { year, period_no, email, }});
console.log('employee details: ', response.data);
return response.data;
},