refactor(timesheet): update appearance, work on expense dialog, plugging to backend.

This commit is contained in:
Nicolas Drolet 2025-11-11 12:45:38 -05:00
parent 62385461d5
commit 1274a1b65b
10 changed files with 78 additions and 109 deletions

View File

@ -5,6 +5,7 @@
const NEXT = 1;
const PREVIOUS = -1;
const PAY_PERIOD_DATE_LIMIT = '2023/12/17';
const timesheet_store = useTimesheetStore();
@ -125,7 +126,7 @@
class="q-mt-xl"
today-btn
mask="YYYY-MM-DD"
:options="date => date > '2023/12/16'"
:options="date => date >= PAY_PERIOD_DATE_LIMIT"
@update:model-value="onDateSelected"
/>
</q-dialog>

View File

@ -31,8 +31,8 @@
};
const cancelUpdateMode = () => {
expenses_store.current_expense = new Expense;
expenses_store.initial_expense = new Expense;
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
expenses_store.initial_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
expenses_store.mode = 'create';
};
@ -40,6 +40,12 @@
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
};
const getExpenseCalendarRange = (current_date: string) => {
const period = timesheet_store.pay_period;
if (period !== undefined) return current_date >= period.period_start && current_date <= period.period_end;
return false;
}
</script>
<template>
@ -83,6 +89,7 @@
v-model="expenses_store.current_expense.date"
mask="YYYY-MM-DD"
event-color="accent"
:options="getExpenseCalendarRange"
@update:model-value="is_navigator_open = false"
/>
</q-dialog>

View File

@ -2,7 +2,8 @@
setup
lang="ts"
>
import { computed, inject, ref } from 'vue';
import { date } from 'quasar';
import { computed, ref } from 'vue';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { deepEqual } from 'src/utils/deep-equal';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
@ -22,7 +23,6 @@
const expenses_store = useExpensesStore();
const auth_store = useAuthStore();
const expenses_api = useExpensesApi();
const employee_email = inject<string>('employeeEmail') ?? '';
const refresh_key = ref(1);
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
@ -31,10 +31,7 @@
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
const requestExpenseDeletion = async () => {
// expenses_store.mode = 'delete';
expenses_store.initial_expense = expense;
expenses_store.current_expense = new Expense;
await expenses_api.deleteExpenseByEmployeeEmail(employee_email, expenses_store.initial_expense.date);
await expenses_api.deleteExpenseById(expense.id);
}
const onExpenseClicked = () => {
@ -45,9 +42,9 @@
}
const onUpdateClicked = () => {
if (deepEqual(expense, expenses_store.current_expense)){
if (deepEqual(expense, expenses_store.current_expense)) {
expenses_store.mode = 'create';
expenses_store.current_expense = new Expense;
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
return;
}
@ -138,7 +135,10 @@
v-if="!horizontal"
top
>
<q-item-label lines="1" class="text-weight-medium text-uppercase">
<q-item-label
lines="1"
class="text-weight-medium text-uppercase"
>
{{ $t('timesheet.expense.employee_comment') }}
</q-item-label>
<q-item-label
@ -155,7 +155,10 @@
v-if="is_authorized_to_approve"
top
>
<q-item-label lines="1" class="text-weight-medium text-uppercase">
<q-item-label
lines="1"
class="text-weight-medium text-uppercase"
>
{{ $t('timesheet.expense.supervisor_comment') }}
</q-item-label>
<q-item-label
@ -170,9 +173,10 @@
<q-item-section :side="$q.screen.gt.sm">
<q-btn
flat
color="accent"
icon="edit"
size="lg"
icon="edit"
color="accent"
:disable="expense.is_approved"
class="q-pa-none z-top"
:class="expense.is_approved ? 'invisible no-pointer' : ''"
@click.stop="onUpdateClicked"
@ -182,10 +186,11 @@
<q-item-section :side="$q.screen.gt.sm">
<q-btn
flat
:color="expense.is_approved ? 'white' : 'negative'"
:icon="expense.is_approved ? 'verified' : 'close'"
size="lg"
:icon="expense.is_approved ? 'verified' : 'close'"
:color="expense.is_approved ? 'white' : 'negative'"
class="q-pa-none z-top"
:class="expense.is_approved ? 'no-pointer' : ''"
@click.stop="requestExpenseDeletion"
/>
</q-item-section>

View File

@ -2,14 +2,23 @@
setup
lang="ts"
>
import { useExpensesStore } from 'src/stores/expense-store';
import { computed } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore();
const { horizontal = false } = defineProps<{
horizontal?: boolean;
}>();
const expenses_list = computed(() => {
if (timesheet_store.timesheets !== undefined) {
return timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses);
}
return [];
})
</script>
<template>
@ -20,14 +29,14 @@
:class="horizontal ? 'row flex-center' : ''"
>
<q-item-label
v-if="expenses_store.pay_period_expenses?.length === 0"
v-if="expenses_list.length > 0"
class="text-italic q-px-sm"
>
{{ $t('timesheet.expense.empty_list') }}
</q-item-label>
<ExpenseDialogListItem
v-for="(expense, index) in expenses_store.pay_period_expenses"
v-for="(expense, index) in expenses_list"
:key="index"
v-model="expense.is_approved"
:index="index"

View File

@ -84,7 +84,6 @@
<template>
<q-slide-item
right-color="negative"
class="q-my-xs rounded-5 bg-transparent"
@right="details => slideDeleteShift(details.reset)"

View File

@ -7,7 +7,7 @@ import type { Expense } from "src/modules/timesheets/models/expense.models";
export const useExpensesApi = () => {
const expenses_store = useExpensesStore();
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
};
@ -16,13 +16,13 @@ export const useExpensesApi = () => {
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
};
const deleteExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
const deleteExpenseById = async (expense_id: number): Promise<void> => {
await expenses_store.deleteExpenseById(expense_id);
};
return {
createExpenseByEmployeeEmail,
updateExpenseByEmployeeEmail,
deleteExpenseByEmployeeEmail,
deleteExpenseById,
};
};

View File

@ -14,35 +14,12 @@ export class Expense {
supervisor_comment?: string;
is_approved: boolean;
constructor() {
constructor(date: string) {
this.id = -1;
this.date = '';
this.date = date;
this.type = 'EXPENSES';
this.amount = 0;
this.comment = '';
this.is_approved = false;
};
};
export const test_expenses: Expense[] = [
{
id: 201,
date: '2025-01-06',
type: 'EXPENSES',
amount: 15.5,
comment: 'Lunch receipt',
is_approved: false,
},
{
id: 202,
date: '2025-01-07',
type: 'MILEAGE',
amount: 0,
mileage: 32.4,
comment: 'Travel to client site',
is_approved: true,
},
];
};

View File

@ -1,13 +1,19 @@
import { api } from "src/boot/axios";
import type { Expense } from "src/modules/timesheets/models/expense.models";
export const ExpenseService = {
getExpensesByTimesheetId: async (timesheet_id: number) => {
const response = await api.get(`timesheet/${timesheet_id}`);
createExpense: async (expense: Expense) => {
const response = await api.post('expense/create', expense);
return response.data;
},
upsertOrDeleteExpenseById: async (expense_id: number) => {
const response = await api.post(`epxense/${expense_id}`);
updateExpenseById: async (expense: Expense) => {
const response = await api.patch(`expense/update`, expense);
return response.data;
},
deleteExpenseById: async (expense_id: number): Promise<{ok: boolean, id: number, error?: unknown}> => {
const response = await api.delete(`expense/delete/${expense_id}`);
return response.data;
}
};

View File

@ -1,8 +1,8 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense-validation.models";
import { test_expenses, Expense } from "src/modules/timesheets/models/expense.models";
import { Expense } from "src/modules/timesheets/models/expense.models";
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
import { date } from "quasar";
@ -10,82 +10,47 @@ export const useExpensesStore = defineStore('expenses', () => {
const is_open = ref(false);
const is_loading = ref(false);
const mode = ref<'create' | 'update' | 'delete'>('create');
const pay_period_expenses = ref<Expense[]>(test_expenses);
const current_expense = ref<Expense>(new Expense);
const initial_expense = ref<Expense>(new Expense);
const error = ref<string | null>(null);
// const setErrorFrom = (err: unknown) => {
// const e = err as any;
// error.value = e?.message || 'Unknown error';
// };
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const open = (): void => {
is_open.value = true;
error.value = null;
current_expense.value = new Expense;
initial_expense.value = new Expense;
current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
mode.value = 'create';
}
const close = () => {
error.value = null;
is_open.value = false;
};
const getPayPeriodExpensesByTimesheetId = async (timesheet_id: number): Promise<void> => {
is_loading.value = true;
error.value = null;
const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => {
try {
const expenses = await ExpenseService.getExpensesByTimesheetId(timesheet_id);
pay_period_expenses.value = expenses;
} catch (err: unknown) {
if (typeof err === 'object') {
const error = err as GenericApiError;
const status_code: number = error.status_code ?? 500;
// const data = error.context ?? '';
// error.value = data.message || data.error || err.message;
throw new ExpensesApiError({
status_code,
// error_code: data.error_code,
// message: data.message || data.error || err.message,
// context: data.context,
});
if (expense_id < 0) {
const data = await ExpenseService.createExpense(expense);
return data;
}
} finally {
is_loading.value = false;
}
};
const upsertOrDeleteExpensesById = async (expense_id: number): Promise<void> => {
is_loading.value = true;
error.value = null;
try {
await ExpenseService.upsertOrDeleteExpenseById(expense_id);
// TODO: Save response data into proper ref
} catch (err) {
// setErrorFrom(err);
console.error(err);
} finally {
is_loading.value = false;
}
};
const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
const data = await ExpenseService.deleteExpenseById(expense_id);
return data.ok;
}
return {
is_open,
is_loading,
mode,
pay_period_expenses,
current_expense,
initial_expense,
error,
open,
getPayPeriodExpensesByTimesheetId,
upsertOrDeleteExpensesById,
upsertExpensesById,
deleteExpenseById,
close,
};
});

View File

@ -1,13 +1,13 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { useAuthStore } from 'src/stores/auth-store';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
export const useTimesheetStore = defineStore('timesheet', () => {