fix(timesheet): fix issue with expense not updating properly in approval module

Also rework expense item appearance in list to better divide space between components for visual clarity.
This commit is contained in:
Nic D 2026-03-10 15:52:40 -04:00
parent c6187305d9
commit 1271d1eb61
11 changed files with 411 additions and 377 deletions

View File

@ -66,10 +66,13 @@ export default defineConfig((ctx) => {
// polyfillModulePreload: true,
// distDir
extendViteConf: (_config) => ({
extendViteConf: (config) => ({
optimizeDeps: {
exclude: ['tesseract.js']
}
},
define: {
__VUE_PROD_DEVTOOLS__: config.mode !== 'production'
},
}),
// viteVuePluginOptions: {},

View File

@ -2,20 +2,21 @@
setup
lang="ts"
>
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
import { useExpensesStore } from 'src/stores/expense-store';
import { Expense } from 'src/modules/timesheets/models/expense.models';
import { date } from 'quasar';
// ========== state ========================================
@ -25,6 +26,7 @@
const timesheetApprovalApi = useTimesheetApprovalApi();
const shiftApi = useShiftApi();
const isDialogOpen = ref(false);
const refreshKey = ref(0);
// ========== computed ========================================
@ -34,9 +36,9 @@
t('shared.label.unlock') :
t('shared.label.lock')
);
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
);
@ -60,13 +62,22 @@
expenseStore.is_showing_create_form = false;
}
const onClickExpenseCreate = () => {
const onClickNewExpense = () => {
expenseStore.mode = 'create';
if (timesheetStore.pay_period)
expenseStore.current_expense = new Expense(timesheetStore.pay_period.period_start);
else
expenseStore.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
}
const onClickSaveNewExpense = () => {
expenseStore.is_showing_create_form = false;
}
const onShowDetailsDialog = () => {
isDialogOpen.value = true;
expenseStore.is_showing_create_form = false;
}
</script>
<template>
@ -77,7 +88,7 @@
transition-show="jump-down"
transition-hide="jump-down"
backdrop-filter="blur(6px)"
@show="isDialogOpen = true"
@show="onShowDetailsDialog"
@hide="isDialogOpen = false"
@before-hide="timesheetStore.getTimesheetOverviews"
>
@ -154,7 +165,10 @@
class="q-mx-md"
/>
<ExpenseDialogList mode="approval" />
<ExpenseDialogList
mode="approval"
:key="refreshKey + 1"
/>
<q-expansion-item
v-if="!isApproved"
@ -162,7 +176,7 @@
hide-expand-icon
:dense="!$q.platform.is.mobile"
group="expenses"
@show="onClickExpenseCreate()"
@show="onClickNewExpense()"
header-class="bg-accent text-white q-mx-md rounded-5"
>
<template #header>
@ -180,7 +194,11 @@
</div>
</template>
<ExpenseDialogForm :email="timesheetStore.current_pay_period_overview?.email"/>
<ExpenseDialogForm
:email="timesheetStore.current_pay_period_overview?.email"
:key="refreshKey"
@click-save="onClickSaveNewExpense"
/>
</q-expansion-item>
<q-separator

View File

@ -116,7 +116,7 @@ watch(selected_company, (company) => {
left-label
color="white"
dense
:label="$t(company.label)"
:label="company.label"
:val="company.value"
checked-icon="radio_button_checked"
unchecked-icon="radio_button_unchecked"

View File

@ -2,16 +2,14 @@
setup
lang="ts"
>
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, inject, onMounted, ref } from 'vue';
import { computed, onMounted, 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 ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
import { useAuthStore } from 'src/stores/auth-store';
// ================= state ======================
@ -19,293 +17,290 @@
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
const file = defineModel<File>('file');
const {email} = defineProps<{
const { email } = defineProps<{
email?: string | undefined;
}>();
const emit = defineEmits<{
'clickSave': [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 timesheetStore = useTimesheetStore();
const expenseStore = useExpensesStore();
const expensesApi = useExpensesApi();
const isNavigatorOpen = ref(false);
const rules = useExpenseRules(t);
const expense_options: ExpenseOption[] = [
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
const expenseOptions: ExpenseOption[] = [
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
{ 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<ExpenseOption | undefined>();
const employeeEmail = inject<string>('employeeEmail');
const expenseSelected = ref<ExpenseOption | undefined>();
// ================== computed ===================
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 period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
const period_end_date = computed(() => timesheetStore.pay_period?.period_end.replaceAll('-', '/') ?? '');
const isSaveDisabled = computed(() =>
JSON.stringify(expenseStore.current_expense) === JSON.stringify(expenseStore.initial_expense)
);
// ==================== method =======================
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 ?? '';
isNavigatorOpen.value = true;
if (expenseStore.current_expense.date === undefined) {
expenseStore.current_expense.date = timesheetStore.pay_period?.period_start ?? '';
}
};
const closeDatePicker = (date: string) => {
is_navigator_open.value = false;
expenses_store.current_expense.date = date;
isNavigatorOpen.value = false;
expenseStore.current_expense.date = date;
}
const requestExpenseCreationOrUpdate = async () => {
if (file.value)
await expenses_api.upsertExpense(
expenses_store.current_expense,
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
file.value
);
else
await expenses_api.upsertExpense(
expenses_store.current_expense,
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
);
expenses_store.is_showing_create_form = true;
expenses_store.mode = 'create';
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
const success = await expensesApi.upsertExpense(expenseStore.current_expense, email, file.value);
if (success) {
expenseStore.is_showing_create_form = false;
emit('clickSave');
}
};
onMounted(() => {
if (expense.value)
expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type);
expenseSelected.value = expenseOptions.find(expense_option => expense_option.value === expense.value.type);
else
expense_selected.value = expense_options[1];
expenseSelected.value = expenseOptions[0];
})
</script>
<template>
<q-form
v-if="!expenses_store.current_expense.is_approved"
flat
@submit.prevent="requestExpenseCreationOrUpdate"
<div
v-if="!expenseStore.current_expense.is_approved"
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' : ''"
<q-form
flat
@submit.prevent="requestExpenseCreationOrUpdate"
>
<!-- 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"
<div
class="row justify-between items-start rounded-5 q-pb-sm"
:class="expenseStore.mode === 'create' ? 'q-px-lg' : ''"
>
<!-- date selection input -->
<div class="col q-px-xs">
<q-input
v-model="expenseStore.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>
</template>
<template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.date') }}
</span>
</template>
</q-input>
</div>
<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>
</template>
<!-- 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 z-top"
options-selected-class="text-weight-bolder text-white bg-accent"
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 #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.date') }}
</span>
</template>
</q-input>
</div>
<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"
>
<!-- expenses type selection -->
<div class="col q-px-xs">
<q-select
v-model="expenseSelected"
standout
dense
:options="expenseOptions"
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 z-top"
options-selected-class="text-weight-bolder text-white bg-accent"
popup-content-style="border: 2px solid var(--q-accent)"
:rules="[rules.typeRequired]"
@update:model-value="option => expenseStore.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(expenseStore.current_expense?.type ?? 'EXPENSES')"
v-model.number="expenseStore.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="expenseStore.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="expenseStore.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="file"
standout
dense
stack-label
label-slot
type="file"
accept="image/*"
>
<template #prepend>
<q-icon
:name="scope.opt.icon"
size="xs"
class="col-auto q-mx-xs"
name="attach_file"
size="sm"
color="accent"
/>
<span
style="line-height: 1em;"
class="col-auto ellipsis text-uppercase"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
</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>
<!-- 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>
<div class="col row full-width items-center">
<q-space />
<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>
<q-btn
push
:disable="isSaveDisabled"
:color="isSaveDisabled ? 'grey-5' : 'accent'"
:icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
:label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
class="q-px-sm "
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
type="submit"
/>
</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="file"
standout
dense
stack-label
label-slot
type="file"
accept="image/*"
>
<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>
</q-form>
</div>
</template>
<style scoped>

View File

@ -4,10 +4,9 @@
>
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import { computed, ref } from 'vue';
import { computed, ref, toRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { date, Notify } from 'quasar';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
@ -23,44 +22,44 @@
}>();
const { t } = useI18n();
const expenses_api = useExpensesApi();
const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore();
const is_showing_update_form = ref(false);
const expensesApi = useExpensesApi();
const expenseStore = useExpensesStore();
const timesheetStore = useTimesheetStore();
const isShowingUpdateForm = ref(false);
// ========== computed ===================================
const attachmentButtonColor = computed(() => expense.value.attachment_name ?
(expense.value.is_approved ? 'white' : 'accent') :
const attachmentButtonColor = computed(() => expense.value.attachment_name ?
(expense.value.is_approved ? 'white' : 'accent') :
'grey-5');
// ===================== methods =========================
const requestExpenseDeletion = async () => {
await expenses_api.deleteExpenseById(expense.value.id);
await expensesApi.deleteExpenseById(expense.value.id);
}
const onClickExpenseUpdate = () => {
if (expense.value.is_approved) return;
expenses_store.mode = 'update';
expenses_store.current_expense = expense.value;
expenses_store.initial_expense = unwrapAndClone(expense.value);
expenseStore.mode = 'update';
expenseStore.current_expense = structuredClone(toRaw(expense.value));
expenseStore.initial_expense = structuredClone(toRaw(expense.value));
}
const onClickApproval = async () => {
expenses_store.current_expense = unwrapAndClone(expense.value);
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
expenseStore.current_expense = structuredClone(toRaw(expense.value));
expenseStore.current_expense.is_approved = !expenseStore.current_expense.is_approved;
const success = await expenses_store.upsertExpense(
expenses_store.current_expense,
timesheet_store.current_pay_period_overview?.email
const success = await expenseStore.upsertExpense(
expenseStore.current_expense,
timesheetStore.current_pay_period_overview?.email
);
if (success) {
expense.value.is_approved = !expense.value.is_approved;
} else {
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
expenseStore.current_expense.is_approved = !expenseStore.current_expense.is_approved;
Notify.create({
message: t('timesheet.errors.UPDATE_ERROR'),
color: "negative"
@ -68,16 +67,25 @@
}
}
const getEmployeeEmail = () => {
if (mode === 'approval')
return timesheetStore.current_pay_period_overview?.email;
}
const onClickAttachment = async () => {
expenses_store.isShowingAttachmentDialog = true;
await expenses_store.getAttachmentURL(expense.value.attachment_key);
console.log('image url: ', expenses_store.attachmentURL);
expenseStore.isShowingAttachmentDialog = true;
await expenseStore.getAttachmentURL(expense.value.attachment_key);
console.log('image url: ', expenseStore.attachmentURL);
}
const hideUpdateForm = () => {
isShowingUpdateForm.value = false;
}
</script>
<template>
<q-expansion-item
v-model="is_showing_update_form"
v-model="isShowingUpdateForm"
hide-expand-icon
dense
group="expenses"
@ -106,7 +114,7 @@
</div>
<!-- amount or mileage section -->
<div class="col column">
<div class="col-auto column q-pr-md">
<span
class="text-weight-bolder"
:class="expense.is_approved ? ' bg-accent text-white' : ''"
@ -122,81 +130,74 @@
:class="expense.is_approved ? ' bg-accent text-white' : ''"
>
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
month: 'short', day: 'numeric', weekday:
'long'
month: 'long', day: 'numeric', weekday: 'long'
}) }}
</span>
</div>
<q-separator vertical spaced class="q-my-xs"/>
<!-- attachment file icon -->
<div class="col row items-center justify-start">
<q-btn
push
:disable="expense.attachment_name === undefined"
:color="attachmentButtonColor"
:text-color="expense.is_approved ? 'accent' : 'white'"
class="col-auto q-px-sm q-mr-sm"
icon="attach_file"
@click.stop="onClickAttachment"
/>
<q-item-label class="col">
<span v-if="expense.attachment_name">
{{ expense.attachment_name }}
<!-- comments section -->
<div class="col column">
<div class="col row items-center">
<span
class="col-auto text-weight-medium text-accent text-uppercase q-pr-md"
style="font-size: 1.2em;"
>
{{ $t('timesheet.expense.employee_comment') }} :
</span>
<span
v-else
class="text-italic text-blue-grey-5 text-uppercase"
class="col"
:class="expense.is_approved ? ' bg-accent text-white' : ''"
>
{{ $t('timesheet.expense.no_attachment') }}
{{ expense.comment }}
</span>
</q-item-label>
</div>
<q-separator class="q-mr-md"/>
<div class="col row items-center">
<span
class="col-auto text-weight-medium text-accent text-uppercase q-pr-md"
style="font-size: 1.2em; "
:style="expense.supervisor_comment ? '' : 'filter: grayscale(1);'"
>
{{ $t('timesheet.expense.supervisor_comment') }} :
</span>
<span
class="col"
:class="expense.is_approved ? ' bg-accent text-white' : ''"
>
{{ expense.supervisor_comment }}
</span>
</div>
</div>
<!-- comment section -->
<div class="col column no-wrap">
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.employee_comment') }}
</span>
<span
class="col ellipsis"
:class="expense.is_approved ? ' bg-accent text-white' : ''"
style="font-size: 1em;"
<!-- attachment -->
<div class="col-auto">
<q-btn
flat
size="lg"
:disable="expense.attachment_name === undefined"
:color="attachmentButtonColor"
class="col-auto q-px-sm q-mr-sm"
:icon="expense.attachment_key ? 'image' : 'hide_image'"
@click.stop="onClickAttachment"
>
{{ expense.comment }}
</span>
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ expense.comment }}
</q-tooltip>
<q-tooltip
anchor="top middle"
self="center middle"
:offset="[0, 20]"
class="bg-accent text-uppercase text-weight-bold"
>
{{ expense.attachment_name ?? $t('timesheet.expense.no_attachment') }}
</q-tooltip>
</q-btn>
</div>
<!-- supervisor comment section -->
<div
v-if="expense.supervisor_comment"
class="col column"
>
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.supervisor_comment') }}
</span>
<span
class="col"
:class="expense.is_approved ? ' bg-accent text-white' : ''"
style="font-size: 1.3em;"
>
{{ expense.supervisor_comment }}
</span>
</div>
<!-- buttons -->
<div class="col-auto">
<q-btn
v-if="mode === 'approval'"
@ -240,6 +241,10 @@
</div>
</template>
<ExpenseDialogForm v-model="expense" />
<ExpenseDialogForm
v-model="expense"
:email="getEmployeeEmail()"
@click-save="hideUpdateForm"
/>
</q-expansion-item>
</template>

View File

@ -2,7 +2,7 @@
setup
lang="ts"
>
import { computed, inject } from 'vue';
import { computed } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
import ExpenseDialogListItemMobile from 'src/modules/timesheets/components/mobile/expense-dialog-list-item-mobile.vue';
@ -23,10 +23,6 @@
}
return [];
})
// ==================== methods ========================
inject( 'employeeEmail', mode === 'approval' ? timesheet_store.current_pay_period_overview?.email : undefined);
</script>
<template>
@ -45,7 +41,7 @@
</q-item-label>
<div
v-for="(expense, index) in expenses_list"
v-for="(_expense, index) in expenses_list"
:key="index"
>
<ExpenseDialogListItemMobile

View File

@ -11,10 +11,12 @@
import { date } from 'quasar';
import { useExpensesStore } from 'src/stores/expense-store';
import { Expense } from 'src/modules/timesheets/models/expense.models';
import { ref } from 'vue';
const expense_store = useExpensesStore();
const refreshKey = ref(0);
const { isApproved = false} = defineProps<{
const { isApproved = false } = defineProps<{
isApproved?: boolean;
}>();
@ -47,7 +49,7 @@
<q-card-section class="q-pa-none">
<ExpenseDialogHeader />
<ExpenseDialogList />
<ExpenseDialogList :key="refreshKey + 1" />
<q-expansion-item
v-if="!isApproved"
@ -56,6 +58,7 @@
:dense="!$q.platform.is.mobile"
group="expenses"
@show="onClickExpenseCreate()"
@after-hide="refreshKey += 1"
header-class="bg-accent text-white"
>
<template #header>
@ -66,7 +69,7 @@
class="col-auto"
:class="expense_store.is_showing_create_form ? 'invisible' : ''"
/>
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-xs q-mr-sm">
{{ $t('timesheet.expense.add_expense') }}
</span>
@ -74,8 +77,10 @@
</template>
<ExpenseDialogFormMobile v-if="$q.platform.is.mobile" />
<ExpenseDialogForm v-else />
<ExpenseDialogForm
v-else
:key="refreshKey"
/>
</q-expansion-item>
</q-card-section>
</q-card>

View File

@ -2,13 +2,13 @@
import { useExpensesStore } from "src/stores/expense-store";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { Expense } from "src/modules/timesheets/models/expense.models";
import { date } from "quasar";
import { date, Notify } from "quasar";
export const useExpensesApi = () => {
const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore();
const upsertExpense = async (expense: Expense, employee_email: string, file?: File): Promise<string> => {
const upsertExpense = async (expense: Expense, employee_email?: string, file?: File): Promise<boolean> => {
if (file) {
const attachmentKey = await expenses_store.uploadAttachment(file);
@ -19,23 +19,24 @@ export const useExpensesApi = () => {
expense.attachment_name = file.name;
}
}
console.log('employee email provided for expense: ', employee_email)
const success = await expenses_store.upsertExpense(expense, employee_email);
if (success) {
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
return 'SUCCESS';
}
return 'INVALID_EXPENSE';
const success = await expenses_store.upsertExpense(expense, employee_email);
if (success) {
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
return true;
}
return false;
};
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
const success = await expenses_store.deleteExpenseById(expense_id);
if (success) {
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
}
if (success)
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
};
return {

View File

@ -1,5 +1,5 @@
import { date } from "quasar";
import { computed, ref } from "vue";
import { ref } from "vue";
import { defineStore } from "pinia";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { Expense } from "src/modules/timesheets/models/expense.models";
@ -16,7 +16,6 @@ export const useExpensesStore = defineStore('expenses', () => {
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const isShowingAttachmentDialog = ref(false);
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
const open = (): void => {
is_open.value = true;
@ -103,7 +102,6 @@ export const useExpensesStore = defineStore('expenses', () => {
current_expense,
initial_expense,
isShowingAttachmentDialog,
is_save_disabled,
attachmentURL,
open,
upsertExpense,

View File

@ -1,4 +1,4 @@
import { Notify, date } from 'quasar';
import { Notify } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
@ -14,6 +14,7 @@ import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-ap
import { type FederalHoliday, TARGO_HOLIDAY_NAMES_FR } from 'src/modules/timesheets/models/federal-holidays.models';
import type { RouteNames } from 'src/router/router-constants';
import type { RouteRecordNameGeneric } from 'vue-router';
import { isBetweenDateStrings } from 'src/utils/date-and-time-utils';
export const useTimesheetStore = defineStore('timesheet', () => {
const { t } = useI18n();
@ -223,11 +224,7 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const pay_period_event: PayPeriodEvent = JSON.parse(event.data);
// abort notification if event date is not within pay period being currently viewed
const eventDate = date.extractDate(pay_period_event.date, 'YYYY-MM-DD');
const startDate = date.extractDate(pay_period.value!.period_start, 'YYYY-MM-DD');
const endDate = date.extractDate(pay_period.value!.period_end, 'YYYY-MM-DD');
if (!date.isBetweenDates(eventDate, startDate, endDate, { inclusiveFrom: true, inclusiveTo: true, onlyDate: true }))
if (!isBetweenDateStrings(pay_period_event.date, pay_period.value!.period_start, pay_period.value!.period_end))
return;
const overview = pay_period_overviews.value.find(overview => overview.email === pay_period_event.employee_email);

View File

@ -28,7 +28,7 @@ export const getHoursMinutesStringFromHoursFloat = (hours: number, minutes?: num
flatHours += 1;
flatMinutes = 0;
}
return `${flatHours}h${flatMinutes > 1 ? ' ' + flatMinutes : ''}`
}
@ -39,8 +39,24 @@ export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string
const [startHours, startMinutes] = startTime.split(':');
const [endHours, endMinutes] = endTime.split(':');
return {
return {
hours: Number(endHours) - Number(startHours),
minutes: Number(endMinutes) - Number(startMinutes),
}
}
export const isBetweenDateStrings = (evDate: string, start: string, end: string, inclusive: boolean = true) => {
const eventDate = date.extractDate(evDate, 'YYYY-MM-DD');
const startDate = date.extractDate(start, 'YYYY-MM-DD');
const endDate = date.extractDate(end, 'YYYY-MM-DD');
return date.isBetweenDates(
eventDate,
startDate,
endDate,
{
inclusiveFrom: inclusive,
inclusiveTo: inclusive,
onlyDate: true
});
}