refactor(approvals): massive refactor of names, DRY component scripts, separation of concern, trim unnecessary code

This commit is contained in:
Nicolas Drolet 2025-09-26 11:06:07 -04:00
parent f91a664a92
commit 3bf8c57f74
47 changed files with 902 additions and 1275 deletions

View File

@ -31,5 +31,5 @@ body.body--dark {
.body--light { .body--light {
--q-dark: #FFF; --q-dark: #FFF;
color: $grey-8; color: $blue-grey-8;
} }

View File

@ -74,6 +74,7 @@ export default {
}, },
label: { label: {
search: "search", search: "search",
filter: "filters",
loading: "loading...", loading: "loading...",
language: "Language", language: "Language",
add: "ajouter", add: "ajouter",

View File

@ -74,6 +74,7 @@ export default {
}, },
label: { label: {
search: 'recherche', search: 'recherche',
filter: "filtres",
loading: 'chargement en cours...', loading: 'chargement en cours...',
language: 'langue', language: 'langue',
add: "ajouter", add: "ajouter",

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import HeaderBarAvatar from './header-bar-avatar.vue'; import HeaderBarNotification from './main-layout-header-bar-notification.vue';
const uiStore = useUiStore(); const uiStore = useUiStore();
</script> </script>
@ -14,7 +14,7 @@
</q-btn> </q-btn>
</q-toolbar-title> </q-toolbar-title>
<q-item class="q-pa-none"> <q-item class="q-pa-none">
<HeaderBarAvatar /> <HeaderBarNotification />
</q-item> </q-item>
</q-toolbar> </q-toolbar>
</q-header> </q-header>

View File

@ -26,8 +26,16 @@
</script> </script>
<template> <template>
<q-drawer overlay elevated side="left" :mini="miniState" @mouseenter="miniState = false" <q-drawer
@mouseleave="miniState = true" v-model="uiStore.isRightDrawerOpen"> v-model="uiStore.isRightDrawerOpen"
overlay
elevated
side="left"
:mini="miniState"
@mouseenter="miniState = false"
@mouseleave="miniState = true"
class="bg-dark"
>
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<q-list> <q-list>
<!-- Home --> <!-- Home -->

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import HeaderBar from 'src/modules/shared/components/navigation/header-bar.vue'; import HeaderBar from 'src/modules/layouts/components/main-layout-header-bar.vue';
import FooterBar from 'src/modules/shared/components/navigation/footer-bar.vue'; import FooterBar from 'src/modules/layouts/components/main-layout-footer-bar.vue';
import RightDrawer from 'src/modules/shared/components/navigation/right-drawer.vue'; import RightDrawer from 'src/modules/layouts/components/main-layout-right-drawer.vue';
</script> </script>
<template> <template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
const { title, startDate = "", endDate = "" } = defineProps<{
title: string;
startDate: string;
endDate: string;
}>();
const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', };
</script>
<template>
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
<span class="col">{{ $t(title) }}</span>
<div
v-if="startDate.length > 0"
class="col row flex-center full-width q-py-none q-my-none"
>
<div class="text-primary text-weight-bold text-h6">
{{ $d(new Date(startDate), date_format_options) }}
</div>
<div class="text-body2 q-mx-md text-weight-medium">
{{ $t('shared.misc.to') }}
</div>
<div class="text-primary text-weight-bold text-h6">
{{ $d(new Date(endDate), date_format_options) }}
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
const search_model = defineModel<string | number | null>({ default: null, required: true });
</script>
<template>
<!-- Filters toggle -->
<q-btn-dropdown
push
class="q-mr-md bg-white text-primary left-rounded"
:label="$t('shared.label.filter')"
icon="filter_alt"
/>
<!-- Search bar -->
<q-input
v-model="search_model"
outlined
dense
debounce="300"
class="right-rounded"
:label="$t('shared.label.search')"
label-color="primary"
bg-color="white"
color="primary"
>
<template #before>
<q-icon
name="search"
color="primary"
/>
</template>
</q-input>
</template>
<style scoped>
.left-rounded {
border-radius: 50% 0 0 50%;
}
.right-rounded {
border-radius: 0 50% 50% 0;
}
</style>

View File

@ -1,67 +0,0 @@
<script setup lang="ts">
/* eslint-disable */
import { ref } from 'vue';
import { date } from 'quasar';
import type { QDateDetails } from 'src/modules/shared/types/q-date-details';
const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
const props = defineProps<{
isDisabled: boolean,
isPreviousLimit: boolean,
}>();
const emit = defineEmits<{
'date-selected': [value: string, reason?: string, details?: QDateDetails]
'pressed-previous-button': []
'pressed-next-button': []
}>();
const onDateSelected = (value: string, reason: string, details: QDateDetails) => {
calendar_date.value = value;
is_showing_calendar_picker.value = false;
emit('date-selected', value, reason, details);
}
</script>
<template>
<div class="row justify-center">
<q-btn
push rounded
icon="keyboard_arrow_left"
color="primary"
@click="emit('pressed-previous-button')"
:disable="props.isPreviousLimit || props.isDisabled"
class="q-mr-sm q-px-sm"
/>
<q-btn
push rounded
icon="date_range"
color="primary"
@click="is_showing_calendar_picker = true"
:disable="props.isDisabled"
class="q-px-lg"
/>
<q-btn
push rounded
icon="keyboard_arrow_right"
color="primary"
@click="emit('pressed-next-button')"
:disable="props.isDisabled"
class="q-ml-sm q-px-sm"
/>
</div>
<q-dialog v-model="is_showing_calendar_picker" transition-show="jump-down" transition-hide="jump-up" position="top">
<q-date
v-model="calendar_date"
color="primary"
class="q-mt-xl"
today-btn
mask="YYYY-MM-DD"
:options="date => date > '2023/12/16'"
@update:model-value="onDateSelected"
/>
</q-dialog>
</template>

View File

@ -1,42 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
searchModel: string | number | null
}>();
const emit = defineEmits<{
onSearchValueUpdated: [value: string | number | null]
}>();
</script>
<template>
<!-- Filters toggle -->
<q-btn-dropdown
rounded
push
class="q-mr-md bg-white text-primary"
label="filters"
icon="filter_alt"
/>
<!-- Search bar -->
<q-input
outlined
dense
rounded
debounce="300"
:label="$t('shared.label.search')"
label-color="primary"
bg-color="white"
color="primary"
:model-value="props.searchModel"
@update:model-value="value => emit('onSearchValueUpdated', value)"
>
<template v-slot:append>
<q-icon
name="search"
color="primary"
/>
</template>
</q-input>
</template>

View File

@ -5,4 +5,13 @@ export interface PayPeriod {
payday: string; payday: string;
pay_year: number; pay_year: number;
label: string; label: string;
} }
export const default_pay_period: PayPeriod = {
pay_period_no: -1,
period_start: '',
period_end: '',
payday: '',
pay_year: -1,
label: ''
};

View File

@ -1,15 +0,0 @@
export interface QDateDetails {
year: number;
month: number;
day: number;
from?: {
year: number;
month: number;
day: number;
};
to?: {
year: number;
month: number;
day: number;
};
}

View File

@ -4,7 +4,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
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';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/pay-period-employee-details';
import type { Expense } from 'src/modules/timesheets/types/timesheet-details-interface'; import type { Expense } from 'src/modules/timesheets/types/timesheet-details-interface';
const { t } = useI18n(); const { t } = useI18n();

View File

@ -5,7 +5,7 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/pay-period-employee-details';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();

View File

@ -6,7 +6,7 @@
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { Doughnut } from 'vue-chartjs'; import { Doughnut } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js';
import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface'; import type { PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar(); const $q = useQuasar();
@ -17,7 +17,7 @@
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161'; ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodOverviewEmployee | undefined; rawData: PayPeriodEmployeeOverview | undefined;
}>(); }>();
const shift_type_labels = ref<string[]>([]); const shift_type_labels = ref<string[]>([]);

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface'; import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
import { ref } from 'vue'; import { ref } from 'vue';
const props = defineProps<{ const props = defineProps<{
shift: Shift; shift: Shift;

View File

@ -1,27 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue'; import DetailedShiftListRow from 'src/modules/timesheet-approval/components/detailed-shift-list-row.vue';
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue'; import DetailedShiftListHeader from 'src/modules/timesheet-approval/components/detailed-shift-list-header.vue';
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface'; import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface'; import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/pay-period-employee-details';
const props = defineProps<{ const { rawData, currentPayPeriod } = defineProps<{
rawData: PayPeriodEmployeeDetails; rawData: PayPeriodEmployeeDetails;
currentPayPeriod: PayPeriod; currentPayPeriod: PayPeriod;
}>(); }>();
const weeks = [ rawData.week1, rawData.week2 ];
const shifts_or_placeholder = (shifts: Shift[]): Shift[] => { const shifts_or_placeholder = (shifts: Shift[]): Shift[] => {
return shifts.length > 0 ? shifts : [default_shift]; return shifts.length > 0 ? shifts : [default_shift];
}; };
const getDate = (shift_date: string): Date => { const getDate = (shift_date: string): Date => {
return new Date(props.currentPayPeriod.pay_year.toString() + '/' + shift_date); return new Date(currentPayPeriod.pay_year.toString() + '/' + shift_date);
}; };
</script> </script>
<template> <template>
<div <div
v-for="week, index in props.rawData" v-for="week, index in weeks"
:key="index" :key="index"
class="q-px-xs q-pt-xs rounded-5 col" class="q-px-xs q-pt-xs rounded-5 col"
> >
@ -40,20 +42,27 @@
<q-item-label <q-item-label
style="font-size: 0.7em;" style="font-size: 0.7em;"
class="text-uppercase" class="text-uppercase"
>{{ $d(getDate(day.short_date), {weekday: $q.screen.lt.md ? 'short' : 'long'}) }}</q-item-label> >
{{ $d(getDate(day.short_date), {weekday: $q.screen.lt.md ? 'short' : 'long'}) }}
</q-item-label>
<q-item-label <q-item-label
class="text-weight-bolder" class="text-weight-bolder"
style="font-size: 2.5em; line-height: 90% !important;" style="font-size: 2.5em; line-height: 90% !important;"
>{{ day.short_date.split('/')[1] }}</q-item-label> >
{{ day.short_date.split('/')[1] }}
</q-item-label>
<q-item-label <q-item-label
style="font-size: 0.7em;" style="font-size: 0.7em;"
class="text-uppercase" class="text-uppercase"
>{{ $d(getDate(day.short_date), {month: $q.screen.lt.md ? 'short' : 'long'}) }}</q-item-label> >
{{ $d(getDate(day.short_date), {month: $q.screen.lt.md ? 'short' : 'long'}) }}
</q-item-label>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section class="col q-pa-none"> <q-card-section class="col q-pa-none">
<TimesheetApprovalEmployeeDetailsShiftsRowHeader /> <DetailedShiftListHeader />
<TimesheetApprovalEmployeeDetailsShiftsRow <DetailedShiftListRow
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)" v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
:key="shift_index" :key="shift_index"
:shift="shift" :shift="shift"

View File

@ -0,0 +1,123 @@
<script setup lang="ts">
import type { PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
const modelApproval = defineModel<boolean>();
const { row } = defineProps<{ row: PayPeriodEmployeeOverview; }>();
const emit = defineEmits<{ clickDetails: []; }>();
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
</script>
<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">
<q-card class="rounded-10">
<!-- Card header with employee name and details button-->
<q-card-section horizontal class="q-py-none q-px-sm q-ma-none justify-between items-center">
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
<!-- Buttons to view detailed shifts or view employee timesheet -->
<q-btn
flat
dense
square
unelevated
class="col-auto q-pa-none q-ma-none"
color="primary"
icon="work_history"
@click="emit('clickDetails')"
>
<q-tooltip
anchor="top middle"
self="center middle"
class="bg-primary text-uppercase text-weight-bold"
>
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
</q-tooltip>
</q-btn>
</q-card-section>
<q-separator size="2px" />
<!-- Main body of pay period card -->
<q-card-section class="q-py-none q-px-sm q-mt-sm q-mb-md">
<div class="row no-wrap">
<!-- left portion of pay period card -->
<div class="col column no-wrap q-px-sm">
<!-- Regular hours segment -->
<div class="column" :class="$q.screen.lt.md ? 'col' : 'col-8'">
<span :class="stack_label_class"> {{ $t('shared.shift_type.regular') }} </span>
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
</div>
<q-separator class="q-mx-sm" />
<!-- Other hour types segment -->
<div class="row q-px-xs">
<div class="col column no-wrap">
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.evening') }} </span>
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.evening_hours }} </span>
</div>
<div class="col column no-wrap">
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.emergency') }} </span>
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.emergency_hours }} </span>
</div>
<div class="col column no-wrap">
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.overtime') }} </span>
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.overtime_hours }} </span>
</div>
</div>
</div>
<q-separator
vertical
class="q-mt-xs q-mb-none"
/>
<!-- Right portion of pay period card -->
<div class="col-auto column q-px-sm">
<div class="col column no-wrap">
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.expenses }} </span>
</div>
<div class="col column no-wrap">
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.mileage }} </span>
</div>
</div>
</div>
</q-card-section>
<q-separator color="primary" size="2px" />
<!-- Validate Pay Period section -->
<q-card-section
horizontal
class="justify-between items-center text-weight-bold q-px-sm"
:class="row.is_approved ? 'text-white bg-primary' : 'bg-dark'"
>
<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-weight-bold text-caption q-ml-xs"> total </span>
</div>
<q-checkbox
v-model="modelApproval"
dense
left-label
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="row.is_approved ? 'white' : 'primary'"
:label="row.is_approved ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="col-auto text-uppercase"
/>
</q-card-section>
</q-card>
</div>
</template>

View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import OverviewListItem from 'src/modules/timesheet-approval/components/employee-overview/overview-list-item.vue';
import QTableFilters from 'src/modules/shared/components/utils/q-table-filters.vue';
import { pay_period_employee_overview_columns, type PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
const timesheet_store = useTimesheetStore();
// const FORWARD = 1
// const BACKWARD = -1
const filter = ref<string | number | null>('');
const onClickedDetails = async ( employee_email: string ) => {
await timesheet_store.getPayPeriodEmployeeDetailsByEmployeeEmail(employee_email);
};
</script>
<template>
<div class="q-pa-md">
<q-table
:rows="timesheet_store.pay_period_employee_overview_list"
:columns="pay_period_employee_overview_columns"
row-key="email"
:filter="filter"
grid
dense
hide-pagination
color="primary"
:rows-per-page-options="[0]"
card-container-class="justify-center"
:loading="timesheet_store.is_loading"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
>
<template #top>
<div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
<!-- Calendar Picker goes here -->
<q-space />
<!-- Grid-or-List toggle goes here -->
<QTableFilters v-model="filter"/>
</div>
</template>
<!-- Template for individual employee cards -->
<template #item="props: { row: PayPeriodEmployeeOverview, key: string }">
<OverviewListItem
v-model="props.row.is_approved"
:row="props.row"
@click-details="onClickedDetails(props.row.email)"
/>
</template>
<!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }">
<div class="full-width column items-center text-primary q-gutter-sm">
<span class="text-h6 q-mt-xl">
{{ message }}
</span>
<q-icon
size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'"
/>
</div>
</template>
</q-table>
</div>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { default_pay_period_report_filters, type PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/pay-period-report-options';
const report_filter_options = ref<PayPeriodReportFilters>(default_pay_period_report_filters);
const company_options = [
{ label: 'Targo', value: report_filter_options.value.companies.targo },
{ label: 'Solucom', value: report_filter_options.value.companies.solucom },
];
const type_options = [
{ label: 'timesheet_approvals.print_report.shifts', value: report_filter_options.value.types.shifts },
{ label: 'timesheet_approvals.print_report.expenses', value: report_filter_options.value.types.expenses },
{ label: 'shared.shift_type.holiday', value: report_filter_options.value.types.holiday },
{ label: 'shared.shift_type.vacation', value: report_filter_options.value.types.vacation },
];
const is_download_button_disabled = computed(() => {
return company_options.map( option => option.value ).filter( value => value === true ).length > 0 ||
type_options.map( option => option.value ).filter( value => value === true ).length > 0;
});
</script>
<template>
<q-btn-group rounded push>
<q-btn
rounded
push
color="primary"
icon="print"
:disable="is_download_button_disabled"
/>
<q-btn-dropdown
rounded
push
color="white"
text-color="primary"
icon="checklist"
>
<q-list class="row">
<q-item class="col">
<q-item-label class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">
{{$t('timesheet_approvals.print_report.company')}}
</q-item-label>
<q-item-section row no-wrap>
<q-checkbox
v-for="option, index in company_options"
:key="index"
v-model="option.value"
:val="option.label"
:label="option.label"
/>
</q-item-section>
</q-item>
<q-separator
spaced
vertical
color="primary"
/>
<q-item class="col">
<q-item-section row no-wrap>
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.type')}}</p>
<q-checkbox
v-for="option, index in type_options"
:key="index"
v-model="option.value"
:val="option.label"
:label="option.label"
/>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-btn-group>
</template>

View File

@ -1,160 +0,0 @@
<script setup lang="ts">
import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface';
type TableColumn = {
name: string;
label: string;
value: unknown;
};
const { cols, row, initialState } = defineProps<{
cols: TableColumn[];
row: PayPeriodOverviewEmployee;
initialState: boolean;
}>();
const emit = defineEmits<{
clickDetails: [ email: string ];
updateApproval: [ value: boolean ];
}>();
</script>
<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">
<q-card class="rounded-10">
<!-- Card header with employee name and details button-->
<q-card-section
horizontal
class="q-py-none q-pl-md relative"
>
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">
{{ row.employee_name }}
</div>
<q-space />
<!-- Buttons to view detailed shifts or view employee timesheet -->
<q-btn
flat
dense
square
unelevated
class="q-py-none q-my-xs"
color="primary"
icon="work_history"
@click="emit('clickDetails', row.email)"
>
<q-tooltip
anchor="top middle"
self="center middle"
class="bg-primary text-uppercase text-weight-bold"
>
{{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
</q-tooltip>
</q-btn>
</q-card-section>
<q-separator size="2px" />
<!-- Main body of pay period card -->
<q-card-section class="q-pa-none q-mt-xs q-mb-sm">
<div class="row no-wrap">
<!-- left portion of pay period card -->
<div
class="column no-wrap"
:class="$q.screen.lt.md ? 'col' : 'col-8'"
>
<!-- Regular hours segment -->
<q-item
dense
class="column"
:class="$q.screen.lt.md ? 'col' : 'col-8'"
>
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption">
{{ cols.find(c => c.name === 'regular_hours')?.label }}
</q-item-label>
<q-item-label class="text-weight-bolder text-h3 q-py-none">
{{ cols.find(c => c.name === 'regular_hours')?.value }}
</q-item-label>
</q-item>
<q-separator class="q-mx-sm" />
<!-- Other hour types segment -->
<div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'">
<q-item
dense
class="column ellipsis "
v-for="col in cols.slice(3, 6)"
:key="col.label"
>
<q-item-label
class="text-weight-bold text-primary q-pa-none text-uppercase text-caption"
style="font-size: 0.65em;"
>
{{ col.label }}
</q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }}
</q-item-label>
</q-item>
</div>
</div>
<q-separator
vertical
class="q-mt-xs q-mb-none"
/>
<!-- Right portion of pay period card -->
<div class="no-wrap ellipsis col">
<q-item
dense
class="column"
v-for="col in cols.slice(6, )"
:key="col.label"
>
<q-item-label
class="text-weight-bold text-primary q-pa-none text-uppercase text-caption ellipsis"
style="font-size: 0.8em;"
>
{{ col.label }}
</q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }}
</q-item-label>
</q-item>
</div>
</div>
</q-card-section>
<q-separator
color="primary"
style="height: 2px;"
/>
<!-- Validate entire Pay Period section -->
<q-card-section
horizontal
class="q-pa-sm text-weight-bold"
:class="initialState ? 'text-white bg-primary' : 'bg-dark'"
>
<q-item-label class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours + ' h' }} </q-item-label>
<q-item-label class="text-uppercase text-weight-bold q-ml-xs"> total </q-item-label>
<q-space />
<q-checkbox
dense
left-label
size="lg"
checked-icon="lock"
unchecked-icon="lock_open"
:color="initialState ? 'white' : 'primary'" keep-color
:model-value="initialState"
@update:model-value="val => emit('updateApproval', val)"
:label="initialState ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="text-uppercase"
/>
</q-card-section>
</q-card>
</div>
</template>

View File

@ -1,267 +0,0 @@
<script setup lang="ts">
/* eslint-disable */
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { date, type QTableColumn } from 'quasar';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import PayPeriodCalendarPicker from 'src/modules/shared/components/utils/pay-period-calendar-picker.vue';
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
import TimesheetApprovalEmployeeDetails from 'src/modules/timesheet-approval/pages/timesheet-approval-employee-details.vue';
import { type PayPeriodOverviewEmployee } from '../types/timesheet-approval-pay-period-overview-employee-interface';
const { t } = useI18n();
const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi();
const FORWARD = 1
const BACKWARD = -1
const filter = ref<string | number | null>('');
const original_approvals = ref<Record<string, boolean>>({});
const is_showing_details = ref<boolean>(false);
const report_filter_company = ref<boolean[]>([true, true]);
const report_filter_type = ref<boolean[]>([true, true, true, true]);
const clicked_employee_name = ref<string>('');
const clicked_employee_email = ref<string>('');
const update_key = ref<number>(0);
const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [
{
name: 'employee_name',
label: t('timesheet_approvals.table.full_name'),
field: 'employee_name',
sortable: true
},
{
name: 'email',
label: t('timesheet_approvals.table.email'),
field: 'email',
sortable: true,
},
{
name: 'regular_hours',
label: t('shared.shift_type.regular'),
field: 'regular_hours',
sortable: true
},
{
name: 'evening_hours',
label: t('shared.shift_type.evening'),
field: 'evening_hours'
},
{
name: 'emergency_hours',
label: t('shared.shift_type.emergency'),
field: 'emergency_hours'
},
{
name: 'overtime_hours',
label: t('shared.shift_type.overtime'),
field: 'overtime_hours'
},
{
name: 'expenses',
label: t('timesheet_approvals.table.expenses'),
field: 'expenses',
sortable: true
},
{
name: 'mileage',
label: t('timesheet_approvals.table.mileage'),
field: 'mileage',
sortable: true
}
]);
// const has_changes = computed(() => {
// return timesheet_store.pay_period_overview_employees.some(emp => {
// return emp.is_approved !== original_approvals.value[emp.email];
// });
// });
const is_not_enough_filters = computed(() => {
return report_filter_company.value.filter(val => val === true).length < 1 ||
report_filter_type.value.filter(val => val === true).length < 1;
});
const filter_types_labels = [
t('timesheet_approvals.print_report.shifts'),
t('timesheet_approvals.print_report.expenses'),
t('shared.shift_type.holiday'),
t('shared.shift_type.vacation'),
];
const is_calendar_limit = computed( () => {
return timesheet_store.current_pay_period.pay_year === 2024 &&
timesheet_store.current_pay_period.pay_period_no <= 1;
});
const getEmployeeApprovalStatusReference = (email: string): boolean => {
const approval_status = timesheet_store.pay_period_overview_employee_approval_statuses?.find( status => status.key === email);
if (approval_status) {
return approval_status.value;
}
return false;
}
const updateEmployeeApprovalStatus = (email: string, value: boolean) => {
const approval_status = timesheet_store.pay_period_overview_employee_approval_statuses?.find( status => status.key === email);
if (approval_status) {
approval_status.value = value;
}
}
const getEmployeeOverview = (email: string): PayPeriodOverviewEmployee => {
return timesheet_approval_api.getPayPeriodOverviewByEmployeeEmail(email);
}
const onDateSelected = async (date_string: string) => {
await timesheet_approval_api.getPayPeriodOverviewByDate(date_string);
};
const onClickedDetails = async (email: string, name: string) => {
clicked_employee_name.value = name;
clicked_employee_email.value = email;
is_showing_details.value = true;
await timesheet_approval_api.getTimesheetsByPayPeriodAndEmail(email);
};
const onClickPrintReport = async () => {
await timesheet_approval_api.getTimesheetApprovalCSVReport(report_filter_company.value, report_filter_type.value);
};
onMounted( async () => {
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
await timesheet_approval_api.getPayPeriodOverviewByDate(today);
const approvals = timesheet_store.pay_period_overview_employees.map(emp => [emp.email, emp.is_approved]);
original_approvals.value = Object.fromEntries(approvals);
})
</script>
<template>
<q-dialog
v-model="is_showing_details"
transition-show="jump-down"
transition-hide="jump-down"
@before-show="() => update_key += 1"
full-width
:full-height="$q.screen.gt.sm"
>
<TimesheetApprovalEmployeeDetails
:is-loading="timesheet_store.is_loading"
:employee-name="clicked_employee_name"
:employee-overview="getEmployeeOverview(clicked_employee_email)"
:employee-details="timesheet_store.pay_period_employee_details"
:current-pay-period="timesheet_store.current_pay_period"
:update-key="update_key"
/>
</q-dialog>
<div class="q-pa-md">
<q-table
:rows="timesheet_store.pay_period_overview_employees"
:columns="columns"
row-key="email"
:filter="filter"
grid
dense
hide-pagination
color="primary"
:rows-per-page-options="[0]"
card-container-class="justify-center"
:loading="timesheet_store.is_loading"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
>
<!-- Top Bar that contains Date Picker, Search, Filters, Print Report, etc -->
<template #top>
<div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
<!-- Date Picker -->
<PayPeriodCalendarPicker
:is-disabled="timesheet_store.is_loading"
:is-previous-limit="is_calendar_limit"
@date-selected="onDateSelected"
@pressed-previous-button="timesheet_approval_api.getNextPayPeriodOverview(BACKWARD)"
@pressed-next-button="timesheet_approval_api.getNextPayPeriodOverview(FORWARD)"
/>
<q-space />
<q-btn-group rounded push>
<q-btn
rounded
push
color="primary"
icon="print"
:disable="is_not_enough_filters"
@click="onClickPrintReport"
/>
<q-btn-dropdown
rounded
push
color="white"
text-color="primary"
icon="checklist"
>
<q-list>
<q-item>
<q-item-section row no-wrap>
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.company')}}</p>
<q-checkbox
v-for="label, index in ['Targo', 'Solucom']"
v-model="report_filter_company[index]"
:val="label"
:label=label
:key="index"
/>
</q-item-section>
</q-item>
<q-separator color="primary" class="q-mx-md"/>
<q-item>
<q-item-section row no-wrap>
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.type')}}</p>
<q-checkbox
v-for="label, index in filter_types_labels"
v-model="report_filter_type[index]"
:val="label"
:label="label"
:key="index"
/>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-btn-group>
</div>
</template>
<!-- Template for individual employee cards -->
<template #item="props: {
cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[],
row: PayPeriodOverviewEmployee,
key: string,
}">
<TimesheetApprovalEmployeeOverviewListItem
:cols="props.cols"
:row="props.row"
:initial-state="getEmployeeApprovalStatusReference(props.key)"
@click-details="email => onClickedDetails(email, props.row.employee_name)"
@update-approval="value => updateEmployeeApprovalStatus(props.key, value)"
/>
</template>
<!-- Template for custome failed-to-load state -->
<template #no-data="{ message, filter }">
<div class="full-width column items-center text-primary q-gutter-sm">
<span class="text-h6 q-mt-xl">
{{ message }}
</span>
<q-icon
size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'"
/>
</div>
</template>
</q-table>
</div>
</template>

View File

@ -1,36 +1,39 @@
import { useTimesheetStore } from "src/stores/timesheet-store"; import { useTimesheetStore } from "src/stores/timesheet-store";
import { useAuthStore } from "src/stores/auth-store"; import { useAuthStore } from "src/stores/auth-store";
import type { PayPeriodReportFilters } from "../types/timesheet-approval-pay-period-report-interface"; import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/pay-period-report";
import { default_pay_period_overview_employee, type PayPeriodOverviewEmployee } from "../types/timesheet-approval-pay-period-overview-employee-interface"; import { default_pay_period_employee_overview, type PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/pay-period-employee-overview";
import { date } from "quasar"; import { date } from "quasar";
export const useTimesheetApprovalApi = () => { export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore(); const auth_store = useAuthStore();
const getPayPeriodOverviewByDate = async (date_string: string) => { const getPayPeriodOverviewByDate = async (date_string: string): Promise<void> => {
const success = await timesheet_store.getPayPeriodByDate(date_string); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
if (success) { if (success) {
const current_pay_period = timesheet_store.current_pay_period; await timesheet_store.getPayPeriodEmployeeOverviewListBySupervisorEmail(
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(current_pay_period.pay_year, current_pay_period.pay_period_no, auth_store.user.email); timesheet_store.pay_period.pay_year,
timesheet_store.pay_period.pay_period_no,
auth_store.user.email
);
} }
} };
const getPayPeriodOverviewByEmployeeEmail = (email: string): PayPeriodOverviewEmployee => { const getPayPeriodOverviewByEmployeeEmail = (email: string): void => {
const employee_overview = timesheet_store.pay_period_overview_employees.find(overview => overview.email === email); const employee_overview = timesheet_store.getPayPeriodOverviewByEmployeeEmail(email);
if (employee_overview !== undefined) return employee_overview;
return default_pay_period_overview_employee;
}; };
/* This method attempts to get the next or previous pay period. /* This method attempts to get the next or previous pay period.
It checks if pay period number is within a certain range, adjusts pay period and year accordingly. It checks if pay period number is within a certain range, adjusts pay period and year accordingly.
It then requests the matching pay period object to set as current pay period from server. It then requests the matching pay period object to set as current pay period from server.
If successful, it then requests pay period overviews from that new pay period. */ If successful, it then requests pay period overviews from that new pay period, using either the current user or
const getNextPayPeriodOverview = async (direction: number) => { any other supervisor email provided. */
const current_pay_period = timesheet_store.current_pay_period; const getNextOrPreviousPayPeriodOverviewList = async (direction: number, supervisor_email?: string): Promise<void> => {
let new_pay_period_no = current_pay_period.pay_period_no + direction; const email = supervisor_email ?? auth_store.user.email;
let new_pay_year = current_pay_period.pay_year;
let new_pay_period_no = timesheet_store.pay_period.pay_period_no + direction;
let new_pay_year = timesheet_store.pay_period.pay_year;
if (new_pay_period_no > 26) { if (new_pay_period_no > 26) {
new_pay_period_no = 1; new_pay_period_no = 1;
@ -42,17 +45,13 @@ export const useTimesheetApprovalApi = () => {
new_pay_year -= 1; new_pay_year -= 1;
} }
const success = await timesheet_store.getPayPeriodByYearAndPeriodNumber(new_pay_year, new_pay_period_no); const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(new_pay_year, new_pay_period_no);
if (success) { if (success) {
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(new_pay_year, new_pay_period_no, auth_store.user.email); await timesheet_store.getPayPeriodEmployeeOverviewListBySupervisorEmail(new_pay_year, new_pay_period_no, email);
} }
}; };
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
await timesheet_store.getTimesheetsByPayPeriodAndEmail(employee_email);
};
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[] ) => { const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[] ) => {
const [ targo, solucom ] = report_filter_company; const [ targo, solucom ] = report_filter_company;
const [ shifts, expenses, holiday, vacation ] = report_filter_type; const [ shifts, expenses, holiday, vacation ] = report_filter_type;
@ -64,29 +63,10 @@ export const useTimesheetApprovalApi = () => {
await timesheet_store.getTimesheetApprovalCSVReport(options); await timesheet_store.getTimesheetApprovalCSVReport(options);
}; };
const getCurrentPayPerdioOverview = async (): Promise<void> => {
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
const success = await timesheet_store.getPayPeriodByDate(today);
if(!success) return;
const { pay_year, pay_period_no } = timesheet_store.current_pay_period;
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(
pay_year,
pay_period_no,
auth_store.user.email
);
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
};
return { return {
getPayPeriodOverviewByDate, getPayPeriodOverviewByDate,
getNextPayPeriodOverview, getNextOrPreviousPayPeriodOverviewList,
getPayPeriodOverviewByEmployeeEmail, getPayPeriodOverviewByEmployeeEmail,
getTimesheetsByPayPeriodAndEmail,
getTimesheetApprovalCSVReport, getTimesheetApprovalCSVReport,
getCurrentPayPerdioOverview
} }
}; };

View File

@ -0,0 +1,151 @@
<script setup lang="ts">
import { ref } from 'vue';
import DetailedShiftList from 'src/modules/timesheet-approval/components/detailed-shift-list.vue';
import DetailedChartHoursWorked from 'src/modules/timesheet-approval/components/graphs/detailed-chart-hours-worked.vue';
import DetailedChartShiftTypes from 'src/modules/timesheet-approval/components/graphs/detailed-chart-shift-types.vue';
import DetailedChartExpenses from 'src/modules/timesheet-approval/components/graphs/detailed-chart-expenses.vue';
import type { PayPeriodEmployeeOverview } from 'src/modules/timesheet-approval/types/pay-period-employee-overview';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/pay-period-employee-details';
import { shift_type_legend } from 'src/modules/timesheet-approval/types/detailed-shift-color';
import { useTimesheetStore } from 'src/stores/timesheet-store';
const dialog_model = defineModel<boolean>('dialog', { default: false });
defineProps<{
isLoading: boolean;
employeeOverview: PayPeriodEmployeeOverview;
employeeDetails: PayPeriodEmployeeDetails;
}>();
const timesheet_store = useTimesheetStore();
const is_showing_graph = ref<boolean>(true);
</script>
<template>
<q-dialog
v-model="dialog_model"
full-width
transition-show="jump-down"
transition-hide="jump-down"
>
<q-card
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
>
<!-- loader -->
<q-card-section
v-if="isLoading"
class="column flex-center text-center"
>
<q-spinner
color="primary"
size="5em"
:thickness="10"
class="col-auto"
/>
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
{{ $t('shared.loading') }}
</div>
</q-card-section>
<!-- employee name -->
<q-card-section
v-if="!isLoading"
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
>
<span> {{ employeeDetails.employee_full_name }} </span>
<q-separator
spaced
size="2px"
/>
<q-card-actions
align="center"
class="q-pa-none"
>
<q-btn-toggle
v-model="is_showing_graph"
color="white"
text-color="primary"
toggle-color="primary"
:options="[
{ icon: 'bar_chart', value: true },
{ icon: 'edit', value: false },
]"
/>
</q-card-actions>
</q-card-section>
<!-- employee timesheet details edit -->
<q-card-section
v-if="!is_showing_graph"
class="q-pa-none"
>
<!-- shift type color legend -->
<q-card-section class="q-py-xs q-px-none text-center q-my-s">
<q-badge
v-for="shift_type, index in shift_type_legend"
:key="index"
:color="shift_type.background_color"
:label="$t(shift_type.type_label)"
:text-color="shift_type.font_color"
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
style="width: 120px; font-size: 0.8em;"
/>
</q-card-section>
<!-- list of shifts, broken down into weekly columns -->
<q-card-section
:horizontal="$q.screen.gt.sm"
class="q-pa-none bg-secondary rounded-10"
>
<DetailedShiftList
:raw-data="employeeDetails"
:current-pay-period="timesheet_store.pay_period"
/>
</q-card-section>
</q-card-section>
<!-- employee timesheet details with chart -->
<q-card-section
v-if="is_showing_graph"
class="q-pa-md col column full-width no-wrap"
>
<q-card-section
:horizontal="!$q.screen.lt.md"
class="q-pa-none col no-wrap"
style="min-height: 300px;"
>
<DetailedChartHoursWorked
:raw-data="employeeDetails"
class="col-7"
/>
<q-separator
spaced
:vertical="!$q.screen.lt.md"
/>
<div class="column col justify-center no-wrap q-pa-none">
<DetailedChartShiftTypes
:raw-data="employeeOverview"
class="col-5"
/>
<q-separator
spaced
:vertical="!$q.screen.lt.md"
/>
<DetailedChartExpenses
:raw-data="employeeDetails"
class="col"
/>
</div>
</q-card-section>
</q-card-section>
</q-card>
</q-dialog>
</template>

View File

@ -1,164 +0,0 @@
<script setup lang="ts">
/*eslint-disable*/
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import TimesheetApprovalEmployeeDetailsShifts from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts.vue';
import TimesheetApprovalEmployeeDetailsHoursWorkedChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-details-hours-worked-chart.vue';
import TimesheetApprovalEmployeeDetailsShiftTypesChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-details-shift-types-chart.vue';
import TimesheetApprovalEmployeeExpensesChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-expenses-chart.vue';
import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface';
import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface';
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
const props = defineProps<{
isLoading: boolean;
employeeName: string;
employeeOverview: PayPeriodOverviewEmployee;
employeeDetails: PayPeriodEmployeeDetails;
currentPayPeriod: PayPeriod;
updateKey: number;
}>();
const { t } = useI18n();
const is_showing_graph = ref<boolean>(true);
type shiftColor = {
type: string;
color: string;
text_color?: string;
}
const shift_type_legend: shiftColor[] = [
{
type: t('shared.shift_type.regular'),
color: 'secondary',
text_color: '',
},
{
type: t('shared.shift_type.evening'),
color: 'warning',
},
{
type: t('shared.shift_type.emergency'),
color: 'amber-10',
},
{
type: t('shared.shift_type.overtime'),
color: 'negative',
},
{
type: t('shared.shift_type.vacation'),
color: 'purple-10',
},
{
type: t('shared.shift_type.holiday'),
color: 'purple-8',
},
{
type: t('shared.shift_type.sick'),
color: 'grey-8',
},
]
</script>
<template>
<q-card
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.lt.md ? '' : 'width: 60vw !important; height: 70vh !important;' "
>
<!-- loader -->
<q-card-section
v-if="props.isLoading"
class="absolute-center text-center"
>
<q-spinner
color="primary"
size="5em"
:thickness="10"
/>
<div class="text-primary text-h6 text-weight-bold">
{{ $t('shared.loading') }}
</div>
</q-card-section>
<!-- employee name -->
<q-card-section
v-if="!props.isLoading"
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
>
{{ props.employeeName }}
<q-separator spaced size="2px" />
<q-card-actions align="center" class="q-pa-none">
<q-card flat class="bg-secondary rounded-5 q-pa-xs">
<q-btn-toggle
color="white"
text-color="primary"
toggle-color="primary"
v-model="is_showing_graph"
:options="[
{icon: 'bar_chart', value: true},
{icon: 'edit', value: false},
]"
/>
</q-card>
</q-card-actions>
</q-card-section>
<!-- employee timesheet details edit -->
<q-card-section
v-if="!props.isLoading && !is_showing_graph"
class="q-pa-none"
>
<!-- shift type color legend -->
<q-card-section class="q-py-xs q-px-none text-center q-my-s">
<q-badge
v-for="shift_type in shift_type_legend"
:color="shift_type.color"
:label="shift_type.type"
:text-color="shift_type.text_color || 'white'"
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
style="width: 120px; font-size: 0.8em;"
/>
</q-card-section>
<!-- list of shifts, broken down into weekly columns -->
<q-card-section
:horizontal="$q.screen.gt.sm"
class="q-pa-none bg-secondary rounded-10"
>
<TimesheetApprovalEmployeeDetailsShifts
:raw-data="props.employeeDetails"
:current-pay-period="props.currentPayPeriod"
/>
</q-card-section>
</q-card-section>
<!-- employee timesheet details with graphs -->
<q-card-section v-if="!props.isLoading && is_showing_graph" class="q-pa-md col column full-width no-wrap">
<q-card-section :horizontal="!$q.screen.lt.md" class="q-pa-none col no-wrap" style="min-height: 300px;">
<TimesheetApprovalEmployeeDetailsHoursWorkedChart
:raw-data="props.employeeDetails"
class="col-7"
/>
<q-separator vertical spaced />
<div class="column col justify-center no-wrap q-pa-none">
<TimesheetApprovalEmployeeDetailsShiftTypesChart
:raw-data="props.employeeOverview"
class="col-5"
/>
<q-separator :vertical="$q.screen.lt.md" spaced />
<TimesheetApprovalEmployeeExpensesChart
:raw-data="props.employeeDetails"
class="col"
/>
</div>
</q-card-section>
</q-card-section>
</q-card>
</template>

View File

@ -1,23 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import TimesheetApprovalEmployeeOverviewList from '../components/timesheet-approval-employee-overview-list.vue'; import { onMounted } from 'vue';
import { computed } from 'vue'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import EmployeeOverviewList from 'src/modules/timesheet-approval/components/employee-overview/overview-list.vue';
import TimesheetApprovalDetailed from 'src/modules/timesheet-approval/pages/timesheet-approval-detailed.vue';
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
const { d } = useI18n(); const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const pay_period_label = computed(() => { onMounted( async () => {
const dates = timesheet_store.current_pay_period.label.split('.'); await timesheet_approval_api.getPayPeriodOverviewByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
if ( dates.length < 2 ) {
return { start_date: '—', end_date: '—' }
}
const start_date = d(new Date(dates[0] as string), { day: 'numeric', month: 'long', year: 'numeric', });
const end_date = d(new Date(dates[1] as string), { day: 'numeric', month: 'long', year: 'numeric', });
return { start_date, end_date };
}); });
</script> </script>
@ -26,21 +20,18 @@
padding padding
class="q-pa-md bg-secondary " class="q-pa-md bg-secondary "
> >
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4"> <PageHeaderTemplate
{{ $t('timesheet_approvals.page_title') }} title="timesheet_approvals.page_title"
<div class="col row items-center justify-center full-width q-py-none q-my-none"> :start-date="timesheet_store.pay_period.period_start"
<div class="text-primary text-weight-bold text-h6"> :end-date="timesheet_store.pay_period.period_end"
{{ pay_period_label.start_date }} />
</div>
<div class="text-body2 q-mx-md text-weight-medium"> <TimesheetApprovalDetailed
{{ $t('shared.misc.to') }} :is-loading="timesheet_store.is_loading"
</div> :employee-overview="timesheet_store.pay_period_employee_overview"
<div class="text-primary text-weight-bold text-h6"> :employee-details="timesheet_store.pay_period_employee_details"
{{ pay_period_label.end_date }} />
</div>
</div>
</div>
<TimesheetApprovalEmployeeOverviewList /> <EmployeeOverviewList />
</q-page> </q-page>
</template> </template>

View File

@ -1,8 +1,8 @@
import { api } from "src/boot/axios"; import { api } from "src/boot/axios";
import type { PayPeriodOverview } from "../types/timesheet-approval-pay-period-overview-interface"; import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/pay-period-overview";
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface"; import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
import type { PayPeriodEmployeeDetails } from "../types/timesheet-approval-pay-period-employee-details-interface"; import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/pay-period-employee-details";
import type { PayPeriodReportFilters } from "../types/timesheet-approval-pay-period-report-interface"; import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/pay-period-report";
export const timesheetApprovalService = { export const timesheetApprovalService = {
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => { getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
@ -16,13 +16,13 @@ export const timesheetApprovalService = {
}, },
getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => { getPayPeriodEmployeeOverviewListBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD // TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data; return response.data;
}, },
getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => { getPayPeriodEmployeeDetailsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => {
const response = await api.get('timesheets', { params: { year, period_no, email, }}); const response = await api.get('timesheets', { params: { year, period_no, email, }});
return response.data; return response.data;
}, },

View File

@ -1,287 +0,0 @@
// import type { PayPeriod } from "../shared/types/pay-period-interface";
// import type { PayPeriodEmployeeOverview } from "./types/timesheet-approval-pay-period-employee-overview-interface"
// export const mock_pay_period_employee_overviews: PayPeriodEmployeeOverview[] = [
// {
// "email": 'EMP-001',
// "employee_name": 'Alice Johnson',
// "regular_hours": 75,
// "evening_hours": 12,
// "emergency_hours": 3,
// "overtime_hours": 5,
// "expenses": 120.50,
// "mileage": 45,
// "is_approved": false
// },
// {
// "email": 'EMP-002',
// "employee_name": 'Brian Smith',
// "regular_hours": 80,
// "evening_hours": 8,
// "emergency_hours": 0,
// "overtime_hours": 2,
// "expenses": 75.00,
// "mileage": 12,
// "is_approved": true
// },
// {
// "email": 'EMP-003',
// "employee_name": 'Chloe Ramirez',
// "regular_hours": 68,
// "evening_hours": 15,
// "emergency_hours": 1,
// "overtime_hours": 0,
// "expenses": 200.00,
// "mileage": 88,
// "is_approved": false
// },
// {
// "email": 'EMP-004',
// "employee_name": 'David Lee',
// "regular_hours": 82,
// "evening_hours": 5,
// "emergency_hours": 4,
// "overtime_hours": 6,
// "expenses": 50.75,
// "mileage": 20,
// "is_approved": true
// },
// {
// "email": 'EMP-005',
// "employee_name": 'Emily Carter',
// "regular_hours": 78,
// "evening_hours": 10,
// "emergency_hours": 2,
// "overtime_hours": 3,
// "expenses": 95.25,
// "mileage": 60,
// "is_approved": false
// },
// {
// "email": 'EMP-006',
// "employee_name": 'Maxime Murray Gendron',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-007',
// "employee_name": 'Marc-André Henrico',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-008',
// "employee_name": 'Jessy Sharock',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-009',
// "employee_name": 'David Richer',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-010',
// "employee_name": 'Nicolas Drolet',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-011',
// "employee_name": 'Frederick Pruneau',
// "regular_hours": 16,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-012',
// "employee_name": 'Matthieu Haineault Gervais',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-013',
// "employee_name": 'Robinson Viaud',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-014',
// "employee_name": 'Geneviève Bourdon',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-015',
// "employee_name": 'Frédérique Soulard',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-016',
// "employee_name": 'Patrick Doucet',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-017',
// "employee_name": 'Dahlia Tremblay',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-018',
// "employee_name": 'Louis Morneau',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// },
// {
// "email": 'EMP-019',
// "employee_name": 'Michel Blais',
// "regular_hours": 80,
// "evening_hours": 0,
// "emergency_hours": 0,
// "overtime_hours": 0,
// "expenses": 0,
// "mileage": 0,
// "is_approved": false
// }
// ];
// export const mock_pay_periods: PayPeriod[] = [
// {
// "period_number": 15,
// "start_date": "2025-07-27",
// "end_date": "2025-08-09",
// "year": 2025,
// "label": "2025-07-27 → 2025-08-09"
// },
// {
// "period_number": 14,
// "start_date": "2025-07-13",
// "end_date": "2025-07-26",
// "year": 2025,
// "label": "2025-07-13 → 2025-07-26"
// },
// {
// "period_number": 13,
// "start_date": "2025-06-29",
// "end_date": "2025-07-12",
// "year": 2025,
// "label": "2025-06-29 → 2025-07-12"
// },
// {
// "period_number": 12,
// "start_date": "2025-06-15",
// "end_date": "2025-06-28",
// "year": 2025,
// "label": "2025-06-15 → 2025-06-28"
// },
// {
// "period_number": 11,
// "start_date": "2025-06-01",
// "end_date": "2025-06-14",
// "year": 2025,
// "label": "2025-06-01 → 2025-06-14"
// },
// {
// "period_number": 10,
// "start_date": "2025-05-18",
// "end_date": "2025-05-31",
// "year": 2025,
// "label": "2025-05-18 → 2025-05-31"
// },
// {
// "period_number": 9,
// "start_date": "2025-05-04",
// "end_date": "2025-05-17",
// "year": 2025,
// "label": "2025-05-04 → 2025-05-17"
// },
// {
// "period_number": 8,
// "start_date": "2025-04-20",
// "end_date": "2025-05-03",
// "year": 2025,
// "label": "2025-04-20 → 2025-05-03"
// },
// {
// "period_number": 7,
// "start_date": "2025-04-06",
// "end_date": "2025-04-19",
// "year": 2025,
// "label": "2025-04-06 → 2025-04-19"
// },
// {
// "period_number": 6,
// "start_date": "2025-03-23",
// "end_date": "2025-04-05",
// "year": 2025,
// "label": "2025-03-23 → 2025-04-05"
// }
// ]

View File

@ -0,0 +1,43 @@
export interface shiftColor {
type_label: string;
background_color: string;
font_color: string;
}
export const shift_type_legend: shiftColor[] = [
{
type_label: 'shared.shift_type.regular',
background_color: 'blue-grey-4',
font_color: 'blue-grey-8',
},
{
type_label: 'shared.shift_type.evening',
background_color: 'warning',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.emergency',
background_color: 'amber-10',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.overtime',
background_color: 'negative',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.vacation',
background_color: 'purple-10',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.holiday',
background_color: 'purple-8',
font_color: 'blue-grey-2',
},
{
type_label: 'shared.shift_type.sick',
background_color: 'grey-8',
font_color: 'blue-grey-2',
},
]

View File

@ -3,9 +3,11 @@ import { default_timesheet_details_week, type TimesheetDetailsWeek } from "src/m
export interface PayPeriodEmployeeDetails { export interface PayPeriodEmployeeDetails {
week1: TimesheetDetailsWeek; week1: TimesheetDetailsWeek;
week2: TimesheetDetailsWeek; week2: TimesheetDetailsWeek;
employee_full_name: string;
}; };
export const default_pay_period_employee_details = { export const default_pay_period_employee_details = {
week1: default_timesheet_details_week(), week1: default_timesheet_details_week(),
week2: default_timesheet_details_week(), week2: default_timesheet_details_week(),
employee_full_name: "",
} }

View File

@ -0,0 +1,82 @@
export interface PayPeriodEmployeeOverview {
email: string;
employee_name: string;
regular_hours: number;
evening_hours: number;
emergency_hours: number;
overtime_hours: number;
total_hours: number;
expenses: number;
mileage: number;
is_approved: boolean;
};
export const default_pay_period_employee_overview: PayPeriodEmployeeOverview = {
email: '',
employee_name: '',
regular_hours: -1,
evening_hours: -1,
emergency_hours: -1,
overtime_hours: -1,
total_hours: -1,
expenses: -1,
mileage: -1,
is_approved: false
}
export const pay_period_employee_overview_columns = [
{
name: 'employee_name',
label: 'timesheet_approvals.table.full_name',
field: 'employee_name',
sortable: true
},
{
name: 'email',
label: 'timesheet_approvals.table.email',
field: 'email',
sortable: true,
},
{
name: 'regular_hours',
label: 'shared.shift_type.regular',
field: 'regular_hours',
sortable: true,
},
{
name: 'evening_hours',
label: 'shared.shift_type.evening',
field: 'evening_hours',
sortable: true,
},
{
name: 'emergency_hours',
label: 'shared.shift_type.emergency',
field: 'emergency_hours',
sortable: true,
},
{
name: 'overtime_hours',
label: 'shared.shift_type.overtime',
field: 'overtime_hours',
sortable: true,
},
{
name: 'expenses',
label: 'timesheet_approvals.table.expenses',
field: 'expenses',
sortable: true,
},
{
name: 'mileage',
label: 'timesheet_approvals.table.mileage',
field: 'mileage',
sortable: true,
},
{
name: 'is_approved',
label: 'timesheet_approvals.table.is_approved',
field: 'is_approved',
sortable: true,
}
];

View File

@ -1,4 +1,4 @@
import type { PayPeriodOverviewEmployee } from "./timesheet-approval-pay-period-overview-employee-interface"; import type { PayPeriodEmployeeOverview } from "./pay-period-employee-overview";
export interface PayPeriodOverview { export interface PayPeriodOverview {
pay_period_no: number; pay_period_no: number;
@ -7,5 +7,5 @@ export interface PayPeriodOverview {
period_start: string; period_start: string;
period_end: string; period_end: string;
label: string; label: string;
employees_overview: PayPeriodOverviewEmployee[]; employees_overview: PayPeriodEmployeeOverview[];
}; };

View File

@ -0,0 +1,25 @@
export interface PayPeriodReportFilters {
types: {
shifts: boolean;
expenses: boolean;
holiday: boolean;
vacation: boolean;
};
companies: {
targo: boolean;
solucom: boolean;
};
};
export const default_pay_period_report_filters: PayPeriodReportFilters = {
types: {
shifts: true,
expenses: true,
holiday: true,
vacation: true,
},
companies: {
targo: true,
solucom: true,
},
};

View File

@ -1,25 +0,0 @@
export interface PayPeriodOverviewEmployee {
email: string;
employee_name: string;
regular_hours: number;
evening_hours: number;
emergency_hours: number;
overtime_hours: number;
total_hours: number;
expenses: number;
mileage: number;
is_approved: boolean;
};
export const default_pay_period_overview_employee: PayPeriodOverviewEmployee = {
email: '',
employee_name: '',
regular_hours: -1,
evening_hours: -1,
emergency_hours: -1,
overtime_hours: -1,
total_hours: -1,
expenses: -1,
mileage: -1,
is_approved: false
}

View File

@ -2,9 +2,9 @@ import { api } from "src/boot/axios";
import type {Timesheet} from "src/modules/timesheets/types/timesheet-interface"; import type {Timesheet} from "src/modules/timesheets/types/timesheet-interface";
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/timesheet-shifts-payload-interface"; import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/timesheet-shifts-payload-interface";
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface"; import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface"; import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/pay-period-employee-details";
import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-interface"; import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/pay-period-overview";
import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface"; import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/pay-period-report";
export const timesheetTempService = { export const timesheetTempService = {
//GET //GET

View File

@ -25,4 +25,13 @@ type Expenses = {
comment: string; comment: string;
supervisor_comment: string; supervisor_comment: string;
is_approved: boolean; is_approved: boolean;
} }
export const default_timesheet: Timesheet = {
start_day: '',
end_day: '',
label: '',
is_approved: false,
shifts: [],
expenses: [],
};

View File

@ -1,181 +1,166 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval'; import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services'; import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/pay-period-employee-details';
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface'; import { default_pay_period, type PayPeriod } from 'src/modules/shared/types/pay-period-interface';
import type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface"; import { default_pay_period_employee_overview, type PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/pay-period-employee-overview";
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface'; import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/pay-period-report';
import type { Timesheet } from 'src/modules/timesheets/types/timesheet-interface'; import { default_timesheet, type Timesheet } from 'src/modules/timesheets/types/timesheet-interface';
import type { CreateShiftPayload } from 'src/modules/timesheets/types/timesheet-shifts-payload-interface'; import type { CreateShiftPayload } from 'src/modules/timesheets/types/timesheet-shifts-payload-interface';
import { withLoading } from 'src/utils/store-helpers';
const default_pay_period: PayPeriod = {
pay_period_no: -1,
period_start: '',
period_end: '',
payday: '',
pay_year: -1,
label: ''
};
//employee timesheet
const default_timesheet: Timesheet = {
start_day: '',
end_day: '',
label: '',
is_approved: false,
shifts: [],
expenses: [],
};
export const useTimesheetStore = defineStore('timesheet', () => { export const useTimesheetStore = defineStore('timesheet', () => {
const is_loading = ref<boolean>(false); const is_loading = ref<boolean>(false);
const current_pay_period = ref<PayPeriod>(default_pay_period); const pay_period = ref<PayPeriod>(default_pay_period);
const pay_period_overview_employees = ref<PayPeriodOverviewEmployee[]>([]); const pay_period_employee_overview_list = ref<PayPeriodEmployeeOverview[]>([]);
const pay_period_overview_employee_approval_statuses = ref<{key: string, value: boolean}[] | undefined>(); const pay_period_employee_overview = ref<PayPeriodEmployeeOverview>(default_pay_period_employee_overview);
const pay_period_employee_details = ref<PayPeriodEmployeeDetails>(default_pay_period_employee_details); const pay_period_employee_details = ref<PayPeriodEmployeeDetails>(default_pay_period_employee_details);
const pay_period_report = ref(); const pay_period_report = ref();
const timesheet = ref<Timesheet>(default_timesheet);
//employee timesheet const getPayPeriodByDateOrYearAndNumber = (date_or_year: string | number, period_number?: number): Promise<boolean> => {
const current_timesheet = ref<Timesheet>(default_timesheet); return withLoading( is_loading, async () => {
try {
let response;
const getPayPeriodByDate = async (date_string: string): Promise<boolean> => { if (typeof date_or_year === 'string') {
is_loading.value = true; response = await timesheetApprovalService.getPayPeriodByDate(date_or_year);
return true;
}
else if ( typeof date_or_year === 'number' && period_number ) {
response = await timesheetApprovalService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
return true;
}
else response = default_pay_period;
try { pay_period.value = response;
const response = await timesheetApprovalService.getPayPeriodByDate(date_string); return false;
current_pay_period.value = response; } catch(error){
is_loading.value = false; console.error('Could not get current pay period: ', error );
pay_period.value = default_pay_period;
pay_period_employee_overview_list.value = [];
//TODO: More in-depth error-handling here
}
return true; return false;
} catch(error){ });
console.error('Could not get current pay period: ', error );
current_pay_period.value = default_pay_period;
pay_period_overview_employees.value = [];
//TODO: More in-depth error-handling here
}
is_loading.value = false;
return false;
}; };
const getPayPeriodByYearAndPeriodNumber = async (year: number, period_number: number): Promise<boolean> => { const getPayPeriodEmployeeOverviewListBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise<boolean> => {
is_loading.value = true; return withLoading( is_loading, async () => {
try {
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviewListBySupervisorEmail( pay_year, period_number, supervisor_email );
pay_period_employee_overview_list.value = response.employees_overview;
return true;
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
pay_period_employee_overview_list.value = [];
// TODO: More in-depth error-handling here
}
try { return false;
const response = await timesheetApprovalService.getPayPeriodByYearAndPeriodNumber(year, period_number); });
current_pay_period.value = response;
is_loading.value = false;
return true;
} catch(error){
console.error('Could not get current pay period: ', error );
current_pay_period.value = default_pay_period;
pay_period_overview_employees.value = [];
//TODO: More in-depth error-handling here
}
is_loading.value = false;
return false;
}; };
const getTimesheetApprovalPayPeriodEmployeeOverviews = async (pay_year: number, period_number: number, supervisor_email: string) => { const getPayPeriodOverviewByEmployeeEmail = (email: string): PayPeriodEmployeeOverview => {
is_loading.value = true; const response = pay_period_employee_overview_list.value?.find( employee_overview => employee_overview.email === email);
if (typeof response === 'undefined') {
try { pay_period_employee_overview.value = default_pay_period_employee_overview;
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews( } else {
pay_year, pay_period_employee_overview.value = response;
period_number,
supervisor_email
);
pay_period_overview_employees.value = response.employees_overview;
pay_period_overview_employee_approval_statuses.value = response.employees_overview.map( employee => ({ key: employee.email, value: employee.is_approved }) );
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
pay_period_overview_employees.value = [];
// TODO: More in-depth error-handling here
} }
is_loading.value = false; return pay_period_employee_overview.value;
}; };
//employee timesheet //employee timesheet
const getTimesheetByEmail = async (employee_email: string) => { const getTimesheetByEmail = async (employee_email: string) => {
is_loading.value = true; return withLoading( is_loading, async () => {
try{ try{
const response = await timesheetTempService.getTimesheetsByEmail(employee_email); const response = await timesheetTempService.getTimesheetsByEmail(employee_email);
current_timesheet.value = response; timesheet.value = response;
}catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error); return true;
current_timesheet.value = { ...default_timesheet } }catch (error) {
} finally { console.error('There was an error retrieving timesheet details for this employee: ', error);
is_loading.value = false; timesheet.value = { ...default_timesheet }
} }
}
//employee timesheet return false;
const createTimesheetShifts = async (employee_email: string, shifts: CreateShiftPayload[], offset = 0) => { });
is_loading.value = true;
try{
const timesheet = await timesheetTempService.createTimesheetShifts(employee_email, shifts, offset);
current_timesheet.value = timesheet;
} catch (err) {
console.error('createTimesheetShifts error: ', err);
} finally {
is_loading.value = false;
}
}; };
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => { //employee timesheet
is_loading.value = true; const createTimesheetShifts = async (employee_email: string, shifts: CreateShiftPayload[], offset = 0) => {
return withLoading( is_loading, async () => {
try{
const response = await timesheetTempService.createTimesheetShifts(employee_email, shifts, offset);
timesheet.value = response;
try { return true;
const response = await timesheetApprovalService.getTimesheetsByPayPeriodAndEmail( } catch (err) {
current_pay_period.value.pay_year, console.error('createTimesheetShifts error: ', err);
current_pay_period.value.pay_period_no, }
employee_email
);
pay_period_employee_details.value = response;
} catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error);
// TODO: More in-depth error-handling here
}
is_loading.value = false; return false;
});
};
const getPayPeriodEmployeeDetailsByEmployeeEmail = async (employee_email: string) => {
return withLoading( is_loading, async () => {
try {
const response = await timesheetApprovalService.getPayPeriodEmployeeDetailsByPayPeriodAndEmail(
pay_period.value.pay_year,
pay_period.value.pay_period_no,
employee_email
);
pay_period_employee_details.value = response;
return true;
} catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error);
// TODO: More in-depth error-handling here
}
pay_period_employee_details.value = default_pay_period_employee_details;
return false;
});
}; };
const getTimesheetApprovalCSVReport = async (report_filters?: PayPeriodReportFilters) => { const getTimesheetApprovalCSVReport = async (report_filters?: PayPeriodReportFilters) => {
is_loading.value = true; return withLoading( is_loading, async () => {
try {
const response = await timesheetApprovalService.getTimesheetApprovalCSVReport(
pay_period.value.pay_year,
pay_period.value.pay_period_no,
report_filters
);
pay_period_report.value = response;
try { return true;
const response = await timesheetApprovalService.getTimesheetApprovalCSVReport( } catch (error) {
current_pay_period.value.pay_year, console.error('There was an error retrieving the report CSV: ', error);
current_pay_period.value.pay_period_no, // TODO: More in-depth error-handling here
report_filters }
);
pay_period_report.value = response;
} catch (error) {
console.error('There was an error retrieving the report CSV: ', error);
// TODO: More in-depth error-handling here
}
is_loading.value = false; return false;
});
}; };
return { return {
current_pay_period, pay_period,
pay_period_overview_employees, pay_period_employee_overview_list,
pay_period_overview_employee_approval_statuses, pay_period_employee_overview,
pay_period_employee_details, pay_period_employee_details,
current_timesheet, timesheet,
is_loading, is_loading,
getPayPeriodByDate, getPayPeriodByDateOrYearAndNumber,
getTimesheetByEmail, getTimesheetByEmail,
createTimesheetShifts, createTimesheetShifts,
getPayPeriodByYearAndPeriodNumber, getPayPeriodEmployeeOverviewListBySupervisorEmail,
getTimesheetApprovalPayPeriodEmployeeOverviews, getPayPeriodOverviewByEmployeeEmail,
getTimesheetsByPayPeriodAndEmail, getPayPeriodEmployeeDetailsByEmployeeEmail,
getTimesheetApprovalCSVReport, getTimesheetApprovalCSVReport,
}; };
}); });

View File

@ -0,0 +1,10 @@
import type { Ref } from "vue";
export const withLoading = async <T>( loading_state: Ref<boolean>, fn: () => Promise<T> ) => {
loading_state.value = true;
try {
return await fn();
} finally {
loading_state.value = false;
}
};