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:
parent
c6187305d9
commit
1271d1eb61
|
|
@ -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: {},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ========================================
|
||||||
|
|
||||||
|
|
@ -34,9 +36,9 @@
|
||||||
t('shared.label.unlock') :
|
t('shared.label.unlock') :
|
||||||
t('shared.label.lock')
|
t('shared.label.lock')
|
||||||
);
|
);
|
||||||
|
|
||||||
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
|
const approveButtonIcon = computed(() => isApproved.value ? 'las la-lock' : 'las la-unlock');
|
||||||
|
|
||||||
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
|
const hasExpenses = computed(() => timesheetStore.timesheets.some(timesheet =>
|
||||||
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
|
Object.values(timesheet.weekly_expenses).some(hours => hours > 0))
|
||||||
);
|
);
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,293 +17,290 @@
|
||||||
|
|
||||||
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,
|
if (success) {
|
||||||
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
expenseStore.is_showing_create_form = false;
|
||||||
file.value
|
emit('clickSave');
|
||||||
);
|
}
|
||||||
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'));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
||||||
<q-form
|
<div
|
||||||
v-if="!expenses_store.current_expense.is_approved"
|
v-if="!expenseStore.current_expense.is_approved"
|
||||||
flat
|
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
|
||||||
class="full-width q-mt-md q-px-md"
|
class="full-width q-mt-md q-px-md"
|
||||||
>
|
>
|
||||||
<div
|
<q-form
|
||||||
class="row justify-between items-start rounded-5 q-pb-sm"
|
flat
|
||||||
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
>
|
>
|
||||||
<!-- date selection input -->
|
<div
|
||||||
<div class="col q-px-xs">
|
class="row justify-between items-start rounded-5 q-pb-sm"
|
||||||
<q-input
|
:class="expenseStore.mode === 'create' ? 'q-px-lg' : ''"
|
||||||
v-model="expenses_store.current_expense.date"
|
>
|
||||||
dense
|
<!-- date selection input -->
|
||||||
standout
|
<div class="col q-px-xs">
|
||||||
readonly
|
<q-input
|
||||||
stack-label
|
v-model="expenseStore.current_expense.date"
|
||||||
color="primary"
|
dense
|
||||||
input-class="text-weight-medium"
|
standout
|
||||||
input-style="font-size: 1em;"
|
readonly
|
||||||
:label="$t('timesheet.expense.date')"
|
stack-label
|
||||||
>
|
color="primary"
|
||||||
<template #prepend>
|
input-class="text-weight-medium"
|
||||||
<q-btn
|
input-style="font-size: 1em;"
|
||||||
push
|
:label="$t('timesheet.expense.date')"
|
||||||
dense
|
>
|
||||||
icon="event"
|
<template #prepend>
|
||||||
color="accent"
|
<q-btn
|
||||||
class="q-mr-sm"
|
push
|
||||||
@click="openDatePicker"
|
dense
|
||||||
/>
|
icon="event"
|
||||||
|
color="accent"
|
||||||
<q-dialog
|
class="q-mr-sm"
|
||||||
v-model="is_navigator_open"
|
@click="openDatePicker"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #label>
|
<q-dialog
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
v-model="isNavigatorOpen"
|
||||||
{{ $t('timesheet.expense.date') }}
|
transition-show="jump-right"
|
||||||
</span>
|
transition-hide="jump-right"
|
||||||
</template>
|
class="z-top"
|
||||||
</q-input>
|
>
|
||||||
</div>
|
<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 -->
|
<template #label>
|
||||||
<div class="col q-px-xs">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
<q-select
|
{{ $t('timesheet.expense.date') }}
|
||||||
v-model="expense_selected"
|
</span>
|
||||||
standout
|
</template>
|
||||||
dense
|
</q-input>
|
||||||
:options="expense_options"
|
</div>
|
||||||
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 #selected-item="scope">
|
<!-- expenses type selection -->
|
||||||
<div
|
<div class="col q-px-xs">
|
||||||
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
<q-select
|
||||||
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
v-model="expenseSelected"
|
||||||
:tabindex="scope.tabindex"
|
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
|
<q-icon
|
||||||
:name="scope.opt.icon"
|
name="attach_file"
|
||||||
size="xs"
|
size="sm"
|
||||||
class="col-auto q-mx-xs"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
<span
|
</template>
|
||||||
style="line-height: 1em;"
|
|
||||||
class="col-auto ellipsis text-uppercase"
|
<template #label>
|
||||||
>{{ scope.opt.label }}</span>
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
</div>
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||||
</template>
|
</span>
|
||||||
</q-select>
|
</template>
|
||||||
|
</q-file>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- amount input -->
|
<div class="col row full-width items-center">
|
||||||
<div class="col q-px-xs">
|
<q-space />
|
||||||
<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>
|
|
||||||
|
|
||||||
<q-input
|
<q-btn
|
||||||
v-else
|
push
|
||||||
v-model="expenses_store.current_expense.mileage"
|
:disable="isSaveDisabled"
|
||||||
standout
|
:color="isSaveDisabled ? 'grey-5' : 'accent'"
|
||||||
dense
|
:icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
|
||||||
label-slot
|
:label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
stack-label
|
class="q-px-sm "
|
||||||
suffix="km"
|
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
||||||
type="number"
|
type="submit"
|
||||||
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>
|
</div>
|
||||||
|
</q-form>
|
||||||
<!-- employee comment input -->
|
</div>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -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,44 +22,44 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
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 ===================================
|
||||||
|
|
||||||
const attachmentButtonColor = computed(() => expense.value.attachment_name ?
|
const attachmentButtonColor = computed(() => expense.value.attachment_name ?
|
||||||
(expense.value.is_approved ? 'white' : 'accent') :
|
(expense.value.is_approved ? 'white' : 'accent') :
|
||||||
'grey-5');
|
'grey-5');
|
||||||
|
|
||||||
// ===================== 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>
|
||||||
|
|
||||||
|
<q-separator vertical spaced class="q-my-xs"/>
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<!-- comments section -->
|
||||||
<div class="col row items-center justify-start">
|
<div class="col column">
|
||||||
<q-btn
|
<div class="col row items-center">
|
||||||
push
|
<span
|
||||||
:disable="expense.attachment_name === undefined"
|
class="col-auto text-weight-medium text-accent text-uppercase q-pr-md"
|
||||||
:color="attachmentButtonColor"
|
style="font-size: 1.2em;"
|
||||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
>
|
||||||
class="col-auto q-px-sm q-mr-sm"
|
{{ $t('timesheet.expense.employee_comment') }} :
|
||||||
icon="attach_file"
|
|
||||||
@click.stop="onClickAttachment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-item-label class="col">
|
|
||||||
<span v-if="expense.attachment_name">
|
|
||||||
{{ expense.attachment_name }}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-else
|
class="col"
|
||||||
class="text-italic text-blue-grey-5 text-uppercase"
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.no_attachment') }}
|
{{ expense.comment }}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- comment section -->
|
<!-- attachment -->
|
||||||
<div class="col column no-wrap">
|
<div class="col-auto">
|
||||||
<span class="col-auto text-weight-bold text-accent text-uppercase text-caption">
|
<q-btn
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
flat
|
||||||
</span>
|
size="lg"
|
||||||
|
:disable="expense.attachment_name === undefined"
|
||||||
<span
|
:color="attachmentButtonColor"
|
||||||
class="col ellipsis"
|
class="col-auto q-px-sm q-mr-sm"
|
||||||
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
:icon="expense.attachment_key ? 'image' : 'hide_image'"
|
||||||
style="font-size: 1em;"
|
@click.stop="onClickAttachment"
|
||||||
>
|
>
|
||||||
{{ expense.comment }}
|
<q-tooltip
|
||||||
</span>
|
anchor="top middle"
|
||||||
|
self="center middle"
|
||||||
<q-tooltip
|
:offset="[0, 20]"
|
||||||
anchor="top middle"
|
class="bg-accent text-uppercase text-weight-bold"
|
||||||
self="center middle"
|
>
|
||||||
:offset="[0, 20]"
|
{{ expense.attachment_name ?? $t('timesheet.expense.no_attachment') }}
|
||||||
class="bg-accent text-uppercase text-weight-bold"
|
</q-tooltip>
|
||||||
>
|
</q-btn>
|
||||||
{{ expense.comment }}
|
|
||||||
</q-tooltip>
|
|
||||||
</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>
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@
|
||||||
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>
|
||||||
|
|
@ -66,7 +69,7 @@
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
:class="expense_store.is_showing_create_form ? 'invisible' : ''"
|
: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">
|
<span class="col-auto text-uppercase text-weight-bold text-h6 q-ml-xs q-mr-sm">
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
||||||
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 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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export const getHoursMinutesStringFromHoursFloat = (hours: number, minutes?: num
|
||||||
flatHours += 1;
|
flatHours += 1;
|
||||||
flatMinutes = 0;
|
flatMinutes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${flatHours}h${flatMinutes > 1 ? ' ' + flatMinutes : ''}`
|
return `${flatHours}h${flatMinutes > 1 ? ' ' + flatMinutes : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,8 +39,24 @@ export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string
|
||||||
const [startHours, startMinutes] = startTime.split(':');
|
const [startHours, startMinutes] = startTime.split(':');
|
||||||
const [endHours, endMinutes] = endTime.split(':');
|
const [endHours, endMinutes] = endTime.split(':');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hours: Number(endHours) - Number(startHours),
|
hours: Number(endHours) - Number(startHours),
|
||||||
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user