refactor(many): change some components to respect 'props down, events up' convention for modularity and reusability. Keep branch open for future possible refactors.

This commit is contained in:
Nicolas Drolet 2025-08-22 11:11:14 -04:00
parent b0dcfdc73c
commit 7dbb30259c
11 changed files with 285 additions and 140 deletions

View File

@ -1,25 +1,18 @@
<script setup lang="ts">
/* eslint-disable */
import { useEmployeeStore } from 'src/stores/employee-store';
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
const employeeStore = useEmployeeStore();
const employeeListApi = useEmployeeListApi();
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
const getEmployeeAvatar = (first_name: string, last_name: string) => {
// add logic here to see if user has an avatar image and return that instead of initials
return first_name.charAt(0) + last_name.charAt(0);
};
const onProfileCardClick = (email: string) => {
employeeStore.isShowingEmployeeAddModifyWindow = true;
console.log("clicked profile!");
employeeListApi.getEmployeeDetails(email);
}
const props = defineProps<{
row: EmployeeListTableItem
}>()
const emit = defineEmits<{
onProfileClick: [email: string]
}>();
</script>
<template>
@ -27,7 +20,7 @@ import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-em
v-ripple
class="rounded-15 bg-white col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer q-ma-sm"
style="max-width: 230px;"
@click="onProfileCardClick(props.row.email)"
@click="emit('onProfileClick', props.row.email)"
>
<q-card-section class="text-center col-5">
<q-avatar color="primary" size="8em">

View File

@ -0,0 +1,42 @@
<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"
v-model="props.searchModel"
:label="$t('shared.searchBar')"
label-color="primary"
bg-color="white"
color="primary"
@update:model-value="value => emit('onSearchValueUpdated', value)"
>
<template v-slot:append>
<q-icon
name="search"
color="primary"
/>
</template>
</q-input>
</template>

View File

@ -1,6 +0,0 @@
import type { PayPeriod } from "./pay-period-interface";
export interface PayPeriodBundle {
current: PayPeriod;
periods: PayPeriod[];
}

View File

@ -0,0 +1,15 @@
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

@ -18,19 +18,34 @@
<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">
<!-- Card header with employee name -->
<q-card-section horizontal class="q-py-none q-px-md">
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">{{ props.row.employee_name }}</div>
<q-card-section
horizontal
class="q-py-none q-px-md"
>
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">
{{ props.row.employee_name }}
</div>
</q-card-section>
<q-separator color="accent" style="height: 2px;"/>
<q-separator
color="accent"
style="height: 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'">
<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
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">
{{ props.cols.find(c => c.name === 'regular_hours')?.label }}
</q-item-label>
@ -39,12 +54,23 @@
</q-item-label>
</q-item>
<q-separator color="accent" class="q-mx-sm"/>
<q-separator
color="accent"
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 props.cols.slice(2, 5)" :key="col.label">
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption" style="font-size: 0.65em;">
<q-item
dense
class="column ellipsis "
v-for="col in props.cols.slice(2, 5)"
: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 text-grey-8">
@ -54,12 +80,24 @@
</div>
</div>
<q-separator vertical color="accent" class="q-mt-xs q-mb-none"/>
<q-separator
vertical
color="accent"
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 props.cols.slice(5, )" :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;">
<q-item
dense
class="column"
v-for="col in props.cols.slice(5, )"
: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 text-grey-8">
@ -70,13 +108,16 @@
</div>
</q-card-section>
<q-separator color="primary" style="height: 2px;" />
<q-separator
color="primary"
style="height: 2px;"
/>
<!-- Validate entire Pay Period section -->
<q-card-section
horizontal
class="q-pa-sm text-weight-bold"
:class="props.modelValue ? 'text-white bg-primary' : 'text-primary bg-white'"
horizontal
class="q-pa-sm text-weight-bold"
:class="props.modelValue ? 'text-white bg-primary' : 'text-primary bg-white'"
>
<q-space />
<q-checkbox
@ -90,7 +131,7 @@
@update:model-value="val => $emit('update:modelValue', val)"
:label="props.modelValue ? $t('timeSheetValidations.timeSheetStatusVerified') : $t('timeSheetValidations.timeSheetStatusUnverified')"
class="text-uppercase"
/>
/>
</q-card-section>
</q-card>
</div>

View File

@ -2,54 +2,96 @@
/* eslint-disable */
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface';
import type { QTableColumn } from 'quasar';
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { getCurrentPayPeriod } from 'src/utils/pay-period-calculator';
import { useAuthStore } from 'src/stores/auth-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue';
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
import { date, type QTableColumn } from 'quasar';
import type { PayPeriodEmployeeOverview } from '../types/timesheet-approval-pay-period-employee-overview-interface';
const { t } = useI18n();
const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi();
const currentPayPeriod = getCurrentPayPeriod();
const currentYear = (new Date()).getFullYear();
const FORWARD = 1
const BACKWARD = -1
const originalApprovals = ref<Record<string, boolean>>({});
const hasChanges = computed(() => {
return timesheetStore.payPeriodEmployeeOverviews.some(emp => {
return emp.is_approved !== originalApprovals.value[emp.email];
const filter = ref<string | number | null>('');
const original_approvals = ref<Record<string, boolean>>({});
const columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [
{
name: 'employee_name',
label: t('timeSheetValidations.tableColumnLabelFullname'),
field: 'employee_name',
sortable: true
},
{
name: 'regular_hours',
label: t('timeSheetValidations.tableColumnLabelRegularHours'),
field: 'regular_hours',
sortable: true
},
{
name: 'evening_hours',
label: t('timeSheetValidations.tableColumnLabelEveningHours'),
field: 'evening_hours'
},
{
name: 'emergency_hours',
label: t('timeSheetValidations.tableColumnLabelEmergencyHours'),
field: 'emergency_hours'
},
{
name: 'overtime_hours',
label: t('timeSheetValidations.tableColumnLabelOvertime'),
field: 'overtime_hours'
},
{
name: 'expenses',
label: t('timeSheetValidations.tableColumnLabelExpenses'),
field: 'expenses',
sortable: true
},
{
name: 'mileage',
label: t('timeSheetValidations.tableColumnLabelMileage'),
field: 'mileage',
sortable: true
}
]);
const has_changes = computed(() => {
return timesheet_store.pay_period_employee_overviews.some(emp => {
return emp.is_approved !== original_approvals.value[emp.email];
});
});
const authStore = useAuthStore();
const timesheetStore = useTimesheetStore();
const timesheetApprovalApi = useTimesheetApprovalApi();
const is_calendar_limit = computed( () => {
return timesheet_store.current_pay_period.pay_year === 2024 &&
timesheet_store.current_pay_period.pay_period_no <= 1;
});
const columns = computed((): QTableColumn<PayPeriodEmployeeOverview>[] => [
{ name: 'employee_name', label: t('timeSheetValidations.tableColumnLabelFullname'), field: 'employee_name', sortable: true },
{ name: 'regular_hours', label: t('timeSheetValidations.tableColumnLabelRegularHours'), field: 'regular_hours', sortable: true },
{ name: 'evening_hours', label: t('timeSheetValidations.tableColumnLabelEveningHours'), field: 'evening_hours' },
{ name: 'emergency_hours', label: t('timeSheetValidations.tableColumnLabelEmergencyHours'), field: 'emergency_hours' },
{ name: 'overtime_hours', label: t('timeSheetValidations.tableColumnLabelOvertime'), field: 'overtime_hours' },
{ name: 'expenses', label: t('timeSheetValidations.tableColumnLabelExpenses'), field: 'expenses', sortable: true },
{ name: 'mileage', label: t('timeSheetValidations.tableColumnLabelMileage'), field: 'mileage', sortable: true }
]);
const filter = ref('');
const onDateSelected = async (date_string: string) => {
await timesheet_approval_api.getPayPeriodOverviewByDate(date_string);
}
onMounted( async () => {
await timesheetApprovalApi.getPayPeriodOverviewByDate(new Date());
originalApprovals.value = Object.fromEntries( timesheetStore.payPeriodEmployeeOverviews.map(emp => [emp.email, emp.is_approved]));
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
await timesheet_approval_api.getPayPeriodOverviewByDate(today);
const approvals = timesheet_store.pay_period_employee_overviews.map(emp => [emp.email, emp.is_approved]);
original_approvals.value = Object.fromEntries(approvals);
})
</script>
<template>
<div class="q-pa-md">
<q-table
:rows="timesheetStore.payPeriodEmployeeOverviews"
:rows="timesheet_store.pay_period_employee_overviews"
:columns="columns"
row-key="email"
:filter="filter"
@ -59,7 +101,7 @@
color="primary"
:rows-per-page-options="[0]"
card-container-class="justify-center"
:loading="timesheetStore.isLoading"
:loading="timesheet_store.is_loading"
:no-data-label="$t('shared.failedToLoad')"
:no-results-label="$t('shared.failedToSearch')"
:loading-label="$t('shared.loading')"
@ -68,37 +110,25 @@
<template v-slot:top>
<div :class="$q.screen.lt.md ? 'column justify-center items-center' : 'full-width row'">
<!-- Date Picker -->
<TimesheetApprovalPeriodPicker />
<TimesheetApprovalPeriodPicker
: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 />
<!-- 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
v-model="filter"
:label="$t('shared.searchBar')"
label-color="primary" bg-color="white" color="primary"
>
<template v-slot:append>
<q-icon name="search" color="primary"/>
</template>
</q-input>
</div>
</template>
<!-- Template for individual employee cards -->
<template v-slot:item="props: { cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[], row: PayPeriodEmployeeOverview }">
<template v-slot:item="props: {
cols: (QTableColumn<PayPeriodEmployeeOverview> & { value: unknown })[],
row: PayPeriodEmployeeOverview
}">
<TimesheetApprovalEmployeeOverviewListItem
:cols="props.cols"
:row="props.row"
@ -111,7 +141,10 @@
<span class="text-h6 q-mt-xl">
{{ message }}
</span>
<q-icon size="4em" :name="filter ? 'filter_alt_off' : 'error_outline'" />
<q-icon
size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'"
/>
</div>
</template>
</q-table>

View File

@ -1,18 +1,28 @@
<script setup lang="ts">
/* eslint-disable */
import { ref, computed } from 'vue';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { ref } from 'vue';
import { date } from 'quasar';
import type { QDateDetails } from 'src/modules/shared/types/q-date-details';
const timesheet_approval_api = useTimesheetApprovalApi();
const timesheet_store = useTimesheetStore();
const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY/MM/DD' ));
const is_calendar_limit = computed( () => {
return timesheet_store.currentPayPeriod.pay_year === 2024 && timesheet_store.currentPayPeriod.pay_period_no <= 1;
});
const props = defineProps<{
isDisabled: boolean,
isPreviousLimit: boolean,
}>();
const emit = defineEmits<{
'date-selected': [value: string, reason?: string, details?: QDateDetails]
'pressed-previous-button': void
'pressed-next-button': void
}>();
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>
@ -21,8 +31,8 @@
push rounded
icon="keyboard_arrow_left"
color="primary"
@click="timesheet_approval_api.getNextPayPeriodOverview(-1)"
:disable="is_calendar_limit || timesheet_store.isLoading"
@click="$emit('pressed-previous-button')"
:disable="props.isPreviousLimit || props.isDisabled"
class="q-mr-sm q-px-sm"
/>
<q-btn
@ -30,26 +40,28 @@
icon="date_range"
color="primary"
@click="is_showing_calendar_picker = true"
:disable="timesheet_store.isLoading"
:disable="props.isDisabled"
class="q-px-lg"
/>
<q-btn
push rounded
icon="keyboard_arrow_right"
color="primary"
@click="timesheet_approval_api.getNextPayPeriodOverview(1)"
:disable="timesheet_store.isLoading"
@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="slide-down" transition-hide="slide-up" position="top">
<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
:options="date => date > '2023/12/16'"
mask="YYYY-MM-DD"
:options="date => date > '2023-12-16'"
@update:model-value="onDateSelected"
/>
</q-dialog>
</template>

View File

@ -5,11 +5,11 @@ export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore();
const getPayPeriodOverviewByDate = async (date: Date) => {
const success = await timesheet_store.getPayPeriodByDate(date);
const getPayPeriodOverviewByDate = async (date_string: string) => {
const success = await timesheet_store.getPayPeriodByDate(date_string);
if (success) {
const current_pay_period = timesheet_store.currentPayPeriod;
const current_pay_period = timesheet_store.current_pay_period;
await timesheet_store.getTimesheetApprovalPayPeriodEmployeeOverviews(current_pay_period.pay_year, current_pay_period.pay_period_no, auth_store.user.email);
}
}
@ -19,7 +19,7 @@ export const useTimesheetApprovalApi = () => {
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. */
const getNextPayPeriodOverview = async (direction: number) => {
const current_pay_period = timesheet_store.currentPayPeriod;
const current_pay_period = timesheet_store.current_pay_period;
let new_pay_period_no = current_pay_period.pay_period_no + direction;
let new_pay_year = current_pay_period.pay_year;

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import TimesheetApprovalEmployeeOverviewList from '../components/timesheet-approval-employee-overview-list.vue';
import TimesheetApprovalEmployeeOverviewList from '/src/modules/timesheet-approval/\
components/timesheet-approval-employee-overview-list.vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTimesheetStore } from 'src/stores/timesheet-store';
@ -15,9 +16,12 @@
const pay_period_label = computed(() => {
const dates = timesheet_store.currentPayPeriod.label.split('.');
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
const end_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
const dates = timesheet_store.current_pay_period.label.split('.');
const start_date = new Intl.DateTimeFormat(locale.value, date_options);
const end_date = new Intl.DateTimeFormat(locale.value, date_options);
start_date.format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
end_date.format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
return {
start_date: start_date,
@ -27,12 +31,23 @@
</script>
<template>
<q-page padding class="q-pa-md bg-secondary">
<div class="text-h4 row justify-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">{{ $t('pageTitles.timeSheetValidations') }}</div>
<q-page
padding
class="q-pa-md bg-secondary"
>
<div class="text-h4 row justify-center q-mt-lg text-uppercase text-weight-bolder text-grey-8">
{{ $t('pageTitles.timeSheetValidations') }}
</div>
<div class="row items-center justify-center q-py-none q-my-none">
<div class="text-primary text-h6 text-uppercase">{{ pay_period_label.start_date }}</div>
<div class="text-grey-8 text-weight-bold text-uppercase q-mx-md">{{ $t('timeSheet.dateRangesTo') }}</div>
<div class="text-primary text-h6 text-uppercase">{{ pay_period_label.end_date }}</div>
<div class="text-primary text-h6 text-uppercase">
{{ pay_period_label.start_date }}
</div>
<div class="text-grey-8 text-weight-bold text-uppercase q-mx-md">
{{ $t('timeSheet.dateRangesTo') }}
</div>
<div class="text-primary text-h6 text-uppercase">
{{ pay_period_label.end_date }}
</div>
</div>
<TimesheetApprovalEmployeeOverviewList />
</q-page>

View File

@ -3,8 +3,8 @@ import type { PayPeriodOverview } from "../types/timesheet-approval-pay-period-o
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
export const timesheetApprovalService = {
getPayPeriodByDate: async (date: Date): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date.toISOString()}`);
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date_string}`);
return response.data;
},

View File

@ -14,17 +14,17 @@ const default_pay_period: PayPeriod = {
};
export const useTimesheetStore = defineStore('timesheet', () => {
const currentPayPeriod = ref<PayPeriod>(default_pay_period);
const payPeriodEmployeeOverviews = ref<PayPeriodEmployeeOverview[]>([]);
const isLoading = ref<boolean>(false);
const current_pay_period = ref<PayPeriod>(default_pay_period);
const pay_period_employee_overviews = ref<PayPeriodEmployeeOverview[]>([]);
const is_loading = ref<boolean>(false);
const getPayPeriodByDate = async (date: Date): Promise<boolean> => {
isLoading.value = true;
const getPayPeriodByDate = async (date_string: string): Promise<boolean> => {
is_loading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodByDate(date);
currentPayPeriod.value = response;
isLoading.value = false;
const response = await timesheetApprovalService.getPayPeriodByDate(date_string);
current_pay_period.value = response;
is_loading.value = false;
return true;
} catch(error){
@ -32,18 +32,18 @@ export const useTimesheetStore = defineStore('timesheet', () => {
//TODO: More in-depth error-handling here
}
isLoading.value = false;
is_loading.value = false;
return false;
};
const getPayPeriodByYearAndPeriodNumber = async (year: number, period_number: number): Promise<boolean> => {
isLoading.value = true;
is_loading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodByYearAndPeriodNumber(year, period_number);
currentPayPeriod.value = response;
isLoading.value = false;
current_pay_period.value = response;
is_loading.value = false;
return true;
} catch(error){
@ -51,30 +51,30 @@ export const useTimesheetStore = defineStore('timesheet', () => {
//TODO: More in-depth error-handling here
}
isLoading.value = false;
is_loading.value = false;
return false;
};
const getTimesheetApprovalPayPeriodEmployeeOverviews = async (pay_year: number, period_number: number, supervisor_email: string) => {
isLoading.value = true;
is_loading.value = true;
try {
const response = await timesheetApprovalService.getPayPeriodEmployeeOverviews(pay_year, period_number, supervisor_email);
payPeriodEmployeeOverviews.value = response.employees_overview;
pay_period_employee_overviews.value = response.employees_overview;
} catch (error) {
console.error('There was an error retrieving Employee Pay Period overviews: ', error);
payPeriodEmployeeOverviews.value = [];
pay_period_employee_overviews.value = [];
// TODO: trigger an alert window with an error message here!
}
isLoading.value = false;
is_loading.value = false;
};
return {
currentPayPeriod,
payPeriodEmployeeOverviews,
isLoading,
current_pay_period,
pay_period_employee_overviews,
is_loading,
getPayPeriodByDate,
getPayPeriodByYearAndPeriodNumber,
getTimesheetApprovalPayPeriodEmployeeOverviews,