fix(timesheets): implementing validation for shifts, fix service response changes.

This commit is contained in:
Nicolas Drolet 2025-11-24 16:42:40 -05:00
parent 3669f65fe4
commit 712e831653
9 changed files with 52 additions and 110 deletions

View File

@ -26,12 +26,12 @@
@hide="is_dialog_open = false" @hide="is_dialog_open = false"
> >
<q-card <q-card
class="shadow-12 rounded-15 column no-wrap relative bg-secondary hide-scrollbar" class="shadow-12 rounded-15 bg-secondary hide-scrollbar"
:style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')" :style="($q.screen.lt.md ? '' : 'width:80vw !important;') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
> >
<!-- employee name --> <!-- employee name -->
<q-card-section class="col-auto text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm"> <q-card-section class="text-h4 text-weight-bolder text-center text-uppercase q-px-none q-py-sm">
<span>{{ timesheet_store.selected_employee_name }}</span> <span>{{ timesheet_store.selected_employee_name }}</span>
</q-card-section> </q-card-section>
@ -39,24 +39,22 @@
<q-card-section <q-card-section
v-if="is_dialog_open" v-if="is_dialog_open"
:horizontal="!$q.screen.lt.md" :horizontal="!$q.screen.lt.md"
class="col-auto q-px-md rounded-10 no-wrap" class="q-px-md rounded-10 no-wrap"
> >
<DetailsDialogChartHoursWorked class="col" /> <DetailsDialogChartHoursWorked class="col" />
<DetailsDialogChartShiftTypes class="col q-ma-lg" /> <DetailsDialogChartShiftTypes class="col q-ma-lg" />
<DetailsDialogChartExpenses class="col" /> <DetailsDialogChartExpenses class="col" />
</q-card-section> </q-card-section>
<q-card-section class="col-auto"> <q-card-section>
<ExpenseDialogList /> <ExpenseDialogList />
</q-card-section> </q-card-section>
<!-- list of shifts --> <!-- list of shifts -->
<q-card-section <q-card-section class="col-auto">
:horizontal="$q.screen.gt.sm"
class="col-auto q-px-sm rounded-5 no-wrap"
>
<TimesheetWrapper mode="approval" /> <TimesheetWrapper mode="approval" />
</q-card-section> </q-card-section>
<q-separator />
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>

View File

@ -4,8 +4,8 @@ import type { PayPeriodOverviewResponse } from "src/modules/timesheet-approval/m
export const timesheetApprovalService = { export const timesheetApprovalService = {
getPayPeriodOverviews: async (year: number, period_number: number): Promise<PayPeriodOverviewResponse> => { getPayPeriodOverviews: async (year: number, period_number: number): Promise<PayPeriodOverviewResponse> => {
const response = await api.get(`pay-periods/overview/${year}/${period_number}`); const response = await api.get<{success: boolean, data: PayPeriodOverviewResponse, error? : string}>(`pay-periods/overview/${year}/${period_number}`);
return response.data; return response.data.data;
}, },
getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => { getPayPeriodReportByYearAndPeriodNumber: async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {

View File

@ -25,7 +25,6 @@
<template> <template>
<div <div
class="column flex-center full-width" class="column flex-center full-width"
:class="mode === 'approval' ? 'bg-dark q-px-sm q-pb-sm q-mb-md rounded-10 shadow-10' : ''"
> >
<LoadingOverlay v-model="timesheet_store.is_loading" /> <LoadingOverlay v-model="timesheet_store.is_loading" />
@ -92,11 +91,7 @@
<ShiftList :mode="mode" /> <ShiftList :mode="mode" />
<q-card-section <q-card-actions align="right">
horizontal
class="q-my-md"
>
<q-space />
<q-btn <q-btn
v-if="mode === 'approval'" v-if="mode === 'approval'"
push push
@ -108,7 +103,7 @@
class="q-mr-md" class="q-mr-md"
@click="shift_api.saveShiftChanges" @click="shift_api.saveShiftChanges"
/> />
</q-card-section> </q-card-actions>
</q-card> </q-card>
<ExpenseDialog /> <ExpenseDialog />
</div> </div>

View File

@ -1,78 +0,0 @@
// import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
// import type { Normalizer } from "src/utils/normalize-object";
// export interface ApiErrorPayload {
// status_code: number;
// error_code?: string;
// message?: string;
// context?: Record<string, unknown>;
// };
// export abstract class ApiError extends Error {
// status_code: number;
// error_code?: string;
// context?: Record<string, unknown>;
// constructor(payload: ApiErrorPayload, defaultMessage: string) {
// super(payload.message || defaultMessage);
// this.status_code = payload.status_code;
// this.error_code = payload.error_code ?? "unknown";
// this.context = payload.context ?? {'unknown': 'unknown error has occured', };
// }
// };
// export class GenericApiError extends ApiError {
// constructor(payload: ApiErrorPayload) {
// super(payload, 'Encountered an error processing request');
// this.name = 'GenericApiError';
// }
// };
// export class ExpensesValidationError extends ApiError {
// constructor(payload: ApiErrorPayload) {
// super(payload, 'Invalid expense payload');
// this.name = 'ExpensesValidationError';
// }
// };
// export class ExpensesApiError extends ApiError {
// constructor(payload: ApiErrorPayload) {
// super(payload, 'Request failed');
// this.name = 'ExpensesApiError';
// }
// };
// export const expense_validation_schema: Normalizer<Expense> = {
// id: v => typeof v === 'number' ? v : -1,
// date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
// type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
// amount: v => typeof v === "number" ? v : -1,
// mileage: v => typeof v === "number" ? v : undefined,
// comment: v => typeof v === 'string' ? v.trim() : '',
// supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
// is_approved: v => !!v,
// };
// export function toExpensesError(err: unknown): ExpensesValidationError | ExpensesApiError {
// if (err instanceof ExpensesValidationError || err instanceof ExpensesApiError) {
// return err;
// }
// if (typeof err === 'object' && err !== null && 'status_code' in err) {
// const payload = err as ApiErrorPayload;
// // Don't know how to differentiate both types of errors, can be updated here
// if (payload.error_code?.startsWith('API_')) {
// return new ExpensesApiError(payload);
// }
// return new ExpensesValidationError(payload);
// }
// // Fallback with ValidationError as default
// return new ExpensesValidationError({
// status_code: 500,
// message: err instanceof Error ? err.message : 'Unknown error',
// context: { original: err }
// });
// }

View File

@ -28,6 +28,7 @@ export class Shift {
comment: string | undefined; comment: string | undefined;
is_approved: boolean; is_approved: boolean;
is_remote: boolean; is_remote: boolean;
error?: string;
constructor() { constructor() {
this.id = -1; this.id = -1;

View File

@ -5,18 +5,18 @@ import type { TimesheetOverview } from "src/modules/timesheet-approval/models/ti
export const timesheetService = { export const timesheetService = {
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => { getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date_string}`); const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/date/${date_string}`);
return response.data; return response.data.data;
}, },
getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => { getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/${year}/${period_number}`); const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/${year}/${period_number}`);
return response.data; return response.data.data;
}, },
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => { getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get<{success: boolean, data: TimesheetOverview[], error? : string}>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data; return response.data.data;
}, },
getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<TimesheetResponse> => { getTimesheetsByPayPeriodAndOptionalEmail: async (year: number, period_number: number, employee_email?: string): Promise<TimesheetResponse> => {

View File

@ -0,0 +1,26 @@
import { date } from "quasar";
import type { Shift } from "src/modules/timesheets/models/shift.models";
export const isShiftOverlap = (shifts: Shift[]): boolean => {
if (shifts.length < 2) return false;
const parsed_shifts = shifts.map(shift => ({
start: date.extractDate(`${shift.date} ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
end: date.extractDate(`${shift.date} ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
}));
for (let i = 0; i < parsed_shifts.length; i++) {
for (let j = i + 1; j < parsed_shifts.length; j++) {
const parsed_shift_a = parsed_shifts[i];
const parsed_shift_b = parsed_shifts[j];
if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue;
if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) {
return true; // overlap found
}
}
}
return false;
};

View File

@ -1,3 +1,4 @@
/* eslint-disable */
import { ref } from "vue"; import { ref } from "vue";
import { Notify } from "quasar"; import { Notify } from "quasar";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -20,13 +21,12 @@ export const useShiftStore = defineStore('shift_store', () => {
}; };
const createNewShifts = async (): Promise<boolean> => { const createNewShifts = async (): Promise<boolean> => {
if (timesheet_store.timesheets === undefined) { if (timesheet_store.timesheets === undefined) return false;
console.log('no changes in existing shifts detected'); const has_errors = false;
return false;
}
try { try {
const new_shifts = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.shifts).filter(shift => shift.id < 0); const days = timesheet_store.timesheets.flatMap(week => week.days);
const new_shifts = days.flatMap(day => day.shifts).filter(shift => shift.id < 0);
if (new_shifts?.length > 0) { if (new_shifts?.length > 0) {
const response = await ShiftService.createNewShifts(new_shifts); const response = await ShiftService.createNewShifts(new_shifts);
@ -41,7 +41,7 @@ export const useShiftStore = defineStore('shift_store', () => {
} }
return false; return false;
} catch (error) { } catch (error) {
console.error('Error creating new shifts: ', error); Notify.create('Error creating new shifts');
return false; return false;
} }
}; };
@ -60,11 +60,10 @@ export const useShiftStore = defineStore('shift_store', () => {
} }
} }
console.log('No shifts to update'); Notify.create('No shifts to update')
Notify.create('no shifts to update')
return false; return false;
} catch (error) { } catch (error) {
console.error('Error updating shifts: ', error); Notify.create('Error updating shifts');
return false; return false;
} }
} }

View File

@ -69,6 +69,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => { const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
if (pay_period.value === undefined) return; if (pay_period.value === undefined) return;
console.log('pay period: ', pay_period.value);
is_loading.value = true; is_loading.value = true;
let response; let response;