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, // polyfillModulePreload: true,
// distDir // distDir
extendViteConf: (_config) => ({ extendViteConf: (config) => ({
optimizeDeps: { optimizeDeps: {
exclude: ['tesseract.js'] exclude: ['tesseract.js']
} },
define: {
__VUE_PROD_DEVTOOLS__: config.mode !== 'production'
},
}), }),
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},

View File

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

View File

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

View File

@ -2,16 +2,14 @@
setup setup
lang="ts" lang="ts"
> >
import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; 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 { useUiStore } from 'src/stores/ui-store';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; 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 { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
import { useAuthStore } from 'src/stores/auth-store';
// ================= state ====================== // ================= state ======================
@ -19,90 +17,86 @@
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) }) const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
const file = defineModel<File>('file'); const file = defineModel<File>('file');
const { email } = defineProps<{ const { email } = defineProps<{
email?: string | undefined; email?: string | undefined;
}>(); }>();
const emit = defineEmits<{
'clickSave': [void];
}>();
const { t } = useI18n(); const { t } = useI18n();
const ui_store = useUiStore(); const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheetStore = useTimesheetStore();
const expenses_store = useExpensesStore(); const expenseStore = useExpensesStore();
const auth_store = useAuthStore(); const expensesApi = useExpensesApi();
const expenses_api = useExpensesApi(); const isNavigatorOpen = ref(false);
const is_navigator_open = ref(false);
const rules = useExpenseRules(t); const rules = useExpenseRules(t);
const expense_options: ExpenseOption[] = [ const expenseOptions: 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.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.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') }, { label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
] ]
const expense_selected = ref<ExpenseOption | undefined>(); const expenseSelected = ref<ExpenseOption | undefined>();
const employeeEmail = inject<string>('employeeEmail');
// ================== computed =================== // ================== computed ===================
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? ''); const period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.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 ======================= // ==================== method =======================
const openDatePicker = () => { const openDatePicker = () => {
is_navigator_open.value = true; isNavigatorOpen.value = true;
if (expenses_store.current_expense.date === undefined) { if (expenseStore.current_expense.date === undefined) {
expenses_store.current_expense.date = timesheet_store.pay_period?.period_start ?? ''; expenseStore.current_expense.date = timesheetStore.pay_period?.period_start ?? '';
} }
}; };
const closeDatePicker = (date: string) => { const closeDatePicker = (date: string) => {
is_navigator_open.value = false; isNavigatorOpen.value = false;
expenses_store.current_expense.date = date; expenseStore.current_expense.date = date;
} }
const requestExpenseCreationOrUpdate = async () => { const requestExpenseCreationOrUpdate = async () => {
if (file.value) const success = await expensesApi.upsertExpense(expenseStore.current_expense, email, 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; if (success) {
expenses_store.mode = 'create'; expenseStore.is_showing_create_form = false;
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')); emit('clickSave');
}
}; };
onMounted(() => { onMounted(() => {
if (expense.value) 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 else
expense_selected.value = expense_options[1]; expenseSelected.value = expenseOptions[0];
}) })
</script> </script>
<template> <template>
<div
v-if="!expenseStore.current_expense.is_approved"
class="full-width q-mt-md q-px-md"
>
<q-form <q-form
v-if="!expenses_store.current_expense.is_approved"
flat flat
@submit.prevent="requestExpenseCreationOrUpdate" @submit.prevent="requestExpenseCreationOrUpdate"
class="full-width q-mt-md q-px-md"
> >
<div <div
class="row justify-between items-start rounded-5 q-pb-sm" class="row justify-between items-start rounded-5 q-pb-sm"
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''" :class="expenseStore.mode === 'create' ? 'q-px-lg' : ''"
> >
<!-- date selection input --> <!-- date selection input -->
<div class="col q-px-xs"> <div class="col q-px-xs">
<q-input <q-input
v-model="expenses_store.current_expense.date" v-model="expenseStore.current_expense.date"
dense dense
standout standout
readonly readonly
@ -123,13 +117,13 @@
/> />
<q-dialog <q-dialog
v-model="is_navigator_open" v-model="isNavigatorOpen"
transition-show="jump-right" transition-show="jump-right"
transition-hide="jump-right" transition-hide="jump-right"
class="z-top" class="z-top"
> >
<q-date <q-date
v-model="expenses_store.current_expense.date" v-model="expenseStore.current_expense.date"
mask="YYYY-MM-DD" mask="YYYY-MM-DD"
event-color="accent" event-color="accent"
:options="date => date >= period_start_date && date <= period_end_date" :options="date => date >= period_start_date && date <= period_end_date"
@ -149,10 +143,10 @@
<!-- expenses type selection --> <!-- expenses type selection -->
<div class="col q-px-xs"> <div class="col q-px-xs">
<q-select <q-select
v-model="expense_selected" v-model="expenseSelected"
standout standout
dense dense
:options="expense_options" :options="expenseOptions"
hide-dropdown-icon hide-dropdown-icon
stack-label stack-label
label-slot label-slot
@ -165,7 +159,7 @@
options-selected-class="text-weight-bolder text-white bg-accent" options-selected-class="text-weight-bolder text-white bg-accent"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
:rules="[rules.typeRequired]" :rules="[rules.typeRequired]"
@update:model-value="option => expenses_store.current_expense.type = option.value" @update:model-value="option => expenseStore.current_expense.type = option.value"
> >
<template #label> <template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption"> <span class="text-weight-bold text-accent text-uppercase text-caption">
@ -196,8 +190,8 @@
<!-- amount input --> <!-- amount input -->
<div class="col q-px-xs"> <div class="col q-px-xs">
<q-input <q-input
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')" v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
v-model="expenses_store.current_expense.amount" v-model.number="expenseStore.current_expense.amount"
standout standout
dense dense
label-slot label-slot
@ -219,7 +213,7 @@
<q-input <q-input
v-else v-else
v-model="expenses_store.current_expense.mileage" v-model="expenseStore.current_expense.mileage"
standout standout
dense dense
label-slot label-slot
@ -243,7 +237,7 @@
<!-- employee comment input --> <!-- employee comment input -->
<div class="col q-px-xs"> <div class="col q-px-xs">
<q-input <q-input
v-model="expenses_store.current_expense.comment" v-model="expenseStore.current_expense.comment"
standout standout
dense dense
stack-label stack-label
@ -296,16 +290,17 @@
<q-btn <q-btn
push push
:disable="expenses_store.is_save_disabled" :disable="isSaveDisabled"
:color="expenses_store.is_save_disabled ? 'grey-5' : 'accent'" :color="isSaveDisabled ? 'grey-5' : 'accent'"
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'" :icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')" :label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
class="q-px-sm " class="q-px-sm "
:class="expenses_store.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'" :class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
type="submit" type="submit"
/> />
</div> </div>
</q-form> </q-form>
</div>
</template> </template>
<style scoped> <style scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,3 +44,19 @@ export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string
minutes: Number(endMinutes) - Number(startMinutes), 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
});
}