feat(deatils): add component to show row details, finalize appearance of chart layout, finetune mobile layout

This commit is contained in:
Nicolas Drolet 2025-09-04 17:04:03 -04:00
parent 89148343b6
commit 5bd19c4a9c
23 changed files with 398 additions and 204 deletions

View File

@ -24,7 +24,7 @@ $dark: #000;
$dark-page: #323232; $dark-page: #323232;
$positive: #21ba45; $positive: #21ba45;
$negative: #ff576ba9; $negative: #e6364b;
$info: #54bbdd; $info: #6bb9e7;
$warning: #eec964; $warning: #eec964;
$white: white; $white: white;

View File

@ -260,13 +260,13 @@ export default {
shiftComment: 'Comment', shiftComment: 'Comment',
overTimeTitle: 'Overtime regular hours: ', overTimeTitle: 'Overtime regular hours: ',
totalPayedHours: 'Total hours worked: ', totalPayedHours: 'Total hours worked: ',
//shiftOptions // shift options
regularShift: 'Regular', shiftRegular: 'regular',
eveningShift: 'Evening', shiftEvening: 'evening',
emergencyShift: 'Emergency', shiftEmergency: 'emergency',
sickDay: 'Sick working day', shiftSick: 'sick',
vacancyDay: 'vacation', shiftVacation: 'vacation',
holiday: 'Holiday', shiftHoliday: 'holiday',
dateRangesFrom: 'from', dateRangesFrom: 'from',
dateRangesTo: 'to', dateRangesTo: 'to',
shiftBankedHours: 'Total hours to bank', shiftBankedHours: 'Total hours to bank',
@ -369,7 +369,7 @@ export default {
message_start: 'Attention: You will be automatically logged out in', message_start: 'Attention: You will be automatically logged out in',
message_end: 'seconds if you do not interact with the screen.', message_end: 'seconds if you do not interact with the screen.',
}, },
daysOfWeek: { weekdays: {
Sunday: ' Sunday', Sunday: ' Sunday',
Monday: 'Monday', Monday: 'Monday',
Tuesday: 'Tuesday', Tuesday: 'Tuesday',

View File

@ -55,7 +55,7 @@ export default {
message_start: 'Attention : vous serez automatiquement déconnecté dans', message_start: 'Attention : vous serez automatiquement déconnecté dans',
message_end: 'secondes si vous ninteragissez pas avec lécran.', message_end: 'secondes si vous ninteragissez pas avec lécran.',
}, },
daysOfWeek: { weekdays: {
Sunday: 'dimanche', Sunday: 'dimanche',
Monday: 'lundi', Monday: 'lundi',
Tuesday: 'mardi', Tuesday: 'mardi',
@ -310,13 +310,13 @@ export default {
shiftComment: 'Commentaire', shiftComment: 'Commentaire',
overTimeTitle: 'Heures régulières supplémentaires: ', overTimeTitle: 'Heures régulières supplémentaires: ',
totalPayedHours: 'Total des heures travaillées: ', totalPayedHours: 'Total des heures travaillées: ',
//shiftOptions // shift options
regularShift: 'Régulier', shiftRegular: 'régulier',
eveningShift: 'Soir', shiftEvening: 'soir',
emergencyShift: 'Urgence', shiftEmergency: 'urgence',
sickDay: 'Maladie', shiftSick: 'maladie',
vacancyDay: 'Vacances', shiftVacation: 'vacances',
holiday: 'Férié', shiftHoliday: 'férié',
dateRangesFrom: 'du', dateRangesFrom: 'du',
dateRangesTo: 'au', dateRangesTo: 'au',
shiftBankedHours: 'Totale dheures à banquer', shiftBankedHours: 'Totale dheures à banquer',

View File

@ -68,7 +68,9 @@
<template v-slot:top> <template v-slot:top>
<div class="row full-width q-mb-sm"> <div class="row full-width q-mb-sm">
<q-btn push icon="person_add" color="primary" :label="$t('usersListPage.addButton')"/> <q-btn push icon="person_add" color="primary" :label="$t('usersListPage.addButton')"/>
<q-space /> <q-space />
<q-btn-toggle push class="q-mr-md" color="white" text-color="primary" toggle-color="primary" v-model="isGridMode" <q-btn-toggle push class="q-mr-md" color="white" text-color="primary" toggle-color="primary" v-model="isGridMode"
:options="[ :options="[
{icon: 'grid_view', value: true}, {icon: 'grid_view', value: true},

View File

@ -14,7 +14,7 @@
<q-item-section :side="$q.screen.gt.sm"> <q-item-section :side="$q.screen.gt.sm">
<q-avatar rounded > <q-avatar rounded >
<q-img src="src/assets/targo-default-avatar.png" /> <q-img src="src/assets/targo-default-avatar.png" />
<q-badge floating color="red" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge> <q-badge floating color="negative" v-if="notifAmount > 0" >{{ notifAmount }}</q-badge>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>

View File

@ -47,7 +47,7 @@
{ {
label: t('timeSheetValidations.hoursWorkedEmergency'), label: t('timeSheetValidations.hoursWorkedEmergency'),
data: emergency_hours, data: emergency_hours,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-accent').trim(),
}, },
{ {
label: t('timeSheetValidations.hoursWorkedOvertime'), label: t('timeSheetValidations.hoursWorkedOvertime'),

View File

@ -35,7 +35,7 @@
backgroundColor: [ backgroundColor: [
getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
getComputedStyle(document.body).getPropertyValue('--q-info').trim(), getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), getComputedStyle(document.body).getPropertyValue('--q-accent').trim(),
getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
] ]
}]; }];

View File

@ -1,35 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
/*eslint-disable*/ /*eslint-disable*/
import { ref } from 'vue'; import { ref } from 'vue';
import { Line } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, PointElement, LineController, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, 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/timesheet-approval-pay-period-employee-details-interface';
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();
ChartJS.register(Title, Tooltip, Legend, LineElement, LineController, PointElement, CategoryScale, LinearScale, TimeScale); ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale);
ChartJS.defaults.font.family = '"Roboto", sans-serif'; ChartJS.defaults.font.family = '"Roboto", sans-serif';
// ChartJS.defaults.maintainAspectRatio = false; ChartJS.defaults.maintainAspectRatio = false;
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
rawData: PayPeriodEmployeeDetails | undefined; rawData: PayPeriodEmployeeDetails | undefined;
options?: ChartOptions<"line"> | undefined; options?: ChartOptions<"bar"> | undefined;
plugins?: Plugin<"line">[] | undefined; plugins?: Plugin<"bar">[] | undefined;
}>(), { }>(), {
options: () => ({}), options: () => ({}),
plugins: () => [], plugins: () => [],
}); });
const expenses_dataset = ref<ChartDataset<'line'>[]>([]); const expenses_dataset = ref<ChartDataset<'bar'>[]>([]);
const expenses_labels = ref<string[]>([]); const expenses_labels = ref<string[]>([]);
const getExpensesData = (): ChartData<'line'> => { const getExpensesData = (): ChartData<'bar'> => {
if (props.rawData) { if (props.rawData) {
const all_weeks = [props.rawData.week1, props.rawData.week2]; const all_weeks = [props.rawData.week1, props.rawData.week2];
const all_days = all_weeks.flatMap( week => Object.values(week.expenses)); const all_days = all_weeks.flatMap( week => Object.values(week.expenses));
console.log('all days: ', all_days);
const all_days_dates = all_weeks.flatMap( week => Object.values(week.shifts)) const all_days_dates = all_weeks.flatMap( week => Object.values(week.shifts))
const all_costs = all_days.map( day => getTotalAmounts(day.cash)); const all_costs = all_days.map( day => getTotalAmounts(day.cash));
@ -40,16 +39,12 @@
{ {
label: t('timeSheet.refund'), label: t('timeSheet.refund'),
data: all_costs, data: all_costs,
borderColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
borderWidth: 2,
}, },
{ {
label: t('timeSheet.mileage'), label: t('timeSheet.mileage'),
data: all_mileage, data: all_mileage,
borderColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(), backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
borderWidth: 2,
} }
] ]
@ -74,7 +69,7 @@
</script> </script>
<template> <template>
<Line <Bar
:data="getExpensesData()" :data="getExpensesData()"
:options="props.options" :options="props.options"
/> />

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { date } from 'quasar';
import type { TimesheetDetailsDailySchedule } from 'src/modules/timesheets/types/timesheet-details-interface';
const props = defineProps<{
dayData: TimesheetDetailsDailySchedule;
}>();
const getWeekDay = (shift_date: string): string => {
const d = date.extractDate(shift_date, 'YYYY/MM/DD');
return date.formatDate(d, 'dddd');
};
</script>
<template>
<q-card>
<q-card-section
horizontal
class="q-pa-xs"
>
<q-card-section>
<div class="bg-primary rounded-10">
<span></span>
</div>
</q-card-section>
</q-card-section>
</q-card>
</template>

View File

@ -1,14 +1,77 @@
<script setup lang="ts"> <script setup lang="ts">
import { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; /*eslint-disable*/
import { useI18n } from 'vue-i18n';
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodEmployeeDetails; rawData: PayPeriodEmployeeDetails;
}>(); }>();
const weeks = [ props.rawData.week1, props.rawData?.week2 ];
const days = weeks.flatMap( week => Object.values(week.shifts) );
const getShiftColor = (type: string): string => {
switch(type) {
case 'REGULAR': return 'primary';
case 'EVENING': return 'info';
case 'EMERGENCY': return 'teal-14';
case 'OVERTIME': return 'negative';
case 'VACATION': return 'purple-10';
case 'HOLIDAY': return 'indigo-13';
case 'SICK': return 'grey-9';
default : return '';
}
};
</script> </script>
<template> <template>
<div class="bg-secondary full-width q-pa-sm rounded-5">
<q-card
v-for="day, index in days"
>
<q-card-section>
<TimesheetApprovalEmployeeDetailsShiftsRow
:day-data="day"
/>
</q-card-section>
</q-card>
</div>
<!-- <q-card-section :horizontal="$q.screen.gt.sm" class="row">
<q-card-section v-for="week in props.rawData" class="q-pa-none q-ma-none col">
<div v-for="day, index in week.shifts">
<div class=" text-primary text-uppercase q-pa-none q-ma-none"> {{ day_labels[index] }} </div>
<div v-if="day.shifts.length === 0" class="q-pa-none q-ma-none">---</div>
<div v-for="shift in day.shifts">
<div class="text-caption row items-center">
<div class="col row">
{{ shift.start_time }}
<q-space />
<q-separator vertical :color="getShiftColor(shift.type)"/>
</div>
<q-separator class="col" :color="getShiftColor(shift.type)"/>
<div class="col-auto">
<q-badge
:color="getShiftColor(shift.type)"
class="text-lowercase text-center"
style="font-size: 0.8em;"
>
{{ shift.type }}
</q-badge>
</div>
<q-separator class="col" :color="getShiftColor(shift.type)"/>
<div class="col row">
<q-separator vertical :color="getShiftColor(shift.type)"/>
<q-space />
{{ shift.end_time }}
</div>
</div>
</div>
</div>
</q-card-section>
</q-card-section> -->
</template> </template>

View File

@ -1,125 +0,0 @@
<script setup lang="ts">
/*eslint-disable*/
import { useI18n } from 'vue-i18n';
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';
const props = defineProps<{
isLoading: boolean;
employeeName: string;
employeeOverview: PayPeriodOverviewEmployee | undefined;
employeeDetails: PayPeriodEmployeeDetails | undefined;
updateKey: number;
}>();
const { t } = useI18n();
</script>
<template>
<q-card class="q-pa-md bg-white shadow-12 full-width rounded-15">
<!-- loader -->
<q-card-section
v-if="props.isLoading"
class="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 full-width text-primary q-pa-none text-uppercase">
{{ props.employeeName }}
<q-separator class="q-mb-sm" color="accent" size="2px" />
</q-card-section>
<!-- employee timesheet details -->
<q-card-section v-if="!props.isLoading" class="q-pa-none justify-center">
<TimesheetApprovalEmployeeDetailsHoursWorkedChart
ref="chart1"
:key="props.updateKey"
:raw-data="props.employeeDetails"
style="min-height: 300px;"
:options="({
indexAxis: $q.screen.lt.md? 'y' : 'x',
plugins: {
legend: {
labels: {
boxWidth: 15,
},
},
title: {
display: true,
text: t('timeSheetValidations.hoursWorkedChartTitle'),
color: '#616161'
}
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
}
}
})"
/>
</q-card-section>
<q-separator class="q-my-sm"/>
<q-card-section :horizontal="$q.screen.gt.sm">
<q-card-section class="q-pa-none q-ma-none">
<TimesheetApprovalEmployeeDetailsShiftTypesChart
:key="props.updateKey + 1"
style="max-height: 200px;"
:raw-data="props.employeeOverview"
:options="({
plugins:{
legend:{
labels:{
boxWidth: 15,
}
}
}
})"
/>
</q-card-section>
<q-separator :vertical="$q.screen.gt.sm" class="q-my-sm" />
<q-card-section class="q-py-none q-ma-none">
<TimesheetApprovalEmployeeExpensesChart
:style="$q.screen.lt.md ? 'min-height: 200px;': '' "
:raw-data="props.employeeDetails"
:options="({
plugins: {
title: {
display: true,
text: t('timeSheetValidations.reportFilterExpenses'),
color: '#616161'
},
legend:{
labels:{
boxWidth: 15,
}
}
},
elements: {
point:{
pointStyle: false,
}
},
})"
/>
</q-card-section>
</q-card-section>
</q-card>
</template>

View File

@ -52,7 +52,7 @@
dense dense
v-for="(button, index) in card_buttons" v-for="(button, index) in card_buttons"
:key="index" :key="index"
class="q-py-none bg-white q-my-xs" class="q-py-none q-my-xs"
color="primary" color="primary"
:icon="button.icon" :icon="button.icon"
@click="button.onClick" @click="button.onClick"

View File

@ -8,7 +8,7 @@
import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue'; import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue';
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue'; import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
import TimesheetApprovalEmployeeDetails from '../components/timesheet-approval-employee-details.vue'; import TimesheetApprovalEmployeeDetails from 'src/modules/timesheet-approval/pages/timesheet-approval-employee-details.vue';
import { date, type QTableColumn } from 'quasar'; import { date, type QTableColumn } from 'quasar';
import type { PayPeriodOverviewEmployee } from '../types/timesheet-approval-pay-period-overview-employee-interface'; import type { PayPeriodOverviewEmployee } from '../types/timesheet-approval-pay-period-overview-employee-interface';
@ -115,7 +115,7 @@
} }
} }
const getEmployeeOverview = (email: string): PayPeriodOverviewEmployee | undefined => { const getEmployeeOverview = (email: string): PayPeriodOverviewEmployee => {
return timesheet_approval_api.getPayPeriodOverviewByEmployeeEmail(email); return timesheet_approval_api.getPayPeriodOverviewByEmployeeEmail(email);
} }
@ -150,7 +150,8 @@
transition-show="jump-down" transition-show="jump-down"
transition-hide="jump-down" transition-hide="jump-down"
@before-show="() => update_key += 1" @before-show="() => update_key += 1"
class="full-width" :full-width="$q.screen.gt.sm"
:full-height="$q.screen.gt.sm"
> >
<TimesheetApprovalEmployeeDetails <TimesheetApprovalEmployeeDetails
:is-loading="timesheet_store.is_loading" :is-loading="timesheet_store.is_loading"
@ -179,7 +180,7 @@
> >
<!-- Top Bar that contains Search, Title, Filters --> <!-- Top Bar that contains Search, Title, Filters -->
<template v-slot:top> <template v-slot:top>
<div :class="$q.screen.lt.md ? 'column justify-center items-center' : 'full-width row'"> <div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
<!-- Date Picker --> <!-- Date Picker -->
<TimesheetApprovalPeriodPicker <TimesheetApprovalPeriodPicker
:is-disabled="timesheet_store.is_loading" :is-disabled="timesheet_store.is_loading"

View File

@ -26,7 +26,7 @@
</script> </script>
<template> <template>
<div class="row"> <div class="row justify-center">
<q-btn <q-btn
push rounded push rounded
icon="keyboard_arrow_left" icon="keyboard_arrow_left"

View File

@ -1,7 +1,7 @@
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 "../types/timesheet-approval-pay-period-report-interface";
import type { PayPeriodOverviewEmployee } from "../types/timesheet-approval-pay-period-overview-employee-interface"; import { default_pay_period_overview_employee, type PayPeriodOverviewEmployee } from "../types/timesheet-approval-pay-period-overview-employee-interface";
export const useTimesheetApprovalApi = () => { export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
@ -16,8 +16,10 @@ export const useTimesheetApprovalApi = () => {
} }
} }
const getPayPeriodOverviewByEmployeeEmail = (email: string): PayPeriodOverviewEmployee | undefined => { const getPayPeriodOverviewByEmployeeEmail = (email: string): PayPeriodOverviewEmployee => {
return timesheet_store.pay_period_overview_employees.find(overview => overview.email === email); const employee_overview = timesheet_store.pay_period_overview_employees.find(overview => overview.email === 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.

View File

@ -0,0 +1,166 @@
<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';
const props = defineProps<{
isLoading: boolean;
employeeName: string;
employeeOverview: PayPeriodOverviewEmployee;
employeeDetails: PayPeriodEmployeeDetails;
updateKey: number;
}>();
const { t } = useI18n();
const is_showing_graph = ref<boolean>(true);
</script>
<template>
<q-card
class="q-pa-md bg-white shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !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 class="q-mb-sm" color="accent" size="2px" />
<q-card-actions align="center">
<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: 'mode_edit_outline', value: false},
]"
/>
</q-card>
</q-card-actions>
</q-card-section>
<!-- employee timesheet details edit -->
<q-card-section v-if="!props.isLoading && !is_showing_graph">
<TimesheetApprovalEmployeeDetailsShifts
:raw-data="props.employeeDetails"
/>
</q-card-section>
<!-- employee timesheet details, but look at these graphs -->
<q-card-section v-if="!props.isLoading && is_showing_graph" class="q-pa-md col column full-width no-wrap">
<q-card-section class="q-pa-none col no-wrap" style="min-height: 300px;">
<TimesheetApprovalEmployeeDetailsHoursWorkedChart
:raw-data="props.employeeDetails"
:options="({
indexAxis: $q.screen.lt.md? 'y' : 'x',
plugins: {
legend: {
labels: {
boxWidth: 15,
},
},
title: {
display: true,
text: t('timeSheetValidations.hoursWorkedChartTitle'),
color: '#616161'
}
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
suggestedMin: 0,
suggestedMax: 10,
}
}
})"
/>
</q-card-section>
<q-separator class="q-ma-sm"/>
<q-card-section
:horizontal="$q.screen.gt.sm"
class="justify-center no-wrap col full-width q-pa-none"
>
<q-card-section class="q-pa-none q-ma-none col-4">
<TimesheetApprovalEmployeeDetailsShiftTypesChart
:raw-data="props.employeeOverview"
:options="({
plugins:{
legend:{
labels:{
boxWidth: 15,
}
}
}
})"
/>
</q-card-section>
<q-separator :vertical="$q.screen.gt.sm" class="q-ma-md" />
<q-card-section class="q-pa-none q-ma-none col" :class="$q.screen.lt.md ? 'full-width' : ''">
<TimesheetApprovalEmployeeExpensesChart
:style="$q.screen.lt.md ? 'min-height: 300px;': '' "
:raw-data="props.employeeDetails"
:options="({
indexAxis: $q.screen.lt.md? 'y' : 'x',
plugins: {
title: {
display: true,
text: t('timeSheetValidations.reportFilterExpenses'),
color: '#616161'
},
legend:{
labels:{
boxWidth: 15,
}
}
},
scales: {
x: {
stacked: true
},
y: {
suggestedMin: 0,
suggestedMax: 100,
stacked: true
}
}
})"
/>
</q-card-section>
</q-card-section>
</q-card-section>
</q-card>
</template>

View File

@ -34,17 +34,26 @@
padding padding
class="q-pa-md bg-secondary" class="q-pa-md bg-secondary"
> >
<div class="text-h4 row justify-center q-mt-lg text-uppercase text-weight-bolder text-grey-8"> <div class="text-h4 row justify-center text-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">
{{ $t('pageTitles.timeSheetValidations') }} {{ $t('pageTitles.timeSheetValidations') }}
</div> </div>
<div class="row items-center justify-center q-py-none q-my-none"> <div class="row items-center justify-center q-py-none q-my-none">
<div class="text-primary text-h6 text-uppercase"> <div
class="text-primary text-uppercase text-weight-bold"
:class="$q.screen.lt.md ? '' : 'text-h6'"
>
{{ pay_period_label.start_date }} {{ pay_period_label.start_date }}
</div> </div>
<div class="text-grey-8 text-weight-bold text-uppercase q-mx-md"> <div
class="text-grey-8 text-uppercase q-mx-md"
:class="$q.screen.lt.md ? 'text-weight-medium text-caption' : 'text-weight-bold'"
>
{{ $t('timeSheet.dateRangesTo') }} {{ $t('timeSheet.dateRangesTo') }}
</div> </div>
<div class="text-primary text-h6 text-uppercase"> <div
class="text-primary text-uppercase text-center text-weight-bold"
:class="$q.screen.lt.md ? '' : 'text-h6'"
>
{{ pay_period_label.end_date }} {{ pay_period_label.end_date }}
</div> </div>
</div> </div>

View File

@ -25,6 +25,7 @@ export const timesheetApprovalService = {
getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => { getTimesheetsByPayPeriodAndEmail: 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, }});
console.log('employee details: ', response.data);
return response.data; return response.data;
}, },

View File

@ -1,6 +1,11 @@
import type { TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet-details-interface"; import { default_timesheet_details_week, type TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet-details-interface";
export interface PayPeriodEmployeeDetails { export interface PayPeriodEmployeeDetails {
week1: TimesheetDetailsWeek; week1: TimesheetDetailsWeek;
week2: TimesheetDetailsWeek; week2: TimesheetDetailsWeek;
}; };
export const default_pay_period_employee_details = {
week1: default_timesheet_details_week(),
week2: default_timesheet_details_week(),
}

View File

@ -9,3 +9,15 @@ export interface PayPeriodOverviewEmployee {
mileage: number; mileage: number;
is_approved: boolean; 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,
expenses: -1,
mileage: -1,
is_approved: false
}

View File

@ -16,6 +16,11 @@ export interface TimesheetDetailsDailySchedule {
break_duration?: number; break_duration?: number;
} }
export interface Expense {
is_approved: boolean;
amount: number;
};
type WeekDay<T> = { type WeekDay<T> = {
sun: T; sun: T;
mon: T; mon: T;
@ -33,7 +38,35 @@ interface TimesheetDetailsDailyExpenses {
[otherType: string]: Expense[]; //for possible future types of expenses [otherType: string]: Expense[]; //for possible future types of expenses
} }
export interface Expense {
is_approved: boolean; // empty default builder
amount: number; const makeWeek = <T>(factory: () => T): WeekDay<T> => ({
}; sun: factory(),
mon: factory(),
tue: factory(),
wed: factory(),
thu: factory(),
fri: factory(),
sat: factory(),
});
const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
shifts: [],
regular_hours: 0,
evening_hours: 0,
emergency_hours: 0,
overtime_hours: 0,
short_date: "",
break_duration: 0,
});
const emptyDailyExpenses = (): TimesheetDetailsDailyExpenses => ({
cash: [],
km: [],
});
export const default_timesheet_details_week = (): TimesheetDetailsWeek => ({
is_approved: false,
shifts: makeWeek(emptyDailySchedule),
expenses: makeWeek(emptyDailyExpenses),
});

View File

@ -1,5 +1,7 @@
export interface Shift { export interface Shift {
is_approved: boolean; date : string;
start: string; start_time : string;
end: string; end_time : string;
type : string;
is_approved : boolean;
} }

View File

@ -3,7 +3,7 @@ import { ref } from 'vue';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval'; import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface'; import 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 type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface";
import 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/timesheet-approval-pay-period-employee-details-interface';
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface'; import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
const default_pay_period: PayPeriod = { const default_pay_period: PayPeriod = {
@ -20,7 +20,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const current_pay_period = ref<PayPeriod>(default_pay_period); const current_pay_period = ref<PayPeriod>(default_pay_period);
const pay_period_overview_employees = ref<PayPeriodOverviewEmployee[]>([]); const pay_period_overview_employees = ref<PayPeriodOverviewEmployee[]>([]);
const pay_period_overview_employee_approval_statuses = ref<{key: string, value: boolean}[] | undefined>(); const pay_period_overview_employee_approval_statuses = ref<{key: string, value: boolean}[] | undefined>();
const pay_period_employee_details = ref<PayPeriodEmployeeDetails | undefined>(); const pay_period_employee_details = ref<PayPeriodEmployeeDetails>(default_pay_period_employee_details);
const pay_period_report = ref(); const pay_period_report = ref();
const getPayPeriodByDate = async (date_string: string): Promise<boolean> => { const getPayPeriodByDate = async (date_string: string): Promise<boolean> => {