Merge pull request 'dev/nicolas/timesheet-gui-refactor' (#26) from dev/nicolas/timesheet-gui-refactor into main

Reviewed-on: Targo/targo_frontend#26
This commit is contained in:
Nicolas 2025-11-24 09:17:17 -05:00
commit a2f33b3145
32 changed files with 698 additions and 611 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 7.4 MiB

View File

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

View File

@ -17,7 +17,7 @@ $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",
@ -206,6 +206,7 @@ export default {
table: { table: {
full_name: "full name", full_name: "full name",
email: "email address", email: "email address",
is_approved: "approval",
expenses: "expenses", expenses: "expenses",
mileage: "mileage", mileage: "mileage",
verified: "approved", verified: "approved",

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",
@ -207,6 +207,7 @@ export default {
table: { table: {
full_name: "nom complet", full_name: "nom complet",
email: "courriel", email: "courriel",
is_approved: "approuvé",
expenses: "dépenses", expenses: "dépenses",
mileage: "kilométrage", mileage: "kilométrage",
verified: "approuvé", verified: "approuvé",

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

@ -18,11 +18,11 @@
<transition appear enter-active-class="animated slow flipInX" leave-active-class="animated flipOutX"> <transition appear enter-active-class="animated slow flipInX" leave-active-class="animated flipOutX">
<q-card class="col-3 items-center"> <q-card class="col-3 items-center">
<q-card-section class="row justify-center "> <q-card-section class="row justify-center ">
<q-icon name="check_circle" color="green" size="xl" /> <q-icon name="check_circle" color="accent" size="xl" />
</q-card-section> </q-card-section>
<q-separator inset color="primary" /> <q-separator inset color="accent" />
<q-card-section class="row justify-center"> <q-card-section class="row justify-center">
<span class="row text-primary text-h3">Login Successful!</span> <span class="row text-h3">Login Successful!</span>
</q-card-section> </q-card-section>
</q-card> </q-card>
</transition> </transition>

View File

@ -0,0 +1,21 @@
<script
setup
lang="ts"
>
const is_loading = defineModel < boolean > ({ required: true });
</script>
<template>
<q-dialog
v-model="is_loading"
transition-show="jump-down"
transition-hide="jump-down"
>
<q-card class="q-pa-xl rounded-200 frosted-glass">
<q-spinner-radio
color="accent"
size="20vh"
/>
</q-card>
</q-dialog>
</template>

View File

@ -88,7 +88,7 @@
rounded rounded
icon="calendar_month" icon="calendar_month"
color="accent" color="accent"
@click="is_showing_calendar_picker = true" @click="is_showing_calendar_picker = !is_showing_calendar_picker"
:disable="timesheet_store.is_loading || is_disabled" :disable="timesheet_store.is_loading || is_disabled"
class="q-px-xl" class="q-px-xl"
> >
@ -102,6 +102,8 @@
<!-- date picker calendar --> <!-- date picker calendar -->
<q-menu <q-menu
v-model="is_showing_calendar_picker"
no-parent-event
anchor="bottom middle" anchor="bottom middle"
self="top middle" self="top middle"
:offset="[0, 10]" :offset="[0, 10]"

View File

@ -1,19 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
const search_model = defineModel<string | number | null>({ default: null, required: true }); const search_model = defineModel<string | number | null>('search', { default: null, required: true });
</script> </script>
<template> <template>
<!-- Filters toggle -->
<q-btn-dropdown
push
rounded
icon="filter_alt"
color="accent"
:label="$t('shared.label.filter')"
class="q-mr-md"
/>
<!-- Search bar -->
<q-input <q-input
v-model="search_model" v-model="search_model"
outlined outlined

View File

@ -4,10 +4,10 @@
> >
/* 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 } 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';
@ -21,44 +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'> => { onMounted(() => {
// const all_days = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.expenses)); expenses_dataset.value = [
// const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts)) {
label: t('timesheet_approvals.table.expenses'),
// const all_costs = all_days.map(day => day.total_expenses); data: all_days.map(day => (day.expenses + day.on_call + day.per_diem)),
// console.log('costs, ', all_costs); backgroundColor: colors.getPaletteColor('accent'),
// const all_mileage = all_days.map(day => day.total_mileage); },
{
label: t('timesheet_approvals.table.mileage'),
// expenses_dataset.value = [ data: all_days.map(day => day.mileage),
// { backgroundColor: colors.getPaletteColor('info'),
// label: t('timesheet_approvals.table.expenses'), }
// data: all_costs, ]
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), });
// },
// {
// label: t('timesheet_approvals.table.mileage'),
// data: all_mileage,
// backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
// }
// ]
// expenses_labels.value = all_days_dates.map(day => day.short_date);
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

@ -1,73 +1,78 @@
<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, onMounted } 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 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(() => {
hours_worked_dataset.value = datasetConfig.map(cfg => ({
// const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts)); label: cfg.label,
// const datasetConfig = [ data: all_days.value.map(day => day.daily_hours[cfg.key]),
// { backgroundColor: cfg.color,
// 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 => ({
// label: cfg.label,
// data: all_days.map(day => day[ cfg.key ]),
// backgroundColor: cfg.color,
// }));
// 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>
<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';
@ -17,46 +17,45 @@
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], }]); 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.emergency_hours.toString() + 'h',
timesheet_store.current_pay_period_overview!.other_hours.overtime_hours.toString() + 'h',
]);
const shift_type_totals = ref<ChartDataset<'doughnut'>[]>([]);
// shift_type_totals.value = [{ onMounted(() => {
// data: [ shift_type_totals.value = [{
// current_pay_period_overview.regular_hours, data: [
// current_pay_period_overview.other_hours.evening_hours, timesheet_store.current_pay_period_overview!.regular_hours,
// current_pay_period_overview.other_hours.emergency_hours, timesheet_store.current_pay_period_overview!.other_hours.evening_hours,
// current_pay_period_overview.other_hours.overtime_hours, timesheet_store.current_pay_period_overview!.other_hours.emergency_hours,
// ], timesheet_store.current_pay_period_overview!.other_hours.overtime_hours,
// backgroundColor: [ ],
// colors.getPaletteColor('green-5'), // Regular backgroundColor: [
// colors.getPaletteColor('green-9'), // Evening colors.getPaletteColor('accent'), // Regular
// getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), // Emergency colors.getPaletteColor('green-10'), // Evening
// getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), // Overtime colors.getPaletteColor('warning'), // Emergency
// ] colors.getPaletteColor('negative'), // Overtime
// }]; ]
}]
// shift_type_labels.value = [ });
// current_pay_period_overview.regular_hours.toString() + 'h',
// current_pay_period_overview.other_hours.evening_hours.toString() + 'h',
// current_pay_period_overview.other_hours.emergency_hours.toString() + 'h',
// current_pay_period_overview.other_hours.overtime_hours.toString() + 'h',
// ]
const data = {
labels: shift_type_labels.value,
datasets: shift_type_totals.value,
}
</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,66 +11,43 @@
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 render_key = ref(1); 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"
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 hide-scrollbar"
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'" :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="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto" <span>{{ timesheet_store.selected_employee_name }}</span>
>
<span>TODO: Name goes here</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-md rounded-10 no-wrap"
> >
<DetailsDialogChartHoursWorked <DetailsDialogChartHoursWorked class="col" />
:key="render_key" <DetailsDialogChartShiftTypes class="col q-ma-lg" />
class="col" <DetailsDialogChartExpenses class="col" />
/>
<DetailsDialogChartShiftTypes
:key="render_key + 1"
class="col-2 q-ma-lg"
/>
<DetailsDialogChartExpenses
: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 /> <ExpenseDialogList />
<ExpenseDialogList
horizontal
:employee-email="employeeEmail"
/>
<q-separator />
</q-card-section> </q-card-section>
<!-- list of shifts --> <!-- list of shifts -->
@ -78,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

@ -5,21 +5,37 @@
import type { TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models'; import type { TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
const modelApproval = defineModel<boolean>(); const modelApproval = defineModel<boolean>();
const { row } = defineProps<{ row: TimesheetOverview; }>();
const { row, index = 0 } = defineProps<{
row: TimesheetOverview;
index?: number;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
'clickDetails': [overview: TimesheetOverview]; 'clickDetails': [overview: TimesheetOverview];
}>(); }>();
</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">
<q-card class="rounded-10"> <transition
appear
enter-active-class="animated fadeInUp"
>
<q-card
class="rounded-10 shadow-5"
:style="`animation-delay: ${index / 15}s;`"
>
<!-- Card header with employee name and details button--> <!-- Card header with employee name and details button-->
<q-card-section <q-card-section
horizontal horizontal
class="q-py-none q-px-sm q-ma-none justify-between items-center" class="q-py-none q-px-sm q-ma-none justify-between items-center bg-primary text-white"
> >
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span> <div>
<span class="text-h5 text-uppercase text-weight-medium text-accent q-mr-xs">{{ row.employee_name.split(' ')[0]
}}</span>
<span class="text-uppercase text-weight-light">{{ row.employee_name.split(' ')[1] }}</span>
</div>
<!-- Buttons to view detailed shifts or view employee timesheet --> <!-- Buttons to view detailed shifts or view employee timesheet -->
<q-btn <q-btn
@ -28,32 +44,37 @@
square square
unelevated unelevated
class="col-auto q-pa-none q-ma-none" class="col-auto q-pa-none q-ma-none"
color="primary" color="accent"
icon="work_history" icon="work_history"
@click="emit('clickDetails', row)" @click="emit('clickDetails', row)"
> >
<q-tooltip <q-tooltip
anchor="top middle" anchor="top middle"
self="center middle" self="center middle"
class="bg-primary text-uppercase text-weight-bold" class="bg-accent text-uppercase text-weight-bold"
> >
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }} {{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
</q-card-section> </q-card-section>
<q-separator size="2px" />
<!-- Main body of pay period card --> <!-- Main body of pay period card -->
<q-card-section class="q-py-none q-px-sm q-my-sm"> <q-card-section class="q-py-none q-px-sm q-py-sm bg-dark">
<div class="row"> <div class="row">
<!-- left portion of pay period card --> <!-- left portion of pay period card -->
<div class="col column q-px-sm"> <div class="col column q-px-sm">
<!-- Regular hours segment --> <!-- Regular hours segment -->
<div class="col column"> <div class="col column">
<span class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"> {{ $t('shared.shift_type.regular') }} </span> <span
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span> class="text-weight-bold text-uppercase q-pa-none q-my-none"
<q-separator class="q-mx-sm" /> :class="row.regular_hours > 80 ? 'text-negative' : 'text-accent'"
> {{
$t('shared.shift_type.regular') }} </span>
<span
class="text-weight-bolder text-h3 q-py-none"
:class="row.regular_hours > 80 ? 'text-negative' : ''"
> {{ row.regular_hours }} </span>
<q-separator class="q-mr-sm" />
</div> </div>
<!-- Other hour types segment --> <!-- Other hour types segment -->
@ -65,7 +86,7 @@
:class="hour_type === 0 ? 'invisible' : ''" :class="hour_type === 0 ? 'invisible' : ''"
> >
<span <span
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none" class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
style="font-size: 0.7em;" style="font-size: 0.7em;"
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span> > {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
<span <span
@ -85,57 +106,60 @@
<div class="col-auto column q-px-sm"> <div class="col-auto column q-px-sm">
<div class="col column no-wrap"> <div class="col column no-wrap">
<span <span
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none" class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
style="font-size: 0.8em;" style="font-size: 0.8em;"
> {{ $t('timesheet.expense.types.EXPENSES') }} </span> > {{ $t('timesheet.expense.types.EXPENSES') }} </span>
<span <span
class="text-weight-bolder text-h6 q-pa-none" class="text-weight-bolder text-h6 q-pa-none"
style="line-height: 0.9em;" style="line-height: 0.9em;"
> {{ row.expenses }} </span> > {{ row.expenses }} <span class="text-weight-light">$</span> </span>
</div> </div>
<div class="col column no-wrap"> <div class="col column no-wrap">
<span <span
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none" class="text-weight-bold text-accent text-uppercase q-pa-none q-my-none"
style="font-size: 0.8em;" style="font-size: 0.8em;"
> {{ $t('timesheet.expense.types.MILEAGE') }} </span> > {{ $t('timesheet.expense.types.MILEAGE') }} </span>
<span <span
class="text-weight-bolder text-h6 q-pa-none" class="text-weight-bolder text-h6 q-pa-none"
style="line-height: 0.9em;" style="line-height: 0.9em;"
> {{ row.mileage }} </span> > {{ row.mileage }} <span class="text-weight-light text-body2">km</span></span>
</div> </div>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<q-separator
color="primary"
size="2px"
/>
<!-- Validate Pay Period section --> <!-- Validate Pay Period section -->
<q-card-section <q-card-section
horizontal horizontal
class="justify-between items-center text-weight-bold q-px-sm" class="justify-between items-center text-weight-bold q-pa-none"
:class="row.is_approved ? 'text-white bg-primary' : 'bg-dark'" :class="row.is_approved ? 'text-white bg-accent' : 'bg-dark text-accent'"
> >
<div class="col-auto"> <div class="col-auto">
<span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span> <span class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours }} </span>
<span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span> <span class="text-uppercase text-weight-bold text-caption q-ml-xs"> total </span>
</div> </div>
<div
class="col-auto q-py-xs q-px-md"
style="border: 1px solid var(--q-accent);"
>
<q-checkbox <q-checkbox
v-model="modelApproval" v-model="modelApproval"
dense dense
left-label left-label
keep-color
size="lg" size="lg"
checked-icon="lock" checked-icon="lock"
unchecked-icon="lock_open" unchecked-icon="lock_open"
:color="row.is_approved ? 'white' : 'primary'" :color="row.is_approved ? 'white' : 'accent'"
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')" :label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="col-auto text-uppercase" class="text-uppercase"
:class="row.is_approved ? '' : 'text-accent'"
/> />
</div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</transition>
</div> </div>
</template> </template>

View File

@ -4,96 +4,70 @@
> >
/* eslint-disable */ /* eslint-disable */
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
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 OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue'; import { overview_column_names, pay_period_overview_columns, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import { 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();
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const filter = ref<string | number | null>(''); const visible_columns = ref<string[]>([
const is_grid_mode = ref(true); overview_column_names.REGULAR,
const IS_ABNORMAL_SHIFT = ['OVERTIME', 'EMERGENCY']; overview_column_names.EVENING,
const IS_PTO = ['HOLIDAY', 'VACATION', 'SICK']; overview_column_names.EMERGENCY,
overview_column_names.SICK,
const employeeEmail = defineModel(); overview_column_names.VACATION,
overview_column_names.HOLIDAY,
const visible_columns = ref<string[]>(['REGULAR', 'email']); overview_column_names.OVERTIME,
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;
// 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" />
<transition
appear
enter-active-class="animated fadeInUp"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<q-table <q-table
:key="timesheet_store.is_approval_grid_mode ? 'grid' : 'list'"
:visible-columns="visible_columns" :visible-columns="visible_columns"
:rows="overview_rows" :rows="overview_rows"
:columns="pay_period_overview_columns" :columns="pay_period_overview_columns"
row-key="email" row-key="email"
:filter="filter" :filter="timesheet_store.search_filter"
:grid="is_grid_mode" :grid="timesheet_store.is_approval_grid_mode"
dense :dense="timesheet_store.is_approval_grid_mode"
hide-pagination hide-pagination
color="primary" color="accent"
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
card-container-class="justify-center" card-container-class="justify-center"
:loading="timesheet_store.is_loading" class="bg-transparent"
class="q-py-md bg-transparent" :class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
:class="is_grid_mode ? '' : 'sticky-header-table no-shadow'" table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15"
table-class="q-pa-none q-py-none q-mx-md rounded-10 bg-dark shadow-4'"
: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 #top>
<div
class="full-width"
:class="$q.screen.lt.md ? 'text-center' : 'row'"
>
<PayPeriodNavigator
@date-selected="timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber"
/>
<q-space />
<q-btn-toggle
v-model="is_grid_mode"
push
color="white"
text-color="accent"
toggle-color="accent"
class="q-mr-md"
:options="[
{ icon: 'grid_view', value: true },
{ icon: 'view_list', value: false },
]"
/>
<QTableFilters v-model="filter" />
</div>
</template>
<template #header="props"> <template #header="props">
<q-tr <q-tr
:props="props" :props="props"
@ -104,10 +78,7 @@
:key="col.name" :key="col.name"
:props="props" :props="props"
> >
<span <span class="text-uppercase text-weight-bolder text-white">
v-if="col.label !== 'timesheet_approvals.table.is_approved'"
class="text-uppercase text-weight-bolder text-white"
>
{{ $t(col.label) }} {{ $t(col.label) }}
</span> </span>
</q-th> </q-th>
@ -119,24 +90,56 @@
:props="props" :props="props"
class="text-weight-medium" class="text-weight-medium"
> >
<span <transition
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'" appear
>{{ props.value }}</span> enter-active-class="animated fadeInUp slow"
<q-icon leave-active-class="animated fadeOutDown"
v-if="typeof props.value === 'boolean'" mode="out-in"
:name="props.value ? 'verified' : 'fiber_manual_record'" >
:color="props.value ? 'primary' : 'grey-5'" <div
size="sm" :key="props.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
class="rounded-5"
style="font-size: 1.2em;"
:style="`animation-delay: ${props.rowIndex / 30}s;`"
>
<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, key: string }"> <template #item="props: { row: TimesheetOverview, rowIndex: number }">
<OverviewListItem <OverviewListItem
v-model="props.row.is_approved" v-model="props.row.is_approved"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
:index="props.rowIndex"
:row="props.row" :row="props.row"
@click-details="overview => onClickedDetails(props.row.email, overview)" @click-details="overview => onClickedDetails(overview)"
/> />
</template> </template>
@ -154,6 +157,7 @@
</div> </div>
</template> </template>
</q-table> </q-table>
</transition>
</div> </div>
</template> </template>
@ -167,7 +171,7 @@
position: sticky position: sticky
z-index: 1 z-index: 1
thead tr:first-child th thead tr:first-child th
top: 0 top: 0px
&.q-table--loading thead tr:last-child th &.q-table--loading thead tr:last-child th
top: 48px top: 48px

View File

@ -1,46 +1,29 @@
import { useTimesheetStore } from "src/stores/timesheet-store"; import { useTimesheetStore } from "src/stores/timesheet-store";
import { useAuthStore } from "src/stores/auth-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";
import { NavigatorConstants } from "src/modules/timesheet-approval/models/timesheet-overview.models";
export const useTimesheetApprovalApi = () => { export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore(); const DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
const getPayPeriodOverviewsByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<void> => { const getTimesheetOverviews = async () => {
let success = false; timesheet_store.is_loading = true;
if (typeof date_or_year === 'string') success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year);
else if (typeof date_or_year === 'number' && period_number) success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year, period_number);
if (success) { const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
await timesheet_store.getTimesheetOverviewsByPayPeriod( if (success) await timesheet_store.getTimesheetOverviews();
timesheet_store.pay_period?.pay_year ?? 1,
timesheet_store.pay_period?.pay_period_no ?? 1,
auth_store.user?.email
);
}
};
const getNextOrPreviousPayPeriodOverview = async (direction: number) => { timesheet_store.is_loading = false;
if (timesheet_store.pay_period === undefined) return;
let new_period_number = (timesheet_store.pay_period.pay_period_no) + direction;
let new_year = timesheet_store.pay_period.pay_year;
if ( new_period_number > 26 || new_period_number < 1) {
new_period_number = 1;
new_year += direction;
} }
await getPayPeriodOverviewsByDateOrYearAndNumber(new_year, new_period_number); const getTimesheetOverviewsByDate = async (date: string) => {
}; const valid_date = DATE_REGEX.test(date);
timesheet_store.is_loading = true;
const getNextPayPeriodOverview = async () => { if (valid_date) {
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.NEXT_PERIOD); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date);
}; if (success) await timesheet_store.getTimesheetOverviews();
}
const getPreviousPayPeriodOverview = async () => { timesheet_store.is_loading = false;
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.PREVIOUS_PERIOD);
}; };
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) => {
@ -61,9 +44,8 @@ export const useTimesheetApprovalApi = () => {
}; };
return { return {
getPayPeriodOverviewsByDateOrYearAndNumber, getTimesheetOverviewsByDate,
getTimesheetApprovalCSVReport, getTimesheetApprovalCSVReport,
getNextPayPeriodOverview, getTimesheetOverviews,
getPreviousPayPeriodOverview,
} }
}; };

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

@ -3,8 +3,8 @@ import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-ap
import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models"; import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/models/timesheet-overview.models";
export const timesheetApprovalService = { export const timesheetApprovalService = {
getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverviewResponse> => { getPayPeriodOverviews: async (year: number, period_number: number): Promise<PayPeriodOverviewResponse> => {
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get(`pay-periods/crew/${year}/${period_number}`);
return response.data; return response.data;
}, },

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

@ -105,7 +105,7 @@
<q-btn <q-btn
v-if="ui_store.is_mobile_mode && !dense" v-if="ui_store.is_mobile_mode && !dense"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'" :icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? ((shift.is_approved || isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'" :text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
class="col-auto full-height q-mx-xs rounded-5 shadow-1" class="col-auto full-height q-mx-xs rounded-5 shadow-1"
> >
<q-popup-edit <q-popup-edit
@ -160,8 +160,8 @@
v-model="shift_type_selected" v-model="shift_type_selected"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense dense
:borderless="(shift.is_approved || isTimesheetApproved)" :borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved || isTimesheetApproved)" :readonly="(shift.is_approved && isTimesheetApproved)"
:options-dense="!ui_store.is_mobile_mode" :options-dense="!ui_store.is_mobile_mode"
hide-dropdown-icon hide-dropdown-icon
:menu-offset="[0, 10]" :menu-offset="[0, 10]"
@ -169,8 +169,8 @@
menu-self="top middle" menu-self="top middle"
:options="SHIFT_OPTIONS" :options="SHIFT_OPTIONS"
class="col rounded-5 q-mx-xs bg-dark" class="col rounded-5 q-mx-xs bg-dark"
:class="(shift.is_approved || isTimesheetApproved) ? 'inset-shadow' : ''" :class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5" popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect" @blur="onBlurShiftTypeSelect"
@ -191,33 +191,34 @@
<span <span
style="line-height: 0.9em;" style="line-height: 0.9em;"
class="col-auto ellipsis" class="col-auto ellipsis"
:class="(shift.is_approved || isTimesheetApproved) ? 'text-white' : ''" :class="!shift.is_approved ? '' : 'text-white'"
>{{ scope.opt.label }}</span> >{{ scope.opt.label }}</span>
</div> </div>
</template> </template>
</q-select> </q-select>
</div> </div>
<div class="col row flex-center text-uppercase rounded-5 bg-transparent q-pa-xs"> <div class="col row flex-center text-uppercase rounded-5 q-pa-xs">
<!-- punch in field --> <!-- punch in field -->
<q-input <q-input
v-model="shift.start_time" v-model="shift.start_time"
dense dense
:borderless="(shift.is_approved || isTimesheetApproved)" :borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved || isTimesheetApproved)" :readonly="(shift.is_approved && isTimesheetApproved)"
type="time" type="time"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
label-slot label-slot
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'" :label-color="!shift.is_approved ? 'accent' : 'white'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark" class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
> >
<template #label> <template #label>
<span <span
class="text-weight-bolder" class="text-weight-bolder"
:class="shift.is_approved ? ' q-ml-md' : ''"
style="font-size: 0.95em;" style="font-size: 0.95em;"
>{{ $t('shared.misc.in') }}</span> >{{ $t('shared.misc.in') }}</span>
</template> </template>
@ -228,20 +229,21 @@
v-model="shift.end_time" v-model="shift.end_time"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense dense
:borderless="(shift.is_approved || isTimesheetApproved)" :borderless="(shift.is_approved && isTimesheetApproved)"
:readonly="(shift.is_approved || isTimesheetApproved)" :readonly="(shift.is_approved && isTimesheetApproved)"
type="time" type="time"
label-slot label-slot
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'" :label-color="!shift.is_approved ? 'accent' : 'white'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark" class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''" :style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
> >
<template #label> <template #label>
<span <span
class="text-weight-bolder" class="text-weight-bolder"
:class="shift.is_approved ? ' q-ml-md' : ''"
style="font-size: 0.95em;" style="font-size: 0.95em;"
>{{ $t('shared.misc.out') }}</span> >{{ $t('shared.misc.out') }}</span>
</template> </template>
@ -325,7 +327,14 @@
class="col" class="col"
:class="shift.is_approved ? 'invisible' : ''" :class="shift.is_approved ? 'invisible' : ''"
@click="$emit('requestDelete')" @click="$emit('requestDelete')"
>
<q-badge
v-if="!shift.is_approved"
color="white"
class="absolute"
style="z-index: -1;"
/> />
</q-btn>
</div> </div>
</div> </div>
</div> </div>

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);
} }
} }
@ -44,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"
@ -73,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

@ -6,79 +6,63 @@
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue'; import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue'; import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue'; import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
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"
<q-dialog :class="mode === 'approval' ? 'bg-dark q-px-sm q-pb-sm q-mb-md rounded-10 shadow-10' : ''"
v-model="timesheet_store.is_loading"
transition-show="jump-down"
transition-hide="jump-down"
> >
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass"> <LoadingOverlay v-model="timesheet_store.is_loading" />
<q-spinner-radio
color="accent"
size="20vh"
/>
</q-card>
</q-dialog>
<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="timesheet_api.getTimesheetsByDate(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"
@ -91,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>
@ -106,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;
} }
@ -21,10 +19,10 @@ export const useTimesheetApi = () => {
if (timesheet_store.pay_period === undefined) return false; if (timesheet_store.pay_period === undefined) return false;
timesheet_store.is_loading = true; timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(timesheet_store.pay_period.pay_year, timesheet_store.pay_period.pay_period_no ); 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

@ -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,13 @@ 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}`); if (employee_email !== undefined) {
const response = await api.get<{success: boolean, data: TimesheetResponse, error? : string}>(`timesheets/${year}/${period_number}?employee_email=${employee_email}`);
return response.data.data; 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">
<div class="col-sm-10 col-md-auto">
<LoginConnectionPanel /> <LoginConnectionPanel />
</div>
</transition> </transition>
</q-page> </q-page>
</q-page-container> </q-page-container>

View File

@ -1,4 +1,8 @@
<script setup lang="ts"> <script
setup
lang="ts"
>
/* eslint-disable */
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api'; import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
@ -6,41 +10,76 @@
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue'; import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue'; import OverviewList from 'src/modules/timesheet-approval/components/overview-list.vue';
import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue'; import DetailsDialog from 'src/modules/timesheet-approval/components/details-dialog.vue';
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
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.getPayPeriodOverviewsByDateOrYearAndNumber(date.formatDate( new Date(), 'YYYY-MM-DD')); await timesheet_approval_api.getTimesheetOverviewsByDate(date.formatDate(new Date(), 'YYYY-MM-DD'));
}); });
</script> </script>
<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
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"
/> />
<OverviewList @clickedDetailsButton="onDetailsClicked"/> <div
class="col-auto full-width q-px-lg"
:class="($q.screen.lt.md ? 'column flex-center' : 'row') + (timesheet_store.is_approval_grid_mode ? ' q-mb-sm' : ' q-mb-md')"
>
<PayPeriodNavigator
@date-selected="timesheet_approval_api.getTimesheetOverviews"
@pressed-next-button="timesheet_approval_api.getTimesheetOverviews"
@pressed-previous-button="timesheet_approval_api.getTimesheetOverviews"
:class="$q.screen.lt.md ? 'q-mb-sm' : ''"
/>
<q-space />
<q-btn-toggle
v-model="timesheet_store.is_approval_grid_mode"
push
rounded
color="white"
text-color="accent"
toggle-color="accent"
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-md'"
:options="[
{ icon: 'grid_view', value: true },
{ icon: 'view_list', value: false },
]"
/>
<div class="col-auto row no-wrap flex-center" :class="$q.screen.lt.md ? 'q-mb-md' : ''">
<q-btn-dropdown
push
rounded
icon="filter_alt"
color="accent"
:label="$q.screen.lt.md ? '' : $t('shared.label.filter')"
class="col-auto q-mr-sm"
/>
<QTableFilters v-model:search="timesheet_store.search_filter" />
</div>
</div>
<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

@ -1,6 +1,5 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useAuthStore } from 'src/stores/auth-store';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone'; import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service'; import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service'; import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
@ -11,24 +10,27 @@ import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-ap
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const auth_store = useAuthStore();
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 initial_timesheets = ref<Timesheet[]>([]); const initial_timesheets = ref<Timesheet[]>([]);
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 is_approval_grid_mode = ref<boolean>(true);
const pay_period_report = ref(); const pay_period_report = ref();
const getPayPeriodByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<boolean> => { const getPayPeriodByDateOrYearAndNumber = async (date?: string): Promise<boolean> => {
try { try {
if (typeof date_or_year === 'string') { if (date !== undefined) {
pay_period.value = await timesheetService.getPayPeriodByDate(date_or_year); pay_period.value = await timesheetService.getPayPeriodByDate(date);
} } else if (pay_period.value !== undefined) {
else if (typeof date_or_year === 'number' && period_number) { pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(pay_period.value.pay_year, pay_period.value.pay_period_no);
pay_period.value = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number); } else return false;
}
else pay_period.value = undefined;
return true; return true;
} catch (error) { } catch (error) {
@ -41,11 +43,16 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
}; };
const getTimesheetOverviewsByPayPeriod = async (pay_year: number, period_number: number, supervisor_email?: string): Promise<boolean> => { const getTimesheetOverviews = async (): Promise<boolean> => {
is_loading.value = true; is_loading.value = true;
try { try {
const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail(pay_year, period_number, supervisor_email ?? auth_store.user?.email ?? ''); if (pay_period.value === undefined) {
is_loading.value = false;
return false;
}
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;
@ -60,15 +67,19 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} }
}; };
const getTimesheetsByEmployeeEmail = async (employee_email?: string) => { const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
is_loading.value = true;
if (pay_period.value === undefined) return; if (pay_period.value === undefined) return;
is_loading.value = true;
let response;
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;
@ -99,14 +110,18 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return { return {
is_loading, is_loading,
is_approval_grid_mode,
is_details_dialog_open,
search_filter,
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,
getTimesheetOverviewsByPayPeriod, getTimesheetOverviews,
getTimesheetsByEmployeeEmail, getTimesheetsByOptionalEmployeeEmail,
getPayPeriodReportByYearAndPeriodNumber, getPayPeriodReportByYearAndPeriodNumber,
}; };
}); });