feat(approvals): add timeline compact viewer. Forego slider/range implementation for custom vue/typescript method

This commit is contained in:
Nicolas Drolet 2025-08-25 17:07:08 -04:00
parent 911a959567
commit 6248cb3354
15 changed files with 314 additions and 125 deletions

View File

@ -297,6 +297,7 @@ export default {
}, },
timeSheetValidations: { timeSheetValidations: {
tableColumnLabelFullname: 'Full name', tableColumnLabelFullname: 'Full name',
tableColumnLabelEmail: 'email address',
tableColumnLabelRegularHours: 'regular hours', tableColumnLabelRegularHours: 'regular hours',
tableColumnLabelEveningHours: 'evening', tableColumnLabelEveningHours: 'evening',
tableColumnLabelEmergencyHours: 'emergency', tableColumnLabelEmergencyHours: 'emergency',

View File

@ -347,6 +347,7 @@ export default {
}, },
timeSheetValidations: { timeSheetValidations: {
tableColumnLabelFullname: 'nom complet', tableColumnLabelFullname: 'nom complet',
tableColumnLabelEmail: 'courriel',
tableColumnLabelRegularHours: 'heures régulières', tableColumnLabelRegularHours: 'heures régulières',
tableColumnLabelEveningHours: 'soir', tableColumnLabelEveningHours: 'soir',
tableColumnLabelEmergencyHours: 'urgence', tableColumnLabelEmergencyHours: 'urgence',

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { computed } from "vue";
import type { Shift } from "src/modules/timesheets/types/timesheet-shift-interface";
import { date } from 'quasar';
const totalMinutes = 24 * 60;
const props = defineProps<{
weekdayShifts: (Shift | null)[];
}>();
const toMinutes = (time: string): number => {
const parsed_time = date.extractDate(time, 'HH:mm');
return parsed_time.getHours() * 60 + parsed_time.getMinutes();
};
const bars = computed(() => {
return props.weekdayShifts
.filter((s): s is Shift => s !== null) // skip null shifts
.map((s) => {
const start = toMinutes(s.start_time);
const end = toMinutes(s.end_time);
const start_percent = (start / totalMinutes) * 100;
const width_percent = ((end - start) / totalMinutes) * 100;
return { start_percent, width_percent };
});
});
</script>
<template>
<div class="relative full-width bg-grey-4 rounded-10" style="height: 8px; margin: 7px 0;">
<div
v-for="(bar, i) in bars"
:key="i"
class="absolute bg-primary"
:style="{
left: bar.start_percent + '%',
width: bar.width_percent + '%',
height: '14px',
transform: 'translateY(-3px)',
}"
></div>
</div>
</template>
<!-- <script setup lang="ts">
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
import { date } from 'quasar';
import { ref } from 'vue';
const props = defineProps<{
weekdayShifts: Shift[];
}>();
const extractTimeValue = (time: string): number => {
const parsed_time = date.extractDate(time, 'HH:mm');
return parsed_time.getHours() + (parsed_time.getMinutes() / 60);
};
const slider_model = ref<number>(0);
const shifts = ref<Shift[]>(props.weekdayShifts);
const ranges = ref(
shifts.value.map( shift =>
shift
? {
min: extractTimeValue(shift.start_time),
max: extractTimeValue(shift.end_time)
}
: {
min: 0,
max: 0
}
)
);
</script>
<template>
<div class="relative-position q-pa-none q-ma-none col">
<div class="absolute-full row justify-between">
<div></div>
<q-separator vertical color="accent" />
<q-separator vertical color="accent" />
<q-separator vertical color="accent" />
<q-separator vertical color="accent" />
<q-separator vertical color="accent" />
<div></div>
</div>
<q-slider
v-model="slider_model"
track-color="grey-6"
track-size="8px"
thumb-size="0px"
:max="24"
readonly
class="absolute-full q-pt-xs"
/>
<div
v-for="(_range, index) in ranges"
:key="index"
class="absolute-full"
>
<q-range
v-model="ranges[index]"
track-color="transparent"
inner-track-color="transparent"
color="primary"
:max="24"
thumb-size="0px"
track-size="16px"
label-color="accent"
label-text-color="primary"
label
:left-label-value="shifts[index]?.start_time"
:right-label-value="shifts[index]?.end_time"
readonly
class="z-top"
/>
</div>
</div>
</template> -->

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface';
import ShiftPreviewBar from 'src/modules/timesheet-approval/components/shifts/shift-preview-bar.vue';
const props = defineProps<{
isLoading: boolean;
employeeDetails: PayPeriodEmployeeDetails | undefined;
}>();
</script>
<template>
<q-card class="q-pa-sm bg-white shadow-5 full-width">
<!-- loader -->
<q-card-section v-if="props.isLoading">
<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 timesheet details -->
<q-card-section
v-if="!props.isLoading"
class="column"
>
<div class="absolute-full row justify-center text-center text-primary text-weight-bolder text-caption q-py-none q-my-none q-px-xl q-mx-sm">
<q-space />
<div class="col">4</div>
<div class="col">8</div>
<div class="col">12</div>
<div class="col">4</div>
<div class="col">8</div>
<q-space />
</div>
<ShiftPreviewBar
v-for="(shifts, index) in employeeDetails?.week1.shifts"
:key="index"
:weekday-shifts="shifts"
class="q-mt-xs"
/>
<!-- {{ employeeDetails }} -->
</q-card-section>
</q-card>
</template>

View File

@ -1,29 +0,0 @@
<script setup lang="ts">
import type { TimesheetDetailsWeek } from 'src/modules/timesheets/types/timesheet-details-interface';
const props = defineProps<{
isLoading: boolean;
employeeDetails: TimesheetDetailsWeek;
}>();
</script>
<template>
<q-card class="q-pa-sm bg-white shadow-5">
<!-- loader -->
<q-card-section v-if="props.isLoading">
<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 timesheet details -->
<q-card-section v-if="!props.isLoading">
{{ employeeDetails }}
</q-card-section>
</q-card>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface'; import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface';
interface TableColumn { interface TableColumn {
name: string; name: string;
@ -9,36 +9,40 @@
const props = defineProps<{ const props = defineProps<{
cols: TableColumn[]; cols: TableColumn[];
row: PayPeriodEmployeeOverview; row: PayPeriodOverviewEmployee;
initialState: boolean; initialState: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
isDetailsButtonPressed: [] clickDetails: [email: string];
'update:modelValue': [value: string | number | null] 'update:modelValue': [value: boolean | null];
}>(); }>();
</script> </script>
<template> <template>
<div class="q-px-sm q-pb-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition"> <div class="q-px-sm q-pb-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
<q-card class="rounded-10"> <q-card class="rounded-10">
<!-- Button to get full timesheet details --> <!-- Card header with employee name and details button-->
<q-btn
flat
unelevated
class="q-pa-sm"
icon="open_in_full"
@click="emit('isDetailsButtonPressed')"
/>
<!-- Card header with employee name -->
<q-card-section <q-card-section
horizontal horizontal
class="q-py-none q-px-md" class="q-py-none q-pl-md"
> >
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden"> <div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">
{{ props.row.employee_name }} {{ props.row.employee_name }}
</div> </div>
<q-space />
<!-- Button to get full timesheet details -->
<q-btn
flat
unelevated
rounded
class="q-py-none"
color="primary"
icon="open_in_full"
@click="emit('clickDetails', props.row.email)"
/>
</q-card-section> </q-card-section>
<q-separator <q-separator

View File

@ -8,9 +8,10 @@
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 { date, type QTableColumn } from 'quasar'; import { date, type QTableColumn } from 'quasar';
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-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();
@ -21,14 +22,21 @@
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 columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [ const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [
{ {
name: 'employee_name', name: 'employee_name',
label: t('timeSheetValidations.tableColumnLabelFullname'), label: t('timeSheetValidations.tableColumnLabelFullname'),
field: 'employee_name', field: 'employee_name',
sortable: true sortable: true
}, },
{
name: 'email',
label: t('timeSheetValidations.tableColumnLabelEmail'),
field: 'email',
sortable: true,
},
{ {
name: 'regular_hours', name: 'regular_hours',
label: t('timeSheetValidations.tableColumnLabelRegularHours'), label: t('timeSheetValidations.tableColumnLabelRegularHours'),
@ -65,7 +73,7 @@
]); ]);
const has_changes = computed(() => { const has_changes = computed(() => {
return timesheet_store.pay_period_employee_overviews.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];
}); });
}); });
@ -79,19 +87,36 @@
await timesheet_approval_api.getPayPeriodOverviewByDate(date_string); await timesheet_approval_api.getPayPeriodOverviewByDate(date_string);
} }
const onClickedDetails = async (email: string) => {
is_showing_details.value = true;
console.log('employee email is: ', email);
await timesheet_approval_api.getTimesheetsByPayPeriodAndEmail(email);
}
onMounted( async () => { onMounted( async () => {
const today = date.formatDate(new Date(), 'YYYY-MM-DD'); const today = date.formatDate(new Date(), 'YYYY-MM-DD');
await timesheet_approval_api.getPayPeriodOverviewByDate(today); await timesheet_approval_api.getPayPeriodOverviewByDate(today);
const approvals = timesheet_store.pay_period_employee_overviews.map(emp => [emp.email, emp.is_approved]); const approvals = timesheet_store.pay_period_overview_employees.map(emp => [emp.email, emp.is_approved]);
original_approvals.value = Object.fromEntries(approvals); original_approvals.value = Object.fromEntries(approvals);
console.log(timesheet_store.pay_period_overview_employees);
}) })
</script> </script>
<template> <template>
<q-dialog
v-model="is_showing_details"
transition-show="scale"
transition-hide="jump-down"
>
<TimesheetApprovalEmployeeDetails
:is-loading="timesheet_store.is_loading"
:employee-details="timesheet_store.pay_period_employee_details"
/>
</q-dialog>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table <q-table
:rows="timesheet_store.pay_period_employee_overviews" :rows="timesheet_store.pay_period_overview_employees"
:columns="columns" :columns="columns"
row-key="email" row-key="email"
:filter="filter" :filter="filter"
@ -126,13 +151,15 @@
<!-- Template for individual employee cards --> <!-- Template for individual employee cards -->
<template v-slot:item="props: { <template v-slot:item="props: {
cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[],
row: PayPeriodEmployeeOverview row: PayPeriodOverviewEmployee
}"> }">
<TimesheetApprovalEmployeeOverviewListItem <TimesheetApprovalEmployeeOverviewListItem
:cols="props.cols" :cols="props.cols"
:row="props.row" :row="props.row"
:initial-state="props.row.is_approved"/> :initial-state="props.row.is_approved"
@click-details="onClickedDetails"
/>
</template> </template>
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->

View File

@ -40,8 +40,13 @@ export const useTimesheetApprovalApi = () => {
} }
} }
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
await timesheet_store.getTimesheetsByPayPeriodAndEmail(employee_email);
}
return { return {
getPayPeriodOverviewByDate, getPayPeriodOverviewByDate,
getNextPayPeriodOverview, getNextPayPeriodOverview,
getTimesheetsByPayPeriodAndEmail
} }
}; };

View File

@ -13,7 +13,6 @@
year: 'numeric', year: 'numeric',
}; };
const pay_period_label = computed(() => { const pay_period_label = computed(() => {
const dates = timesheet_store.current_pay_period.label.split('.'); const dates = timesheet_store.current_pay_period.label.split('.');
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD')); const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
@ -42,6 +41,7 @@
{{ pay_period_label.end_date }} {{ pay_period_label.end_date }}
</div> </div>
</div> </div>
<TimesheetApprovalEmployeeOverviewList /> <TimesheetApprovalEmployeeOverviewList />
</q-page> </q-page>
</template> </template>

View File

@ -17,58 +17,12 @@ export const timesheetApprovalService = {
getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => { getPayPeriodEmployeeOverviews: 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_number: number, employee_email: string): Promise<PayPeriodEmployeeDetails> => { getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => {
const response = await api.get(`/timesheets/${year}/${period_number}/${employee_email}`) || { data: MOCK_DATA_TIMESHEET_DETAILS, status: 200, statusText: 'OK'}; const response = await api.get('timesheets', { params: { year, period_no, email, }});
return response.data; return response.data;
} }
}; };
const MOCK_DATA_TIMESHEET_DETAILS = {
is_approved: false,
week1: {
is_approved: true,
shifts: {
sun: [],
mon: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
tue: [ { is_approved: true, start_time: '08:00', end_time: '11:45' }, { is_approved: true, start_time: '12:45', end_time: '17:00' } ],
wed: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
thu: [ { is_approved: false, start_time: '13:00', end_time: '17:00' } ],
fri: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
sat: []
},
expenses: {
sun: { costs: [], mileage: [] },
mon: { costs: [ { is_approved: true, amount: 156.49 } ], mileage: [] },
tue: { costs: [], mileage: [] },
wed: { costs: [], mileage: [] },
thu: { costs: [], mileage: [ { is_approved: false, amount: 16 } ] },
fri: { costs: [], mileage: [] },
sat: { costs: [], mileage: [] }
}
},
week2: {
is_approved: true,
shifts: {
sun: [],
mon: [],
tue: [],
wed: [],
thu: [],
fri: [],
sat: []
},
expenses: {
sun: { costs: [], mileage: [] },
mon: { costs: [], mileage: [] },
tue: { costs: [], mileage: [] },
wed: { costs: [], mileage: [] },
thu: { costs: [], mileage: [] },
fri: { costs: [], mileage: [] },
sat: { costs: [], mileage: [] }
}
},
} as PayPeriodEmployeeDetails;

View File

@ -1,4 +1,4 @@
export interface PayPeriodEmployeeOverview { export interface PayPeriodOverviewEmployee {
email: string; email: string;
employee_name: string; employee_name: string;
regular_hours: number; regular_hours: number;

View File

@ -1,4 +1,4 @@
import type { PayPeriodEmployeeOverview } from "./timesheet-approval-pay-period-employee-overview-interface"; import type { PayPeriodOverviewEmployee } from "./timesheet-approval-pay-period-overview-employee-interface";
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: PayPeriodEmployeeOverview[]; employees_overview: PayPeriodOverviewEmployee[];
}; };

View File

@ -1,3 +1,5 @@
import type { Shift } from "./timesheet-shift-interface";
export interface TimesheetDetailsWeek { export interface TimesheetDetailsWeek {
is_approved: boolean; is_approved: boolean;
shifts: WeekDay<TimesheetDetailsWeekDayShifts>; shifts: WeekDay<TimesheetDetailsWeekDayShifts>;
@ -16,12 +18,6 @@ type WeekDay<T> = {
type TimesheetDetailsWeekDayShifts = Shift[]; type TimesheetDetailsWeekDayShifts = Shift[];
interface Shift {
is_approved: boolean;
start_time: string;
end_time: string;
}
interface TimesheetDetailsWeekDayExpenses { interface TimesheetDetailsWeekDayExpenses {
costs: Expense[]; costs: Expense[];
mileage: Expense[]; mileage: Expense[];

View File

@ -0,0 +1,5 @@
export interface Shift {
is_approved: boolean;
start_time: string;
end_time: string;
}

View File

@ -2,8 +2,8 @@ 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/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 { PayPeriodEmployeeOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-overview-interface"; import type { PayPeriodOverviewEmployee } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface";
import { 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 default_pay_period: PayPeriod = { const default_pay_period: PayPeriod = {
pay_period_no: -1, pay_period_no: -1,
@ -17,8 +17,8 @@ const default_pay_period: PayPeriod = {
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 current_pay_period = ref<PayPeriod>(default_pay_period);
const pay_period_employee_overviews = ref<PayPeriodEmployeeOverview[]>([]); const pay_period_overview_employees = ref<PayPeriodOverviewEmployee[]>([]);
const pay_period_employee_details = ref<PayPeriodEmployeeDetails | {}>({}); const pay_period_employee_details = ref<PayPeriodEmployeeDetails | undefined>();
const getPayPeriodByDate = async (date_string: string): Promise<boolean> => { const getPayPeriodByDate = async (date_string: string): Promise<boolean> => {
is_loading.value = true; is_loading.value = true;
@ -62,27 +62,35 @@ export const useTimesheetStore = defineStore('timesheet', () => {
is_loading.value = true; is_loading.value = true;
try { try {
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews(pay_year, period_number, supervisor_email); const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews(
pay_period_employee_overviews.value = response.employees_overview; pay_year,
period_number,
supervisor_email
);
pay_period_overview_employees.value = response.employees_overview;
} catch (error) { } catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error); console.error('There was an error retrieving Employee Pay Period overviews: ', error);
pay_period_employee_overviews.value = []; pay_period_overview_employees.value = [];
// TODO: More in-depth error-handling here // TODO: More in-depth error-handling here
} }
is_loading.value = false; is_loading.value = false;
}; };
const getTimesheetsByPayPeriodAndEmail = async (year: number, period_number: number, employee_email: string) => { const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
is_loading.value = true; is_loading.value = true;
try { try {
const response = await timesheetApprovalService.getTimesheetsByPayPeriodAndEmail(year, period_number, employee_email); const response = await timesheetApprovalService.getTimesheetsByPayPeriodAndEmail(
current_pay_period.value.pay_year,
current_pay_period.value.pay_period_no,
employee_email
);
pay_period_employee_details.value = response; pay_period_employee_details.value = response;
} catch (error) { } catch (error) {
console.error('There was an error retrieving timesheet details for this employee: ', error); console.error('There was an error retrieving timesheet details for this employee: ', error);
pay_period_employee_details.value = {}; pay_period_employee_details.value = MOCK_DATA_TIMESHEET_DETAILS;
// TODO: More in-depth error-handling here // TODO: More in-depth error-handling here
} }
@ -91,10 +99,58 @@ export const useTimesheetStore = defineStore('timesheet', () => {
return { return {
current_pay_period, current_pay_period,
pay_period_employee_overviews, pay_period_overview_employees,
pay_period_employee_details,
is_loading, is_loading,
getPayPeriodByDate, getPayPeriodByDate,
getPayPeriodByYearAndPeriodNumber, getPayPeriodByYearAndPeriodNumber,
getTimesheetApprovalPayPeriodEmployeeOverviews, getTimesheetApprovalPayPeriodEmployeeOverviews,
getTimesheetsByPayPeriodAndEmail,
}; };
}); });
const MOCK_DATA_TIMESHEET_DETAILS = {
is_approved: false,
week1: {
is_approved: true,
shifts: {
sun: [],
mon: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
tue: [ { is_approved: true, start_time: '08:00', end_time: '11:45' }, { is_approved: true, start_time: '12:45', end_time: '17:00' } ],
wed: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
thu: [ { is_approved: false, start_time: '13:00', end_time: '17:00' } ],
fri: [ { is_approved: true, start_time: '08:00', end_time: '12:00' }, { is_approved: true, start_time: '13:00', end_time: '17:00' } ],
sat: []
},
expenses: {
sun: { costs: [], mileage: [] },
mon: { costs: [ { is_approved: true, amount: 156.49 } ], mileage: [] },
tue: { costs: [], mileage: [] },
wed: { costs: [], mileage: [] },
thu: { costs: [], mileage: [ { is_approved: false, amount: 16 } ] },
fri: { costs: [], mileage: [] },
sat: { costs: [], mileage: [] }
}
},
week2: {
is_approved: true,
shifts: {
sun: [],
mon: [],
tue: [],
wed: [],
thu: [],
fri: [],
sat: []
},
expenses: {
sun: { costs: [], mileage: [] },
mon: { costs: [], mileage: [] },
tue: { costs: [], mileage: [] },
wed: { costs: [], mileage: [] },
thu: { costs: [], mileage: [] },
fri: { costs: [], mileage: [] },
sat: { costs: [], mileage: [] }
}
},
} as PayPeriodEmployeeDetails;