fix(approvals): reimplement charts with new structures, clean UI/UX, refine list view

This commit is contained in:
Nicolas Drolet 2025-11-20 14:41:50 -05:00
parent 75ca572040
commit a47222a7b8
17 changed files with 251 additions and 186 deletions

View File

@ -5,7 +5,6 @@
} }
} }
.text-fb-blue { .text-fb-blue {
color: #4267B2 !important; color: #4267B2 !important;
} }

View File

@ -15,9 +15,9 @@
$primary : #30303A; $primary : #30303A;
$secondary : #DAE0E7; $secondary : #DAE0E7;
$accent : #0c9a3b; $accent : #0c9a3b;
$accent2 : #0a7d32; $accent2 : #0a7d32;
$dark-shadow-color : #173625; $dark-shadow-color : #000000;
$elevation-dark-umbra : rgba($dark-shadow-color, 1); $elevation-dark-umbra : rgba($dark-shadow-color, 1);
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.75); $elevation-dark-penumbra : rgba($dark-shadow-color, 0.75);

View File

@ -130,8 +130,8 @@ export default {
nav_button: { nav_button: {
calendar_date_picker: "Calendar", calendar_date_picker: "Calendar",
current_week: "This week", current_week: "This week",
next_week: "Next week", next_week: "Next period",
previous_week: "Previous week", previous_week: "Previous period",
}, },
save_button: "Save", save_button: "Save",
cancel_button: "Cancel", cancel_button: "Cancel",

View File

@ -131,8 +131,8 @@ export default {
nav_button: { nav_button: {
calendar_date_picker: "Calendrier", calendar_date_picker: "Calendrier",
current_week: "Semaine actuelle", current_week: "Semaine actuelle",
next_week: "Prochaine semaine", next_week: "Prochaine période",
previous_week: "Semaine précédente", previous_week: "Période précédente",
}, },
save_button: "Enregistrer", save_button: "Enregistrer",
cancel_button: "Annuler", cancel_button: "Annuler",

View File

@ -7,7 +7,7 @@
import { ref } from 'vue'; import { 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 } 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 ChartData, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartDataset } from 'chart.js';
@ -25,28 +25,26 @@
const expenses_labels = ref<string[]>([]); const expenses_labels = ref<string[]>([]);
const getExpensesData = (): ChartData<'bar'> => { const getExpensesData = (): ChartData<'bar'> => {
// const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses)); const all_days = timesheet_store.timesheets.flatMap(week => week.days.flatMap(day => day.daily_expenses));
// const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
// const all_costs = all_days.map(day => day.total_expenses); const all_costs = all_days.map(day => (day.expenses + day.on_call + day.per_diem));
// console.log('costs, ', all_costs); const all_mileage = all_days.map(day => day.mileage);
// const all_mileage = all_days.map(day => day.total_mileage);
// expenses_dataset.value = [ expenses_dataset.value = [
// { {
// label: t('timesheet_approvals.table.expenses'), label: t('timesheet_approvals.table.expenses'),
// data: all_costs, data: all_costs,
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), backgroundColor: colors.getPaletteColor('primary'),
// }, },
// { {
// label: t('timesheet_approvals.table.mileage'), label: t('timesheet_approvals.table.mileage'),
// data: all_mileage, data: all_mileage,
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(), backgroundColor: colors.getPaletteColor('info'),
// } }
// ] ]
// expenses_labels.value = all_days_dates.map(day => day.short_date); expenses_labels.value = timesheet_store.timesheets.flatMap(week => week.days.map(day => day.date.slice(-5, )));
return { return {
datasets: expenses_dataset.value, datasets: expenses_dataset.value,

View File

@ -1,67 +1,79 @@
<script setup lang="ts"> <script
/* eslint-disable */ setup
import { ref } from 'vue'; lang="ts"
import { colors } from 'quasar'; >
import { Bar } from 'vue-chartjs'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { colors, useQuasar } from 'quasar';
import { Bar } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartDataset } 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';
interface ChartConfigHoursWorked {
key: keyof Pick<TotalHours, 'regular' | 'evening' | 'emergency' | 'overtime'>;
label: string;
color: string;
}
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
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.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false; // ChartJS.defaults.maintainAspectRatio = false;
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161'; ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const hours_worked_labels = ref<string[]>([]); const hours_worked_labels = ref<string[]>([]);
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]); const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
const getHoursWorkedData = (): ChartData<'bar'> => { const getHoursWorkedData = (): ChartData<'bar'> => {
// const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
// const datasetConfig = [
// {
// key: 'regular_hours',
// label: t('shared.shift_type.regular'),
// color: colors.getPaletteColor('green-5'),
// },
// {
// key: 'evening_hours',
// label: t('shared.shift_type.evening'),
// color: colors.getPaletteColor('green-9'),
// },
// {
// key: 'emergency_hours',
// label: t('shared.shift_type.emergency'),
// color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
// },
// {
// key: 'overtime_hours',
// label: t('shared.shift_type.overtime'),
// color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
// },
// ] as const;
// hours_worked_dataset.value = datasetConfig.map(cfg => ({ const all_days = computed(() => timesheet_store.timesheets.flatMap(week => week.days));
// label: cfg.label, const datasetConfig: ChartConfigHoursWorked[] = [
// data: all_days.map(day => day[ cfg.key ]), {
// backgroundColor: cfg.color, key: 'regular',
// })); label: t('shared.shift_type.regular'),
color: colors.getPaletteColor('primary'),
},
{
key: 'evening',
label: t('shared.shift_type.evening'),
color: colors.getPaletteColor('accent'),
},
{
key: 'emergency',
label: t('shared.shift_type.emergency'),
color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
},
{
key: 'overtime',
label: t('shared.shift_type.overtime'),
color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
},
] as const;
// hours_worked_labels.value = all_days.map(day => day.short_date); hours_worked_dataset.value = datasetConfig.map(cfg => ({
label: cfg.label,
data: all_days.value.map(day => day.daily_hours[cfg.key]),
backgroundColor: cfg.color,
}));
hours_worked_labels.value = all_days.value.map(day => day.date.slice(-5, ));
console.log('all days: ', all_days.value);
console.log('hours worked labels: ', hours_worked_labels.value);
console.log('hours worked dataset: ', hours_worked_dataset.value);
return { return {
labels: hours_worked_labels.value, labels: hours_worked_labels.value,
datasets: hours_worked_dataset.value, datasets: hours_worked_dataset.value,
}; };
}; };
</script> </script>
<template> <template>
@ -69,7 +81,7 @@
<Bar <Bar
:data="getHoursWorkedData()" :data="getHoursWorkedData()"
:options="({ :options="({
indexAxis: $q.screen.lt.md? 'y' : 'x', indexAxis: $q.screen.lt.md ? 'y' : 'x',
plugins: { plugins: {
legend: { legend: {
labels: { labels: {
@ -89,10 +101,10 @@
stacked: true, stacked: true,
suggestedMin: 0, suggestedMin: 0,
suggestedMax: 10, suggestedMax: 10,
} }
} }
})" })"
/> />
</div> </div>
</template> </template>

View File

@ -17,33 +17,33 @@
ChartJS.defaults.maintainAspectRatio = false; ChartJS.defaults.maintainAspectRatio = false;
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161'; ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const { current_pay_period_overview } = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const shift_type_labels = ref<string[]>([]); const shift_type_labels = ref<string[]>([]);
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]); const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([{ data: [40, 0, 2, 5], }]);
// shift_type_totals.value = [{ shift_type_totals.value = [{
// data: [ data: [
// current_pay_period_overview.regular_hours, timesheet_store.current_pay_period_overview!.regular_hours,
// current_pay_period_overview.other_hours.evening_hours, timesheet_store.current_pay_period_overview!.other_hours.evening_hours,
// current_pay_period_overview.other_hours.emergency_hours, timesheet_store.current_pay_period_overview!.other_hours.emergency_hours,
// current_pay_period_overview.other_hours.overtime_hours, timesheet_store.current_pay_period_overview!.other_hours.overtime_hours,
// ], ],
// backgroundColor: [ backgroundColor: [
// colors.getPaletteColor('green-5'), // Regular colors.getPaletteColor('primary'), // Regular
// colors.getPaletteColor('green-9'), // Evening colors.getPaletteColor('accent'), // Evening
// getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency colors.getPaletteColor('warning'), // Emergency
// getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime colors.getPaletteColor('negative'), // Overtime
// ] ]
// }]; }];
// shift_type_labels.value = [ shift_type_labels.value = [
// current_pay_period_overview.regular_hours.toString() + 'h', timesheet_store.current_pay_period_overview!.regular_hours.toString() + 'h',
// current_pay_period_overview.other_hours.evening_hours.toString() + 'h', timesheet_store.current_pay_period_overview!.other_hours.evening_hours.toString() + 'h',
// current_pay_period_overview.other_hours.emergency_hours.toString() + 'h', timesheet_store.current_pay_period_overview!.other_hours.emergency_hours.toString() + 'h',
// current_pay_period_overview.other_hours.overtime_hours.toString() + 'h', timesheet_store.current_pay_period_overview!.other_hours.overtime_hours.toString() + 'h',
// ] ]
const data = { const data = {

View File

@ -17,7 +17,7 @@
const dialog_model = defineModel<boolean>('dialog', { default: false }); const dialog_model = defineModel<boolean>('dialog', { default: false });
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const render_key = ref(1); const is_dialog_open = ref(false);
provide('employeeEmail', employeeEmail); provide('employeeEmail', employeeEmail);
</script> </script>
@ -29,45 +29,37 @@
full-height full-height
transition-show="jump-down" transition-show="jump-down"
transition-hide="jump-down" transition-hide="jump-down"
@show="render_key += 1" @show="is_dialog_open = true"
@hide="is_dialog_open = false"
> >
<q-card <q-card
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative" class="shadow-12 rounded-15 column no-wrap relative bg-secondary"
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'" :style="($q.screen.lt.md ? '' : 'width: 60vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
> >
<!-- employee name --> <!-- employee name -->
<q-card-section <q-card-section
class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto" class="col-auto text-h5 text-weight-bolder text-center text-uppercase text-white q-px-none q-py-xs bg-primary"
> >
<span>TODO: Name goes here</span> <span>{{ timesheet_store.selected_employee_name }}</span>
</q-card-section> </q-card-section>
<!-- employee pay period details using chart --> <!-- employee pay period details using chart -->
<q-card-section <q-card-section
v-if="is_dialog_open"
:horizontal="!$q.screen.lt.md" :horizontal="!$q.screen.lt.md"
class=" col-auto q-px-sm no-wrap" class="col-auto q-px-sm no-wrap"
> >
<DetailsDialogChartHoursWorked <DetailsDialogChartHoursWorked class="col" />
:key="render_key"
class="col"
/>
<DetailsDialogChartShiftTypes <DetailsDialogChartShiftTypes class="col q-ma-lg" />
:key="render_key + 1"
class="col-2 q-ma-lg"
/>
<DetailsDialogChartExpenses <DetailsDialogChartExpenses class="col" />
:key="render_key + 2"
class="col"
/>
</q-card-section> </q-card-section>
<q-card-section class="col-auto"> <q-card-section class="col-auto">
<q-separator /> <q-separator />
<ExpenseDialogList <ExpenseDialogList
horizontal
:employee-email="employeeEmail" :employee-email="employeeEmail"
/> />
<q-separator /> <q-separator />

View File

@ -17,13 +17,13 @@
</script> </script>
<template> <template>
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition"> <div class="q-px-sm q-pb-md col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
<transition <transition
appear appear
enter-active-class="animated fadeInUp" enter-active-class="animated fadeInUp"
> >
<q-card <q-card
class="rounded-10 shadow-15" class="rounded-10 shadow-5"
:style="`animation-delay: ${index / 15}s;`" :style="`animation-delay: ${index / 15}s;`"
> >
<!-- Card header with employee name and details button--> <!-- Card header with employee name and details button-->

View File

@ -9,7 +9,7 @@
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api'; import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models'; import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
const expenses_store = useExpensesStore(); const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
@ -17,7 +17,16 @@
const employeeEmail = defineModel(); const employeeEmail = defineModel();
const visible_columns = ref<string[]>(['employee_name', 'REGULAR', 'email', 'is_approved']); const visible_columns = ref<string[]>([
overview_column_names.REGULAR,
overview_column_names.EVENING,
overview_column_names.EMERGENCY,
overview_column_names.SICK,
overview_column_names.VACATION,
overview_column_names.HOLIDAY,
overview_column_names.OVERTIME,
overview_column_names.IS_APPROVED,
]);
const emit = defineEmits<{ const emit = defineEmits<{
'clickedDetailsButton': [email: string]; 'clickedDetailsButton': [email: string];
@ -34,12 +43,11 @@
emit('clickedDetailsButton', employee_email); emit('clickedDetailsButton', employee_email);
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email); await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
}; };
</script> </script>
<template> <template>
<div class="q-pa-md"> <div class="q-px-md full-height">
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
<transition <transition
appear appear
@ -55,18 +63,18 @@
row-key="email" row-key="email"
:filter="timesheet_store.search_filter" :filter="timesheet_store.search_filter"
:grid="timesheet_store.is_approval_grid_mode" :grid="timesheet_store.is_approval_grid_mode"
dense :dense="timesheet_store.is_approval_grid_mode"
hide-pagination hide-pagination
color="accent" color="accent"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
card-container-class="justify-center" card-container-class="justify-center"
class="q-py-md bg-transparent" class="bg-transparent"
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'" :class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
table-class="q-pa-none q-py-none q-mx-md rounded-10 bg-dark shadow-15" table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15"
:no-data-label="$t('shared.error.no_data_found')" :no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')" :loading-label="$t('shared.label.loading')"
> >
<template #header="props"> <template #header="props">
<q-tr <q-tr
:props="props" :props="props"
@ -83,28 +91,54 @@
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
<template #body-cell="props"> <template #body-cell="props">
<q-td <q-td
:props="props" :props="props"
class="text-weight-medium" class="text-weight-medium"
> >
<q-icon <transition
v-if="props.col.name === 'is_approved'" appear
:name="props.value ? 'verified' : 'remove_circle_outline'" enter-active-class="animated fadeInUp"
:color="props.value ? 'accent' : 'grey-5'" leave-active-class="animated fadeOutDown"
:class="props.value ? 'bg-white rounded-20' : ''" mode="out-in"
size="sm" >
/> <div
:key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
<div v-else-if="props.col.name === 'employee_name'"> class="rounded-5"
<span class="text-h5 text-uppercase text-accent q-mr-xs">{{ props.value.split(' ')[0]}}</span> style="font-size: 1.2em;"
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1] }}</span> :style="`animation-delay: ${props.rowIndex / 30}s;`"
</div> >
<span v-else>{{ props.value }}</span> <transition
v-if="props.col.name === 'is_approved'"
enter-active-class="animated swing"
mode="out-in"
>
<q-btn
:key="props.row.is_approved"
flat
dense
:icon="props.value ? 'lock' : 'lock_open'"
:color="props.value ? 'white' : 'grey-5'"
class="rounded-5 z-top"
:class="props.value ? 'bg-accent' : ''"
@click.stop="props.row.is_approved = !props.row.is_approved"
/>
</transition>
<div v-else-if="props.col.name === 'employee_name'">
<span class="text-h5 text-uppercase text-accent q-mr-xs">
{{ props.value.split(' ')[0] }}
</span>
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1]
}}</span>
</div>
<span v-else>{{ props.value }}</span>
</div>
</transition>
</q-td> </q-td>
</template> </template>
<!-- Template for individual employee cards --> <!-- Template for individual employee cards -->
<template #item="props: { row: TimesheetOverview, rowIndex: number }"> <template #item="props: { row: TimesheetOverview, rowIndex: number }">
<OverviewListItem <OverviewListItem
@ -115,7 +149,7 @@
@click-details="overview => onClickedDetails(props.row.email, overview)" @click-details="overview => onClickedDetails(props.row.email, overview)"
/> />
</template> </template>
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }"> <template #no-data="{ message, filter }">
<div class="full-width column items-center text-accent q-gutter-sm"> <div class="full-width column items-center text-accent q-gutter-sm">
@ -123,7 +157,7 @@
size="4em" size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'" :name="filter ? 'filter_alt_off' : 'error_outline'"
/> />
<span class="text-h6"> <span class="text-h6">
{{ message }} {{ message }}
</span> </span>

View File

@ -1,4 +1,3 @@
import { ref } from "vue";
import { useTimesheetStore } from "src/stores/timesheet-store"; import { useTimesheetStore } from "src/stores/timesheet-store";
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models"; import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
@ -6,44 +5,41 @@ export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; const DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
const getTimesheetOverviews = async (date?: string) => { const getTimesheetOverviews = async () => {
timesheet_store.is_loading = true; timesheet_store.is_loading = true;
const success = ref(false);
if (date !== undefined) { const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
success.value = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date); if (success) await timesheet_store.getTimesheetOverviews();
} else {
success.value = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
}
if (success.value === true) { timesheet_store.is_loading = false;
await timesheet_store.getTimesheetOverviews();
}
} }
const getTimesheetOverviewsByDate = async (date: string) => { const getTimesheetOverviewsByDate = async (date: string) => {
const valid_date = DATE_REGEX.test(date); const valid_date = DATE_REGEX.test(date);
timesheet_store.is_loading = true;
if (valid_date) { if (valid_date) {
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheet_store.getTimesheetOverviews(); if (success) await timesheet_store.getTimesheetOverviews();
} }
timesheet_store.is_loading = false;
}; };
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number ) => { const getTimesheetApprovalCSVReport = async (report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number) => {
if (timesheet_store.pay_period === undefined) return; if (timesheet_store.pay_period === undefined) return;
const [ targo, solucom ] = report_filter_company; const [targo, solucom] = report_filter_company;
const [ shifts, expenses, holiday, vacation ] = report_filter_type; const [shifts, expenses, holiday, vacation] = report_filter_type;
const options = { const options = {
types: { shifts, expenses, holiday, vacation }, types: { shifts, expenses, holiday, vacation },
companies: { targo, solucom }, companies: { targo, solucom },
} as TimesheetApprovalCSVReportFilters; } as TimesheetApprovalCSVReportFilters;
await timesheet_store.getPayPeriodReportByYearAndPeriodNumber( await timesheet_store.getPayPeriodReportByYearAndPeriodNumber(
year ?? timesheet_store.pay_period.pay_year, year ?? timesheet_store.pay_period.pay_year,
period_number ?? timesheet_store.pay_period.pay_period_no, period_number ?? timesheet_store.pay_period.pay_period_no,
options options
); );
}; };

View File

@ -52,76 +52,102 @@ export const default_pay_period_overview: TimesheetOverview = {
is_approved: false is_approved: false
} }
export const overview_column_names = {
EMPLOYEE_NAME: 'employee_name',
EMAIL: 'email',
REGULAR: 'REGULAR',
EVENING: 'EVENING',
EMERGENCY: 'EMERGENCY',
SICK: 'SICK',
HOLIDAY: 'HOLIDAY',
VACATION: 'VACATION',
OVERTIME: 'OVERTIME',
EXPENSES: 'expenses',
MILEAGE: 'mileage',
IS_APPROVED: 'is_approved',
}
export const pay_period_overview_columns: QTableColumn[] = [ export const pay_period_overview_columns: QTableColumn[] = [
{ {
name: 'employee_name', name: overview_column_names.EMPLOYEE_NAME,
label: 'timesheet_approvals.table.full_name', label: 'timesheet_approvals.table.full_name',
align: 'left', align: 'left',
field: 'employee_name', field: 'employee_name',
sortable: true sortable: true,
required: true,
}, },
{ {
name: 'email', name: overview_column_names.EMAIL,
label: 'timesheet_approvals.table.email', label: 'timesheet_approvals.table.email',
align: 'left',
field: 'email', field: 'email',
sortable: true, sortable: true,
}, },
{ {
name: 'REGULAR', name: overview_column_names.REGULAR,
label: 'shared.shift_type.regular', label: 'shared.shift_type.regular',
align: 'left',
field: 'regular_hours', field: 'regular_hours',
sortable: true, sortable: true,
}, },
{ {
name: 'EVENING', name: overview_column_names.EVENING,
label: 'shared.shift_type.evening', label: 'shared.shift_type.evening',
align: 'left',
field: row => row.other_hours.evening_hours, field: row => row.other_hours.evening_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'EMERGENCY', name: overview_column_names.EMERGENCY,
label: 'shared.shift_type.emergency', label: 'shared.shift_type.emergency',
align: 'left',
field: row => row.other_hours.emergency_hours, field: row => row.other_hours.emergency_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'SICK', name: overview_column_names.SICK,
label: 'shared.shift_type.sick', label: 'shared.shift_type.sick',
align: 'left',
field: row => row.other_hours.sick_hours, field: row => row.other_hours.sick_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'HOLIDAY', name: overview_column_names.HOLIDAY,
label: 'shared.shift_type.holiday', label: 'shared.shift_type.holiday',
align: 'left',
field: row => row.other_hours.holiday_hours, field: row => row.other_hours.holiday_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'VACATION', name: overview_column_names.VACATION,
label: 'shared.shift_type.vacation', label: 'shared.shift_type.vacation',
align: 'left',
field: row => row.other_hours.vacation_hours, field: row => row.other_hours.vacation_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'OVERTIME', name: overview_column_names.OVERTIME,
label: 'shared.shift_type.overtime', label: 'shared.shift_type.overtime',
align: 'left',
field: row => row.other_hours.overtime_hours, field: row => row.other_hours.overtime_hours,
sortable: true, sortable: true,
}, },
{ {
name: 'expenses', name: overview_column_names.EXPENSES,
label: 'timesheet_approvals.table.expenses', label: 'timesheet_approvals.table.expenses',
align: 'left',
field: 'expenses', field: 'expenses',
sortable: true, sortable: true,
}, },
{ {
name: 'mileage', name: overview_column_names.MILEAGE,
label: 'timesheet_approvals.table.mileage', label: 'timesheet_approvals.table.mileage',
align: 'left',
field: 'mileage', field: 'mileage',
sortable: true, sortable: true,
}, },
{ {
name: 'is_approved', name: overview_column_names.IS_APPROVED,
label: 'timesheet_approvals.table.is_approved', label: 'timesheet_approvals.table.is_approved',
field: 'is_approved', field: 'is_approved',
sortable: true, sortable: true,

View File

@ -31,7 +31,6 @@
const day = timesheet_store.timesheets[timesheet_index]!.days[day_index]!; const day = timesheet_store.timesheets[timesheet_index]!.days[day_index]!;
const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0); const shifts_without_deleted_shift = day.shifts.filter(shift => shift.id !== 0);
day.shifts = shifts_without_deleted_shift; day.shifts = shifts_without_deleted_shift;
console.log("day's shifts after cleanup: ", day.shifts);
} }
} }

View File

@ -5,7 +5,7 @@ export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/; export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export interface TimesheetResponse { export interface TimesheetResponse {
employee_full_name: string; employee_fullname: string;
timesheets: Timesheet[]; timesheets: Timesheet[];
} }
@ -38,6 +38,8 @@ export interface TotalHours {
export interface TotalExpenses { export interface TotalExpenses {
expenses: number; expenses: number;
per_diem: number;
on_call: number;
mileage: number; mileage: number;
} }

View File

@ -19,8 +19,8 @@ export const timesheetService = {
return response.data; return response.data;
}, },
getTimesheetsByPayPeriod: async (year: number, period_number: number): Promise<TimesheetResponse> => { getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<TimesheetResponse> => {
const response = await api.get(`timesheets/${year}/${period_number}`); const response = await api.get<{success: boolean, data: TimesheetResponse, error? : string}>(`timesheets/${year}/${period_number}/${employee_email}`);
return response.data.data; return response.data.data;
}, },
}; };

View File

@ -31,12 +31,13 @@
<template> <template>
<q-page <q-page
padding padding
class="q-pa-md bg-secondary " class="column q-pa-md bg-secondary"
> >
<PageHeaderTemplate <PageHeaderTemplate
title="timesheet_approvals.page_title" title="timesheet_approvals.page_title"
:start-date="timesheet_store.pay_period?.period_start ?? ''" :start-date="timesheet_store.pay_period?.period_start ?? ''"
:end-date="timesheet_store.pay_period?.period_end ?? ''" :end-date="timesheet_store.pay_period?.period_end ?? ''"
class="col-auto"
/> />
<DetailsDialog <DetailsDialog
@ -48,8 +49,8 @@
/> />
<div <div
class="full-width q-mb-sm" class="col-auto full-width q-px-lg"
:class="$q.screen.lt.md ? 'text-center' : 'row'" :class="($q.screen.lt.md ? 'text-center' : 'row') + (timesheet_store.is_approval_grid_mode ? ' q-mb-sm' : ' q-mb-md')"
> >
<PayPeriodNavigator <PayPeriodNavigator
@date-selected="timesheet_approval_api.getTimesheetOverviews" @date-selected="timesheet_approval_api.getTimesheetOverviews"
@ -85,6 +86,6 @@
<QTableFilters v-model:search="timesheet_store.search_filter" /> <QTableFilters v-model:search="timesheet_store.search_filter" />
</div> </div>
<OverviewList @clickedDetailsButton="onDetailsClicked" /> <OverviewList class="col" @clickedDetailsButton="onDetailsClicked" />
</q-page> </q-page>
</template> </template>

View File

@ -15,6 +15,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const pay_period_overviews = ref<TimesheetOverview[]>([]); const pay_period_overviews = ref<TimesheetOverview[]>([]);
const current_pay_period_overview = ref<TimesheetOverview>(); const current_pay_period_overview = ref<TimesheetOverview>();
const timesheets = ref<Timesheet[]>([]); const timesheets = ref<Timesheet[]>([]);
const selected_employee_name = ref<string>();
const initial_timesheets = ref<Timesheet[]>([]); const initial_timesheets = ref<Timesheet[]>([]);
const is_approval_grid_mode = ref<boolean>(true); const is_approval_grid_mode = ref<boolean>(true);
const search_filter = ref<string | number | null>(''); const search_filter = ref<string | number | null>('');
@ -22,7 +23,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => { const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
try { try {
if (date!== undefined) { if (date !== undefined) {
pay_period.value = await timesheetService.getPayPeriodByDate(date); pay_period.value = await timesheetService.getPayPeriodByDate(date);
} else if (pay_period.value !== undefined) { } else if (pay_period.value !== undefined) {
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no); pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no);
@ -47,7 +48,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
is_loading.value = false; is_loading.value = false;
return false; return false;
} }
const response = await timesheetApprovalService.getPayPeriodOverviews(pay_period.value.pay_year, pay_period.value.pay_period_no); const response = await timesheetApprovalService.getPayPeriodOverviews(pay_period.value.pay_year, pay_period.value.pay_period_no);
pay_period_overviews.value = response.employees_overview; pay_period_overviews.value = response.employees_overview;
is_loading.value = false; is_loading.value = false;
@ -65,13 +66,17 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const getTimesheetsByEmployeeEmail = async (employee_email?: string) => { const getTimesheetsByEmployeeEmail = async (employee_email?: string) => {
is_loading.value = true; is_loading.value = true;
let response;
if (pay_period.value === undefined) return; if (pay_period.value === undefined) return;
try { try {
if (employee_email) { if (employee_email) {
console.log('email: ', employee_email); response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no, employee_email);
} else {
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no);
} }
const response = await timesheetService.getTimesheetsByPayPeriod(pay_period.value.pay_year, pay_period.value.pay_period_no);
selected_employee_name.value = response.employee_fullname;
timesheets.value = response.timesheets; timesheets.value = response.timesheets;
initial_timesheets.value = unwrapAndClone(timesheets.value); initial_timesheets.value = unwrapAndClone(timesheets.value);
is_loading.value = false; is_loading.value = false;
@ -107,6 +112,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
pay_period, pay_period,
pay_period_overviews, pay_period_overviews,
current_pay_period_overview, current_pay_period_overview,
selected_employee_name,
timesheets, timesheets,
initial_timesheets, initial_timesheets,
getPayPeriodByDateOrYearAndNumber, getPayPeriodByDateOrYearAndNumber,