314 lines
13 KiB
Vue
314 lines
13 KiB
Vue
<script
|
|
setup
|
|
lang="ts"
|
|
>
|
|
import { computed, ref, onMounted } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
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 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();
|
|
|
|
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 fit"
|
|
: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> |