164 lines
5.2 KiB
Vue
164 lines
5.2 KiB
Vue
<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 { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
|
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 { EXPENSE_TYPE } from '../../types/expense.types';
|
|
import { ExpensesValidationError } from '../../types/expense-validation.interface';
|
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
|
|
|
/* eslint-disable */
|
|
|
|
const { t , locale } = useI18n();
|
|
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
|
|
|
//------------------ props ------------------
|
|
const props = 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(()=> !!props.is_approved);
|
|
|
|
const { state: is_open_date_picker } = useToggle();
|
|
const { draft, setType, reset, showAmount } = useExpenseDraft();
|
|
const { validateAnd } = useExpenseForm();
|
|
const { items, addFromDraft, removeAt, validateAll, payload } = useExpenseItems({
|
|
initial_expenses: props.initial_expenses,
|
|
draft,
|
|
is_approved: is_readonly,
|
|
});
|
|
const totals = computed(()=> computeExpenseTotals(items.value));
|
|
|
|
//------------------ actions ------------------
|
|
const onSave = () => {
|
|
try {
|
|
validateAll();
|
|
reset();
|
|
emit('save', buildExpenseSavePayload({
|
|
pay_period_no: props.pay_period_no,
|
|
pay_year: props.pay_year,
|
|
email: props.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 onFormSubmit = async () => {
|
|
try {
|
|
await validateAnd(async () => {
|
|
addFromDraft();
|
|
reset();
|
|
});
|
|
} catch (err: any) {
|
|
const e = err instanceof ExpensesValidationError
|
|
? err
|
|
: new ExpensesValidationError({
|
|
status_code: 400,
|
|
message: String(err?.message || err)
|
|
});
|
|
emit('error', e);
|
|
}
|
|
};
|
|
|
|
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="removeAt"
|
|
/>
|
|
<ExpenseForm
|
|
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> |