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"
>
<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)' : '')"
>
<!-- 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>
</q-card-section>
@ -39,24 +39,22 @@
<q-card-section
v-if="is_dialog_open"
: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" />
<DetailsDialogChartShiftTypes class="col q-ma-lg" />
<DetailsDialogChartExpenses class="col" />
</q-card-section>
<q-card-section class="col-auto">
<q-card-section>
<ExpenseDialogList />
</q-card-section>
<!-- list of shifts -->
<q-card-section
:horizontal="$q.screen.gt.sm"
class="col-auto q-px-sm rounded-5 no-wrap"
>
<q-card-section class="col-auto">
<TimesheetWrapper mode="approval" />
</q-card-section>
<q-separator />
</q-card>
</q-dialog>
</template>

View File

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

View File

@ -25,7 +25,6 @@
<template>
<div
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" />
@ -92,11 +91,7 @@
<ShiftList :mode="mode" />
<q-card-section
horizontal
class="q-my-md"
>
<q-space />
<q-card-actions align="right">
<q-btn
v-if="mode === 'approval'"
push
@ -108,7 +103,7 @@
class="q-mr-md"
@click="shift_api.saveShiftChanges"
/>
</q-card-section>
</q-card-actions>
</q-card>
<ExpenseDialog />
</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;
is_approved: boolean;
is_remote: boolean;
error?: string;
constructor() {
this.id = -1;

View File

@ -5,18 +5,18 @@ import type { TimesheetOverview } from "src/modules/timesheet-approval/models/ti
export const timesheetService = {
getPayPeriodByDate: async (date_string: string): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/date/${date_string}`);
return response.data;
const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/date/${date_string}`);
return response.data.data;
},
getPayPeriodByYearAndPeriodNumber: async (year: number, period_number: number): Promise<PayPeriod> => {
const response = await api.get(`pay-periods/${year}/${period_number}`);
return response.data;
const response = await api.get<{success: boolean, data: PayPeriod, error? : string}>(`pay-periods/${year}/${period_number}`);
return response.data.data;
},
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data;
const response = await api.get<{success: boolean, data: TimesheetOverview[], error? : string}>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data.data;
},
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 { Notify } from "quasar";
import { defineStore } from "pinia";
@ -20,13 +21,12 @@ export const useShiftStore = defineStore('shift_store', () => {
};
const createNewShifts = async (): Promise<boolean> => {
if (timesheet_store.timesheets === undefined) {
console.log('no changes in existing shifts detected');
return false;
}
if (timesheet_store.timesheets === undefined) return false;
const has_errors = false;
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) {
const response = await ShiftService.createNewShifts(new_shifts);
@ -41,7 +41,7 @@ export const useShiftStore = defineStore('shift_store', () => {
}
return false;
} catch (error) {
console.error('Error creating new shifts: ', error);
Notify.create('Error creating new shifts');
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;
} catch (error) {
console.error('Error updating shifts: ', error);
Notify.create('Error updating shifts');
return false;
}
}

View File

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