feat(details): add ui elements to shift rows in employee details

This commit is contained in:
Nicolas Drolet 2025-09-05 17:08:43 -04:00
parent 5bd19c4a9c
commit fbf8114666
10 changed files with 231 additions and 103 deletions

View File

@ -342,13 +342,13 @@ export default {
reportFilterVacation: 'Vacation', reportFilterVacation: 'Vacation',
}, },
shiftColumns: { shiftColumns: {
title: 'Shifts', title: 'shifts',
column_1: 'Type', labelType: 'type',
column_2: 'Start time', labelIn: 'start',
column_3: 'End time', labelOut: 'end',
column_4: 'Comment', labelComment: 'comment',
column_5: 'Status', labelState: 'state',
column_6: 'Supervisors report', labelSupervisorReport: 'supervisor report',
}, },
expenseColumns: { expenseColumns: {
title: 'Expenses', title: 'Expenses',

View File

@ -236,12 +236,12 @@ export default {
}, },
shiftColumns: { shiftColumns: {
title: 'Quarts de travail', title: 'Quarts de travail',
column_1: 'Type', labelType: 'type',
column_2: 'Entrée', labelIn: 'entrée',
column_3: 'Sortie', labelOut: 'sortie',
column_4: 'Commentaire', labelComment: 'commentaire',
column_5: tat', labelState: tat',
column_6: 'Rapport du superviseur', labelSupervisorReport: 'rapport du superviseur',
}, },
shiftsTemplate: { shiftsTemplate: {
tabTitle1: 'Quarts de travail', tabTitle1: 'Quarts de travail',

View File

@ -0,0 +1,58 @@
<template>
<!-- This header component is placed in absolute top in order to keep the timestamps
below it centered. As such it must imitate the column width of the shift row below it
as much as possible, hence the empty q-card-sections. -->
<q-card-section
horizontal
class="text-uppercase text-center items-center q-px-xs"
>
<!-- Date widget -->
<q-card-section class="q-px-xs q-py-none col-auto" style="width: 75px;"></q-card-section>
<!-- shift row itself -->
<q-card-section horizontal class="q-px-xs col">
<!-- punch-in timestamps -->
<q-card-section class="col q-px-xs q-py-none">
<q-item-label class="text-weight-bolder text-primary">
{{ $t('shiftColumns.labelIn') }}
</q-item-label>
</q-card-section>
<!-- arrows pointing to punch-out timestamps -->
<q-card-section class="col-auto q-pa-none q-mx-sm">
<q-icon
name="double_arrow"
color="transparent"
size="24px"
style="transform: translateX(5px);"
/>
<q-icon
name="double_arrow"
color="transparent"
size="24px"
style="transform: translateX(-5px);"
/>
</q-card-section>
<!-- punch-out timestamps -->
<q-card-section class="col q-px-xs q-py-none">
<q-item-label class="text-weight-bolder text-primary">
{{ $t('shiftColumns.labelOut') }}
</q-item-label>
</q-card-section>
<!-- shift type badge -->
<q-card-section class="col q-pa-none"></q-card-section>
<!-- comment button -->
<q-card-section class="col-auto q-pa-none">
<q-btn
icon="chat_bubble_outline"
color="transparent"
flat
class="q-pa-sm"
/>
</q-card-section>
</q-card-section>
</q-card-section>
</template>

View File

@ -1,28 +1,112 @@
<script setup lang="ts"> <script setup lang="ts">
import { date } from 'quasar'; import { computed } from 'vue';
import { default_shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
import type { TimesheetDetailsDailySchedule } from 'src/modules/timesheets/types/timesheet-details-interface'; import type { TimesheetDetailsDailySchedule } from 'src/modules/timesheets/types/timesheet-details-interface';
const props = defineProps<{ const props = defineProps<{
dayData: TimesheetDetailsDailySchedule; dayData: TimesheetDetailsDailySchedule;
}>(); }>();
const shifts_or_placeholder = computed(() => {
const getWeekDay = (shift_date: string): string => { return props.dayData.shifts.length > 0 ? props.dayData.shifts : [default_shift];
const d = date.extractDate(shift_date, 'YYYY/MM/DD'); })
return date.formatDate(d, 'dddd');
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 'transparent';
}
}; };
</script> </script>
<template> <template>
<q-card> <q-card-section
<q-card-section horizontal
horizontal class="q-pa-xs text-uppercase text-center items-center"
class="q-pa-xs" >
> <!-- punch-in timestamps -->
<q-card-section> <q-card-section class="q-pa-none col">
<div class="bg-primary rounded-10"> <q-item-label
<span></span> v-for="shift, index in props.dayData.shifts"
</div> :key="index"
</q-card-section> class="text-weight-bolder text-grey-8 bg-secondary q-pa-xs rounded-5"
style="font-size: 1.75em; line-height: 80% !important;"
>
{{ shift.start_time }}
</q-item-label>
<q-item-label v-if="props.dayData.shifts.length === 0"
class="text-weight-bolder text-grey-5"
style="font-size: 1.75em; line-height: 80% !important;"
>
---
</q-item-label>
</q-card-section> </q-card-section>
</q-card>
<!-- arrows pointing to punch-out timestamps -->
<q-card-section
horizontal class="items-center justify-center q-ma-sm col-auto">
<q-icon
name="double_arrow"
:color=" props.dayData.shifts.length > 0 ? 'accent' : 'transparent'"
size="24px"
style="transform: translateX(5px);"
/>
<q-icon
name="double_arrow"
:color=" props.dayData.shifts.length > 0 ? 'primary' : 'transparent'"
size="24px"
style="transform: translateX(-5px);"
/>
</q-card-section>
<!-- punch-out timestamps -->
<q-card-section class="q-pa-none col">
<q-item-label
v-for="shift, index in props.dayData.shifts"
:key="index"
class="text-weight-bolder text-grey-8 bg-secondary q-pa-xs rounded-5"
style="font-size: 1.75em; line-height: 80% !important;"
>
{{ shift.end_time }}
</q-item-label>
<q-item-label v-if="props.dayData.shifts.length === 0"
class="text-weight-bolder text-grey-5"
style="font-size: 1.75em; line-height: 80% !important;"
>
---
</q-item-label>
</q-card-section>
<!-- shift type badge -->
<q-card-section class="col q-pa-none">
<q-badge
v-for="shift, index in shifts_or_placeholder"
:key="index"
:color="shift.type ? getShiftColor(shift.type) : 'transparent'"
:label="shift.type || ''"
class="text-weight-medium justify-center"
style="width: 80px; font-size: 0.8em;"
/>
</q-card-section>
<!-- comment button -->
<q-card-actions
class="col-auto q-pa-none"
>
<q-btn
v-for="(shift, index) in shifts_or_placeholder"
:key="index"
flat
:disable="shift.type === ''"
icon="chat_bubble_outline"
:color="shift.type === '' ? 'grey-5' : 'grey-8'"
class="q-pa-sm"
/>
</q-card-actions>
</q-card-section>
</template> </template>

View File

@ -1,77 +1,57 @@
<script setup lang="ts"> <script setup lang="ts">
/*eslint-disable*/
import { useI18n } from 'vue-i18n';
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue'; import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue';
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/timesheet-approval-pay-period-employee-details-interface';
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodEmployeeDetails; rawData: PayPeriodEmployeeDetails;
}>(); currentPayPeriod: PayPeriod;
}>();
const weeks = [ props.rawData.week1, props.rawData?.week2 ];
const days = weeks.flatMap( week => Object.values(week.shifts) ); const getDate = (shift_date: string): Date => {
return new Date(props.currentPayPeriod.pay_year.toString() + '/' + shift_date);
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"> <div
v-for="week, index in props.rawData"
:key="index"
class="q-px-xs q-pt-xs rounded-5 col"
>
<q-card <q-card
v-for="day, index in days" v-for="day, day_index in week.shifts"
:key="day_index"
flat
bordered
class="row items-center rounded-10 q-mb-xs"
> >
<q-card-section> <TimesheetApprovalEmployeeDetailsShiftsRowHeader class="absolute-top" />
<q-card-section class="col-auto q-pa-xs text-white">
<div
class="bg-primary rounded-10 q-pa-xs text-center"
style="width: 75px;"
>
<q-item-label
style="font-size: 0.8em;"
class="text-uppercase"
>{{ $d(getDate(day.short_date), {weekday: 'long'}) }}</q-item-label>
<q-item-label
class="text-weight-bolder"
style="font-size: 3em; line-height: 90% !important;"
>{{ day.short_date.split('/')[1] }}</q-item-label>
<q-item-label
style="font-size: 0.8em;"
class="text-uppercase"
>{{ $d(getDate(day.short_date), {month: 'long'}) }}</q-item-label>
</div>
</q-card-section>
<q-card-section class="col q-pa-none">
<TimesheetApprovalEmployeeDetailsShiftsRow <TimesheetApprovalEmployeeDetailsShiftsRow
:day-data="day" :day-data="day"
/> />
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </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

@ -60,7 +60,7 @@
<q-tooltip <q-tooltip
anchor="top middle" anchor="top middle"
self="center middle" self="center middle"
class="bg-primary uppercase text-weight-bold" class="bg-primary text-uppercase text-weight-bold"
> >
{{$t(button.label)}} {{$t(button.label)}}
</q-tooltip> </q-tooltip>

View File

@ -2,24 +2,19 @@
/* eslint-disable */ /* eslint-disable */
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api'; import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
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 'src/modules/timesheet-approval/pages/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';
const { t } = useI18n(); const { t } = useI18n();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi(); const timesheet_approval_api = useTimesheetApprovalApi();
const FORWARD = 1 const FORWARD = 1
const BACKWARD = -1 const BACKWARD = -1
const filter = ref<string | number | null>(''); const filter = ref<string | number | null>('');
const original_approvals = ref<Record<string, boolean>>({}); const original_approvals = ref<Record<string, boolean>>({});
const is_showing_details = ref<boolean>(false); const is_showing_details = ref<boolean>(false);
@ -28,7 +23,6 @@
const clicked_employee_name = ref<string>(''); const clicked_employee_name = ref<string>('');
const clicked_employee_email = ref<string>(''); const clicked_employee_email = ref<string>('');
const update_key = ref<number>(0); const update_key = ref<number>(0);
const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [ const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [
{ {
name: 'employee_name', name: 'employee_name',
@ -76,25 +70,21 @@
sortable: true sortable: true
} }
]); ]);
const has_changes = computed(() => { const has_changes = computed(() => {
return timesheet_store.pay_period_overview_employees.some(emp => { return timesheet_store.pay_period_overview_employees.some(emp => {
return emp.is_approved !== original_approvals.value[emp.email]; return emp.is_approved !== original_approvals.value[emp.email];
}); });
}); });
const is_not_enough_filters = computed(() => { const is_not_enough_filters = computed(() => {
return report_filter_company.value.filter(val => val === true).length < 1 || return report_filter_company.value.filter(val => val === true).length < 1 ||
report_filter_type.value.filter(val => val === true).length < 1; report_filter_type.value.filter(val => val === true).length < 1;
}) })
const filter_types_labels = [ const filter_types_labels = [
t('timeSheetValidations.reportFilterShifts'), t('timeSheetValidations.reportFilterShifts'),
t('timeSheetValidations.reportFilterExpenses'), t('timeSheetValidations.reportFilterExpenses'),
t('timeSheetValidations.reportFilterHoliday'), t('timeSheetValidations.reportFilterHoliday'),
t('timeSheetValidations.reportFilterVacation'), t('timeSheetValidations.reportFilterVacation'),
] ]
const is_calendar_limit = computed( () => { const is_calendar_limit = computed( () => {
return timesheet_store.current_pay_period.pay_year === 2024 && return timesheet_store.current_pay_period.pay_year === 2024 &&
timesheet_store.current_pay_period.pay_period_no <= 1; timesheet_store.current_pay_period.pay_period_no <= 1;
@ -150,7 +140,7 @@
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"
:full-width="$q.screen.gt.sm" full-width
:full-height="$q.screen.gt.sm" :full-height="$q.screen.gt.sm"
> >
<TimesheetApprovalEmployeeDetails <TimesheetApprovalEmployeeDetails
@ -158,6 +148,7 @@
:employee-name="clicked_employee_name" :employee-name="clicked_employee_name"
:employee-overview="getEmployeeOverview(clicked_employee_email)" :employee-overview="getEmployeeOverview(clicked_employee_email)"
:employee-details="timesheet_store.pay_period_employee_details" :employee-details="timesheet_store.pay_period_employee_details"
:current-pay-period="timesheet_store.current_pay_period"
:update-key="update_key" :update-key="update_key"
/> />
</q-dialog> </q-dialog>

View File

@ -8,12 +8,14 @@
import TimesheetApprovalEmployeeExpensesChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-expenses-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 { 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 { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface';
import { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
const props = defineProps<{ const props = defineProps<{
isLoading: boolean; isLoading: boolean;
employeeName: string; employeeName: string;
employeeOverview: PayPeriodOverviewEmployee; employeeOverview: PayPeriodOverviewEmployee;
employeeDetails: PayPeriodEmployeeDetails; employeeDetails: PayPeriodEmployeeDetails;
currentPayPeriod: PayPeriod;
updateKey: number; updateKey: number;
}>(); }>();
@ -23,7 +25,7 @@
<template> <template>
<q-card <q-card
class="q-pa-md bg-white shadow-12 rounded-15 column no-wrap relative" class="q-pa-sm bg-white shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !important;' : '' " :style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !important;' : '' "
> >
<!-- loader --> <!-- loader -->
@ -58,7 +60,7 @@
v-model="is_showing_graph" v-model="is_showing_graph"
:options="[ :options="[
{icon: 'bar_chart', value: true}, {icon: 'bar_chart', value: true},
{icon: 'mode_edit_outline', value: false}, {icon: 'edit', value: false},
]" ]"
/> />
</q-card> </q-card>
@ -66,9 +68,14 @@
</q-card-section> </q-card-section>
<!-- employee timesheet details edit --> <!-- employee timesheet details edit -->
<q-card-section v-if="!props.isLoading && !is_showing_graph"> <q-card-section
v-if="!props.isLoading && !is_showing_graph"
:horizontal="$q.screen.gt.sm"
class="q-pa-none bg-secondary rounded-10"
>
<TimesheetApprovalEmployeeDetailsShifts <TimesheetApprovalEmployeeDetailsShifts
:raw-data="props.employeeDetails" :raw-data="props.employeeDetails"
:current-pay-period="props.currentPayPeriod"
/> />
</q-card-section> </q-card-section>

View File

@ -4,4 +4,12 @@ export interface Shift {
end_time : string; end_time : string;
type : string; type : string;
is_approved : boolean; is_approved : boolean;
}
export const default_shift: Shift = {
date: '',
start_time: '',
end_time: '',
type: '',
is_approved: false,
} }

View File

@ -13,4 +13,4 @@ export const getCurrentPayPeriod = (today = new Date()): number => {
console.log(current_period); console.log(current_period);
return current_period; return current_period;
} }