fix(approvals, timesheet): separation of concern, refactor timesheet route to use optional email, fix frontend routes and streamline store, simplify logic in many places.

This commit is contained in:
Nicolas Drolet 2025-11-21 12:01:46 -05:00
parent a47222a7b8
commit 39ce63603e
18 changed files with 185 additions and 212 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 7.4 MiB

View File

@ -13,7 +13,7 @@
</script> </script>
<template> <template>
<q-card class="rounded-15 shadow-10"> <q-card class="rounded-15 shadow-10 full-width">
<q-card-section class="text-center bg-primary q-pa-lg"> <q-card-section class="text-center bg-primary q-pa-lg">
<q-img <q-img
src="/src/assets/logo-targo-white.svg" src="/src/assets/logo-targo-white.svg"
@ -38,7 +38,7 @@
label-color="accent" label-color="accent"
class="rounded-5 inset-shadow bg-white" class="rounded-5 inset-shadow bg-white"
label-slot label-slot
input-class="text-h6 text-dark" input-class="text-h6 text-primary"
> >
<template #label> <template #label>
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span> <span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>

View File

@ -3,8 +3,8 @@
lang="ts" lang="ts"
> >
/* eslint-disable */ /* eslint-disable */
import { ref } from 'vue'; import { onMounted, 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';
@ -21,42 +21,37 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const all_days = 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_dataset = ref<ChartDataset<'bar'>[]>([]); const expenses_dataset = ref<ChartDataset<'bar'>[]>([]);
const expenses_labels = ref<string[]>([]);
const getExpensesData = (): ChartData<'bar'> => {
const all_days = timesheet_store.timesheets.flatMap(week => week.days.flatMap(day => day.daily_expenses));
const all_costs = all_days.map(day => (day.expenses + day.on_call + day.per_diem));
const all_mileage = all_days.map(day => day.mileage);
onMounted(() => {
expenses_dataset.value = [ expenses_dataset.value = [
{ {
label: t('timesheet_approvals.table.expenses'), label: t('timesheet_approvals.table.expenses'),
data: all_costs, data: all_days.map(day => (day.expenses + day.on_call + day.per_diem)),
backgroundColor: colors.getPaletteColor('primary'), backgroundColor: colors.getPaletteColor('accent'),
}, },
{ {
label: t('timesheet_approvals.table.mileage'), label: t('timesheet_approvals.table.mileage'),
data: all_mileage, data: all_days.map(day => day.mileage),
backgroundColor: colors.getPaletteColor('info'), backgroundColor: colors.getPaletteColor('info'),
} }
] ]
});
expenses_labels.value = timesheet_store.timesheets.flatMap(week => week.days.map(day => day.date.slice(-5, )));
return {
datasets: expenses_dataset.value,
labels: expenses_labels.value
};
};
</script> </script>
<template> <template>
<div> <div
class="bg-dark rounded-10 q-pa-sm"
:style="`min-height: ${$q.screen.lt.md ? '350px;' : '200px'}`"
>
<Bar <Bar
:data="getExpensesData()" :data="{
datasets: expenses_dataset,
labels: expenses_labels,
}"
:options="({ :options="({
indexAxis: $q.screen.lt.md ? 'y' : 'x', indexAxis: $q.screen.lt.md ? 'y' : 'x',
plugins: { plugins: {

View File

@ -2,7 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { colors, useQuasar } from 'quasar'; import { colors, useQuasar } from 'quasar';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
@ -26,60 +26,53 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const hours_worked_labels = ref<string[]>([]); const all_days = computed(() => timesheet_store.timesheets.flatMap(week => week.days));
const datasetConfig: ChartConfigHoursWorked[] = [
{
key: 'regular',
label: t('shared.shift_type.regular'),
color: colors.getPaletteColor('accent'),
},
{
key: 'evening',
label: t('shared.shift_type.evening'),
color: colors.getPaletteColor('green-10'),
},
{
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(),
},
];
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_dataset = ref<ChartDataset<'bar'>[]>([]);
const getHoursWorkedData = (): ChartData<'bar'> => { onMounted(() => {
const all_days = computed(() => timesheet_store.timesheets.flatMap(week => week.days));
const datasetConfig: ChartConfigHoursWorked[] = [
{
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_dataset.value = datasetConfig.map(cfg => ({ hours_worked_dataset.value = datasetConfig.map(cfg => ({
label: cfg.label, label: cfg.label,
data: all_days.value.map(day => day.daily_hours[cfg.key]), data: all_days.value.map(day => day.daily_hours[cfg.key]),
backgroundColor: cfg.color, 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 {
labels: hours_worked_labels.value,
datasets: hours_worked_dataset.value,
};
};
</script> </script>
<template> <template>
<div> <div
class="bg-dark rounded-10 q-pa-sm"
:style="`min-height: ${$q.screen.lt.md ? '450px;' : '200px'}`"
>
<Bar <Bar
:data="getHoursWorkedData()" :data="{
labels: hours_worked_labels,
datasets: hours_worked_dataset,
}"
:options="({ :options="({
indexAxis: $q.screen.lt.md ? 'y' : 'x', indexAxis: $q.screen.lt.md ? 'y' : 'x',
plugins: { plugins: {

View File

@ -3,7 +3,7 @@
lang="ts" lang="ts"
> >
/* eslint-disable */ /* eslint-disable */
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
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';
@ -19,44 +19,43 @@
const timesheet_store = 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], }]);
shift_type_totals.value = [{
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('primary'), // Regular
colors.getPaletteColor('accent'), // Evening
colors.getPaletteColor('warning'), // Emergency
colors.getPaletteColor('negative'), // Overtime
]
}];
shift_type_labels.value = [
timesheet_store.current_pay_period_overview!.regular_hours.toString() + 'h', 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.evening_hours.toString() + 'h',
timesheet_store.current_pay_period_overview!.other_hours.emergency_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', timesheet_store.current_pay_period_overview!.other_hours.overtime_hours.toString() + 'h',
] ]);
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]);
const data = { onMounted(() => {
labels: shift_type_labels.value, shift_type_totals.value = [{
datasets: shift_type_totals.value, 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
]
}]
});
</script> </script>
<template> <template>
<div> <div
style="min-height: 100px;"
:style="$q.screen.lt.md ? 'max-height: 150px;' : ''"
>
<Doughnut <Doughnut
:data="data" :data="{
labels: shift_type_labels,
datasets: shift_type_totals,
}"
:options="({ :options="({
plugins: { plugins: {
legend: { legend: {

View File

@ -3,7 +3,7 @@
lang="ts" lang="ts"
> >
/* eslint-disable */ /* eslint-disable */
import { provide, ref } from 'vue'; import { ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue'; import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue'; import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
@ -11,20 +11,13 @@
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue'; import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue'; import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
const { employeeEmail } = defineProps<{
employeeEmail: string;
}>();
const dialog_model = defineModel<boolean>('dialog', { default: false });
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const is_dialog_open = ref(false); const is_dialog_open = ref(false);
provide('employeeEmail', employeeEmail);
</script> </script>
<template> <template>
<q-dialog <q-dialog
v-model="dialog_model" v-model="timesheet_store.is_details_dialog_open"
full-width full-width
full-height full-height
transition-show="jump-down" transition-show="jump-down"
@ -33,14 +26,12 @@
@hide="is_dialog_open = false" @hide="is_dialog_open = false"
> >
<q-card <q-card
class="shadow-12 rounded-15 column no-wrap relative bg-secondary" class="shadow-12 rounded-15 column no-wrap relative bg-secondary hide-scrollbar"
:style="($q.screen.lt.md ? '' : 'width: 60vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')" :style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
> >
<!-- employee name --> <!-- employee name -->
<q-card-section <q-card-section class="col-auto text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
class="col-auto text-h5 text-weight-bolder text-center text-uppercase text-white q-px-none q-py-xs bg-primary"
>
<span>{{ timesheet_store.selected_employee_name }}</span> <span>{{ timesheet_store.selected_employee_name }}</span>
</q-card-section> </q-card-section>
@ -48,21 +39,15 @@
<q-card-section <q-card-section
v-if="is_dialog_open" 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-md rounded-10 no-wrap"
> >
<DetailsDialogChartHoursWorked class="col" /> <DetailsDialogChartHoursWorked class="col" />
<DetailsDialogChartShiftTypes class="col q-ma-lg" /> <DetailsDialogChartShiftTypes class="col q-ma-lg" />
<DetailsDialogChartExpenses class="col" /> <DetailsDialogChartExpenses class="col" />
</q-card-section> </q-card-section>
<q-card-section class="col-auto"> <q-card-section class="col-auto">
<q-separator /> <ExpenseDialogList />
<ExpenseDialogList
:employee-email="employeeEmail"
/>
<q-separator />
</q-card-section> </q-card-section>
<!-- list of shifts --> <!-- list of shifts -->
@ -70,10 +55,7 @@
:horizontal="$q.screen.gt.sm" :horizontal="$q.screen.gt.sm"
class="col-auto q-px-sm rounded-5 no-wrap" class="col-auto q-px-sm rounded-5 no-wrap"
> >
<TimesheetWrapper <TimesheetWrapper mode="approval" />
dense
:employee-email="employeeEmail"
/>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>

View File

@ -15,8 +15,6 @@
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const employeeEmail = defineModel();
const visible_columns = ref<string[]>([ const visible_columns = ref<string[]>([
overview_column_names.REGULAR, overview_column_names.REGULAR,
overview_column_names.EVENING, overview_column_names.EVENING,
@ -28,21 +26,16 @@
overview_column_names.IS_APPROVED, overview_column_names.IS_APPROVED,
]); ]);
const emit = defineEmits<{
'clickedDetailsButton': [email: string];
}>();
const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ? const overview_rows = computed(() => timesheet_store.pay_period_overviews[0]?.regular_hours === -1 ?
[] : [] :
timesheet_store.pay_period_overviews timesheet_store.pay_period_overviews
) )
const onClickedDetails = async (employee_email: string, row: TimesheetOverview) => { const onClickedDetails = async (row: TimesheetOverview) => {
employeeEmail.value = employee_email;
timesheet_store.current_pay_period_overview = row; timesheet_store.current_pay_period_overview = row;
emit('clickedDetailsButton', employee_email); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email); timesheet_store.is_details_dialog_open = true;
}; };
</script> </script>
@ -99,7 +92,7 @@
> >
<transition <transition
appear appear
enter-active-class="animated fadeInUp" enter-active-class="animated fadeInUp slow"
leave-active-class="animated fadeOutDown" leave-active-class="animated fadeOutDown"
mode="out-in" mode="out-in"
> >
@ -146,7 +139,7 @@
:key="props.row.email + timesheet_store.pay_period?.pay_period_no" :key="props.row.email + timesheet_store.pay_period?.pay_period_no"
:index="props.rowIndex" :index="props.rowIndex"
:row="props.row" :row="props.row"
@click-details="overview => onClickedDetails(props.row.email, overview)" @click-details="overview => onClickedDetails(overview)"
/> />
</template> </template>

View File

@ -15,9 +15,7 @@
const expenses_list = computed(() => { const expenses_list = computed(() => {
if (timesheet_store.timesheets !== undefined) { if (timesheet_store.timesheets !== undefined) {
const current_expenses = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses); return timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses);
console.log('current expenses: ', current_expenses);
return current_expenses;
} }
return []; return [];
}) })

View File

@ -43,7 +43,6 @@
<template> <template>
<div <div
:class="$q.screen.lt.md ? 'column full-width' : 'row'" :class="$q.screen.lt.md ? 'column full-width' : 'row'"
:style="$q.screen.lt.md ? 'width: 90vw !important;' : ''"
> >
<div <div
v-for="timesheet, timesheet_index in timesheet_store.timesheets" v-for="timesheet, timesheet_index in timesheet_store.timesheets"
@ -72,7 +71,7 @@
<q-card-section <q-card-section
class="text-weight-bolder text-uppercase text-h6 q-py-xs" class="text-weight-bolder text-uppercase text-h6 q-py-xs"
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-dark text-white' : 'bg-primary text-white'" :class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
style="line-height: 1em;" style="line-height: 1em;"
> >
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), { <span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {

View File

@ -10,65 +10,59 @@
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api'; import { useTimesheetApi } from 'src/modules/timesheets/composables/use-timesheet-api';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { provide } from 'vue';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
const { open } = useExpensesStore(); const expenses_store = useExpensesStore();
const shift_api = useShiftApi();
const { employeeEmail, dense = false } = defineProps<{
employeeEmail: string;
dense?: boolean;
}>();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_api = useTimesheetApi(); const timesheet_api = useTimesheetApi();
const shift_api = useShiftApi();
provide('employeeEmail', employeeEmail); const { mode = 'normal' } = defineProps<{
mode?: 'approval' | 'normal';
}>();
</script> </script>
<template> <template>
<div class="column flex-center full-width"> <div
class="column flex-center full-width"
<LoadingOverlay v-model="timesheet_store.is_loading"/> :class="mode === 'approval' ? 'bg-dark q-px-sm q-pb-sm q-mb-md rounded-10 shadow-10' : ''"
>
<LoadingOverlay v-model="timesheet_store.is_loading" />
<q-card <q-card
flat flat
class="transparent full-width" class="transparent full-width"
> >
<q-card-section <q-card-section
v-if="!dense"
:horizontal="$q.screen.gt.sm" :horizontal="$q.screen.gt.sm"
class="q-px-md items-center q-mb-md" class="q-px-md items-center q-mb-md"
:class="$q.screen.lt.md ? 'column' : ''" :class="$q.screen.lt.md ? 'column' : ''"
> >
<!-- navigation btn --> <!-- navigation btn -->
<PayPeriodNavigator <PayPeriodNavigator
v-if="!dense" v-if="mode === 'normal'"
@date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value, employeeEmail)" @date-selected="date_value => timesheet_api.getTimesheetsByDate(date_value)"
@pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)" @pressed-previous-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
@pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod(employeeEmail)" @pressed-next-button="timesheet_api.getTimesheetsByCurrentPayPeriod"
/> />
<!-- mobile expenses button --> <!-- mobile expenses button -->
<q-btn <q-btn
v-if="$q.screen.lt.md" v-if="$q.screen.lt.md && mode === 'normal'"
push push
rounded rounded
color="accent" color="accent"
icon="receipt_long" icon="receipt_long"
:label="$t('timesheet.expense.open_btn')" :label="$t('timesheet.expense.open_btn')"
class="q-mt-sm" class="q-mt-sm"
@click="open" @click="expenses_store.open"
/> />
<!-- shift's colored legend -->
<!-- <ShiftListLegend :is-loading="false" /> -->
<q-space /> <q-space />
<!-- save timesheet changes button --> <!-- save timesheet changes button -->
<q-btn <q-btn
v-if="$q.screen.gt.sm" v-if="mode === 'normal'"
push push
rounded rounded
:disable="timesheet_store.is_loading" :disable="timesheet_store.is_loading"
@ -81,13 +75,13 @@
<!-- desktop expenses button --> <!-- desktop expenses button -->
<q-btn <q-btn
v-if="$q.screen.gt.sm" v-if="mode === 'normal'"
push push
rounded rounded
color="accent" color="accent"
icon="receipt_long" icon="receipt_long"
:label="$t('timesheet.expense.open_btn')" :label="$t('timesheet.expense.open_btn')"
@click="open" @click="expenses_store.open"
/> />
</q-card-section> </q-card-section>
@ -96,7 +90,25 @@
<TimesheetErrorWidget /> <TimesheetErrorWidget />
</q-card-section> </q-card-section>
<ShiftList :dense="dense" /> <ShiftList :mode="mode" />
<q-card-section
horizontal
class="q-my-md"
>
<q-space />
<q-btn
v-if="mode === 'approval'"
push
rounded
:disable="timesheet_store.is_loading"
color="accent"
icon="upload"
:label="$t('shared.label.save')"
class="q-mr-md"
@click="shift_api.saveShiftChanges"
/>
</q-card-section>
</q-card> </q-card>
<ExpenseDialog /> <ExpenseDialog />
</div> </div>

View File

@ -10,14 +10,14 @@ export const useExpensesApi = () => {
const upsertExpense = async (expense: Expense): Promise<void> => { const upsertExpense = async (expense: Expense): Promise<void> => {
const success = await expenses_store.upsertExpense(expense); const success = await expenses_store.upsertExpense(expense);
if (success) { if (success) {
timesheet_store.getTimesheetsByEmployeeEmail(); timesheet_store.getTimesheetsByOptionalEmployeeEmail();
} }
}; };
const deleteExpenseById = async (expense_id: number): Promise<void> => { const deleteExpenseById = async (expense_id: number): Promise<void> => {
const success = await expenses_store.deleteExpenseById(expense_id); const success = await expenses_store.deleteExpenseById(expense_id);
if (success) { if (success) {
timesheet_store.getTimesheetsByEmployeeEmail(); timesheet_store.getTimesheetsByOptionalEmployeeEmail();
} }
}; };

View File

@ -12,7 +12,7 @@ export const useShiftApi = () => {
const success = await shift_store.deleteShiftById(shift_id); const success = await shift_store.deleteShiftById(shift_id);
if (success) { if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(auth_store.user?.email ?? '');
} }
timesheet_store.is_loading = false; timesheet_store.is_loading = false;
@ -25,7 +25,7 @@ export const useShiftApi = () => {
const update_success = await shift_store.updateShifts(); const update_success = await shift_store.updateShifts();
if (create_success || update_success){ if (create_success || update_success){
await timesheet_store.getTimesheetsByEmployeeEmail(auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(auth_store.user?.email ?? '');
} }
timesheet_store.is_loading = false; timesheet_store.is_loading = false;

View File

@ -1,16 +1,14 @@
import { useAuthStore } from "src/stores/auth-store";
import { useTimesheetStore } from "src/stores/timesheet-store" import { useTimesheetStore } from "src/stores/timesheet-store"
export const useTimesheetApi = () => { export const useTimesheetApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore();
const getTimesheetsByDate = async (date_string: string, employee_email?: string) => { const getTimesheetsByDate = async (date_string: string, employee_email?: string) => {
timesheet_store.is_loading = true; timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
if (success) { if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
timesheet_store.is_loading = false; timesheet_store.is_loading = false;
} }
@ -24,7 +22,7 @@ export const useTimesheetApi = () => {
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
if (success) { if (success) {
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email ?? auth_store.user?.email ?? ''); await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
timesheet_store.is_loading = false; timesheet_store.is_loading = false;
} }

View File

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

View File

@ -5,10 +5,12 @@
<template> <template>
<q-layout view="hHh lpR fFf"> <q-layout view="hHh lpR fFf">
<q-page-container class="bg-secondary"> <q-page-container class="bg-secondary">
<q-page class="column"> <q-page class="row">
<q-img src="src/assets/village.png" fit="contain" class="col absolute-bottom-right" style="opacity: 50%;" /> <q-img src="src/assets/village.png" fit="cover" :class="$q.screen.lt.md ? 'absolute-bottom' : 'absolute-right'" />
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" class="col absolute-center"> <transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" class="col-xs-10 absolute-center">
<LoginConnectionPanel /> <div class="col-sm-10 col-md-auto">
<LoginConnectionPanel />
</div>
</transition> </transition>
</q-page> </q-page>
</q-page-container> </q-page-container>

View File

@ -15,13 +15,6 @@
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const is_details_dialog_open = ref(false);
const employee_email = ref('');
const onDetailsClicked = (email: string) => {
employee_email.value = email;
is_details_dialog_open.value = true;
};
onMounted(async () => { onMounted(async () => {
await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD')); await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
@ -41,8 +34,6 @@
/> />
<DetailsDialog <DetailsDialog
v-model:dialog="is_details_dialog_open"
:employee-email="employee_email"
:is-loading="timesheet_store.is_loading" :is-loading="timesheet_store.is_loading"
:employee-overview="timesheet_store.current_pay_period_overview" :employee-overview="timesheet_store.current_pay_period_overview"
:timesheets="timesheet_store.timesheets" :timesheets="timesheet_store.timesheets"
@ -50,12 +41,13 @@
<div <div
class="col-auto full-width q-px-lg" class="col-auto full-width q-px-lg"
:class="($q.screen.lt.md ? 'text-center' : 'row') + (timesheet_store.is_approval_grid_mode ? ' q-mb-sm' : ' q-mb-md')" :class="($q.screen.lt.md ? 'column flex-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"
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews" @pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews" @pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
:class="$q.screen.lt.md ? 'q-mb-sm' : ''"
/> />
<q-space /> <q-space />
@ -67,25 +59,27 @@
color="white" color="white"
text-color="accent" text-color="accent"
toggle-color="accent" toggle-color="accent"
class="q-mr-md" :class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-md'"
:options="[ :options="[
{ icon: 'grid_view', value: true }, { icon: 'grid_view', value: true },
{ icon: 'view_list', value: false }, { icon: 'view_list', value: false },
]" ]"
/> />
<q-btn-dropdown <div class="col-auto row no-wrap flex-center" :class="$q.screen.lt.md ? 'q-mb-md' : ''">
push <q-btn-dropdown
rounded push
icon="filter_alt" rounded
color="accent" icon="filter_alt"
:label="$t('shared.label.filter')" color="accent"
class="q-mr-md" :label="$q.screen.lt.md ? '' : $t('shared.label.filter')"
/> class="col-auto q-mr-sm"
/>
<QTableFilters v-model:search="timesheet_store.search_filter" /> <QTableFilters v-model:search="timesheet_store.search_filter" />
</div>
</div> </div>
<OverviewList class="col" @clickedDetailsButton="onDetailsClicked" /> <OverviewList class="col" />
</q-page> </q-page>
</template> </template>

View File

@ -45,7 +45,6 @@ export const useExpensesStore = defineStore('expenses', () => {
const deleteExpenseById = async (expense_id: number): Promise<boolean> => { const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
const data = await ExpenseService.deleteExpenseById(expense_id); const data = await ExpenseService.deleteExpenseById(expense_id);
console.log('data received from expense deletion: ', data);
return data.success; return data.success;
} }

View File

@ -12,13 +12,16 @@ import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-ap
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const is_loading = ref<boolean>(false); const is_loading = ref<boolean>(false);
const pay_period = ref<PayPeriod>(); const pay_period = ref<PayPeriod>();
const pay_period_overviews = 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 pay_period_overviews = ref<TimesheetOverview[]>([]);
const is_details_dialog_open = ref(false);
const selected_employee_name = ref<string>();
const current_pay_period_overview = ref<TimesheetOverview>();
const search_filter = ref<string | number | null>(''); const search_filter = ref<string | number | null>('');
const is_approval_grid_mode = ref<boolean>(true);
const pay_period_report = ref(); const pay_period_report = ref();
const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => { const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
@ -64,11 +67,11 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
}; };
const getTimesheetsByEmployeeEmail = async (employee_email?: string) => { const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
if (pay_period.value === undefined) return;
is_loading.value = true; is_loading.value = true;
let response; let response;
if (pay_period.value === undefined) return;
try { try {
if (employee_email) { if (employee_email) {
response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no, employee_email); response = await timesheetService.getTimesheetsByPayPeriodAndOptionalEmail(pay_period.value.pay_year, pay_period.value.pay_period_no, employee_email);
@ -108,6 +111,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return { return {
is_loading, is_loading,
is_approval_grid_mode, is_approval_grid_mode,
is_details_dialog_open,
search_filter, search_filter,
pay_period, pay_period,
pay_period_overviews, pay_period_overviews,
@ -117,7 +121,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
initial_timesheets, initial_timesheets,
getPayPeriodByDateOrYearAndNumber, getPayPeriodByDateOrYearAndNumber,
getTimesheetOverviews, getTimesheetOverviews,
getTimesheetsByEmployeeEmail, getTimesheetsByOptionalEmployeeEmail,
getPayPeriodReportByYearAndPeriodNumber, getPayPeriodReportByYearAndPeriodNumber,
}; };
}); });