targo-frontend/src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue
Nic D 505fdf0e62 feat(expense): finalize implementation of S3 in expenses
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.
2026-02-11 07:52:07 -05:00

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>