fix(timesheet-approval): overview list now updates when closing details dialog.

Graphs at top of details dialog now update dynamically if user makes changes to employee timesheet.
This commit is contained in:
Nic D. 2026-01-26 12:16:52 -05:00
parent 1cac8966be
commit b0de761645
5 changed files with 56 additions and 67 deletions

View File

@ -2,12 +2,12 @@
setup setup
lang="ts" lang="ts"
> >
import { onMounted, ref } from 'vue'; import { computed, ref } from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar, colors } from 'quasar'; import { useQuasar, colors } from 'quasar';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale } from 'chart.js';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -19,27 +19,21 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const all_days = timesheet_store.timesheets.flatMap(week => week.days.flatMap(day => day.daily_expenses)); const all_days = computed(() => timesheet_store.timesheets.flatMap(week => week.days.flatMap(day => day.daily_expenses)));
const expenses_labels = ref<string[]>(timesheet_store.timesheets.flatMap(week => week.days.map(day => day.date.slice(-5,)))); const expenses_labels = ref<string[]>(timesheet_store.timesheets.flatMap(week => week.days.map(day => day.date.slice(-5,))));
const expenses_dataset = ref<ChartDataset<'bar'>[]>([]); const expenses_dataset = computed(() => [
onMounted(() => {
setTimeout(() => {
expenses_dataset.value = [
{ {
label: t('timesheet_approvals.table.expenses'), label: t('timesheet_approvals.table.expenses'),
data: all_days.map(day => (day.expenses + day.on_call + day.per_diem)), data: all_days.value.map(day => (day.expenses + day.on_call + day.per_diem)),
backgroundColor: colors.getPaletteColor('accent'), backgroundColor: colors.getPaletteColor('accent'),
}, },
{ {
label: t('timesheet_approvals.table.mileage'), label: t('timesheet_approvals.table.mileage'),
data: all_days.map(day => day.mileage), data: all_days.value.map(day => day.mileage),
backgroundColor: colors.getPaletteColor('info'), backgroundColor: colors.getPaletteColor('info'),
} }
] ]);
}, 100)
});
</script> </script>
<template> <template>

View File

@ -2,17 +2,17 @@
setup setup
lang="ts" lang="ts"
> >
import { ref, onMounted } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { colors, date, useQuasar } from 'quasar'; import { colors, date, useQuasar } from 'quasar';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale } from 'chart.js';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import type { TotalHours } from 'src/modules/timesheets/models/timesheet.models'; import type { TotalHours } from 'src/modules/timesheets/models/timesheet.models';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils'; import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
interface ChartConfigHoursWorked { interface ChartConfigHoursWorked {
key: keyof Pick<TotalHours, 'regular' | 'evening' | 'emergency' | 'overtime'| 'vacation' | 'holiday'>; key: keyof Pick<TotalHours, 'regular' | 'evening' | 'emergency' | 'overtime' | 'vacation' | 'holiday'>;
label: string; label: string;
color: string; color: string;
} }
@ -27,7 +27,7 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const all_days = timesheet_store.timesheets.flatMap(week => week.days); const all_days = computed(() => timesheet_store.timesheets.flatMap(week => week.days));
const datasetConfig: ChartConfigHoursWorked[] = [ const datasetConfig: ChartConfigHoursWorked[] = [
{ {
@ -62,18 +62,12 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
}, },
]; ];
const hours_worked_labels = ref<string[]>(all_days.map(day => day.date.slice(-5,))); const hours_worked_labels = ref<string[]>(all_days.value.map(day => day.date.slice(-5,)));
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]); const hours_worked_datasets = computed(() => datasetConfig.map(cfg => ({
onMounted(() => {
setTimeout(() => {
hours_worked_dataset.value = datasetConfig.map(cfg => ({
label: cfg.label, label: cfg.label,
data: all_days.map(day => day.daily_hours[cfg.key]), data: all_days.value.map(day => day.daily_hours[cfg.key]),
backgroundColor: cfg.color, backgroundColor: cfg.color,
})); })))
}, 100);
});
</script> </script>
<template> <template>
@ -83,7 +77,7 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
> >
<Bar <Bar
:data="{ :data="{
datasets: hours_worked_dataset, datasets: hours_worked_datasets,
labels: hours_worked_labels, labels: hours_worked_labels,
}" }"
:options="({ :options="({
@ -92,7 +86,7 @@ import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-uti
tooltip: { tooltip: {
callbacks: { callbacks: {
title: function (context) { title: function (context) {
return $d(date.extractDate(`2025-${context[0]!.label}`, 'YYYY-MM-DD'), {month: 'long', day: 'numeric'}); return $d(date.extractDate(`2025-${context[0]!.label}`, 'YYYY-MM-DD'), { month: 'long', day: 'numeric' });
}, },
label: function (context) { label: function (context) {
return getHoursMinutesStringFromHoursFloat(context.parsed.y); return getHoursMinutesStringFromHoursFloat(context.parsed.y);

View File

@ -2,15 +2,15 @@
setup setup
lang="ts" lang="ts"
> >
/* eslint-disable */ import { computed } from 'vue';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { colors } from 'quasar'; import { colors } from 'quasar';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { Doughnut } from 'vue-chartjs'; import { Doughnut } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale } from 'chart.js';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils'; import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
import type { Timesheet, TotalHours } from 'src/modules/timesheets/models/timesheet.models';
const $q = useQuasar(); const $q = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -22,33 +22,27 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const shift_type_labels = ref<string[]>([ const TRACKABLE_SHIFT_TYPES: (keyof TotalHours)[] = ['regular', 'evening', 'emergency', 'overtime', 'holiday', 'vacation']
const SHIFT_TYPE_LABELS = [
t('shared.shift_type.regular'), t('shared.shift_type.regular'),
t('shared.shift_type.evening'), t('shared.shift_type.evening'),
t('shared.shift_type.emergency'), t('shared.shift_type.emergency'),
t('shared.shift_type.overtime'), t('shared.shift_type.overtime'),
]); t('shared.shift_type.holiday'),
t('shared.shift_type.vacation'),
];
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]); const shift_type_data = computed(() => {
const initial_totals = new Array(TRACKABLE_SHIFT_TYPES.length).fill(0);
onMounted(() => { return timesheet_store.timesheets.reduce((accumulator: number[], timesheet: Timesheet) => {
setTimeout(() => { TRACKABLE_SHIFT_TYPES.forEach( (trackable_shift_type, index) => {
shift_type_totals.value = [{ accumulator[index]! += timesheet.weekly_hours[trackable_shift_type] ?? 0;
data: [ });
timesheet_store.current_pay_period_overview!.regular_hours,
timesheet_store.current_pay_period_overview!.other_hours.evening_hours,
timesheet_store.current_pay_period_overview!.other_hours.emergency_hours,
timesheet_store.current_pay_period_overview!.other_hours.overtime_hours,
],
backgroundColor: [
colors.getPaletteColor('accent'), // Regular
colors.getPaletteColor('green-10'), // Evening
colors.getPaletteColor('warning'), // Emergency
colors.getPaletteColor('negative'), // Overtime
]
}]
}, 100); return accumulator;
}, initial_totals);
}); });
</script> </script>
@ -59,8 +53,16 @@
> >
<Doughnut <Doughnut
:data="{ :data="{
labels: shift_type_labels, labels: SHIFT_TYPE_LABELS,
datasets: shift_type_totals, datasets: [{
data: shift_type_data,
backgroundColor: [
colors.getPaletteColor('accent'), // Regular
colors.getPaletteColor('green-10'), // Evening
colors.getPaletteColor('warning'), // Emergency
colors.getPaletteColor('negative'), // Overtime
]
}],
}" }"
:options="({ :options="({
plugins: { plugins: {

View File

@ -25,6 +25,7 @@
backdrop-filter="blur(6px)" backdrop-filter="blur(6px)"
@show="is_dialog_open = true" @show="is_dialog_open = true"
@hide="is_dialog_open = false" @hide="is_dialog_open = false"
@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-pa-sm no-wrap"

View File

@ -72,8 +72,6 @@
expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type); expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type);
else else
expense_selected.value = expense_options[1]; expense_selected.value = expense_options[1];
console.log('expense mount triggered: current expense type is ', expenses_store.current_expense.type, ', matching option is: ', expense_selected.value);
}) })
</script> </script>