refactor(timesheet): redo expense dialog to work with Expansion Items, improve UI/UX

This commit is contained in:
Nicolas Drolet 2025-12-16 11:06:59 -05:00
parent 1b4e59b292
commit faa239784b
20 changed files with 117 additions and 197 deletions

View File

@ -52,4 +52,16 @@ body.body--dark {
.q-btn--push:active::before { .q-btn--push:active::before {
border-bottom-width: 1px; border-bottom-width: 1px;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
} }

View File

@ -24,7 +24,7 @@
auth_store.logout(); auth_store.logout();
router.push({ name: 'login' }).catch(err => { router.push({ name: 'login' }).catch(err => {
console.log('could not log you out: ', err); console.error('could not log you out: ', err);
}) })
} }
</script> </script>

View File

@ -38,10 +38,7 @@ export const useEmployeeListApi = () => {
weekday.is_error = isShiftOverlap(weekday.shifts); weekday.is_error = isShiftOverlap(weekday.shifts);
} }
console.log('current preset: ', preset);
if (preset.weekdays.some(weekday => weekday.is_error)) { if (preset.weekdays.some(weekday => weekday.is_error)) {
console.log('overlap!');
return; return;
} }

View File

@ -10,7 +10,6 @@
const setDisplayLanguage = (locale: MessageLanguages) => { const setDisplayLanguage = (locale: MessageLanguages) => {
if (ui_store.user_preferences !== undefined) { if (ui_store.user_preferences !== undefined) {
ui_store.user_preferences.display_language = locale; ui_store.user_preferences.display_language = locale;
console.log('triggered language change: ', ui_store.user_preferences.display_language);
} }
} }
</script> </script>

View File

@ -2,15 +2,15 @@
setup setup
lang="ts" lang="ts"
> >
import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
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 { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; import { Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
interface ExpenseOption { interface ExpenseOption {
label: string; label: string;
@ -26,7 +26,6 @@
const files = defineModel<File[] | null>('files'); const files = defineModel<File[] | null>('files');
const is_navigator_open = ref(false); const is_navigator_open = ref(false);
const is_initial_expense = ref(true);
const COMMENT_MAX_LENGTH = 280; const COMMENT_MAX_LENGTH = 280;
const rules = useExpenseRules(t); const rules = useExpenseRules(t);
@ -40,15 +39,9 @@
{ 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(expense_options.find(expense => expense.value == expenses_store.current_expense.type)); const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
const expense_monetary_string = ref(expenses_store.current_expense.amount.toString());
const emit = defineEmits<{
'onClickUpdateCancel': [void];
'onClickSaveUpdates': [void];
}>();
const openDatePicker = () => { const openDatePicker = () => {
is_navigator_open.value = true; is_navigator_open.value = true;
if (expenses_store.current_expense.date === undefined) { if (expenses_store.current_expense.date === undefined) {
@ -64,21 +57,10 @@
const requestExpenseCreationOrUpdate = async () => { const requestExpenseCreationOrUpdate = async () => {
await expenses_api.upsertExpense(expenses_store.current_expense); await expenses_api.upsertExpense(expenses_store.current_expense);
if (expenses_store.current_expense.id) { expenses_store.is_showing_create_form = true;
emit('onClickSaveUpdates'); expenses_store.mode = 'create';
} expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
}; };
const saveAndConvert = () => {
expenses_store.current_expense.amount = convertToMonetaryAmount(expense_monetary_string.value);
expense_monetary_string.value = expenses_store.current_expense.amount.toString();
console.log('current expense amount: ', expenses_store.current_expense.amount);
}
watch(expenses_store.current_expense, () => {
is_initial_expense.value = deepEqual(expenses_store.current_expense, expenses_store.initial_expense);
});
</script> </script>
<template> <template>
@ -87,14 +69,8 @@
:key="expenses_store.current_expense.id" :key="expenses_store.current_expense.id"
flat flat
@submit.prevent="requestExpenseCreationOrUpdate" @submit.prevent="requestExpenseCreationOrUpdate"
class="full-width" class="full-width q-mt-md q-px-md"
> >
<div
class="text-uppercase text-weight-medium q-pt-sm q-ma-sm"
:class="expenses_store.mode === 'create' ? 'q-px-lg' : 'invisible'"
>
{{ $t('timesheet.expense.add_expense') }}
</div>
<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="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
@ -195,18 +171,18 @@
<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(expenses_store.current_expense?.type ?? 'EXPENSES')"
v-model="expense_monetary_string" v-model="expenses_store.current_expense.amount"
standout standout
dense dense
label-slot label-slot
stack-label stack-label
suffix="$" suffix="$"
type="number"
color="primary" color="primary"
input-class="text-right text-weight-bold" input-class="text-right text-weight-medium"
:input-style="'font-size: 1.2em;'" input-style="font-size: 1.3em;"
lazy-rules="ondemand" lazy-rules="ondemand"
:rules="[rules.amountRequired]" :rules="[rules.amountRequired]"
@blur="saveAndConvert()"
> >
<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">
@ -217,19 +193,18 @@
<q-input <q-input
v-else v-else
key="mileage"
v-model="expenses_store.current_expense.mileage" v-model="expenses_store.current_expense.mileage"
standout standout
dense dense
stack-label
clearable
label-slot label-slot
input-class="text-right" stack-label
color="primary"
suffix="km" suffix="km"
type="number"
input-class="text-right text-weight-medium"
input-style="font-size: 1.3em;"
color="primary"
lazy-rules="ondemand" lazy-rules="ondemand"
:rules="[rules.mileageRequired]" :rules="[rules.mileageRequired]"
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expense_monetary_string)"
> >
<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">
@ -248,7 +223,8 @@
stack-label stack-label
label-slot label-slot
color="primary" color="primary"
type="text" input-class="text-weight-medium"
input-style="font-size: 1.3em;"
:maxlength="COMMENT_MAX_LENGTH" :maxlength="COMMENT_MAX_LENGTH"
lazy-rules="ondemand" lazy-rules="ondemand"
:rules="[rules.commentRequired]" :rules="[rules.commentRequired]"
@ -292,21 +268,10 @@
<div class="col row full-width items-center"> <div class="col row full-width items-center">
<q-space /> <q-space />
<q-btn
v-if="expenses_store.mode === 'update'"
flat
dense
class="col-auto q-ml-sm"
icon="clear"
color="negative"
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
@click="$emit('onClickUpdateCancel')"
/>
<q-btn <q-btn
push push
:disable="is_initial_expense" :disable="expenses_store.is_save_disabled"
:color="is_initial_expense ? 'grey-5' : 'accent'" :color="expenses_store.is_save_disabled ? 'grey-5' : 'accent'"
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'" :icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')" :label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
class="q-px-sm " class="q-px-sm "

View File

@ -22,8 +22,8 @@
<template> <template>
<div class="column items-center q-pa-none"> <div class="column items-center q-pa-none">
<div class="col row full-width"> <div class="col row full-width bg-primary">
<q-item-label class="col text-h6 text-weight-bolder text-uppercase q-py-sm q-px-md"> <q-item-label class="col text-h6 text-weight-bolder text-uppercase text-white q-py-sm q-px-md">
{{ $t('timesheet.expense.title') }} {{ $t('timesheet.expense.title') }}
</q-item-label> </q-item-label>
@ -39,8 +39,8 @@
/> />
</div> </div>
<div class="col column items-end full-width q-pt-sm q-px-md"> <div class="col row flex-center full-width q-pt-sm q-px-md">
<div class="col-auto row items-center q-px-sm"> <div class="col-auto row items-center q-px-md">
<span <span
v-if="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
class="col-auto text-uppercase text-weight-light text-accent q-mr-xs" class="col-auto text-uppercase text-weight-light text-accent q-mr-xs"
@ -50,7 +50,7 @@
<span <span
class="col-auto text-weight-light" class="col-auto text-weight-light"
style="font-size: 2.5em; line-height: 1em;" style="font-size: 2em; line-height: 1em;"
> >
{{ weekly_totals.expenses.toFixed(2) }} {{ weekly_totals.expenses.toFixed(2) }}
</span> </span>
@ -64,7 +64,7 @@
/> />
</div> </div>
<div class="col-auto row items-center q-px-sm"> <div class="col-auto row items-center q-px-md">
<span <span
v-if="$q.screen.gt.sm" v-if="$q.screen.gt.sm"
class="col text-uppercase text-weight-light text-accent q-mr-xs" class="col text-uppercase text-weight-light text-accent q-mr-xs"
@ -74,7 +74,7 @@
<span <span
class="col-auto text-weight-light" class="col-auto text-weight-light"
style="font-size: 2.5em; line-height: 1em;" style="font-size: 2em; line-height: 1em;"
> >
{{ weekly_totals.mileage.toFixed(1) }} {{ weekly_totals.mileage.toFixed(1) }}
</span> </span>

View File

@ -5,11 +5,12 @@
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 { date } from 'quasar';
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util'; import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
import { Expense } from 'src/modules/timesheets/models/expense.models'; import type { Expense } from 'src/modules/timesheets/models/expense.models';
const expense = defineModel<Expense>({ required: true }); const expense = defineModel<Expense>({ required: true });
@ -17,16 +18,15 @@
const expenses_api = useExpensesApi(); const expenses_api = useExpensesApi();
const is_showing_update_form = ref(false); const is_showing_update_form = ref(false);
const is_current_expense = computed(() => expense.value.id === expenses_store.current_expense.id);
const requestExpenseDeletion = async () => { const requestExpenseDeletion = async () => {
await expenses_api.deleteExpenseById(expense.value.id); await expenses_api.deleteExpenseById(expense.value.id);
} }
const onSaveUpdatesClicked = () => { const onClickExpenseUpdate = () => {
is_showing_update_form.value = false; expenses_store.mode = 'update';
expenses_store.mode = 'create'; expenses_store.current_expense = expense.value;
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')); expenses_store.initial_expense = unwrapAndClone(expense.value);
} }
</script> </script>
@ -36,8 +36,9 @@
hide-expand-icon hide-expand-icon
dense dense
group="expenses" group="expenses"
class="shadow-3 rounded-5 bg-dark" class="shadow-3 rounded-5 bg-dark q-my-sm"
:class="expense.is_approved ? ' bg-accent text-white' : ''" :class="expense.is_approved ? ' bg-accent text-white' : ''"
@before-show="onClickExpenseUpdate()"
> >
<template #header> <template #header>
<div class="col row items-center full-width"> <div class="col row items-center full-width">
@ -47,7 +48,7 @@
:name="getExpenseIcon(expense.type)" :name="getExpenseIcon(expense.type)"
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')" :color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
size="lg" size="lg"
class="q-px-sm" class="q-pr-md"
/> />
</div> </div>
@ -58,8 +59,8 @@
:class="expense.is_approved ? ' bg-accent text-white' : ''" :class="expense.is_approved ? ' bg-accent text-white' : ''"
style="font-size: 1.3em;" style="font-size: 1.3em;"
> >
{{ expense.type === 'MILEAGE' ? `${expense.mileage?.toFixed(1)} km` : `$ {{ expense.type === 'MILEAGE' ? `${Number(expense.mileage).toFixed(1)} km` : `$
${expense.amount.toFixed(2)}` }} ${Number(expense.amount).toFixed(2)}` }}
</span> </span>
<!-- date label --> <!-- date label -->
@ -123,10 +124,7 @@
</div> </div>
<div <div class="col-auto">
class="col-auto row"
:class="is_current_expense ? 'invisible' : ''"
>
<q-icon <q-icon
v-if="expense.is_approved" v-if="expense.is_approved"
name="verified" name="verified"
@ -141,16 +139,13 @@
size="lg" size="lg"
icon="close" icon="close"
color="negative" color="negative"
class="q-py-none z-top q-my-xs" class="q-py-none q-my-xs"
@click.stop="requestExpenseDeletion" @click.stop="requestExpenseDeletion"
/> />
</div> </div>
</div> </div>
</template> </template>
<ExpenseDialogForm <ExpenseDialogForm />
@on-click-update-cancel="is_showing_update_form = false"
@on-click-save-updates="onSaveUpdatesClicked"
/>
</q-expansion-item> </q-expansion-item>
</template> </template>

View File

@ -43,7 +43,7 @@
> >
<ExpenseDialogListItemMobile <ExpenseDialogListItemMobile
v-if="$q.screen.lt.md" v-if="$q.screen.lt.md"
v-model="expense.is_approved" v-model="expenses_list[index]!"
:index="index" :index="index"
:expense="expense" :expense="expense"
:horizontal="horizontal" :horizontal="horizontal"

View File

@ -2,13 +2,21 @@
setup setup
lang="ts" lang="ts"
> >
import { useExpensesStore } from 'src/stores/expense-store';
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 ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue'; import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
import { date } from 'quasar';
import { useExpensesStore } from 'src/stores/expense-store';
import { Expense } from 'src/modules/timesheets/models/expense.models';
const expense_store = useExpensesStore(); const expense_store = useExpensesStore();
const onClickExpenseCreate = () => {
expense_store.mode = 'create';
expense_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
}
</script> </script>
<template> <template>
@ -29,26 +37,45 @@
</q-inner-loading> </q-inner-loading>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<!-- <q-banner
v-if="expenses_error"
dense
class="bg-red-2 col-auto text-negative q-mt-sm"
>
{{ expenses_error }}
</q-banner> -->
<ExpenseDialogHeader /> <ExpenseDialogHeader />
<ExpenseDialogList /> <ExpenseDialogList />
<q-separator v-if="$q.screen.lt.md" spaced color="accent" size="2px" class="q-mx-md" /> <q-separator
v-if="$q.screen.lt.md"
spaced
color="accent"
size="2px"
class="q-mx-md"
/>
<q-slide-transition @hide="expense_store.is_hiding_create_form = true" :duration="200"> <q-expansion-item
<div v-if="!expense_store.current_expense.is_approved && expense_store.mode !== 'update' && expense_store.is_hiding_create_form === false"> v-model="expense_store.is_showing_create_form"
<ExpenseDialogFormMobile v-if="$q.screen.lt.md" /> hide-expand-icon
<ExpenseDialogForm v-else/> dense
</div> group="expenses"
</q-slide-transition> @before-show="onClickExpenseCreate()"
@hide="expense_store.mode = 'update'"
header-class="bg-accent text-white"
>
<template #header>
<div class="row items-center">
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-lg q-mr-sm">
{{ $t('timesheet.expense.add_expense') }}
</span>
<q-icon
v-if="expense_store.mode !== 'create'"
name="las la-plus-square"
size="md"
class="col-auto"
/>
</div>
</template>
<ExpenseDialogFormMobile v-if="$q.screen.lt.md" />
<ExpenseDialogForm v-else />
</q-expansion-item>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>

View File

@ -8,7 +8,7 @@
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 { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
interface ExpenseOption { interface ExpenseOption {
@ -189,7 +189,6 @@
:input-style="'font-size: 1.2em;'" :input-style="'font-size: 1.2em;'"
lazy-rules="ondemand" lazy-rules="ondemand"
:rules="[rules.amountRequired]" :rules="[rules.amountRequired]"
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
> >
<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">

View File

@ -5,7 +5,6 @@
import { date } from 'quasar'; import { date } from 'quasar';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone'; import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { deepEqual } from 'src/utils/deep-equal';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util'; import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
@ -22,7 +21,6 @@
const expenses_api = useExpensesApi(); const expenses_api = useExpensesApi();
const refresh_key = ref(1); const refresh_key = ref(1);
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '') const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
const is_showing_update_form = ref(false); const is_showing_update_form = ref(false);
@ -33,7 +31,7 @@
const onUpdateClicked = () => { const onUpdateClicked = () => {
if (expense.is_approved) return; if (expense.is_approved) return;
if (deepEqual(expense, expenses_store.current_expense)) { if (JSON.stringify(expense) === JSON.stringify(expenses_store.current_expense)) {
expenses_store.mode = 'create'; expenses_store.mode = 'create';
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'));
is_showing_update_form.value = false; is_showing_update_form.value = false;
@ -56,7 +54,7 @@
> >
<template <template
#right #right
v-if="$q.screen.lt.md && !expenses_store.is_hiding_create_form && !expense.is_approved" v-if="$q.screen.lt.md && expenses_store.is_showing_create_form && !expense.is_approved"
> >
<q-icon name="delete" /> <q-icon name="delete" />
</template> </template>
@ -65,7 +63,7 @@
:key="refresh_key" :key="refresh_key"
clickable clickable
class="row q-py-none q-pa-xs rounded-5 full-width" class="row q-py-none q-pa-xs rounded-5 full-width"
:class="background_class + approved_class" :class="approved_class"
@click="onUpdateClicked" @click="onUpdateClicked"
> >
<div class="column col"> <div class="column col">
@ -137,11 +135,11 @@
</q-slide-item> </q-slide-item>
<q-slide-transition <q-slide-transition
@hide="expenses_store.is_hiding_create_form = false" @hide="expenses_store.is_showing_create_form = true"
:duration="200" :duration="200"
> >
<ExpenseDialogFormMobile <ExpenseDialogFormMobile
v-if="is_showing_update_form && expenses_store.is_hiding_create_form" v-if="is_showing_update_form && !expenses_store.is_showing_create_form"
class="q-mt-sm q-pa-sm" class="q-mt-sm q-pa-sm"
@on-click-update-cancel="onUpdateClicked" @on-click-update-cancel="onUpdateClicked"
/> />

View File

@ -14,7 +14,6 @@ export const ShiftService = {
}, },
updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => { updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
console.log('sent shifts: ', existing_shifts)
const response = await api.patch(`/shift/update`, existing_shifts); const response = await api.patch(`/shift/update`, existing_shifts);
return response.data; return response.data;
} }

View File

@ -23,29 +23,4 @@ export const useExpenseRules = (t: (_key: string) => string) => {
mileageRequired, mileageRequired,
commentRequired, commentRequired,
}; };
};
export const convertToMonetaryAmount = (amount: number | string): number => {
if (typeof amount === 'number') return Number(amount.toFixed(2));
if (typeof amount === 'string') {
try {
let cleaned_amount = amount.replace(/[^\d.]/g, '');
const first_dot = cleaned_amount.indexOf('.');
if (first_dot !== -1) {
cleaned_amount =
cleaned_amount.slice(0, first_dot + 1) +
cleaned_amount
.slice(first_dot + 1, first_dot + 3)
.replace(/\./g, '');
}
return Number(cleaned_amount);
} catch (error) {
console.error(error);
}
}
return 0;
}; };

View File

@ -9,8 +9,6 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
start: date.extractDate(`2000-01-01 ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(), start: date.extractDate(`2000-01-01 ${shift.start_time}`, 'YYYY-MM-DD HH:mm').getTime(),
end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(), end: date.extractDate(`2000-01-01 ${shift.end_time}`, 'YYYY-MM-DD HH:mm').getTime(),
})); }));
console.log('parsed_shifts: ', parsed_shifts);
for (let i = 0; i < parsed_shifts.length; i++) { for (let i = 0; i < parsed_shifts.length; i++) {
for (let j = i + 1; j < parsed_shifts.length; j++) { for (let j = i + 1; j < parsed_shifts.length; j++) {
@ -18,8 +16,6 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
const parsed_shift_b = parsed_shifts[j]; const parsed_shift_b = parsed_shifts[j];
if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue; if (parsed_shift_a === undefined || parsed_shift_b === undefined) continue;
console.log('times(a start, b start, a end, b end): ', parsed_shift_a.start, parsed_shift_b.start, parsed_shift_a.end, parsed_shift_b.end);
console.log('result: ', Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end))
if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) { if (Math.max(parsed_shift_a.start, parsed_shift_b.start) < Math.min(parsed_shift_a.end, parsed_shift_b.end)) {
return true; // overlap found return true; // overlap found

View File

@ -33,7 +33,7 @@ export default defineRouter(function (/* { store, ssrContext } */) {
const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' }; const result = await authStore.getProfile() ?? { status: 400, message: 'unknown error occured' };
if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) { if ((destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) || (result.status >= 400 && destinationPage.name !== RouteNames.LOGIN)) {
console.log('no user account found'); console.error('no user account found');
return { name: 'login' }; return { name: 'login' };
} }
}) })

View File

@ -1,18 +1,19 @@
import { ref } from "vue"; import { date } from "quasar";
import { computed, 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";
import { ExpenseService } from "src/modules/timesheets/services/expense-service"; import { ExpenseService } from "src/modules/timesheets/services/expense-service";
import { date } from "quasar";
export const useExpensesStore = defineStore('expenses', () => { export const useExpensesStore = defineStore('expenses', () => {
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const is_open = ref(false); const is_open = ref(false);
const is_loading = ref(false); const is_loading = ref(false);
const is_hiding_create_form = ref(false); const is_showing_create_form = ref(true);
const mode = ref<'create' | 'update' | 'delete'>('create'); const mode = ref<'create' | 'update' | 'delete'>('create');
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 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;
@ -25,7 +26,7 @@ export const useExpensesStore = defineStore('expenses', () => {
const close = () => { const close = () => {
is_open.value = false; is_open.value = false;
is_hiding_create_form.value = false; is_showing_create_form.value = true;
}; };
const upsertExpense = async (expense: Expense): Promise<boolean> => { const upsertExpense = async (expense: Expense): Promise<boolean> => {
@ -51,10 +52,11 @@ export const useExpensesStore = defineStore('expenses', () => {
return { return {
is_open, is_open,
is_loading, is_loading,
is_hiding_create_form, is_showing_create_form,
mode, mode,
current_expense, current_expense,
initial_expense, initial_expense,
is_save_disabled,
open, open,
upsertExpense, upsertExpense,
deleteExpenseById, deleteExpenseById,

View File

@ -70,7 +70,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => { const getTimesheetsByOptionalEmployeeEmail = async (employee_email?: string) => {
if (pay_period.value === undefined) return; if (pay_period.value === undefined) return;
console.log('pay period: ', pay_period.value);
is_loading.value = true; is_loading.value = true;
let response; let response;

View File

@ -68,7 +68,6 @@ export const useUiStore = defineStore('ui', () => {
Dark.set(user_preferences.value.is_dark_mode ?? "auto"); Dark.set(user_preferences.value.is_dark_mode ?? "auto");
locale.value = user_preferences.value.display_language; locale.value = user_preferences.value.display_language;
} }
console.log('quasar dark mode: ', q.dark.mode, 'preferences: ', user_preferences.value.is_dark_mode);
} }
return { return {

View File

@ -10,7 +10,6 @@ export const getCurrentPayPeriod = (today = new Date()): number => {
const periods_since_anchor = Math.floor(days_since_anchor / period_length); const periods_since_anchor = Math.floor(days_since_anchor / period_length);
const current_period = (periods_since_anchor % periods_per_year) + 1; const current_period = (periods_since_anchor % periods_per_year) + 1;
console.log(current_period);
return current_period; return current_period;
} }

View File

@ -1,41 +0,0 @@
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
/**
* Internal recursive function comparing two plain values.
*/
const _deepEqualRecursive = (a: unknown, b: unknown): boolean => {
if (a === b) return true;
if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
return false;
}
// Handle arrays
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((val, i) => _deepEqualRecursive(val, b[i]));
} else if (Array.isArray(a) || Array.isArray(b)) {
return false; // one is array, other is not
}
const aKeys = Object.keys(a as Record<string, unknown>);
const bKeys = Object.keys(b as Record<string, unknown>);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((key) =>
_deepEqualRecursive(
(a as Record<string, unknown>)[key],
(b as Record<string, unknown>)[key]
)
);
};
/**
* Deep equality check that normalizes reactive objects first.
*/
export const deepEqual = (given: unknown, expected: unknown): boolean => {
const a = unwrapAndClone(given as object);
const b = unwrapAndClone(expected as object);
return _deepEqualRecursive(a, b);
};