refactor(timesheet): complete overhaul of expense UI, fix functionality in approval module.

This commit is contained in:
Nic D. 2026-03-11 12:24:06 -04:00
parent 4ab271e66f
commit 9213a42d6b
5 changed files with 224 additions and 173 deletions

View File

@ -3,7 +3,7 @@
lang="ts" lang="ts"
> >
const model = defineModel<string | number | null | undefined>({ required: true }); const model = defineModel<string | number | null | undefined>({ required: true });
const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', {default: false}); const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', { default: false });
defineProps<{ defineProps<{
label?: string | undefined; label?: string | undefined;
@ -11,7 +11,12 @@
maxLength?: number; maxLength?: number;
noTopPadding?: boolean; noTopPadding?: boolean;
backgroundColor?: 'bg-secondary' | 'bg-dark'; backgroundColor?: 'bg-secondary' | 'bg-dark';
appendContent?: string | number;
}>(); }>();
defineOptions({
inheritAttrs: false
})
</script> </script>
<template> <template>
@ -21,6 +26,7 @@
> >
<q-input <q-input
v-model="model" v-model="model"
v-bind="$attrs"
dense dense
borderless borderless
color="accent" color="accent"
@ -43,27 +49,40 @@
</span> </span>
</template> </template>
<template #append v-if="requiresDatePicker"> <template
<q-btn #append
flat v-if="requiresDatePicker || !!appendContent"
dense >
size="lg" <div v-if="requiresDatePicker">
icon="calendar_month" <q-btn
color="accent" flat
@click="is_date_picker_open = true" dense
> size="lg"
<q-dialog icon="calendar_month"
v-model="is_date_picker_open" color="accent"
backdrop-filter="none" @click="is_date_picker_open = true"
> >
<q-date <q-dialog
v-model="model" v-model="is_date_picker_open"
mask="YYYY-MM-DD" backdrop-filter="none"
color="accent" >
@update:model-value="is_date_picker_open = false" <q-date
/> v-model="model"
</q-dialog> mask="YYYY-MM-DD"
</q-btn> color="accent"
@update:model-value="is_date_picker_open = false"
/>
</q-dialog>
</q-btn>
</div>
<div
v-if="!!appendContent"
class="self-end text-uppercase text-bold text-accent"
style="font-size: 0.8em;"
>
{{ appendContent }}
</div>
</template> </template>
</q-input> </q-input>
</div> </div>

View File

@ -197,6 +197,7 @@
<ExpenseDialogForm <ExpenseDialogForm
:email="timesheetStore.current_pay_period_overview?.email" :email="timesheetStore.current_pay_period_overview?.email"
:key="refreshKey" :key="refreshKey"
mode="approval"
@click-save="onClickSaveNewExpense" @click-save="onClickSaveNewExpense"
/> />
</q-expansion-item> </q-expansion-item>

View File

@ -117,6 +117,7 @@
<template> <template>
<div class="full-width"> <div class="full-width">
<LoadingOverlay v-model="timesheetStore.is_loading" /> <LoadingOverlay v-model="timesheetStore.is_loading" />
<q-table <q-table
dense dense
row-key="email" row-key="email"

View File

@ -21,6 +21,7 @@
const file = defineModel<File>('file'); const file = defineModel<File>('file');
const { email } = defineProps<{ const { email } = defineProps<{
email?: string | undefined; email?: string | undefined;
mode?: 'normal' | 'approval';
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'clickSave': [void]; 'clickSave': [void];
@ -91,156 +92,184 @@
flat flat
@submit.prevent="requestExpenseCreationOrUpdate" @submit.prevent="requestExpenseCreationOrUpdate"
> >
<div <div class="column rounded-5 q-pb-sm">
class="row justify-between rounded-5 q-pb-sm" <div class="row">
:class="expenseStore.mode === 'create' ? 'q-px-lg' : ''" <!-- date selection input -->
> <div class="row col items-center q-pl-sm">
<!-- date selection input --> <q-btn
<div class="row col items-center"> push
<q-btn dense
push icon="event"
dense color="accent"
icon="event" class="col-auto"
color="accent" @click="openDatePicker"
class="col-auto"
@click="openDatePicker"
/>
<q-dialog
v-model="isNavigatorOpen"
transition-show="jump-right"
transition-hide="jump-right"
class="z-top"
>
<q-date
v-model="expenseStore.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>
<TargoInput <q-dialog
v-model="expenseStore.current_expense.date" v-model="isNavigatorOpen"
no-top-padding transition-show="jump-right"
:label="$t('timesheet.expense.date')" transition-hide="jump-right"
background-color="bg-dark" class="z-top"
class="col" >
/> <q-date
</div> v-model="expenseStore.current_expense.date"
mask="YYYY-MM-DD"
<!-- expenses type selection --> event-color="accent"
<div class="col"> :options="date => date >= period_start_date && date <= period_end_date"
<q-select @update:model-value="closeDatePicker"
v-model="expenseSelected"
dense
borderless
color="accent"
label-color="white"
stack-label
label-slot
:options="expenseOptions"
hide-dropdown-icon
lazy-rules
no-error-icon
hide-bottom-space
options-selected-class="text-white text-bold bg-accent"
class="q-px-md rounded-5 inset-shadow"
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
popup-content-style="border: 1px solid var(--q-primary);"
menu-anchor="bottom middle"
menu-self="top middle"
:menu-offset="[0, 5]"
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
:rules="[rules.typeRequired]"
@update:model-value="option => expenseStore.current_expense.type = option.value"
>
<template #label>
<span
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
>
{{ $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">
<TargoInput
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
v-model.number="expenseStore.current_expense.amount"
no-top-padding
background-color="bg-dark"
:label="$t('timesheet.expense.amount')"
/>
<TargoInput
v-else
v-model="expenseStore.current_expense.mileage"
no-top-padding
background-color="bg-dark"
:label="$t('timesheet.expense.mileage')"
/>
</div>
<!-- employee comment input -->
<div class="col q-px-xs">
<TargoInput
v-model="expenseStore.current_expense.comment"
no-top-padding
background-color="bg-dark"
:max-length="COMMENT_MAX_LENGTH"
:label="$t('timesheet.expense.employee_comment')"
/>
</div>
<!-- import attach file section -->
<div class="col q-px-xs">
<q-file
v-model="file"
standout
dense
stack-label
label-slot
type="file"
accept="image/*"
>
<template #prepend>
<q-icon
name="attach_file"
size="sm"
color="accent"
/> />
</template> </q-dialog>
<template #label> <TargoInput
<span class="text-weight-bold text-accent text-uppercase text-caption"> v-model="expenseStore.current_expense.date"
{{ $t('timesheet.expense.hints.attach_file') }} no-top-padding
</span> :label="$t('timesheet.expense.date')"
</template> background-color="bg-dark"
</q-file> class="col"
/>
</div>
<!-- expenses type selection -->
<div class="col">
<q-select
v-model="expenseSelected"
dense
borderless
color="accent"
label-color="white"
stack-label
label-slot
:options="expenseOptions"
hide-dropdown-icon
lazy-rules
no-error-icon
hide-bottom-space
options-selected-class="text-white text-bold bg-accent"
class="q-px-md rounded-5 inset-shadow"
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
popup-content-style="border: 1px solid var(--q-primary);"
menu-anchor="bottom middle"
menu-self="top middle"
:menu-offset="[0, 5]"
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
:rules="[rules.typeRequired]"
@update:model-value="option => expenseStore.current_expense.type = option.value"
>
<template #label>
<span
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
>
{{ $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">
<TargoInput
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
v-model.number="expenseStore.current_expense.amount"
no-top-padding
background-color="bg-dark"
type="number"
input-class="text-right"
append-content=" $"
:label="$t('timesheet.expense.amount')"
/>
<TargoInput
v-else
v-model.number="expenseStore.current_expense.mileage"
no-top-padding
background-color="bg-dark"
type="number"
input-class="text-right"
append-content=" km"
:label="$t('timesheet.expense.mileage')"
/>
</div>
</div>
<div class="row q-pt-md">
<!-- employee comment input -->
<div class="col">
<TargoInput
v-model="expenseStore.current_expense.comment"
no-top-padding
background-color="bg-dark"
:max-length="COMMENT_MAX_LENGTH"
:label="$t('timesheet.expense.employee_comment')"
/>
</div>
<div
v-if="mode === 'approval'"
class="col"
>
<TargoInput
v-model="expenseStore.current_expense.supervisor_comment"
no-top-padding
background-color="bg-dark"
:max-length="COMMENT_MAX_LENGTH"
:label="$t('timesheet.expense.supervisor_comment')"
/>
</div>
<!-- import attach file section -->
<div class="col-3 q-px-sm">
<q-file
v-model="file"
dense
borderless
color="accent"
label-color="white"
stack-label
label-slot
type="file"
accept="image/*"
class="q-px-md rounded-5 inset-shadow"
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
>
<template #append>
<q-icon
name="attach_file"
size="sm"
color="accent"
/>
</template>
<template #label>
<span
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
>
{{ $t('timesheet.expense.hints.attach_file') }}
</span>
</template>
</q-file>
</div>
</div> </div>
</div> </div>
@ -266,8 +295,8 @@
scoped scoped
lang="css" lang="css"
> >
:deep(.q-field--dense.q-field--float .q-field__label) { :deep(.q-field--dense.q-field--float .q-field__label) {
transform: translate(-17px, -60%) scale(0.75) !important; transform: translate(-17px, -60%) scale(0.75) !important;
border-radius: 10px 10px 10px 0px; border-radius: 10px 10px 10px 0px;
} }
</style> </style>

View File

@ -244,6 +244,7 @@
<ExpenseDialogForm <ExpenseDialogForm
v-model="expense" v-model="expense"
:email="getEmployeeEmail()" :email="getEmployeeEmail()"
:mode="mode"
@click-save="hideUpdateForm" @click-save="hideUpdateForm"
/> />
</q-expansion-item> </q-expansion-item>