Merge branch 'main' of git.targo.ca:Targo/targo_frontend into dev/nicolas/approvals-DRYing
This commit is contained in:
commit
ebc3bde10c
|
|
@ -2,15 +2,16 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import type { ExpenseType, Expense } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import type { ExpenseType, Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
|
const expense_store = useExpensesStore();
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
|
|
||||||
//------------------ props ------------------
|
//------------------ props ------------------
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|
||||||
type_options: { label: string; value: ExpenseType }[];
|
type_options: { label: string; value: ExpenseType }[];
|
||||||
show_amount: boolean;
|
show_amount: boolean;
|
||||||
is_readonly: boolean;
|
is_readonly: boolean;
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
<q-form
|
<q-form
|
||||||
flat
|
flat
|
||||||
v-if="!is_readonly"
|
v-if="!is_readonly"
|
||||||
@submit.prevent="emit('submit')"
|
@submit.prevent="$emit('submit')"
|
||||||
>
|
>
|
||||||
<div class="text-subtitle2 q-py-sm">
|
<div class="text-subtitle2 q-py-sm">
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
|
|
@ -44,7 +45,7 @@
|
||||||
|
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model.date="draft!.date"
|
v-model="expense_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
readonly
|
readonly
|
||||||
|
|
@ -59,12 +60,12 @@
|
||||||
dense
|
dense
|
||||||
icon="event"
|
icon="event"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="datePickerOpen = true"
|
@click="is_navigator_open = true"
|
||||||
/>
|
/>
|
||||||
<q-dialog v-model="datePickerOpen">
|
<q-dialog v-model="is_navigator_open">
|
||||||
<q-date
|
<q-date
|
||||||
v-model="draft!.date"
|
v-model="expense_store.current_expense.date"
|
||||||
@update:model-value="datePickerOpen = false"
|
@update:model-value="is_navigator_open = false"
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
@ -73,7 +74,7 @@
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
<q-select
|
<q-select
|
||||||
v-model="draft!.type"
|
v-model="expense_store.current_expense.type"
|
||||||
:options="type_options"
|
:options="type_options"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -91,7 +92,7 @@
|
||||||
<template v-if="show_amount">
|
<template v-if="show_amount">
|
||||||
<q-input
|
<q-input
|
||||||
key="amount"
|
key="amount"
|
||||||
v-model.number="draft!.amount"
|
v-model.number="expense_store.current_expense.amount"
|
||||||
filled
|
filled
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
|
|
@ -110,7 +111,7 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<q-input
|
<q-input
|
||||||
key="mileage"
|
key="mileage"
|
||||||
v-model.number="draft!.mileage"
|
v-model.number="expense_store.current_expense.mileage"
|
||||||
filled
|
filled
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
|
|
@ -127,7 +128,7 @@
|
||||||
|
|
||||||
<!-- employee comment input -->
|
<!-- employee comment input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="draft!.comment"
|
v-model="expense_store.current_expense.comment"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -142,7 +143,7 @@
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold ">
|
<span class="text-weight-bold ">
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
{{ $t('timesheet.expense.comment') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
import { expenseTypeIcon } from '../../utils/expense.util';
|
import { expenseTypeIcon } from '../../utils/expense.util';
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
items: TimesheetExpense[];
|
items: TimesheetExpense[];
|
||||||
is_readonly: boolean;
|
is_readonly: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
defineEmits<{
|
||||||
(e: 'remove', index: number): void;
|
(e: 'remove', index: number): void;
|
||||||
|
(e: 'edit' , index: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -36,7 +37,12 @@ const emit = defineEmits<{
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<q-item-section top>
|
<q-item-section top>
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ expense.amount?.toFixed(2) }} $
|
||||||
|
</template>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label v-else>
|
<q-item-label v-else>
|
||||||
{{ expense.amount?.toFixed(2) }} $
|
{{ expense.amount?.toFixed(2) }} $
|
||||||
|
|
@ -78,7 +84,20 @@ const emit = defineEmits<{
|
||||||
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
||||||
{{ expense.supervisor_comment }}
|
{{ expense.supervisor_comment }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- delete btn -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
v-if="!is_readonly"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
icon="edit"
|
||||||
|
@click="$emit('edit', index)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
<!-- delete btn -->
|
<!-- delete btn -->
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
|
|
@ -89,7 +108,7 @@ const emit = defineEmits<{
|
||||||
size="xs"
|
size="xs"
|
||||||
color="negative"
|
color="negative"
|
||||||
icon="close"
|
icon="close"
|
||||||
@click="emit('remove', index)"
|
@click="$emit('remove', index)"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useExpenseForm } from '../../composables/use-expense-form';
|
||||||
|
import { useExpenseDraft } from '../../composables/use-expense-draft';
|
||||||
|
import { useExpenseItems } from '../../composables/use-expense-items';
|
||||||
|
import { useToggle } from 'src/modules/shared/composables/use-toggle';
|
||||||
|
import ExpenseList from './expense-list.vue';
|
||||||
|
import ExpenseForm from './expense-form.vue';
|
||||||
|
import {
|
||||||
|
buildExpenseTypeOptions,
|
||||||
|
computeExpenseTotals,
|
||||||
|
makeExpenseRules,
|
||||||
|
buildExpenseSavePayload
|
||||||
|
} from '../../utils/expense.util';
|
||||||
|
import { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
||||||
|
import { ExpensesValidationError } from '../../types/expense-validation.interface';
|
||||||
|
import { EXPENSE_TYPE } from '../../types/expense.types';
|
||||||
|
import type { ExpenseType } from '../../types/expense.types';
|
||||||
|
import type { ExpenseDay, TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import {
|
||||||
|
createExpenseByDate,
|
||||||
|
deleteExpenseByDate,
|
||||||
|
getPayPeriodExpenses,
|
||||||
|
updateExpenseByDate
|
||||||
|
} from '../../composables/api/use-expense-api';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
||||||
|
|
||||||
|
//------------------ props ------------------
|
||||||
|
const {email, pay_period_no, pay_year, is_approved, initial_expenses} = defineProps<{
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
is_approved?: boolean;
|
||||||
|
initial_expenses?: TimesheetExpense[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
//------------------ emits ------------------
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void;
|
||||||
|
(e: 'save', payload: {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
}): void;
|
||||||
|
(e: 'error', err: ExpensesValidationError): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
//------------------ q-select mapper ------------------
|
||||||
|
const type_options = computed(() => {
|
||||||
|
void locale.value;
|
||||||
|
return buildExpenseTypeOptions(EXPENSE_TYPE, t);
|
||||||
|
})
|
||||||
|
|
||||||
|
//------------------ refs and computed ------------------
|
||||||
|
const files = ref<File[] | null>(null);
|
||||||
|
const is_readonly = computed(() => !!is_approved);
|
||||||
|
const editing_old = ref<ExpenseDay | null>(null);
|
||||||
|
|
||||||
|
const { state: is_open_date_picker } = useToggle();
|
||||||
|
const { draft, setType, reset, showAmount } = useExpenseDraft();
|
||||||
|
const { formRef, validateAnd } = useExpenseForm();
|
||||||
|
const { items, validateAll, payload } = useExpenseItems({
|
||||||
|
initial_expenses: initial_expenses,
|
||||||
|
is_approved: is_readonly,
|
||||||
|
draft,
|
||||||
|
});
|
||||||
|
const totals = computed(() => computeExpenseTotals(items.value));
|
||||||
|
|
||||||
|
//------------------ actions ------------------
|
||||||
|
const onSave = () => {
|
||||||
|
try {
|
||||||
|
validateAll();
|
||||||
|
reset();
|
||||||
|
emit('save', buildExpenseSavePayload({
|
||||||
|
pay_period_no: pay_period_no,
|
||||||
|
pay_year: pay_year,
|
||||||
|
email: email,
|
||||||
|
expenses: payload(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
const e = err instanceof ExpensesValidationError
|
||||||
|
? err
|
||||||
|
: new ExpensesValidationError({
|
||||||
|
status_code: 400,
|
||||||
|
message: String(err?.message || err)
|
||||||
|
});
|
||||||
|
emit('error', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshFromServer = async () => {
|
||||||
|
const fresh = await getPayPeriodExpenses(email, pay_year, pay_period_no);
|
||||||
|
items.value = Array.isArray(fresh.expenses) ? fresh.expenses : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await validateAnd(async () => {
|
||||||
|
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
||||||
|
|
||||||
|
const day = draft.value;
|
||||||
|
if (!day?.date || !day?.type || !day?.comment) {
|
||||||
|
throw new ExpensesValidationError({ status_code: 400, message: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_mileage = String(day.type).toUpperCase() === 'MILEAGE';
|
||||||
|
const new_payload = {
|
||||||
|
date: day.date,
|
||||||
|
type: day.type as ExpenseType,
|
||||||
|
comment: day.comment,
|
||||||
|
...(is_mileage && typeof day.mileage === 'number'
|
||||||
|
? { mileage: day.mileage }
|
||||||
|
: !is_mileage && typeof day.amount === 'number'
|
||||||
|
? { amount: day.amount }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
if(editing_old.value) {
|
||||||
|
await updateExpenseByDate(email, editing_old.value, new_payload as any);
|
||||||
|
editing_old.value = null;
|
||||||
|
} else {
|
||||||
|
await createExpenseByDate(email, new_payload as any);
|
||||||
|
}
|
||||||
|
await refreshFromServer();
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
||||||
|
status_code: 400,
|
||||||
|
message: String(err?.message || err)
|
||||||
|
});
|
||||||
|
emit('error', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = async (index: number) => {
|
||||||
|
try {
|
||||||
|
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
||||||
|
|
||||||
|
const item = items.value[index];
|
||||||
|
if (!item) return;
|
||||||
|
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
||||||
|
|
||||||
|
const old_payload: any = {
|
||||||
|
date: item.date,
|
||||||
|
type: item.type as ExpenseType,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
...(is_mileage && typeof item.mileage === 'number'
|
||||||
|
? { mileage: item.mileage }
|
||||||
|
: !is_mileage && typeof item.amount === 'number'
|
||||||
|
? { amount: item.amount }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await deleteExpenseByDate(email, old_payload as any);
|
||||||
|
await refreshFromServer();
|
||||||
|
} catch (err: any) {
|
||||||
|
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
||||||
|
status_code: 400, message: String(err?.message || err)
|
||||||
|
});
|
||||||
|
emit('error', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEdit = async (index: number) => {
|
||||||
|
if(is_readonly) return;
|
||||||
|
const item = items.value[index];
|
||||||
|
if(!item) return;
|
||||||
|
const old_amount = Number(item.amount) || 0;
|
||||||
|
editing_old.value = {
|
||||||
|
date: item.date,
|
||||||
|
type: item.type as ExpenseType,
|
||||||
|
amount: old_amount,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
is_approved: !!item.is_approved,
|
||||||
|
};
|
||||||
|
|
||||||
|
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
||||||
|
const next: Partial<TimesheetExpense> = {
|
||||||
|
date: item.date,
|
||||||
|
type: item.type,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
...(is_mileage
|
||||||
|
? (typeof item.mileage === 'number' ? { mileage: item.mileage } : {})
|
||||||
|
: (typeof item.amount === 'number' ? { amnount: item.amount } : {})),
|
||||||
|
};
|
||||||
|
(draft as any).value = next;
|
||||||
|
setType(item.type as ExpenseType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => emit('close');
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- header (title with totals)-->
|
||||||
|
<q-item class="row justify-between">
|
||||||
|
<q-item-label
|
||||||
|
header
|
||||||
|
class="text-h6 col-auto"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.title') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-section class="items-center col-auto">
|
||||||
|
<q-badge
|
||||||
|
lines="1"
|
||||||
|
class="q-pa-sm q-px-md"
|
||||||
|
:label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"
|
||||||
|
/>
|
||||||
|
<q-separator spaced />
|
||||||
|
<q-badge
|
||||||
|
lines="2"
|
||||||
|
class="q-pa-sm q-px-md"
|
||||||
|
:label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(1)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<ExpenseList
|
||||||
|
:items="items"
|
||||||
|
:is_readonly="is_readonly"
|
||||||
|
@remove="onRemove"
|
||||||
|
@edit="onEdit"
|
||||||
|
/>
|
||||||
|
<ExpenseForm
|
||||||
|
ref="formRef"
|
||||||
|
v-model:draft="draft"
|
||||||
|
v-model:files="files"
|
||||||
|
v-model:date-picker-open="is_open_date_picker"
|
||||||
|
:type_options="type_options"
|
||||||
|
:show_amount="showAmount"
|
||||||
|
:is_readonly="is_readonly"
|
||||||
|
:rules="rules"
|
||||||
|
:comment_max_length="COMMENT_MAX_LENGTH"
|
||||||
|
:set-type="setType"
|
||||||
|
@submit="onFormSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-separator spaced />
|
||||||
|
|
||||||
|
<div class="row col-auto justify-end">
|
||||||
|
<!-- close btn -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
class="q-mr-sm"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.cancel_button')"
|
||||||
|
@click="onClose"
|
||||||
|
/>
|
||||||
|
<!-- save btn -->
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
unelevated
|
||||||
|
push
|
||||||
|
:disable="is_readonly || items.length === 0"
|
||||||
|
:label="$t('timesheet.save_button')"
|
||||||
|
@click="onSave"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,109 +1,41 @@
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { normalizeObject } from "src/utils/normalize-object";
|
||||||
import { useExpenseItems } from "src/modules/timesheets/composables/use-expense-items";
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { normalizeExpense, validateExpenseUI } from "../../utils/expenses-validators";
|
import { expense_validation_schema, type ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
import type { ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
|
import type { Expense, UpsertExpense } from "src/modules/timesheets/models/expense.models";
|
||||||
import type { Expense, ExpenseType, PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
|
||||||
|
|
||||||
const { pay_period } = useTimesheetStore();
|
export const useExpensesApi = () => {
|
||||||
const expense_items = useExpenseItems(draft);
|
const expenses_store = useExpensesStore();
|
||||||
|
|
||||||
//PUT by employee_email, year and period no
|
const toUpsertExpense = (obj: {
|
||||||
export const putPayPeriodExpensesByEmployeeEmail = async (employee_email: string, expenses: Expense[]): Promise<PayPeriodExpenses> => {
|
old_expense?: Expense;
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
new_expense?: Expense;
|
||||||
const encoded_year = encodeURIComponent(String(pay_period.pay_year));
|
}) => obj as UpsertExpense;
|
||||||
const encoded_pay_period_no = encodeURIComponent(String(pay_period.pay_period_no));
|
|
||||||
|
|
||||||
const flat_expenses = expenses.map(expenses): [];
|
const createExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
|
const upsert_expense = toUpsertExpense({
|
||||||
const normalized: Expense[] = plain.map((exp) => {
|
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||||
const norm = normalizeExpense(exp as TimesheetExpense);
|
|
||||||
validateExpenseUI(norm, 'expense_item');
|
|
||||||
return normalizePayload(norm as unknown as ExpensePayload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const body: UpsertExpensesBody = {expenses: normalized};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await api.put<UpsertExpensesResponse>(
|
|
||||||
// `/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)
|
|
||||||
: [];
|
|
||||||
return {
|
|
||||||
...(data?.data ?? {
|
|
||||||
pay_period_no,
|
|
||||||
pay_year,
|
|
||||||
employee_email: employee_email,
|
|
||||||
is_approved: false,
|
|
||||||
expenses: [],
|
|
||||||
totals: {amount: 0, mileage: 0},
|
|
||||||
}),
|
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
}
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
||||||
};
|
|
||||||
|
|
||||||
export const postPayPeriodExpenses = async (
|
|
||||||
employee_email: string,
|
|
||||||
pay_year: number,
|
|
||||||
pay_period_no: number,
|
|
||||||
new_expenses: TimesheetExpense[]
|
|
||||||
): Promise<PayPeriodExpenses> => {
|
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
|
||||||
const encoded_year = encodeURIComponent(String(pay_year));
|
|
||||||
const encoded_pp = encodeURIComponent(String(pay_period_no));
|
|
||||||
|
|
||||||
const plain = Array.isArray(new_expenses) ? new_expenses.map(toPlain) : [];
|
|
||||||
const normalized: ExpensePayload[] = plain.map((exp) => {
|
|
||||||
const norm = normalizeExpense(exp as TimesheetExpense);
|
|
||||||
validateExpenseUI(norm, 'expense_item');
|
|
||||||
return normalizePayload(norm as unknown as ExpensePayload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const body: UpsertExpensesBody = { expenses: normalized };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await api.post<UpsertExpensesResponse>(
|
|
||||||
`/expenses/${encoded_email}/${encoded_year}/${encoded_pp}`,
|
|
||||||
body,
|
|
||||||
{ headers: { 'content-type': 'application/json' } }
|
|
||||||
);
|
|
||||||
const items = Array.isArray(data?.data?.expenses)
|
|
||||||
? data.data.expenses.map(normalizeExpense)
|
|
||||||
: [];
|
|
||||||
return {
|
|
||||||
...(data?.data ?? {
|
|
||||||
pay_period_no,
|
|
||||||
pay_year,
|
|
||||||
employee_email: employee_email,
|
|
||||||
is_approved: false,
|
|
||||||
expenses: [],
|
|
||||||
totals: { amount: 0, mileage: 0 },
|
|
||||||
}),
|
|
||||||
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 updateExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
|
const upsert_expense = toUpsertExpense({
|
||||||
|
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||||
|
new_expense: normalizeObject(expenses_store.current_expense, expense_validation_schema),
|
||||||
|
});
|
||||||
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteExpenseByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
|
const upsert_expense = toUpsertExpense({
|
||||||
|
old_expense: normalizeObject(expenses_store.initial_expense, expense_validation_schema),
|
||||||
|
});
|
||||||
|
await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, upsert_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createExpenseByEmployeeEmail,
|
||||||
|
updateExpenseByEmployeeEmail,
|
||||||
|
deleteExpenseByEmployeeEmail,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -42,7 +42,7 @@ export class ExpensesApiError extends ApiError {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expense_normalizer: Normalizer<Expense> = {
|
export const expense_validation_schema: Normalizer<Expense> = {
|
||||||
date: v => String(v ?? "1970-01-01").trim(),
|
date: v => String(v ?? "1970-01-01").trim(),
|
||||||
type: v => EXPENSE_TYPE.includes(v) ? v as ExpenseType : "EXPENSES",
|
type: v => EXPENSE_TYPE.includes(v) ? v as ExpenseType : "EXPENSES",
|
||||||
amount: v => typeof v === "number" ? v : undefined,
|
amount: v => typeof v === "number" ? v : undefined,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
export type PayPeriodLabel = {
|
export type PayPeriodLabel = {
|
||||||
start_date: string;
|
start_date: string;
|
||||||
end_date: string;
|
end_date: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||||
|
|
@ -5,9 +5,9 @@ export const normExpenseType = (type: unknown): string =>
|
||||||
String(type ?? '').trim().toUpperCase();
|
String(type ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
const icon_map: Record<string,string> = {
|
const icon_map: Record<string,string> = {
|
||||||
MILEAGE: 'time_to_leave',
|
MILEAGE: 'time_to_leave',
|
||||||
EXPENSES: 'receipt_long',
|
EXPENSES: 'receipt_long',
|
||||||
PER_DIEM: 'hotel',
|
PER_DIEM: 'hotel',
|
||||||
PRIME_GARDE: 'admin_panel_settings',
|
PRIME_GARDE: 'admin_panel_settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,11 +34,11 @@ export const computeExpenseTotals = (items: readonly Expense[]): ExpenseTotals =
|
||||||
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
|
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
|
||||||
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
|
|
||||||
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||||
|
|
||||||
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||||
|
|
||||||
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.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');
|
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
|
||||||
|
|
||||||
|
|
@ -51,14 +51,4 @@ export const makeExpenseRules = (t: (key: string) => string, max_comment_char: n
|
||||||
commentRequired,
|
commentRequired,
|
||||||
commentTooLong,
|
commentTooLong,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
//------------------ saving payload ------------------
|
|
||||||
export const buildExpenseSavePayload = (args: PayPeriodExpenses): PayPeriodExpenses => ({
|
|
||||||
pay_period_no: args.pay_period_no,
|
|
||||||
pay_year: args.pay_year,
|
|
||||||
employee_email: args.employee_email,
|
|
||||||
is_approved: args.is_approved ?? false,
|
|
||||||
expenses: args.expenses,
|
|
||||||
totals: computeExpenseTotals(args.expenses),
|
|
||||||
});
|
|
||||||
|
|
@ -1,127 +1,130 @@
|
||||||
import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "src/modules/timesheets/constants/expense.constants";
|
// import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "src/modules/timesheets/constants/expense.constants";
|
||||||
import { ExpensesValidationError } from "src/modules/timesheets/models/expense.validation";
|
// 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";
|
// import { type Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY, TYPES_WITH_MILEAGE_ONLY } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
//normalization helpers
|
// //normalization helpers
|
||||||
export const toNumOrUndefined = (value: unknown): number | undefined => {
|
// export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||||
if(value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) return undefined;
|
// if(value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) return undefined;
|
||||||
const num = Number(value);
|
// const num = Number(value);
|
||||||
|
|
||||||
return Number.isFinite(num) ? num : undefined;
|
// return Number.isFinite(num) ? num : undefined;
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const normalizeComment = (input?: string): string | undefined => {
|
// export const normalizeComment = (input?: string): string | undefined => {
|
||||||
if(typeof input === 'undefined' || input === null) return undefined;
|
// if(typeof input === 'undefined' || input === null) return undefined;
|
||||||
const trimmed = String(input).trim();
|
// const trimmed = String(input).trim();
|
||||||
|
|
||||||
return trimmed.length ? trimmed : undefined;
|
// return trimmed.length ? trimmed : undefined;
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
|
// export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
export const normalizeExpense = (expense: Expense): Expense => {
|
// export const normalizeExpense = (expense: Expense): Expense => {
|
||||||
const comment = normalizeComment(expense.comment);
|
// const comment = normalizeComment(expense.comment);
|
||||||
const amount = toNumOrUndefined(expense.amount);
|
// const amount = toNumOrUndefined(expense.amount);
|
||||||
const mileage = toNumOrUndefined(expense.mileage);
|
// const mileage = toNumOrUndefined(expense.mileage);
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
date: (expense.date ?? '').trim(),
|
// date: (expense.date ?? '').trim(),
|
||||||
type: normalizeType(expense.type),
|
// type: normalizeType(expense.type),
|
||||||
...(amount !== undefined ? { amount } : {}),
|
// ...(amount !== undefined ? { amount } : {}),
|
||||||
...(mileage !== undefined ? { mileage } : {}),
|
// ...(mileage !== undefined ? { mileage } : {}),
|
||||||
...(comment !== undefined ? { comment } : {}),
|
// ...(comment !== undefined ? { comment } : {}),
|
||||||
...(typeof expense.supervisor_comment === 'string' && expense.supervisor_comment.trim().length
|
// ...(typeof expense.supervisor_comment === 'string' && expense.supervisor_comment.trim().length
|
||||||
? { supervisor_comment: expense.supervisor_comment.trim() }
|
// ? { supervisor_comment: expense.supervisor_comment.trim() }
|
||||||
: {}),
|
// : {}),
|
||||||
...(typeof expense.is_approved === 'boolean' ? { is_approved: expense.is_approved }: {} ),
|
// ...(typeof expense.is_approved === 'boolean' ? { is_approved: expense.is_approved }: {} ),
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
//UI validation error messages
|
// //UI validation error messages
|
||||||
export const validateExpenseUI = (raw: Expense, label: string = 'expense'): void => {
|
// export const validateExpenseUI = (raw: Expense, label: string = 'expense'): void => {
|
||||||
const expense = normalizeExpense(raw);
|
// const expense = normalizeExpense(raw);
|
||||||
|
|
||||||
//Date input validation
|
// //Date input validation
|
||||||
if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
// if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.date_required_or_invalid',
|
// message: 'timesheet.expense.errors.date_required_or_invalid',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
//comment input validation
|
// //comment input validation
|
||||||
if(!expense.comment) {
|
// if(!expense.comment) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.comment_required',
|
// message: 'timesheet.expense.errors.comment_required',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if((expense.comment.length ?? 0) > COMMENT_MAX_LENGTH) {
|
// if((expense.comment.length ?? 0) > COMMENT_MAX_LENGTH) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.comment_too_long',
|
// message: 'timesheet.expense.errors.comment_too_long',
|
||||||
context: { [label]: { ...expense, comment_length: expense.comment?.length } },
|
// context: { [label]: { ...expense, comment_length: expense.comment?.length } },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
//amount input validation
|
// //amount input validation
|
||||||
if(expense.amount !== undefined && expense.amount <= 0) {
|
// if(expense.amount !== undefined && expense.amount <= 0) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.amount_must_be_positive',
|
// message: 'timesheet.expense.errors.amount_must_be_positive',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
//mileage input validation
|
// //mileage input validation
|
||||||
if(expense.mileage !== undefined && expense.mileage <= 0) {
|
// if(expense.mileage !== undefined && expense.mileage <= 0) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.mileage_must_be_positive',
|
// message: 'timesheet.expense.errors.mileage_must_be_positive',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
//cross origin amount/mileage validation
|
// //cross origin amount/mileage validation
|
||||||
const has_amount = typeof expense.amount === 'number' && expense.amount > 0;
|
// const has_amount = typeof expense.amount === 'number' && expense.amount > 0;
|
||||||
const has_mileage = typeof expense.mileage === 'number' && expense.mileage > 0;
|
// const has_mileage = typeof expense.mileage === 'number' && expense.mileage > 0;
|
||||||
|
|
||||||
if(has_amount === has_mileage) {
|
// if(has_amount === has_mileage) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.amount_xor_mileage',
|
// message: 'timesheet.expense.errors.amount_xor_mileage',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
//type constraint validation
|
// //type constraint validation
|
||||||
const type = expense.type as ExpenseType;
|
// 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({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.mileage_required_for_type',
|
// message: 'timesheet.expense.errors.mileage_required_for_type',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
if(TYPES_WITH_AMOUNT_ONLY.includes(type) && !has_amount) {
|
// if(TYPES_WITH_AMOUNT_ONLY.includes(type) && !has_amount) {
|
||||||
throw new ExpensesValidationError({
|
// throw new ExpensesValidationError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: 'timesheet.expense.errors.amount_required_for_type',
|
// message: 'timesheet.expense.errors.amount_required_for_type',
|
||||||
context: { [label]: expense },
|
// context: { [label]: expense },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
// <<<<<<< HEAD
|
||||||
|
|
||||||
//totals per pay-period
|
// //totals per pay-period
|
||||||
export const compute_expense_totals = (items: Expense[]) => items.reduce(
|
// export const compute_expense_totals = (items: Expense[]) => items.reduce(
|
||||||
(acc, raw) => {
|
// (acc, raw) => {
|
||||||
const expense = normalizeExpense(raw);
|
// const expense = normalizeExpense(raw);
|
||||||
if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
// if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
||||||
if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
// if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
||||||
return acc;
|
// return acc;
|
||||||
},
|
// },
|
||||||
{ amount: 0, mileage: 0 }
|
// { amount: 0, mileage: 0 }
|
||||||
);
|
// );
|
||||||
|
// =======
|
||||||
|
// >>>>>>> 1bdbe021facc85fb50cff6c60053278695df6bdc
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
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 { 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";
|
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||||
|
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||||
|
import { ExpensesApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
|
|
||||||
const { pay_period } = useTimesheetStore();
|
const { pay_period } = useTimesheetStore();
|
||||||
|
|
||||||
|
const encodeData = ( email: string, year: number, period_number: number ) => {
|
||||||
|
return { email: encodeURIComponent(email), year: encodeURIComponent(year), period_number: encodeURIComponent(period_number)};
|
||||||
|
}
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const is_open = ref(false);
|
const is_open = ref(false);
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const current_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
||||||
const current_expense = ref<Expense>(default_expense);
|
const current_expense = ref<Expense>(default_expense);
|
||||||
const initial_expense = ref<Expense>(default_expense);
|
const initial_expense = ref<Expense>(default_expense);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
@ -20,40 +25,26 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
error.value = e?.message || 'Unknown error';
|
error.value = e?.message || 'Unknown error';
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = async (employee_email: string) => {
|
const open = async (employee_email: string): Promise<void> => {
|
||||||
is_open.value = true;
|
is_open.value = true;
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
try {
|
await getPayPeriodExpensesByEmployeeEmail(employee_email);
|
||||||
const response = await getPayPeriodExpenses(employee_email, pay_period.pay_year, pay_period.pay_period_no,);
|
is_loading.value = false;
|
||||||
current_expenses.value = response;
|
|
||||||
initial_expenses.value = unwrapAndClone(response);
|
|
||||||
} catch (err) {
|
|
||||||
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<PayPeriodExpenses> => {
|
const getPayPeriodExpensesByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
const encoded_data = encodeData(employee_email, pay_period.pay_year, pay_period.pay_period_no);
|
||||||
const encoded_year = encodeURIComponent(String(pay_period.pay_year));
|
|
||||||
const encoded_pay_period_no = encodeURIComponent(String(pay_period.pay_period_no));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<PayPeriodExpenses>(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
|
const expenses = await timesheetService.getExpensesByPayPeriodAndEmployeeEmail(encoded_data.email, encoded_data.year, encoded_data.period_number);
|
||||||
|
pay_period_expenses.value = expenses;
|
||||||
const items = Array.isArray(data.expenses) ? data.expenses.map(normalizeExpense) : [];
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
expenses: items,
|
|
||||||
};
|
|
||||||
} catch(err:any) {
|
} catch(err:any) {
|
||||||
const status_code: number = err?.response?.status ?? 500;
|
const status_code: number = err?.response?.status ?? 500;
|
||||||
const data = err?.response?.data ?? {};
|
const data = err?.response?.data ?? {};
|
||||||
|
error.value = data.message || data.error || err.message;
|
||||||
|
|
||||||
throw new ExpensesApiError({
|
throw new ExpensesApiError({
|
||||||
status_code,
|
status_code,
|
||||||
error_code: data.error_code,
|
error_code: data.error_code,
|
||||||
|
|
@ -63,41 +54,16 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = () => {
|
const upsertOrDeleteExpensesByEmployeeEmail = async (employee_email: string, expenses: Expense[]): Promise<void> => {
|
||||||
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;
|
is_loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await putPayPeriodExpenses(employee_email, pay_period.pay_year, pay_period.pay_period_no, expenses);
|
const encoded_data = encodeData(employee_email, pay_period.pay_year, pay_period.pay_period_no);
|
||||||
pay_period_expenses.value = updated;
|
const payload = { is_approved: false, expenses };
|
||||||
|
|
||||||
|
const updated_expenses = await timesheetService.upsertOrDeleteExpensesByPayPeriodAndEmployeeEmail(encoded_data.email, encoded_data.year, encoded_data.period_number, payload);
|
||||||
|
pay_period_expenses.value.expenses = updated_expenses;
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorFrom(err);
|
setErrorFrom(err);
|
||||||
|
|
@ -114,11 +80,13 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
current_expenses,
|
pay_period_expenses,
|
||||||
initial_expenses,
|
current_expense,
|
||||||
|
initial_expense,
|
||||||
error,
|
error,
|
||||||
open,
|
open,
|
||||||
upsertOrDeletePayPeriodExpenseByEmployeeEmail,
|
getPayPeriodExpensesByEmployeeEmail,
|
||||||
|
upsertOrDeleteExpensesByEmployeeEmail,
|
||||||
close,
|
close,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue
Block a user