@@ -51,35 +54,34 @@ import { default_shift } from '../../types/shift.defaults';
bordered
class="row items-center rounded-10 q-mb-xs"
>
+
-
-
+ {{ $d(getDate(day.short_date), {weekday: $q.screen.lt.md ? 'short' : 'long'}) }}
- {{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}
+ {{ day.short_date.split('/')[1] }}
- {{ $d(getDate(day.short_date), {month: $q.screen.lt.md ? 'short' : 'long'}) }}
+ >{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}
-
-
+ on_request_edit(to_iso_date(day.short_date), shift )"
- @request-delete="({ shift }) => on_request_delete(to_iso_date(day.short_date), shift )"
+ @request-update="value => openUpdate(to_iso_date(day.short_date), value)"
+ @request-delete="value => openDelete(to_iso_date(day.short_date), value)"
/>
@@ -89,7 +91,7 @@ import { default_shift } from '../../types/shift.defaults';
color="primary"
icon="more_time"
class="q-pa-sm"
- @click="on_request_add(to_iso_date(day.short_date))"
+ @click="openCreate(to_iso_date(day.short_date))"
/>
diff --git a/src/modules/timesheets/components/timesheet/timesheet-save-payload.vue b/src/modules/timesheets/components/timesheet/timesheet-save-payload.vue
deleted file mode 100644
index 9cc7c8b..0000000
--- a/src/modules/timesheets/components/timesheet/timesheet-save-payload.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
- {{ $t('timesheet.save_button') }}
-
-
-
\ No newline at end of file
diff --git a/src/modules/timesheets/components/timesheet/timesheet-wrapper.vue b/src/modules/timesheets/components/timesheet/timesheet-wrapper.vue
new file mode 100644
index 0000000..fa4f330
--- /dev/null
+++ b/src/modules/timesheets/components/timesheet/timesheet-wrapper.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/timesheets/composables/api/use-expense-api.ts b/src/modules/timesheets/composables/api/use-expense-api.ts
index 899e3d0..f4aa15d 100644
--- a/src/modules/timesheets/composables/api/use-expense-api.ts
+++ b/src/modules/timesheets/composables/api/use-expense-api.ts
@@ -1,81 +1,21 @@
-import { api } from "src/boot/axios";
-import { isProxy, toRaw } from "vue";
+import { useTimesheetStore } from "src/stores/timesheet-store";
+import { useExpenseItems } from "src/modules/timesheets/composables/use-expense-items";
import { normalizeExpense, validateExpenseUI } from "../../utils/expenses-validators";
-import type { ExpenseType } from "../../types/expense.types";
-import { ExpensesApiError } from "../../types/expense-validation.interface";
-import type {
- ExpensePayload,
- PayPeriodExpenses,
- TimesheetExpense,
- UpsertExpensesBody,
- UpsertExpensesResponse
-} from "../../types/expense.interfaces";
+import type { ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
+import type { Expense, ExpenseType, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
-/* eslint-disable */
-const toPlain =
(obj:T): T => {
- const raw = isProxy(obj) ? toRaw(obj) : obj;
- if( typeof (globalThis as any).structuredClone === 'function') {
- return (globalThis as any).structuredClone(raw);
- }
- return JSON.parse(JSON.stringify(raw));
-};
+const { pay_period } = useTimesheetStore();
+const expense_items = useExpenseItems(draft);
-const normalizePayload = (expense: ExpensePayload): ExpensePayload => {
- const exp = normalizeExpense(expense as unknown as TimesheetExpense);
- const out: ExpensePayload = {
- date: exp.date,
- type: exp.type as ExpenseType,
- comment: exp.comment || '',
- };
- if(typeof exp.amount === 'number') out.amount = exp.amount;
- if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
- return out;
-}
+//PUT by employee_email, year and period no
+export const putPayPeriodExpensesByEmployeeEmail = async (employee_email: string, expenses: Expense[]): Promise => {
+ const encoded_email = encodeURIComponent(employee_email);
+ const encoded_year = encodeURIComponent(String(pay_period.pay_year));
+ const encoded_pay_period_no = encodeURIComponent(String(pay_period.pay_period_no));
-//GET by email, year and period no
-export const getPayPeriodExpenses = async (
- email: string,
- pay_year: number,
- pay_period_no: number
-) : Promise => {
- const encoded_email = encodeURIComponent(email);
- const encoded_year = encodeURIComponent(String(pay_year));
- const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
+ const flat_expenses = expenses.map(expenses): [];
- try {
- const { data } = await api.get(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
-
- const items = Array.isArray(data.expenses) ? data.expenses.map(normalizeExpense) : [];
- return {
- ...data,
- expenses: items,
- };
- } catch(err:any) {
- const status_code: number = err?.response?.status ?? 500;
- const data = err?.response?.data ?? {};
- throw new ExpensesApiError({
- status_code,
- error_code: data.error_code,
- message: data.message || data.error || err.message,
- context: data.context,
- });
- }
-};
-
-//PUT by email, year and period no
-export const putPayPeriodExpenses = async (
- email: string,
- pay_year: number,
- pay_period_no: number,
- expenses: TimesheetExpense[]
-): Promise => {
- const encoded_email = encodeURIComponent(email);
- const encoded_year = encodeURIComponent(String(pay_year));
- const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
-
- const plain = Array.isArray(expenses) ? expenses.map(toPlain): [];
-
- const normalized: ExpensePayload[] = plain.map((exp) => {
+ const normalized: Expense[] = plain.map((exp) => {
const norm = normalizeExpense(exp as TimesheetExpense);
validateExpenseUI(norm, 'expense_item');
return normalizePayload(norm as unknown as ExpensePayload);
@@ -85,10 +25,10 @@ export const putPayPeriodExpenses = async (
try {
const { data } = await api.put(
- `/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`,
- body,
- { headers: {'Content-Type': 'application/json'}}
- );
+ // `/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`,
+ // body,
+ // { headers: {'Content-Type': 'application/json'}}
+ // );
const items = Array.isArray(data?.data?.expenses)
? data.data.expenses.map(normalizeExpense)
@@ -97,7 +37,7 @@ export const putPayPeriodExpenses = async (
...(data?.data ?? {
pay_period_no,
pay_year,
- employee_email: email,
+ employee_email: employee_email,
is_approved: false,
expenses: [],
totals: {amount: 0, mileage: 0},
@@ -117,12 +57,12 @@ export const putPayPeriodExpenses = async (
};
export const postPayPeriodExpenses = async (
- email: string,
+ employee_email: string,
pay_year: number,
pay_period_no: number,
new_expenses: TimesheetExpense[]
): Promise => {
- const encoded_email = encodeURIComponent(email);
+ const encoded_email = encodeURIComponent(employee_email);
const encoded_year = encodeURIComponent(String(pay_year));
const encoded_pp = encodeURIComponent(String(pay_period_no));
@@ -148,7 +88,7 @@ export const postPayPeriodExpenses = async (
...(data?.data ?? {
pay_period_no,
pay_year,
- employee_email: email,
+ employee_email: employee_email,
is_approved: false,
expenses: [],
totals: { amount: 0, mileage: 0 },
diff --git a/src/modules/timesheets/composables/api/use-shift-api.ts b/src/modules/timesheets/composables/api/use-shift-api.ts
index 855738b..a7f043c 100644
--- a/src/modules/timesheets/composables/api/use-shift-api.ts
+++ b/src/modules/timesheets/composables/api/use-shift-api.ts
@@ -1,141 +1,85 @@
-import { api } from "src/boot/axios";
-import { isProxy, toRaw } from "vue";
-import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "../../constants/shift.constants";
-import type { ShiftPayload } from "../../types/shift.types";
-import type { UpsertShiftsBody, UpsertShiftsResponse } from "../../types/shift.interfaces";
-/* eslint-disable */
+import { unwrapAndClone } from "src/utils/unwrap-and-clone";
+import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
+import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
+import { useShiftStore } from "src/stores/shift-store";
+import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
+import { deepEqual } from "src/utils/deep-equal";
-//normalize payload to match backend data
-export const normalize_comment = (input?: string): string | undefined => {
- if ( typeof input === 'undefined' || input === null) return undefined;
- const trimmed = String(input).trim();
- return trimmed.length ? trimmed : undefined;
-}
+export const useShiftApi = () => {
+ const shift_store = useShiftStore();
-export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
- const comment = normalize_comment(payload.comment);
- return {
- start_time: payload.start_time,
- end_time: payload.end_time,
- type: payload.type,
- is_remote: Boolean(payload.is_remote),
- ...(comment !== undefined ? { comment } : {}),
- };
-};
+ const normalizeShiftPayload = (shift: Shift): Shift => {
+ const comment = shift.comment?.trim() || undefined;
-const toPlain = (obj: T): T => {
- const raw = isProxy(obj) ? toRaw(obj): obj;
- if(typeof (globalThis as any).structuredClone === 'function') {
- return (globalThis as any).structuredClone(raw);
- }
- return JSON.parse(JSON.stringify(raw));
-}
-
-//error handling
-export interface ApiErrorPayload {
- status_code: number;
- error_code?: string;
- message?: string;
- context?: Record;
-}
-
-export class UpsertShiftsError extends Error {
- status_code: number;
- error_code?: string | undefined;
- context?: Record | undefined;
- constructor(payload: ApiErrorPayload) {
- super(payload.message || 'Request failed');
- this.name = 'UpsertShiftsError';
- this.status_code = payload.status_code;
- this.error_code = payload.error_code;
- this.context = payload.context;
- }
-}
-
-const parseHHMM = (s:string): [number, number] => {
- const m = /^(\d{2}):(\d{2})$/.exec(s);
- if(!m) {
- throw new UpsertShiftsError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.`});
- }
-
- const h = Number(m[1]);
- const min = Number(m[2]);
-
- if(Number.isNaN(h) || Number.isNaN(min) || h < 0 || h> 23 || min < 0 || min > 59) {
- throw new UpsertShiftsError({ status_code: 400, message: `Invalid time value: ${s}.`})
- }
- return [h, min];
-}
-
-const toMinutes = (hhmm: string): number => {
- const [h,m] = parseHHMM(hhmm);
- return h * 60 + m;
-}
-
-const validateShift = (payload: ShiftPayload, label: 'old_shift'|'new_shift') => {
- if(!TIME_FORMAT_PATTERN.test(payload.start_time) || !TIME_FORMAT_PATTERN.test(payload.end_time)) {
- throw new UpsertShiftsError({
- status_code: 400,
- message: `Invalid time format in ${label}. Expected HH:MM`,
- context: { [label]: payload }
- });
- }
-
- if(toMinutes(payload.end_time) <= toMinutes(payload.start_time)) {
- throw new UpsertShiftsError({
- status_code: 400,
- message: `Invalid time range in ${label}. The End time must be after the Start time`,
- context: { [label]: payload}
- });
- }
-}
-
-export const upsertShiftsByDate = async (
- email: string,
- date: string,
- body: UpsertShiftsBody,
-): Promise => {
-
- if (!DATE_FORMAT_PATTERN.test(date)){
- throw new UpsertShiftsError({
- status_code: 400,
- message: 'Invalid date format, expected YYYY-MM-DD',
- });
- }
-
- const flatBody: UpsertShiftsBody = {
- ...(body.old_shift ? { old_shift: toPlain(body.old_shift) }: {}),
- ...(body.new_shift ? { new_shift: toPlain(body.new_shift) }: {}),
- };
-
- const normalized: UpsertShiftsBody = {
- ...(flatBody.old_shift ? { old_shift: normalize_payload(flatBody.old_shift) } : {}),
- ...(flatBody.new_shift ? { new_shift: normalize_payload(flatBody.new_shift) } : {}),
- };
-
- if(normalized.old_shift) validateShift(normalized.old_shift, 'old_shift');
- if(normalized.new_shift) validateShift(normalized.new_shift, 'new_shift');
-
- const encoded_email = encodeURIComponent(email);
- const encoded_date = encodeURIComponent(date);
-
- //error handling to be used with notify in case of bad input
- try {
- const { data } = await api.put(
- `/shifts/upsert/${encoded_email}/${encoded_date}`,
- normalized,
- { headers: {'content-type': 'application/json'}}
- );
- return data;
- } catch (err: any) {
- const status_code: number = err?.response?.status ?? 500;
- const data = err?.response?.data ?? {};
- const payload: ApiErrorPayload = {
- status_code,
- error_code: data.error_code,
- message: data.message || data.error || err.message,
- context: data.context,
+ return {
+ date: shift.date,
+ start_time: shift.start_time,
+ end_time: shift.end_time,
+ type: shift.type,
+ is_approved: false,
+ is_remote: shift.is_remote,
+ comment: comment,
};
- throw new UpsertShiftsError(payload);
- }
-};
\ No newline at end of file
+ };
+
+ const parseHHMM = (s: string): [number, number] => {
+ const m = /^(\d{2}):(\d{2})$/.exec(s);
+
+ if (!m) {
+ throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` });
+ }
+
+ const h = Number(m[1]);
+ const min = Number(m[2]);
+
+ if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) {
+ throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` })
+ }
+ return [h, min];
+ };
+
+ const toMinutes = (hhmm: string): number => {
+ const [h, m] = parseHHMM(hhmm);
+ return h * 60 + m;
+ };
+
+ const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => {
+ if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) {
+ throw new GenericApiError({
+ status_code: 400,
+ message: `Invalid time format in ${label}. Expected HH:MM`,
+ context: { [label]: shift }
+ });
+ }
+
+ if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) {
+ throw new GenericApiError({
+ status_code: 400,
+ message: `Invalid time range in ${label}. The End time must be after the Start time`,
+ context: { [label]: shift }
+ });
+ }
+ };
+
+ const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise => {
+ const flat_upsert_shift: UpsertShift = {
+ ...(deepEqual(shift_store.initial_shift, default_shift) ? { old_shift: unwrapAndClone(shift_store.initial_shift) } : {}),
+ ...(deepEqual(shift_store.current_shift, default_shift) ? { new_shift: unwrapAndClone(shift_store.current_shift) } : {}),
+ };
+
+ const normalized_upsert_shift: UpsertShift = {
+ ...(flat_upsert_shift.old_shift ? { old_shift: normalizeShiftPayload(flat_upsert_shift.old_shift) } : {}),
+ ...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}),
+ };
+
+ if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift');
+ if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift');
+
+ await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift);
+ };
+
+ return {
+ upsertOrDeleteShiftByEmployeeEmail,
+ };
+}
+
diff --git a/src/modules/timesheets/composables/api/use-timesheet-api.ts b/src/modules/timesheets/composables/api/use-timesheet-api.ts
index 0b60564..7692ffd 100644
--- a/src/modules/timesheets/composables/api/use-timesheet-api.ts
+++ b/src/modules/timesheets/composables/api/use-timesheet-api.ts
@@ -1,58 +1,53 @@
import { useAuthStore } from "src/stores/auth-store";
import { useTimesheetStore } from "src/stores/timesheet-store"
-/* eslint-disable */
+
export const useTimesheetApi = () => {
const timesheet_store = useTimesheetStore();
const auth_store = useAuthStore();
+ const NEXT = 1;
+ const PREVIOUS = -1;
+
+ const getPayPeriodDetailsByDate = async (date_string: string, employee_email?: string) => {
+ const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
- const getTimesheetsByDate = async (date_string: string) => {
- const success = await timesheet_store.getPayPeriodByDate(date_string);
-
if (success) {
- await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email)
+ await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email)
}
}
- const fetchPayPeriod = async (direction: number) => {
- 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;
+ const getNextOrPreviousPayPeriodDetails = async (direction: number, employee_email?: string) => {
+ const { pay_period } = timesheet_store;
+ let new_number = pay_period.pay_period_no + direction;
+ let new_year = pay_period.pay_year;
- if (new_pay_period_no > 26) {
- new_pay_period_no = 1;
- new_pay_year += 1;
+ if (new_number > 26) {
+ new_number = 1;
+ new_year += 1;
}
- if (new_pay_period_no < 1) {
- new_pay_period_no = 26;
- new_pay_year -= 1;
+ if (new_number < 1) {
+ new_number = 26;
+ new_year -= 1;
}
- const success = await timesheet_store.getPayPeriodByYearAndPeriodNumber(new_pay_year, new_pay_period_no);
-
+ const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(new_year, new_number);
+
if (success) {
- await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
+ await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email ?? auth_store.user.email);
}
};
+
+ const getNextPayPeriodDetails = async (employee_email?: string) => {
+ await getNextOrPreviousPayPeriodDetails(NEXT, employee_email ?? auth_store.user.email);
+ }
- const getNextPayPeriod = async () => fetchPayPeriod(1);
- const getPreviousPayPeriod = async () => fetchPayPeriod(-1);
+ const getPreviousPayPeriodDetails = async (employee_email?: string) => {
+ await getNextOrPreviousPayPeriodDetails(PREVIOUS, employee_email ?? auth_store.user.email);
+ }
- const getPreviousPeriodForUser = async (_employee_email: string) => {
- await getPreviousPayPeriod();
- };
-
- const getNextPeriodForUser = async (_employee_email: string) => {
- await getNextPayPeriod();
- };
-
- return {
- getTimesheetsByDate,
- fetchPayPeriod,
- // getCurrentPayPeriod,
- getNextPayPeriod,
- getPreviousPayPeriod,
- getPreviousPeriodForUser,
- getNextPeriodForUser,
+ return {
+ getPayPeriodDetailsByDate,
+ getNextPayPeriodDetails,
+ getPreviousPayPeriodDetails,
};
};
\ No newline at end of file
diff --git a/src/modules/timesheets/composables/use-expense-items.ts b/src/modules/timesheets/composables/use-expense-items.ts
index 5b90aa8..46d4e8e 100644
--- a/src/modules/timesheets/composables/use-expense-items.ts
+++ b/src/modules/timesheets/composables/use-expense-items.ts
@@ -1,52 +1,55 @@
import { ref, type Ref } from "vue";
import { normalizeExpense, validateExpenseUI } from "../utils/expenses-validators";
import { normExpenseType } from "../utils/expense.util";
-import type { TimesheetExpense } from "../types/expense.interfaces";
+import type { Expense, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
+import { useExpensesStore } from "src/stores/expense-store";
+import { unwrapAndClone } from "src/utils/unwrap-and-clone";
-type UseExpenseItemsParams = {
- initial_expenses?: TimesheetExpense[] | null | undefined;
- draft: Ref>;
- is_approved: Ref | boolean;
-};
-export const useExpenseItems = ({
- initial_expenses,
- draft,
- is_approved
-}: UseExpenseItemsParams) => {
- const items = ref(
- Array.isArray(initial_expenses) ? initial_expenses.map(normalizeExpense) : []
- );
+const expenses_store = useExpensesStore();
+
+export const useExpenseItems = () => {
+ let expenses = unwrapAndClone(expenses_store.pay_period_expenses.expenses.map(normalizeExpense));
+
+ const normalizePayload = (expense: Expense): Expense => {
+ const exp = normalizeExpense(expense);
+ const out: Expense = {
+ date: exp.date,
+ type: exp.type as ExpenseType,
+ comment: exp.comment || '',
+ };
+ if(typeof exp.amount === 'number') out.amount = exp.amount;
+ if(typeof exp.mileage === 'number') out.mileage = exp.mileage;
+ return out;
+}
const addFromDraft = () => {
- const candidate: TimesheetExpense = normalizeExpense({
- date: draft.value.date,
- type: normExpenseType(draft.value.type),
- ...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
- ...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
- comment: String(draft.value.comment ?? '').trim(),
- } as TimesheetExpense);
+ const candidate: Expense = normalizeExpense({
+ date: draft.date,
+ type: normExpenseType(draft.type),
+ ...(typeof draft.amount === 'number' ? { amount: draft.amount }: {}),
+ ...(typeof draft.mileage === 'number' ? { mileage: draft.mileage }: {}),
+ comment: String(draft.comment ?? '').trim(),
+ } as Expense);
validateExpenseUI(candidate, 'expense_draft');
- items.value = [ ...items.value, candidate];
+ expenses = [ ...expenses, candidate];
};
const removeAt = (index: number) => {
- const locked = typeof is_approved === 'boolean' ? is_approved : is_approved.value;
- if(locked) return;
- if(index < 0 || index >= items.value.length) return;
- items.value = items.value.filter((_,i)=> i !== index);
+ if(index < 0 || index >= expenses.length) return;
+ expenses = expenses.filter((_,i)=> i !== index);
};
const validateAll = () => {
- for (const expense of items.value) {
+ for (const expense of expenses) {
validateExpenseUI(expense, 'expense_item');
}
};
- const payload = () => items.value.map(normalizeExpense);
+ const payload = () => expenses.map(normalizeExpense);
return {
- items,
+ expenses,
addFromDraft,
removeAt,
validateAll,
diff --git a/src/modules/timesheets/models/expense-validation.interface.ts b/src/modules/timesheets/models/expense-validation.interface.ts
deleted file mode 100644
index 64c3af0..0000000
--- a/src/modules/timesheets/models/expense-validation.interface.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export interface ApiErrorPayload {
- status_code: number;
- error_code?: string;
- message?: string;
- context?: Record;
-}
-
-export class ExpensesValidationError extends Error {
- status_code: number;
- error_code?: string | undefined;
- context?: Record | undefined;
-
- constructor(payload: ApiErrorPayload) {
- super(payload.message || 'Invalid expense payload');
- this.name = 'ExpensesValidationError';
- this.status_code = payload.status_code;
- this.error_code = payload.error_code;
- this.context = payload.context;
- }
-}
-
-export class ExpensesApiError extends Error {
- status_code: number;
- error_code?: string;
- context?: Record;
- constructor(payload: ApiErrorPayload) {
- super(payload.message || 'Request failed');
- this.name = 'ExpensesApiError';
- this.status_code = payload.status_code;
-
- if(payload.error_code !== undefined) this.error_code = payload.error_code;
- if(payload.context !== undefined) this.context = payload.context;
- }
-}
\ No newline at end of file
diff --git a/src/modules/timesheets/models/expense.models.ts b/src/modules/timesheets/models/expense.models.ts
index 2f4d855..12d8c08 100644
--- a/src/modules/timesheets/models/expense.models.ts
+++ b/src/modules/timesheets/models/expense.models.ts
@@ -1,78 +1,46 @@
-// export const EXPENSE_TYPE = [
-// 'PER_DIEM',
-// 'MILEAGE',
-// 'EXPENSES',
-// 'PRIME_GARDE',
-// ] as const;
-
-// export type ExpenseType = (typeof EXPENSE_TYPE)[number];
-
-// export const TYPES_WITH_MILEAGE_ONLY: Readonly = ['MILEAGE'];
-
-// export const TYPES_WITH_AMOUNT_ONLY: Readonly = [
-// 'PER_DIEM',
-// 'EXPENSES',
-// 'PRIME_GARDE',
-// ];
-
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'PRIME_GARDE';
-export type ExpenseTotals = {
- amount: number;
- mileage: number;
-};
-
-// export type ExpenseSavePayload = {
-// pay_period_no: number;
-// pay_year: number;
-// email: string;
-// expenses: TimesheetExpense[];
-// };
+export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'PRIME_GARDE',];
+export const TYPES_WITH_MILEAGE_ONLY: Readonly = ['MILEAGE'];
+export const TYPES_WITH_AMOUNT_ONLY: Readonly = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE',];
export interface Expense {
-// is_approved: boolean;
-// comment: string;
-// amount: number;
-// supervisor_comment: string;
-// }
-
-// export interface TimesheetExpense {
- date: string;
- type: string;
- amount?: number;
- mileage?: number;
- comment?: string;
+ date: string;
+ type: ExpenseType;
+ amount?: number;
+ mileage?: number;
+ comment: string;
supervisor_comment?: string;
- is_approved?: boolean;
+ is_approved: boolean;
}
-// export interface PayPeriodExpenses {
-export interface TimesheetExpenses {
- pay_period_no: number;
- pay_year: number;
- employee_email: string;
- is_approved: boolean;
- // expenses: TimesheetExpense[];
+export type ExpenseTotals = {
+ amount: number;
+ mileage: number;
+ reimburseable_total?: number;
+};
+
+export interface PayPeriodExpenses {
+ is_approved: boolean;
expenses: Expense[];
- totals?: {
- amount: number;
- mileage: number;
- reimbursable_total?: number;
- }
+ totals?: ExpenseTotals;
}
-// export interface ExpensePayload{
-// date: string;
-// type: ExpenseType;
-// amount?: number;
-// mileage?: number;
-// comment: string;
-// }
+export interface TimesheetDetailsWeekDayExpenses {
+ cash: Expense[];
+ km: Expense[];
+ [otherType: string]: Expense[];
+}
-// export interface UpsertExpensesBody {
-// expenses: ExpensePayload[];
-// }
+export const default_expense: Expense = {
+ date: '',
+ type: 'EXPENSES',
+ amount: 0,
+ comment: '',
+ is_approved: false,
+};
-// export interface UpsertExpensesResponse {
-// data: PayPeriodExpenses;
-// }
\ No newline at end of file
+export const default_pay_period_expenses: PayPeriodExpenses = {
+ is_approved: false,
+ expenses: [],
+}
\ No newline at end of file
diff --git a/src/modules/timesheets/models/expense.validation.ts b/src/modules/timesheets/models/expense.validation.ts
new file mode 100644
index 0000000..11f8aa3
--- /dev/null
+++ b/src/modules/timesheets/models/expense.validation.ts
@@ -0,0 +1,77 @@
+import { Expense, EXPENSE_TYPE, ExpenseType } from "src/modules/timesheets/models/expense.models";
+import { Normalizer } from "src/utils/normalize-object";
+
+export interface ApiErrorPayload {
+ status_code: number;
+ error_code?: string;
+ message?: string;
+ context?: Record;
+};
+
+export abstract class ApiError extends Error {
+ status_code: number;
+ error_code?: string;
+ context?: Record;
+
+ 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_normalizer: Normalizer = {
+ date: v => String(v ?? "1970-01-01").trim(),
+ type: v => EXPENSE_TYPE.includes(v) ? v as ExpenseType : "EXPENSES",
+ amount: v => typeof v === "number" ? v : undefined,
+ mileage: v => typeof v === "number" ? v : undefined,
+ comment: v => String(v ?? "").trim(),
+ supervisor_comment: 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 }
+ });
+}
diff --git a/src/modules/timesheets/models/pay-period-details.models.ts b/src/modules/timesheets/models/pay-period-details.models.ts
new file mode 100644
index 0000000..c19a69f
--- /dev/null
+++ b/src/modules/timesheets/models/pay-period-details.models.ts
@@ -0,0 +1,77 @@
+import type { Shift } from "./shift.models";
+import type { Expense } from "src/modules/timesheets/models/expense.models";
+
+export type Week = {
+ sun: T;
+ mon: T;
+ tue: T;
+ wed: T;
+ thu: T;
+ fri: T;
+ sat: T;
+};
+
+export interface PayPeriodDetails {
+ weeks: PayPeriodDetailsWeek[];
+ employee_full_name: string;
+}
+
+export interface PayPeriodDetailsWeek {
+ is_approved: boolean;
+ shifts: Week
+ expenses: Week;
+}
+
+export interface PayPeriodDetailsWeekDayShifts {
+ shifts: Shift[];
+ regular_hours: number;
+ evening_hours: number;
+ emergency_hours: number;
+ overtime_hours: number;
+ total_hours: number;
+ short_date: string;
+ break_duration?: number;
+}
+
+export interface PayPeriodDetailsWeekDayExpenses {
+ cash: Expense[];
+ km: Expense[];
+ [otherType: string]: Expense[];
+}
+
+const makeWeek = (factory: ()=> T): Week => ({
+ sun: factory(),
+ mon: factory(),
+ tue: factory(),
+ wed: factory(),
+ thu: factory(),
+ fri: factory(),
+ sat: factory(),
+});
+
+const emptyDailySchedule = (): PayPeriodDetailsWeekDayShifts => ({
+ shifts: [],
+ regular_hours: 0,
+ evening_hours: 0,
+ emergency_hours: 0,
+ overtime_hours: 0,
+ total_hours: 0,
+ short_date: "",
+ break_duration: 0,
+});
+
+const emptyDailyExpenses = (): PayPeriodDetailsWeekDayExpenses => ({
+ cash: [],
+ km: [],
+});
+
+export const defaultPayPeriodDetailsWeek = (): PayPeriodDetailsWeek => ({
+ is_approved: false,
+ shifts: makeWeek(emptyDailySchedule),
+ expenses: makeWeek(emptyDailyExpenses),
+});
+
+export const default_pay_period_details: PayPeriodDetails = {
+ weeks: [ defaultPayPeriodDetailsWeek(), ],
+ employee_full_name: "",
+}
\ No newline at end of file
diff --git a/src/modules/timesheets/models/shift.models.ts b/src/modules/timesheets/models/shift.models.ts
index f105851..143d3ed 100644
--- a/src/modules/timesheets/models/shift.models.ts
+++ b/src/modules/timesheets/models/shift.models.ts
@@ -1,82 +1,49 @@
-// export const SHIFT_KEY = [
-// 'REGULAR',
-// 'EVENING',
-// 'EMERGENCY',
-// 'HOLIDAY',
-// 'VACATION',
-// 'SICK'
-// ] as const;
+export const SHIFT_TYPES = [
+ 'REGULAR',
+ 'EVENING',
+ 'EMERGENCY',
+ 'OVERTIME',
+ 'HOLIDAY',
+ 'VACATION',
+ 'SICK'
+];
-// export type ShiftKey = typeof SHIFT_KEY[number];
+export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'OVERTIME' | 'HOLIDAY' | 'VACATION' | 'SICK' ;
-// export type ShiftSelectOption = { value: ShiftKey; label: string };
-
-// export type ShiftPayload = {
-// start_time: string;
-// end_time: string;
-// type: ShiftKey;
-// is_remote: boolean;
-// comment?: string;
-// }
-
-export type ShiftKey = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACATION' | 'SICK';
-
-export type UpsertAction = 'created' | 'updated' | 'deleted';
+export type UpsertAction = 'create' | 'update' | 'delete';
export type ShiftLegendItem = {
- type: ShiftKey;
+ type: ShiftType;
color: string;
- label_key: string;
+ label_type: string;
text_color?: string;
};
export interface Shift {
date: string;
- type: ShiftKey;
+ type: ShiftType;
start_time: string;
end_time: string;
- comment: string;
+ comment: string | undefined;
is_approved: boolean;
is_remote: boolean;
}
export interface UpsertShiftsResponse {
action: UpsertAction;
- // day: DayShift[];
day: Shift[];
}
-// export interface CreateShiftPayload {
-// date: string;
-// type: ShiftKey;
-// start_time: string;
-// end_time: string;
-// comment?: string;
-// is_remote?: boolean;
-// }
-
-// export interface CreateWeekShiftPayload {
-// shifts: CreateShiftPayload[];
-// }
-
-// export interface UpsertShiftsBody {
-// old_shift?: ShiftPayload;
-// new_shift?: ShiftPayload;
-// }
-
-// export interface DayShift {
-// start_time: string;
-// end_time: string;
-// type: string;
-// is_remote: boolean;
-// comment?: string | null;
-// }
+export interface UpsertShift {
+ old_shift?: Shift | undefined;
+ new_shift?: Shift | undefined;
+}
export const default_shift: Readonly = {
date: '',
start_time: '--:--',
end_time: '--:--',
- type:'REGULAR',
+ type: 'REGULAR',
comment: '',
is_approved: false,
is_remote: false,
diff --git a/src/modules/timesheets/models/timesheet.models.ts b/src/modules/timesheets/models/timesheet.models.ts
deleted file mode 100644
index 4735cad..0000000
--- a/src/modules/timesheets/models/timesheet.models.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import type { Shift } from "./shift.models";
-import type { Expense } from "src/modules/timesheets/models/expense.models";
-// import type {
- // TimesheetExpenseEntry,
- // TimesheetShiftEntry,
-// Week
-// } from "./timesheet.types";
-
-// export interface Timesheet {
-// is_approved: boolean;
-// start_day: string;
-// end_day: string;
-// label: string;
-// shifts: TimesheetShiftEntry[];
-// expenses: TimesheetExpenseEntry[];
-// }
-
-// export type TimesheetShiftEntry = {
-// bank_type: string;
-// date: string;
-// start_time: string;
-// end_time: string;
-// comment: string;
-// is_approved: boolean;
-// is_remote: boolean;
-// };
-
-// export type TimesheetExpenseEntry = {
-// bank_type: string;
-// date: string;
-// amount: number;
-// km: number;
-// comment: string;
-// is_approved: boolean;
-// supervisor_comment: string;
-// };
-
-export type Week = {
- sun: T;
- mon: T;
- tue: T;
- wed: T;
- thu: T;
- fri: T;
- sat: T;
-};
-
-export interface TimesheetDetails {
- week1: TimesheetDetailsWeek;
- week2: TimesheetDetailsWeek;
- employee_full_name: string;
-}
-
-export interface TimesheetDetailsWeek {
- is_approved: boolean;
- shifts: Week
- expenses: Week;
-}
-
-export interface TimesheetDetailsWeekDayShifts {
- shifts: Shift[];
- regular_hours: number;
- evening_hours: number;
- emergency_hours: number;
- overtime_hours: number;
- total_hours: number;
- short_date: string;
- break_duration?: number;
-}
-
-export interface TimesheetDetailsWeekDayExpenses {
- cash: Expense[];
- km: Expense[];
- [otherType: string]: Expense[];
-}
-
-// export interface DailyExpense {
-// is_approved: boolean;
-// comment: string;
-// amount: number;
-// supervisor_comment: string;
-// }
-
-// export interface TimesheetPayPeriodDetailsOverview {
-// week1: TimesheetDetailsWeek;
-// week2: TimesheetDetailsWeek;
-// }
-
-const makeWeek = (factory: ()=> T): Week => ({
- sun: factory(),
- mon: factory(),
- tue: factory(),
- wed: factory(),
- thu: factory(),
- fri: factory(),
- sat: factory(),
-});
-
-const emptyDailySchedule = (): TimesheetDetailsWeekDayShifts => ({
- shifts: [],
- regular_hours: 0,
- evening_hours: 0,
- emergency_hours: 0,
- overtime_hours: 0,
- total_hours: 0,
- short_date: "",
- break_duration: 0,
-});
-
-const emptyDailyExpenses = (): TimesheetDetailsWeekDayExpenses => ({
- cash: [],
- km: [],
-});
-
-export const defaultTimesheetDetailsWeek = (): TimesheetDetailsWeek => ({
- is_approved: false,
- shifts: makeWeek(emptyDailySchedule),
- expenses: makeWeek(emptyDailyExpenses),
-});
-
-export const default_timesheet_details: TimesheetDetails = {
- week1: defaultTimesheetDetailsWeek(),
- week2: defaultTimesheetDetailsWeek(),
- employee_full_name: "",
-}
\ No newline at end of file
diff --git a/src/modules/timesheets/models/ui.types.ts b/src/modules/timesheets/models/ui.models.ts
similarity index 60%
rename from src/modules/timesheets/models/ui.types.ts
rename to src/modules/timesheets/models/ui.models.ts
index d738779..3cf536d 100644
--- a/src/modules/timesheets/models/ui.types.ts
+++ b/src/modules/timesheets/models/ui.models.ts
@@ -1,5 +1,3 @@
-export type FormMode = 'create' | 'edit' | 'delete';
-
export type PayPeriodLabel = {
start_date: string;
end_date: string;
diff --git a/src/modules/timesheets/pages/timesheet-page.vue b/src/modules/timesheets/pages/timesheet-page.vue
index 0fedde9..7be7fd6 100644
--- a/src/modules/timesheets/pages/timesheet-page.vue
+++ b/src/modules/timesheets/pages/timesheet-page.vue
@@ -1,182 +1,48 @@
-
-
-
-
- {{ $t('timesheet.title') }}
-
-
-
- {{ pay_period_label.start_date }}
-
-
- {{ $t('timesheet.date_ranges_to') }}
-
-
- {{ pay_period_label.end_date }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/modules/timesheets/services/timesheet-service.ts b/src/modules/timesheets/services/timesheet-service.ts
index a288505..d52d38c 100644
--- a/src/modules/timesheets/services/timesheet-service.ts
+++ b/src/modules/timesheets/services/timesheet-service.ts
@@ -1,23 +1,14 @@
import { api } from "src/boot/axios";
-import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
-import type { PayPeriodEmployeeDetails } from "src/modules/timesheet-approval/types/pay-period-employee-details";
-import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/pay-period-overview";
-import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/pay-period-report";
-import type { Timesheet } from "../types/timesheet.interfaces";
-import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/shift.interfaces";
+import type { UpsertShift } from "src/modules/timesheets/models/shift.models";
+import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
+import type { PayPeriodDetails } from "src/modules/timesheets/models/pay-period-details.models";
+import type { PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
+import type { PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
export const timesheetService = {
- //GET
- getTimesheetsByEmail: async ( email: string, offset = 0): Promise => {
- const response = await api.get(`/timesheets/${encodeURIComponent(email)}`, {params: offset ? { offset } : undefined});
- return response.data as Timesheet;
- },
-
- //POST
- createTimesheetShifts: async ( email: string, shifts: CreateShiftPayload[], offset = 0): Promise => {
- const payload: CreateWeekShiftPayload = { shifts };
- const response = await api.post(`/timesheets/shifts/${encodeURIComponent(email)}`, payload, { params: offset ? { offset }: undefined });
- return response.data as Timesheet;
+ getPayPeriodDetailsByEmployeeEmail: async (email: string): Promise => {
+ const response = await api.get(`/timesheets/${encodeURIComponent(email)}`);
+ return response.data;
},
getPayPeriodByDate: async (date_string: string): Promise => {
@@ -30,22 +21,18 @@ export const timesheetService = {
return response.data;
},
-
- getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise => {
- // TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
+ getPayPeriodOverviewsBySupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise => {
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
- console.log('pay period data: ', response.data);
return response.data;
},
- getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise => {
- const response = await api.get('timesheets', { params: { year, period_no, email, }});
- console.log('employee details: ', response.data);
+ getPayPeriodDetailsByPayPeriodAndEmployeeEmail: async (year: number, period_no: number, email: string): Promise => {
+ const response = await api.get('timesheets', { params: { year, period_no, email, } });
return response.data;
},
- getTimesheetApprovalCSVReport: async (year: number, period_number: number, report_filters?: PayPeriodReportFilters) => {
- const response = await api.get(`csv/${year}/${period_number}`, { params: { report_filters, }});
- return response.data;
+ upsertOrDeletePayPeriodDetailsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[] | PayPeriodExpenses, pay_period: PayPeriod, date?: string): Promise => {
+ if (date) return (await api.put(`/shifts/upsert/${email}/${date}`, payload)).data;
+ else return (await api.put(`/expenses/${email}/${pay_period.pay_year}/${pay_period.pay_period_no}`, payload, { headers: {'Content-Type': 'application/json'}})).data;
},
};
\ No newline at end of file
diff --git a/src/modules/timesheets/utils/expense.util.ts b/src/modules/timesheets/utils/expense.util.ts
index 2ad7e3e..7ccbb04 100644
--- a/src/modules/timesheets/utils/expense.util.ts
+++ b/src/modules/timesheets/utils/expense.util.ts
@@ -1,6 +1,5 @@
-import type { TimesheetExpense } from "../types/expense.interfaces";
-import type { ExpenseSavePayload, ExpenseTotals, ExpenseType } from "../types/expense.types";
-/* eslint-disable */
+import type { Expense, ExpenseTotals, ExpenseType, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
+
//------------------ normalization / icons ------------------
export const normExpenseType = (type: unknown): string =>
String(type ?? '').trim().toUpperCase();
@@ -21,16 +20,8 @@ export const expenseTypeIcon = (type: unknown): string => {
);
};
-//------------------ q-select options ------------------
-export const buildExpenseTypeOptions = ( types: readonly ExpenseType[], t: (key:string) => string):
- { label: string; value: ExpenseType } [] =>
- types.map((val)=> ({
- label: t(`timesheet.expense.types.${val}`),
- value: val,
-}));
-
//------------------ totals ------------------
-export const computeExpenseTotals = (items: readonly TimesheetExpense[]): ExpenseTotals =>
+export const computeExpenseTotals = (items: readonly Expense[]): ExpenseTotals =>
items.reduce(
(acc, e) => ({
amount: acc.amount + (Number(e.amount) || 0),
@@ -47,7 +38,7 @@ export const makeExpenseRules = (t: (key: string) => string, max_comment_char: n
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
- const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.erros.mileage_required_for_type');
+ const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
@@ -63,14 +54,11 @@ export const makeExpenseRules = (t: (key: string) => string, max_comment_char: n
};
//------------------ saving payload ------------------
-export const buildExpenseSavePayload = (args: {
- pay_period_no: number;
- pay_year: number;
- email: string;
- expenses: TimesheetExpense[];
-}): ExpenseSavePayload => ({
+export const buildExpenseSavePayload = (args: PayPeriodExpenses): PayPeriodExpenses => ({
pay_period_no: args.pay_period_no,
pay_year: args.pay_year,
- email: args.email,
+ employee_email: args.employee_email,
+ is_approved: args.is_approved ?? false,
expenses: args.expenses,
+ totals: computeExpenseTotals(args.expenses),
});
\ No newline at end of file
diff --git a/src/modules/timesheets/utils/expenses-validators.ts b/src/modules/timesheets/utils/expenses-validators.ts
index 392d776..c1146d1 100644
--- a/src/modules/timesheets/utils/expenses-validators.ts
+++ b/src/modules/timesheets/utils/expenses-validators.ts
@@ -1,31 +1,29 @@
-import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "../constants/expense.constants";
-import { ExpensesValidationError } from "../types/expense-validation.interface";
-import type { TimesheetExpense } from "../types/expense.interfaces";
-import {
- type ExpenseType,
- TYPES_WITH_AMOUNT_ONLY,
- TYPES_WITH_MILEAGE_ONLY
-} from "../types/expense.types";
+import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "src/modules/timesheets/constants/expense.constants";
+import { ExpensesValidationError } from "src/modules/timesheets/models/expense.validation";
+import { type Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY, TYPES_WITH_MILEAGE_ONLY } from "src/modules/timesheets/models/expense.models";
//normalization helpers
export const toNumOrUndefined = (value: unknown): number | undefined => {
if(value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) return undefined;
const num = Number(value);
+
return Number.isFinite(num) ? num : undefined;
};
export const normalizeComment = (input?: string): string | undefined => {
if(typeof input === 'undefined' || input === null) return undefined;
const trimmed = String(input).trim();
+
return trimmed.length ? trimmed : undefined;
};
export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
-export const normalizeExpense = (expense: TimesheetExpense): TimesheetExpense => {
+export const normalizeExpense = (expense: Expense): Expense => {
const comment = normalizeComment(expense.comment);
const amount = toNumOrUndefined(expense.amount);
const mileage = toNumOrUndefined(expense.mileage);
+
return {
date: (expense.date ?? '').trim(),
type: normalizeType(expense.type),
@@ -40,7 +38,7 @@ export const normalizeExpense = (expense: TimesheetExpense): TimesheetExpense =>
};
//UI validation error messages
-export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expense'): void => {
+export const validateExpenseUI = (raw: Expense, label: string = 'expense'): void => {
const expense = normalizeExpense(raw);
//Date input validation
@@ -60,6 +58,7 @@ export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expens
context: { [label]: expense },
})
}
+
if((expense.comment.length ?? 0) > COMMENT_MAX_LENGTH) {
throw new ExpensesValidationError({
status_code: 400,
@@ -100,7 +99,7 @@ export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expens
//type constraint validation
const type = expense.type as ExpenseType;
- if(TYPES_WITH_MILEAGE_ONLY.includes(type) && !has_mileage) {
+ if( TYPES_WITH_MILEAGE_ONLY.includes(type) && !has_mileage ) {
throw new ExpensesValidationError({
status_code: 400,
message: 'timesheet.expense.errors.mileage_required_for_type',
@@ -117,7 +116,7 @@ export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expens
};
//totals per pay-period
-export const compute_expense_totals = (items: TimesheetExpense[]) => items.reduce(
+export const compute_expense_totals = (items: Expense[]) => items.reduce(
(acc, raw) => {
const expense = normalizeExpense(raw);
if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
diff --git a/src/modules/timesheets/utils/shift.util.ts b/src/modules/timesheets/utils/shift.util.ts
index 11b86fe..ac6f6d6 100644
--- a/src/modules/timesheets/utils/shift.util.ts
+++ b/src/modules/timesheets/utils/shift.util.ts
@@ -1,19 +1,18 @@
-import type { ShiftKey, ShiftPayload, ShiftSelectOption } from "../types/shift.types";
- /* eslint-disable */
+// import type { ShiftKey, ShiftPayload, ShiftSelectOption } from "../types/shift.types";
-export const toShiftPayload = (shift: any): ShiftPayload => ({
- start_time: String(shift.start_time),
- end_time: String(shift.end_time),
- type: String(shift.type).toUpperCase() as ShiftKey,
- is_remote: !!shift.is_remote,
- ...(shift.comment ? { comment: String(shift.comment) } : {}),
-});
+// export const toShiftPayload = (shift: any): ShiftPayload => ({
+// start_time: String(shift.start_time),
+// end_time: String(shift.end_time),
+// type: String(shift.type).toUpperCase() as ShiftKey,
+// is_remote: !!shift.is_remote,
+// ...(shift.comment ? { comment: String(shift.comment) } : {}),
+// });
-export const buildShiftOptions = (
- keys: readonly string[],
- t:(k: string) => string
-): ShiftSelectOption[] =>
- keys.map((key) => ({
- value: key as any,
- label: t(`timesheet.shift.types.${key}`),
- }));
\ No newline at end of file
+// export const buildShiftOptions = (
+// keys: readonly string[],
+// t:(k: string) => string
+// ): ShiftSelectOption[] =>
+// keys.map((key) => ({
+// value: key as any,
+// label: t(`timesheet.shift.types.${key}`),
+// }));
\ No newline at end of file
diff --git a/src/pages/error-page.vue b/src/pages/error-page.vue
index cbe39b9..8bf297d 100644
--- a/src/pages/error-page.vue
+++ b/src/pages/error-page.vue
@@ -1,23 +1,20 @@
-
-
-
-
-
-
-
-
-
-
- {{$t('notFoundPage.pageText')}}
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ $t('notFoundPage.pageText') }}
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/test-page.vue b/src/pages/test-page.vue
index 5ab28f6..2954dd8 100644
--- a/src/pages/test-page.vue
+++ b/src/pages/test-page.vue
@@ -1,56 +1,73 @@
-
-
-
-
-
- Welcome to App Targo!
-
-
+
+
+
+
+ Welcome to App Targo!
+
+
-
- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
- totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
- sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
- consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui
- dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
- incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
- exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem
- vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui
- dolorem eum fugiat quo voluptas nulla pariatur?
-
-
-
- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
-
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
+ totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
+ sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia
+ consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui
+ dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
+ incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum
+ exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem
+ vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum
+ qui
+ dolorem eum fugiat quo voluptas nulla pariatur?
+
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
-
+
+ At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
+ deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
+ provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.
+ Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
+ eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas
+ assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum
+ necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum
+ rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut
+ perferendis doloribus asperiores repellat.
+
-
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
+ ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+ fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
+ deserunt mollit anim id est laborum.
+
-
-
-
-
-
-
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/stores/expense-store.ts b/src/stores/expense-store.ts
index b66aedf..f4895d6 100644
--- a/src/stores/expense-store.ts
+++ b/src/stores/expense-store.ts
@@ -1,88 +1,124 @@
import { ref } from "vue";
import { defineStore } from "pinia";
-import { type PayPeriodExpenses } from "src/modules/timesheets/types/expense.interfaces";
-import { ExpensesApiError } from "src/modules/timesheets/types/expense-validation.interface";
-import { getPayPeriodExpenses, putPayPeriodExpenses } from "src/modules/timesheets/composables/api/use-expense-api";
+import { useTimesheetStore } from "src/stores/timesheet-store";
+import
+import { default_expense, default_pay_period_expenses, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
+import { unwrapAndClone } from "src/utils/unwrap-and-clone";
+
+const { pay_period } = useTimesheetStore();
-/* eslint-disable */
export const useExpensesStore = defineStore('expenses', () => {
- const is_dialog_open = ref(false);
- const is_loading = ref(false);
- const data = ref(null);
- const error = ref(null);
+ const is_open = ref(false);
+ const is_loading = ref(false);
+ const current_expenses = ref(default_pay_period_expenses);
+ const current_expense = ref(default_expense);
+ const initial_expense = ref(default_expense);
+ const error = ref(null);
- const setErrorFrom = (err: unknown, t?: (_key: string) => string) => {
+ const setErrorFrom = (err: unknown) => {
const e = err as any;
- error.value = (err instanceof ExpensesApiError && t
- ? t(e.message): undefined)
- || e?.message
- || 'Unknown error';
+ error.value = e?.message || 'Unknown error';
};
- const openDialog = async (
- params: { email: string; pay_year: number; pay_period_no: number; t?: (_key: string)=> string}) => {
- is_dialog_open.value = true;
- is_loading.value = true;
- error.value = null;
- try {
- const response = await getPayPeriodExpenses(
- params.email,
- params.pay_year,
- params.pay_period_no,
- );
- data.value = response;
- } catch (err) {
- setErrorFrom(err, params.t);
- data.value = {
- pay_period_no: params.pay_period_no,
- pay_year: params.pay_year,
- employee_email: params.email,
- is_approved: false,
- expenses: [],
- totals: { amount: 0, mileage: 0},
- };
- } finally {
- is_loading.value = false;
- }
- }
-
- const saveExpenses = async (payload: {
- email: string;
- pay_year: number;
- pay_period_no: number;
- expenses: any[]; t?: (_key: string) => string
- }) => {
+ const open = async (employee_email: string) => {
+ is_open.value = true;
is_loading.value = true;
- error.value = null;
+ error.value = null;
try {
- const updated = await putPayPeriodExpenses(
- payload.email,
- payload.pay_year,
- payload.pay_period_no,
- payload.expenses
- );
- data.value = updated;
- is_dialog_open.value = false;
+ const response = await getPayPeriodExpenses(employee_email, pay_period.pay_year, pay_period.pay_period_no,);
+ current_expenses.value = response;
+ initial_expenses.value = unwrapAndClone(response);
} catch (err) {
- setErrorFrom(err, payload.t);
+ setErrorFrom(err);
+ current_expenses.value = default_pay_period_expenses;
+ initial_expenses.value = default_pay_period_expenses;
+ } finally {
+ is_loading.value = false;
+ }
+ }
+
+ const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise => {
+ const encoded_email = encodeURIComponent(employee_email);
+ const encoded_year = encodeURIComponent(String(pay_period.pay_year));
+ const encoded_pay_period_no = encodeURIComponent(String(pay_period.pay_period_no));
+
+ try {
+ const { data } = await api.get(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
+
+ const items = Array.isArray(data.expenses) ? data.expenses.map(normalizeExpense) : [];
+ return {
+ ...data,
+ expenses: items,
+ };
+ } catch(err:any) {
+ const status_code: number = err?.response?.status ?? 500;
+ const data = err?.response?.data ?? {};
+ throw new ExpensesApiError({
+ status_code,
+ error_code: data.error_code,
+ message: data.message || data.error || err.message,
+ context: data.context,
+ });
+ }
+ };
+
+ const onSave = () => {
+ try {
+ validateAll();
+ reset();
+ emit('save', buildExpenseSavePayload({
+ pay_period_no: pay_period.pay_period_no,
+ pay_year: pay_period.pay_year,
+ employee_email: employeeEmail,
+ is_approved: false,
+ expenses: payload(),
+ }));
+
+ } catch (err: any) {
+ emit('error', toExpensesError(err));
+ }
+ };
+
+ const onFormSubmit = async () => {
+ try {
+ await validateAnd(async () => {
+ addFromDraft();
+ reset();
+ });
+ } catch (err: any) {
+ emit('error', toExpensesError(err));
+ }
+ };
+
+ const upsertOrDeletePayPeriodExpenseByEmployeeEmail = async (employee_email: string, expenses: Expense[]) => {
+ is_loading.value = true;
+ error.value = null;
+
+ try {
+ const updated = await putPayPeriodExpenses(employee_email, pay_period.pay_year, pay_period.pay_period_no, expenses);
+ pay_period_expenses.value = updated;
+ is_open.value = false;
+ } catch (err) {
+ setErrorFrom(err);
} finally {
is_loading.value = false;
}
};
- const closeDialog = () => {
- error.value = null;
- is_dialog_open.value = false;
+ const close = () => {
+ error.value = null;
+ is_open.value = false;
};
return {
- is_dialog_open,
+ is_open,
is_loading,
- data,
+ current_expenses,
+ initial_expenses,
error,
- openDialog,
- saveExpenses,
- closeDialog,
+ open,
+ upsertOrDeletePayPeriodExpenseByEmployeeEmail,
+ close,
};
});
\ No newline at end of file
diff --git a/src/stores/shift-store.ts b/src/stores/shift-store.ts
index 5acf6b2..c5b33c3 100644
--- a/src/stores/shift-store.ts
+++ b/src/stores/shift-store.ts
@@ -1,50 +1,80 @@
-import { defineStore } from "pinia";
import { ref } from "vue";
-import { toShiftPayload } from "src/modules/timesheets/utils/shift.util";
-import type { FormMode } from "src/modules/timesheets/types/ui.types";
-import type { ShiftPayload } from "src/modules/timesheets/types/shift.types";
+import { defineStore } from "pinia";
+import { unwrapAndClone } from "src/utils/unwrap-and-clone";
+import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
+import { useTimesheetStore } from "src/stores/timesheet-store";
+import { default_shift, type UpsertAction, type Shift, UpsertShift } from "src/modules/timesheets/models/shift.models";
+import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
- /* eslint-disable */
export const useShiftStore = defineStore('shift', () => {
- const is_open = ref(false);
- const mode = ref('create');
- const date_iso = ref('');
- const initial_shift = ref(null);
+ const is_open = ref(false);
+ const mode = ref('create');
+ const date_iso = ref('');
+ const current_shift = ref(default_shift);
+ const initial_shift = ref(default_shift);
- const open = (nextMode: FormMode, date: string, payload: ShiftPayload | null) => {
- mode.value = nextMode;
- date_iso.value = date;
- initial_shift.value = payload;
- is_open.value = true;
+ const timesheet_store = useTimesheetStore();
+
+ const open = (next_mode: UpsertAction, date: string, current: Shift, initial: Shift) => {
+ mode.value = next_mode;
+ date_iso.value = date;
+ current_shift.value = current; // new shift
+ initial_shift.value = initial; // old shift
+ is_open.value = true;
};
const openCreate = (date: string) => {
- open('create', date, null);
+ open('create', date, default_shift, default_shift);
};
- const openEdit = (date: string, shift: any) => {
- open('edit', date, toShiftPayload(shift as any));
+ const openUpdate = (date: string, shift: Shift) => {
+ open('update', date, shift, unwrapAndClone(shift));
};
const openDelete = (date: string, shift: any) => {
- open('delete', date, toShiftPayload(shift as any));
+ open('delete', date, default_shift, shift);
}
- const close = () => {
- is_open.value = false;
- mode.value = 'create';
- date_iso.value = '';
- initial_shift.value = null;
+ const close = () => {
+ is_open.value = false;
+ mode.value = 'create';
+ date_iso.value = '';
+ current_shift.value = default_shift;
+ initial_shift.value = default_shift;
};
+ const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
+ const encoded_email = encodeURIComponent(employee_email);
+ const encoded_date = encodeURIComponent(current_shift.value.date);
+
+ try {
+ const result = await timesheetService.upsertOrDeletePayPeriodShifts(encoded_email, encoded_date, [ upsert_shift, ]);
+ timesheet_store.pay_period_details = result;
+ } catch (err: any) {
+ const status_code: number = err?.response?.status ?? 500;
+ const data = err?.response?.data ?? {};
+ throw new GenericApiError({
+ status_code,
+ error_code: data.error_code,
+ message: data.message || data.error || err.message,
+ context: data.context,
+ });
+ } finally {
+ close();
+ }
+ }
+
+
return {
is_open,
mode,
date_iso,
+ current_shift,
initial_shift,
openCreate,
- openEdit,
+ openUpdate,
openDelete,
close,
+ upsertOrDeleteShiftByEmployeeEmail,
};
})
\ No newline at end of file
diff --git a/src/stores/timesheet-store.ts b/src/stores/timesheet-store.ts
index 577da3e..00c09ed 100644
--- a/src/stores/timesheet-store.ts
+++ b/src/stores/timesheet-store.ts
@@ -3,21 +3,18 @@ import { computed, ref } from 'vue';
import { withLoading } from 'src/utils/store-helpers';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
-import { default_timesheet_approval_overview_crew, type TimesheetApprovalOverviewCrew } from "src/modules/timesheet-approval/models/timesheet-approval-overview.models";
-// import type { Timesheet } from 'src/modules/timesheets/types/timesheet.interfaces';
-import type { TimesheetDetails } from 'src/modules/timesheets/models/timesheet.models';
-import { default_timesheet_details } from 'src/modules/timesheets/types/timesheet.defaults';
+import { default_pay_period_overview, type PayPeriodOverview } from "src/modules/timesheet-approval/models/pay-period-overview.models";
import { default_pay_period, type PayPeriod } from 'src/modules/shared/types/pay-period-interface';
-import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/pay-period-report';
+import { default_pay_period_details, type PayPeriodDetails } from 'src/modules/timesheets/models/pay-period-details.models';
+import { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
export const useTimesheetStore = defineStore('timesheet', () => {
const is_loading = ref(false);
const pay_period = ref(default_pay_period);
- const timesheet_approval_overview_list = ref([]);
- const timesheet_aproval_overview = ref(default_pay_period_employee_overview);
- const pay_period_employee_details = ref(default_timesheet_details);
+ const pay_period_overviews = ref([ default_pay_period_overview, ]);
+ const current_pay_period_overview = ref(default_pay_period_overview);
+ const pay_period_details = ref(default_pay_period_details);
const pay_period_report = ref();
- // const timesheet = ref(default_timesheet);
const is_calendar_limit = computed( ()=>
pay_period.value.pay_year === 2024 &&
pay_period.value.pay_period_no <= 1
@@ -29,11 +26,11 @@ export const useTimesheetStore = defineStore('timesheet', () => {
let response;
if (typeof date_or_year === 'string') {
- response = await timesheetApprovalService.getPayPeriodByDate(date_or_year);
+ response = await timesheetService.getPayPeriodByDate(date_or_year);
return true;
}
else if ( typeof date_or_year === 'number' && period_number ) {
- response = await timesheetApprovalService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
+ response = await timesheetService.getPayPeriodByYearAndPeriodNumber(date_or_year, period_number);
return true;
}
else response = default_pay_period;
@@ -43,7 +40,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
} catch(error){
console.error('Could not get current pay period: ', error );
pay_period.value = default_pay_period;
- pay_period_employee_overview_list.value = [];
+ pay_period_overviews.value = [ default_pay_period_overview, ];
//TODO: More in-depth error-handling here
}
@@ -51,58 +48,15 @@ export const useTimesheetStore = defineStore('timesheet', () => {
});
};
- const getPayPeriodEmployeeOverviewListBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise => {
+ const getPayPeriodDetailsByEmployeeEmail = async (employee_email: string) => {
return withLoading( is_loading, async () => {
try {
- const response = await timesheetApprovalService.getPayPeriodEmployeeOverviewListBySupervisorEmail( pay_year, period_number, supervisor_email );
- pay_period_employee_overview_list.value = response.employees_overview;
- return true;
- } catch (error) {
- console.error('There was an error retrieving Employee Pay Period overviews: ', error);
- pay_period_employee_overview_list.value = [];
- // TODO: More in-depth error-handling here
- }
-
- return false;
- });
- };
-
- const getPayPeriodOverviewByEmployeeEmail = (email: string): PayPeriodEmployeeOverview => {
- const response = pay_period_employee_overview_list.value?.find( employee_overview => employee_overview.email === email);
- if (typeof response === 'undefined') {
- pay_period_employee_overview.value = default_pay_period_employee_overview;
- } else {
- pay_period_employee_overview.value = response;
- }
-
- return pay_period_employee_overview.value;
- };
-
- // const getTimesheetByEmail = async (employee_email: string) => {
- // return withLoading( is_loading, async () => {
- // try{
- // const response = await timesheetTempService.getTimesheetsByEmail(employee_email);
- // timesheet.value = response;
-
- // return true;
- // }catch (error) {
- // console.error('There was an error retrieving timesheet details for this employee: ', error);
- // timesheet.value = { ...default_timesheet }
- // }
-
- // return false;
- // });
- // };
-
- const getPayPeriodEmployeeDetailsByEmployeeEmail = async (employee_email: string) => {
- return withLoading( is_loading, async () => {
- try {
- const response = await timesheetApprovalService.getPayPeriodEmployeeDetailsByPayPeriodAndEmail(
+ const response = await timesheetService.getPayPeriodDetailsByPayPeriodAndEmployeeEmail(
pay_period.value.pay_year,
pay_period.value.pay_period_no,
employee_email
);
- pay_period_employee_details.value = response;
+ pay_period_details.value = response;
return true;
} catch (error) {
@@ -110,17 +64,33 @@ export const useTimesheetStore = defineStore('timesheet', () => {
// TODO: More in-depth error-handling here
}
- pay_period_employee_details.value = default_pay_period_employee_details;
+ pay_period_details.value = default_pay_period_details;
return false;
});
};
- const getTimesheetApprovalCSVReport = async (report_filters?: PayPeriodReportFilters) => {
+ const getPayPeriodOverviewsBySupervisorEmail = async (pay_year: number, period_number: number, supervisor_email: string): Promise => {
return withLoading( is_loading, async () => {
try {
- const response = await timesheetApprovalService.getTimesheetApprovalCSVReport(
- pay_period.value.pay_year,
- pay_period.value.pay_period_no,
+ const response = await timesheetApprovalService.getPayPeriodOverviewsBySupervisorEmail( pay_year, period_number, supervisor_email );
+ pay_period_overviews.value = response;
+ return true;
+ } catch (error) {
+ console.error('There was an error retrieving Employee Pay Period overviews: ', error);
+ pay_period_overviews.value = [ default_pay_period_overview, ];
+ // TODO: More in-depth error-handling here
+ }
+
+ return false;
+ });
+ };
+
+ const getPayPeriodReportByYearAndPeriodNumber = async (year: number, period_number: number, report_filters?: TimesheetApprovalCSVReportFilters) => {
+ return withLoading( is_loading, async () => {
+ try {
+ const response = await timesheetApprovalService.getPayPeriodReportByYearAndPeriodNumber(
+ year,
+ period_number,
report_filters
);
pay_period_report.value = response;
@@ -136,18 +106,15 @@ export const useTimesheetStore = defineStore('timesheet', () => {
};
return {
- pay_period,
- pay_period_employee_overview_list,
- pay_period_employee_overview,
- pay_period_employee_details,
- timesheet,
is_loading,
is_calendar_limit,
+ pay_period,
+ pay_period_overviews,
+ current_pay_period_overview,
+ pay_period_details,
getPayPeriodByDateOrYearAndNumber,
- // getTimesheetByEmail,
- getPayPeriodEmployeeOverviewListBySupervisorEmail,
- getPayPeriodOverviewByEmployeeEmail,
- getPayPeriodEmployeeDetailsByEmployeeEmail,
- getTimesheetApprovalCSVReport,
+ getPayPeriodOverviewsBySupervisorEmail,
+ getPayPeriodDetailsByEmployeeEmail,
+ getPayPeriodReportByYearAndPeriodNumber,
};
});
\ No newline at end of file
diff --git a/src/utils/deep-equal.ts b/src/utils/deep-equal.ts
index 352cce9..3b575f3 100644
--- a/src/utils/deep-equal.ts
+++ b/src/utils/deep-equal.ts
@@ -1,28 +1,41 @@
-export const deepEqual = (a: unknown, b: unknown): boolean => {
- if (a === b) {
- return true;
- }
+import { unwrapAndClone } from "src/utils/unwrap-and-clone";
- if (
- a == null || // handles null and undefined
- b == null ||
- typeof a !== 'object' ||
- typeof b !== 'object'
- ) {
- return false;
- }
+/**
+ * Internal recursive function comparing two plain values.
+ */
+const _deepEqualRecursive = (a: unknown, b: unknown): boolean => {
+ if (a === b) return true;
- const aKeys = Object.keys(a as Record);
- const bKeys = Object.keys(b as Record);
+ if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
+ return false;
+ }
- if (aKeys.length !== bKeys.length) {
- return false;
- }
+ // Handle arrays
+ if (Array.isArray(a) && Array.isArray(b)) {
+ if (a.length !== b.length) return false;
+ return a.every((val, i) => _deepEqualRecursive(val, b[i]));
+ } else if (Array.isArray(a) || Array.isArray(b)) {
+ return false; // one is array, other is not
+ }
- return aKeys.every((key) =>
- deepEqual(
- (a as Record)[key],
- (b as Record)[key]
- )
- );
+ const aKeys = Object.keys(a as Record);
+ const bKeys = Object.keys(b as Record);
+
+ if (aKeys.length !== bKeys.length) return false;
+
+ return aKeys.every((key) =>
+ _deepEqualRecursive(
+ (a as Record)[key],
+ (b as Record)[key]
+ )
+ );
+};
+
+/**
+ * Deep equality check that normalizes reactive objects first.
+ */
+export const deepEqual = (given: unknown, expected: unknown): boolean => {
+ const a = unwrapAndClone(given as object);
+ const b = unwrapAndClone(expected as object);
+ return _deepEqualRecursive(a, b);
};
diff --git a/src/utils/normalize-object.ts b/src/utils/normalize-object.ts
new file mode 100644
index 0000000..2b1c6d4
--- /dev/null
+++ b/src/utils/normalize-object.ts
@@ -0,0 +1,31 @@
+export type Normalizer = {
+ [K in keyof T]: (val: unknown) => T[K];
+};
+
+export const normalizeObject = (raw: any, schema: Normalizer): T => {
+ const result = {} as T;
+ for (const key in schema) {
+ result[key] = schema[key](raw[key]);
+ }
+ return result;
+}
+
+// Example for Expense
+// export interface Expense {
+// date: string;
+// type: "TRAVEL" | "MEAL" | "OTHER";
+// comment: string;
+// amount?: number;
+// mileage?: number;
+// }
+
+// const expenseNormalizer: Normalizer = {
+// date: (v) => String(v ?? ""), // fallback to ""
+// type: (v) => (["TRAVEL", "MEAL", "OTHER"].includes(v) ? v : "OTHER"),
+// comment: (v) => String(v ?? ""),
+// amount: (v) => (typeof v === "number" ? v : undefined),
+// mileage: (v) => (typeof v === "number" ? v : undefined),
+// };
+
+// export const normalizeExpense = (raw: unknown): Expense =>
+// normalizeObject(raw, expenseNormalizer);
\ No newline at end of file
diff --git a/src/utils/store-helpers.ts b/src/utils/store-helpers.ts
index 096b4e5..99c2daf 100644
--- a/src/utils/store-helpers.ts
+++ b/src/utils/store-helpers.ts
@@ -1,10 +1,8 @@
-import type { Ref } from "vue";
-
-export const withLoading = async ( loading_state: Ref, fn: () => Promise ) => {
- loading_state.value = true;
+export const withLoading = async ( loading_state: boolean, fn: () => Promise ) => {
+ loading_state = true;
try {
return await fn();
} finally {
- loading_state.value = false;
+ loading_state = false;
}
};
\ No newline at end of file
diff --git a/src/utils/to-qselect-options.ts b/src/utils/to-qselect-options.ts
new file mode 100644
index 0000000..c12619b
--- /dev/null
+++ b/src/utils/to-qselect-options.ts
@@ -0,0 +1,6 @@
+export const toQSelectOptions = (values: readonly T[], i18n_domain?: string): { label: string; value: T }[] => {
+ return values.map(value => ({
+ label: ((i18n_domain ?? "") + value).toString(),
+ value: value as T
+ }));
+};
\ No newline at end of file
diff --git a/src/utils/unwrap-and-clone.ts b/src/utils/unwrap-and-clone.ts
new file mode 100644
index 0000000..779a358
--- /dev/null
+++ b/src/utils/unwrap-and-clone.ts
@@ -0,0 +1,16 @@
+import { isProxy, toRaw } from "vue";
+
+/**
+ * Converts reactive proxies or objects into a deep, plain clone.
+ */
+export const unwrapAndClone = (obj: T): T => {
+ const raw = isProxy(obj) ? toRaw(obj) : obj;
+
+ // Use structuredClone if available (handles Dates, Maps, Sets, circulars)
+ if (typeof (globalThis as any).structuredClone === "function") {
+ return (globalThis as any).structuredClone(raw);
+ }
+
+ // Fallback for older environments (loses Dates, Sets, Maps)
+ return JSON.parse(JSON.stringify(raw));
+};
\ No newline at end of file