Now able to upload and then view images attached to expenses. Will need to check if further changes need to be made to updating expenses. Minor structural changes here and there.
317 lines
13 KiB
Vue
317 lines
13 KiB
Vue
<script
|
|
setup
|
|
lang="ts"
|
|
>
|
|
import { computed, ref, onMounted } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useUiStore } from 'src/stores/ui-store';
|
|
import { useExpensesStore } from 'src/stores/expense-store';
|
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
|
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
|
import { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
|
import { useAuthStore } from 'src/stores/auth-store';
|
|
|
|
const COMMENT_MAX_LENGTH = 280;
|
|
|
|
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
|
const file = defineModel<File | undefined>('file');
|
|
|
|
const { employeeEmail } = defineProps<{
|
|
employeeEmail?: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
onUpdateClicked: [void];
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
const ui_store = useUiStore();
|
|
const timesheet_store = useTimesheetStore();
|
|
const expenses_store = useExpensesStore();
|
|
const auth_store = useAuthStore();
|
|
const expenses_api = useExpensesApi();
|
|
const is_navigator_open = ref(false);
|
|
const is_showing_comment_dialog_mobile = ref(false);
|
|
|
|
const rules = useExpenseRules(t);
|
|
|
|
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
|
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
|
|
|
const expense_options: ExpenseOption[] = [
|
|
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
|
|
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
|
|
{ label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
|
|
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
|
|
]
|
|
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
|
|
|
const openDatePicker = () => {
|
|
is_navigator_open.value = true;
|
|
if (timesheet_store.pay_period !== undefined) {
|
|
expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
|
|
}
|
|
};
|
|
|
|
const requestExpenseCreationOrUpdate = async () => {
|
|
if (file.value)
|
|
await expenses_api.upsertExpense(
|
|
expenses_store.current_expense,
|
|
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
|
file.value
|
|
);
|
|
else
|
|
await expenses_api.upsertExpense(
|
|
expenses_store.current_expense,
|
|
employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
|
);
|
|
|
|
emit('onUpdateClicked');
|
|
};
|
|
|
|
onMounted(() => {
|
|
if (expense.value)
|
|
expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type);
|
|
else
|
|
expense_selected.value = expense_options[1];
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<q-form
|
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
|
flat
|
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
|
class="column full-width"
|
|
>
|
|
<div
|
|
class="col column items-start rounded-5 q-pb-sm"
|
|
:class="expenses_store.is_showing_create_form ? 'q-px-md' : 'q-px-sm'"
|
|
>
|
|
<!-- date selection input -->
|
|
<div class="col-auto row q-my-xs full-width">
|
|
<q-input
|
|
v-model="expenses_store.current_expense.date"
|
|
dense
|
|
outlined
|
|
readonly
|
|
stack-label
|
|
hide-bottom-space
|
|
color="primary"
|
|
class="col-auto full-width"
|
|
input-class="text-weight-medium"
|
|
input-style="font-size: 1em;"
|
|
:label="$t('timesheet.expense.date')"
|
|
>
|
|
<template #prepend>
|
|
<q-btn
|
|
push
|
|
dense
|
|
icon="event"
|
|
color="accent"
|
|
class="q-mr-sm"
|
|
@click="openDatePicker"
|
|
/>
|
|
|
|
<q-dialog
|
|
v-model="is_navigator_open"
|
|
transition-show="jump-right"
|
|
transition-hide="jump-right"
|
|
class="z-top"
|
|
>
|
|
<q-date
|
|
v-model="expenses_store.current_expense.date"
|
|
mask="YYYY-MM-DD"
|
|
event-color="accent"
|
|
:options="date => date >= period_start_date && date <= period_end_date"
|
|
@update:model-value="is_navigator_open = false"
|
|
/>
|
|
</q-dialog>
|
|
</template>
|
|
|
|
<template #label>
|
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
{{ $t('timesheet.expense.date') }}
|
|
</span>
|
|
</template>
|
|
</q-input>
|
|
</div>
|
|
|
|
<!-- expenses type selection -->
|
|
<div class="col-auto row q-my-xs full-width">
|
|
<q-select
|
|
v-model="expense_selected"
|
|
standout="bg-blue-grey-9 text-white"
|
|
dense
|
|
:options="expense_options"
|
|
hide-dropdown-icon
|
|
stack-label
|
|
label-slot
|
|
hide-bottom-space
|
|
class="col"
|
|
color="primary"
|
|
behavior="menu"
|
|
:menu-offset="[0, 5]"
|
|
:label="$t('timesheet.expense.type')"
|
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5 z-top"
|
|
popup-content-style="border: 3px solid var(--q-accent)"
|
|
options-selected-class="text-weight-bolder text-white bg-accent"
|
|
:rules="[rules.typeRequired]"
|
|
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
|
>
|
|
<template #label>
|
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
{{ $t('timesheet.expense.type') }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #selected-item="scope">
|
|
<div
|
|
class="row items-center text-weight-bold q-ma-none q-pa-none full-width"
|
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
|
:tabindex="scope.tabindex"
|
|
>
|
|
<q-icon
|
|
:name="scope.opt.icon"
|
|
size="sm"
|
|
class="col-auto q-mx-xs"
|
|
/>
|
|
<span class="col text-uppercase ellipsis">{{ scope.opt.label }}</span>
|
|
</div>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
|
|
<!-- amount and comment row -->
|
|
<div class="col row q-my-xs full-width">
|
|
<!-- amount input -->
|
|
<div class="col q-mr-sm">
|
|
<q-input
|
|
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')"
|
|
v-model.number="expenses_store.current_expense.amount"
|
|
key="amount"
|
|
standout="bg-blue-grey-9"
|
|
dense
|
|
label-slot
|
|
stack-label
|
|
hide-bottom-space
|
|
suffix="$"
|
|
color="primary"
|
|
input-class="text-right text-weight-bold"
|
|
:input-style="'font-size: 1.2em;'"
|
|
lazy-rules="ondemand"
|
|
:rules="[rules.amountRequired]"
|
|
>
|
|
<template #label>
|
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
{{ $t('timesheet.expense.amount') }}
|
|
</span>
|
|
</template>
|
|
</q-input>
|
|
|
|
<!-- mileage input -->
|
|
<q-input
|
|
v-else
|
|
v-model.number="expenses_store.current_expense.mileage"
|
|
key="mileage"
|
|
standout="bg-blue-grey-9"
|
|
input-class="text-right"
|
|
dense
|
|
stack-label
|
|
clearable
|
|
hide-bottom-space
|
|
color="primary"
|
|
label-slot
|
|
suffix="km"
|
|
lazy-rules="ondemand"
|
|
:rules="[rules.mileageRequired]"
|
|
>
|
|
<template #label>
|
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
{{ $t('timesheet.expense.mileage') }}
|
|
</span>
|
|
</template>
|
|
</q-input>
|
|
</div>
|
|
|
|
<!-- employee comment input -->
|
|
<q-btn
|
|
push
|
|
color="accent"
|
|
:icon="expenses_store.current_expense.comment ? 'chat' : 'chat_bubble_outline'"
|
|
@click="is_showing_comment_dialog_mobile = true"
|
|
class="col-auto"
|
|
/>
|
|
|
|
<q-dialog
|
|
v-model="is_showing_comment_dialog_mobile"
|
|
class="z-top"
|
|
>
|
|
<q-card class="full-width bg-primary rounded-10">
|
|
<q-card-section class="q-pa-none">
|
|
<span
|
|
class="text-weight-bold text-accent text-uppercase text-caption q-mx-md"
|
|
style="font-size: 1.2em;"
|
|
>
|
|
{{ $t('timesheet.expense.employee_comment') }}
|
|
</span>
|
|
</q-card-section>
|
|
|
|
<q-card-section class="q-pa-none bg-primary rounded-10">
|
|
<q-input
|
|
v-model="expenses_store.current_expense.comment"
|
|
filled
|
|
dense
|
|
hide-bottom-space
|
|
color="accent"
|
|
type="textarea"
|
|
:maxlength="COMMENT_MAX_LENGTH"
|
|
class="bg-white no-border"
|
|
>
|
|
</q-input>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
</div>
|
|
|
|
<!-- import attach file section -->
|
|
<q-file
|
|
v-model="file"
|
|
standout="bg-blue-grey-9"
|
|
stack-label
|
|
:label="$t('timesheet.expense.hints.attach_file')"
|
|
class="col full-width q-my-xs"
|
|
>
|
|
<template #prepend>
|
|
<q-icon
|
|
name="attach_file"
|
|
size="sm"
|
|
color="accent"
|
|
/>
|
|
</template>
|
|
|
|
<template #label>
|
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
|
</span>
|
|
</template>
|
|
</q-file>
|
|
</div>
|
|
|
|
<div
|
|
class="col row full-width items-center"
|
|
:class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''"
|
|
>
|
|
<q-btn
|
|
push
|
|
color="accent"
|
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
|
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
|
class="q-px-sm full-width"
|
|
:class="expenses_store.mode === 'create' ? '' : 'q-mb-sm'"
|
|
type="submit"
|
|
/>
|
|
</div>
|
|
</q-form>
|
|
</template> |