From 1b4e59b292c6103cfe44174378be5ae129687400 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Mon, 15 Dec 2025 17:12:39 -0500 Subject: [PATCH 1/5] refactor(timesheet): working on expense list, optimizing class usage working to refactor expense list and form to instead be q-expansion-items that are part of the same group, will trim a lot of needless code and q-slide-transition use this way. --- .../components/expense-dialog-form.vue | 308 +++++++++--------- .../components/expense-dialog-list-item.vue | 300 +++++++---------- .../components/expense-dialog-list.vue | 4 +- src/modules/timesheets/utils/expense.util.ts | 22 +- 4 files changed, 295 insertions(+), 339 deletions(-) diff --git a/src/modules/timesheets/components/expense-dialog-form.vue b/src/modules/timesheets/components/expense-dialog-form.vue index 846bcc0..325ea56 100644 --- a/src/modules/timesheets/components/expense-dialog-form.vue +++ b/src/modules/timesheets/components/expense-dialog-form.vue @@ -35,13 +35,15 @@ const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? ''); const expense_options: ExpenseOption[] = [ - {label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM')}, - {label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES')}, - {label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE')}, - {label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL')}, + { label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') }, + { label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') }, + { label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') }, + { label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') }, ] const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type)); + const expense_monetary_string = ref(expenses_store.current_expense.amount.toString()); + const emit = defineEmits<{ 'onClickUpdateCancel': [void]; 'onClickSaveUpdates': [void]; @@ -61,13 +63,19 @@ const requestExpenseCreationOrUpdate = async () => { await expenses_api.upsertExpense(expenses_store.current_expense); - + if (expenses_store.current_expense.id) { emit('onClickSaveUpdates'); } }; + 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); }); @@ -92,113 +100,113 @@ :class="expenses_store.mode === 'create' ? 'q-px-lg' : ''" > - - + + + + - - +
+ + - - + + +
-
+
-
- -
\ No newline at end of file + + + \ No newline at end of file diff --git a/src/modules/timesheets/components/expense-dialog-list-item.vue b/src/modules/timesheets/components/expense-dialog-list-item.vue index 7fe6277..a6be764 100644 --- a/src/modules/timesheets/components/expense-dialog-list-item.vue +++ b/src/modules/timesheets/components/expense-dialog-list-item.vue @@ -2,60 +2,25 @@ setup lang="ts" > - import { date } from 'quasar'; - import { computed, ref, toRaw } from 'vue'; - 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 { useExpensesStore } from 'src/stores/expense-store'; - import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util'; - import { useAuthStore } from 'src/stores/auth-store'; - import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models'; - import { Expense } from 'src/modules/timesheets/models/expense.models'; import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue'; - const { expense, horizontal = false } = defineProps<{ - expense: Expense; - index: number; - horizontal?: boolean; - }>(); - const is_approved = defineModel({ required: true }); + import { date } from 'quasar'; + import { computed, ref } from 'vue'; + import { useExpensesStore } from 'src/stores/expense-store'; + import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; + import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util'; + import { Expense } from 'src/modules/timesheets/models/expense.models'; + + const expense = defineModel({ required: true }); const expenses_store = useExpensesStore(); - const auth_store = useAuthStore(); const expenses_api = useExpensesApi(); - 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 is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')) const is_showing_update_form = ref(false); - const is_current_expense = computed(() => expense.id === expenses_store.current_expense.id); + const is_current_expense = computed(() => expense.value.id === expenses_store.current_expense.id); const requestExpenseDeletion = async () => { - await expenses_api.deleteExpenseById(expense.id); - } - - const onExpenseClicked = () => { - if (is_authorized_to_approve.value) { - is_approved.value = !is_approved.value; - refresh_key.value += 1; - } - } - - const onUpdateClicked = () => { - if (deepEqual(expense, expenses_store.current_expense)) { - expenses_store.mode = 'create'; - Object.assign(expense, toRaw(expenses_store.initial_expense)) - expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')); - is_showing_update_form.value = false; - return; - } - - expenses_store.mode = 'update'; - expenses_store.current_expense = expense; - expenses_store.initial_expense = unwrapAndClone(expense); - is_showing_update_form.value = true; + await expenses_api.deleteExpenseById(expense.value.id); } const onSaveUpdatesClicked = () => { @@ -66,155 +31,126 @@ \ No newline at end of file diff --git a/src/modules/timesheets/components/expense-dialog-list.vue b/src/modules/timesheets/components/expense-dialog-list.vue index 207f2cf..ab1ea90 100644 --- a/src/modules/timesheets/components/expense-dialog-list.vue +++ b/src/modules/timesheets/components/expense-dialog-list.vue @@ -51,10 +51,8 @@
diff --git a/src/modules/timesheets/utils/expense.util.ts b/src/modules/timesheets/utils/expense.util.ts index 80d18cd..6cbb182 100644 --- a/src/modules/timesheets/utils/expense.util.ts +++ b/src/modules/timesheets/utils/expense.util.ts @@ -27,17 +27,25 @@ export const useExpenseRules = (t: (_key: string) => string) => { export const convertToMonetaryAmount = (amount: number | string): number => { if (typeof amount === 'number') return Number(amount.toFixed(2)); - + if (typeof amount === 'string') { try { - const single_decimal_amount = amount.replace(/\.(?=.*\.)/g, ''); - const numbers_only_decimal = single_decimal_amount.replace(/[^0-9.]/g, ''); - - return Number(numbers_only_decimal); - } catch(error) { + 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; + return 0; }; \ No newline at end of file From faa239784bd0bf1ba0a0eec54de38c042a47c3dc Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Tue, 16 Dec 2025 11:06:59 -0500 Subject: [PATCH 2/5] refactor(timesheet): redo expense dialog to work with Expansion Items, improve UI/UX --- src/css/app.scss | 12 +++ .../components/main-layout-left-drawer.vue | 2 +- .../composables/use-employee-api.ts | 3 - .../shared/components/language-switch.vue | 1 - .../components/expense-dialog-form.vue | 79 ++++++------------- .../components/expense-dialog-header.vue | 14 ++-- .../components/expense-dialog-list-item.vue | 35 ++++---- .../components/expense-dialog-list.vue | 2 +- .../timesheets/components/expense-dialog.vue | 61 ++++++++++---- .../mobile/expense-dialog-form-mobile.vue | 3 +- .../expense-dialog-list-item-mobile.vue | 12 ++- .../timesheets/services/shift-service.ts | 1 - src/modules/timesheets/utils/expense.util.ts | 25 ------ src/modules/timesheets/utils/shift.util.ts | 4 - src/router/index.ts | 2 +- src/stores/expense-store.ts | 12 +-- src/stores/timesheet-store.ts | 1 - src/stores/ui-store.ts | 1 - src/utils/date-and-time-utils.ts | 3 +- src/utils/deep-equal.ts | 41 ---------- 20 files changed, 117 insertions(+), 197 deletions(-) delete mode 100644 src/utils/deep-equal.ts diff --git a/src/css/app.scss b/src/css/app.scss index 1ade66d..2e7ae8b 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -52,4 +52,16 @@ body.body--dark { .q-btn--push:active::before { 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; } \ No newline at end of file diff --git a/src/layouts/components/main-layout-left-drawer.vue b/src/layouts/components/main-layout-left-drawer.vue index 01e7ed3..5d5de81 100644 --- a/src/layouts/components/main-layout-left-drawer.vue +++ b/src/layouts/components/main-layout-left-drawer.vue @@ -24,7 +24,7 @@ auth_store.logout(); router.push({ name: 'login' }).catch(err => { - console.log('could not log you out: ', err); + console.error('could not log you out: ', err); }) } diff --git a/src/modules/employee-list/composables/use-employee-api.ts b/src/modules/employee-list/composables/use-employee-api.ts index 90efd81..b613fd0 100644 --- a/src/modules/employee-list/composables/use-employee-api.ts +++ b/src/modules/employee-list/composables/use-employee-api.ts @@ -38,10 +38,7 @@ export const useEmployeeListApi = () => { weekday.is_error = isShiftOverlap(weekday.shifts); } - console.log('current preset: ', preset); - if (preset.weekdays.some(weekday => weekday.is_error)) { - console.log('overlap!'); return; } diff --git a/src/modules/shared/components/language-switch.vue b/src/modules/shared/components/language-switch.vue index 5fde2a1..e2ca11c 100644 --- a/src/modules/shared/components/language-switch.vue +++ b/src/modules/shared/components/language-switch.vue @@ -10,7 +10,6 @@ const setDisplayLanguage = (locale: MessageLanguages) => { if (ui_store.user_preferences !== undefined) { ui_store.user_preferences.display_language = locale; - console.log('triggered language change: ', ui_store.user_preferences.display_language); } } diff --git a/src/modules/timesheets/components/expense-dialog-form.vue b/src/modules/timesheets/components/expense-dialog-form.vue index 325ea56..0b66600 100644 --- a/src/modules/timesheets/components/expense-dialog-form.vue +++ b/src/modules/timesheets/components/expense-dialog-form.vue @@ -2,15 +2,15 @@ setup lang="ts" > + import { date } from 'quasar'; import { useI18n } from 'vue-i18n'; - import { computed, ref, watch } from 'vue'; - import { deepEqual } from 'src/utils/deep-equal'; + import { computed, 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 { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; - import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; + import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; + import { Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; interface ExpenseOption { label: string; @@ -26,7 +26,6 @@ const files = defineModel('files'); const is_navigator_open = ref(false); - const is_initial_expense = ref(true); const COMMENT_MAX_LENGTH = 280; const rules = useExpenseRules(t); @@ -40,15 +39,9 @@ { label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') }, { label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') }, ] + const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type)); - const expense_monetary_string = ref(expenses_store.current_expense.amount.toString()); - - const emit = defineEmits<{ - 'onClickUpdateCancel': [void]; - 'onClickSaveUpdates': [void]; - }>(); - const openDatePicker = () => { is_navigator_open.value = true; if (expenses_store.current_expense.date === undefined) { @@ -64,21 +57,10 @@ const requestExpenseCreationOrUpdate = async () => { await expenses_api.upsertExpense(expenses_store.current_expense); - if (expenses_store.current_expense.id) { - emit('onClickSaveUpdates'); - } - + 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 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); - });