targo-frontend/src/modules/timesheets/components/expense-dialog-form.vue

289 lines
11 KiB
Vue

<script
setup
lang="ts"
>
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
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 ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
interface ExpenseOption {
label: string;
value: ExpenseType;
icon: string;
}
const { t } = useI18n();
const ui_store = useUiStore();
const timesheet_store = useTimesheetStore();
const expenses_store = useExpensesStore();
const expenses_api = useExpensesApi();
const files = defineModel<File[] | null>('files');
const is_navigator_open = ref(false);
const COMMENT_MAX_LENGTH = 280;
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 (expenses_store.current_expense.date === undefined) {
expenses_store.current_expense.date = timesheet_store.pay_period?.period_start ?? '';
}
};
const closeDatePicker = (date: string) => {
is_navigator_open.value = false;
expenses_store.current_expense.date = date;
}
const requestExpenseCreationOrUpdate = async () => {
await expenses_api.upsertExpense(expenses_store.current_expense);
expenses_store.is_showing_create_form = true;
expenses_store.mode = 'create';
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
};
</script>
<template>
<q-form
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
flat
@submit.prevent="requestExpenseCreationOrUpdate"
class="full-width q-mt-md q-px-md"
>
<div
class="row justify-between items-start rounded-5 q-pb-sm"
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
>
<!-- date selection input -->
<div class="col q-px-xs">
<q-input
v-model="expenses_store.current_expense.date"
dense
standout
readonly
stack-label
color="primary"
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="closeDatePicker"
/>
</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 q-px-xs">
<q-select
v-model="expense_selected"
standout
dense
:options="expense_options"
hide-dropdown-icon
stack-label
label-slot
color="primary"
:label="$t('timesheet.expense.type')"
:menu-offset="[0, 10]"
menu-anchor="bottom middle"
menu-self="top middle"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-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 no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
:tabindex="scope.tabindex"
>
<q-icon
:name="scope.opt.icon"
size="xs"
class="col-auto q-mx-xs"
/>
<span
style="line-height: 1em;"
class="col-auto ellipsis text-uppercase"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
</div>
<!-- amount input -->
<div class="col q-px-xs">
<q-input
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')"
v-model="expenses_store.current_expense.amount"
standout
dense
label-slot
stack-label
suffix="$"
type="number"
color="primary"
input-class="text-right text-weight-medium"
input-style="font-size: 1.3em;"
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>
<q-input
v-else
v-model="expenses_store.current_expense.mileage"
standout
dense
label-slot
stack-label
suffix="km"
type="number"
input-class="text-right text-weight-medium"
input-style="font-size: 1.3em;"
color="primary"
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 -->
<div class="col q-px-xs">
<q-input
v-model="expenses_store.current_expense.comment"
standout
dense
stack-label
label-slot
color="primary"
input-class="text-weight-medium"
input-style="font-size: 1.3em;"
:maxlength="COMMENT_MAX_LENGTH"
lazy-rules="ondemand"
:rules="[rules.commentRequired]"
>
<template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.employee_comment') }}
</span>
</template>
</q-input>
</div>
<!-- import attach file section -->
<div class="col q-px-xs">
<q-file
v-model="files"
standout
dense
use-chips
multiple
stack-label
label-slot
>
<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>
<div class="col row full-width items-center">
<q-space />
<q-btn
push
:disable="expenses_store.is_save_disabled"
:color="expenses_store.is_save_disabled ? 'grey-5' : '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 "
:class="expenses_store.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
type="submit"
/>
</div>
</q-form>
</template>
<style scoped>
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
border: transparent;
}
</style>