Merge pull request 'release/nicolas/v1.2' (#93) from release/nicolas/v1.2 into main
Reviewed-on: Targo/targo_frontend#93
This commit is contained in:
commit
5bb44ad662
|
|
@ -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: {},
|
||||||
|
|
||||||
|
|
|
||||||
96
src/modules/shared/components/targo-input.vue
Normal file
96
src/modules/shared/components/targo-input.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
const model = defineModel<string | number | null | undefined>({ required: true });
|
||||||
|
const is_date_picker_open = defineModel<boolean>('isDatePickerOpen', { default: false });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
label?: string | undefined;
|
||||||
|
requiresDatePicker?: boolean | undefined;
|
||||||
|
maxLength?: number;
|
||||||
|
noTopPadding?: boolean;
|
||||||
|
backgroundColor?: 'bg-secondary' | 'bg-dark';
|
||||||
|
appendContent?: string | number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="col q-px-sm"
|
||||||
|
:class="noTopPadding ? '' : 'q-pt-md'"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
v-model="model"
|
||||||
|
v-bind="$attrs"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
color="accent"
|
||||||
|
label-color="white"
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
:maxlength="maxLength"
|
||||||
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
|
:class="$q.dark.isActive ? 'bg-primary' : (backgroundColor ?? 'bg-secondary')"
|
||||||
|
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-bold text-uppercase q-px-md"
|
||||||
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template
|
||||||
|
#append
|
||||||
|
v-if="requiresDatePicker || !!appendContent"
|
||||||
|
>
|
||||||
|
<div v-if="requiresDatePicker">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
icon="calendar_month"
|
||||||
|
color="accent"
|
||||||
|
@click="is_date_picker_open = true"
|
||||||
|
>
|
||||||
|
<q-dialog
|
||||||
|
v-model="is_date_picker_open"
|
||||||
|
backdrop-filter="none"
|
||||||
|
>
|
||||||
|
<q-date
|
||||||
|
v-model="model"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
color="accent"
|
||||||
|
@update:model-value="is_date_picker_open = false"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!!appendContent"
|
||||||
|
class="self-end text-uppercase text-bold text-accent"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
>
|
||||||
|
{{ appendContent }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||||
|
transform: translate(-17px, -60%) scale(0.75);
|
||||||
|
border-radius: 10px 10px 10px 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
src/modules/shared/components/targo-select.vue
Normal file
54
src/modules/shared/components/targo-select.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
const model = defineModel<string>({ required: true });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
label?: string | undefined;
|
||||||
|
options: string[];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="col q-px-sm q-pt-md">
|
||||||
|
<q-select
|
||||||
|
v-model="model"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
color="accent"
|
||||||
|
label-color="white"
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
|
:options="options"
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
options-selected-class="text-white text-bold bg-accent"
|
||||||
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
|
:class="$q.dark.isActive ? 'bg-primary' : 'bg-secondary'"
|
||||||
|
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
||||||
|
popup-content-style="border: 1px solid var(--q-primary)"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
:menu-offset="[0, 5]"
|
||||||
|
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||||
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||||
|
transform: translate(-17px, -60%) scale(0.75) !important;
|
||||||
|
border-radius: 10px 10px 10px 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,20 +2,21 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
||||||
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
||||||
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
|
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
import { date } from 'quasar';
|
|
||||||
|
|
||||||
// ========== state ========================================
|
// ========== state ========================================
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@
|
||||||
const timesheetApprovalApi = useTimesheetApprovalApi();
|
const timesheetApprovalApi = useTimesheetApprovalApi();
|
||||||
const shiftApi = useShiftApi();
|
const shiftApi = useShiftApi();
|
||||||
const isDialogOpen = ref(false);
|
const isDialogOpen = ref(false);
|
||||||
|
const refreshKey = ref(0);
|
||||||
|
|
||||||
// ========== computed ========================================
|
// ========== computed ========================================
|
||||||
|
|
||||||
|
|
@ -60,13 +62,22 @@
|
||||||
expenseStore.is_showing_create_form = false;
|
expenseStore.is_showing_create_form = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickExpenseCreate = () => {
|
const onClickNewExpense = () => {
|
||||||
expenseStore.mode = 'create';
|
expenseStore.mode = 'create';
|
||||||
if (timesheetStore.pay_period)
|
if (timesheetStore.pay_period)
|
||||||
expenseStore.current_expense = new Expense(timesheetStore.pay_period.period_start);
|
expenseStore.current_expense = new Expense(timesheetStore.pay_period.period_start);
|
||||||
else
|
else
|
||||||
expenseStore.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
expenseStore.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickSaveNewExpense = () => {
|
||||||
|
expenseStore.is_showing_create_form = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onShowDetailsDialog = () => {
|
||||||
|
isDialogOpen.value = true;
|
||||||
|
expenseStore.is_showing_create_form = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -77,7 +88,7 @@
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
backdrop-filter="blur(6px)"
|
backdrop-filter="blur(6px)"
|
||||||
@show="isDialogOpen = true"
|
@show="onShowDetailsDialog"
|
||||||
@hide="isDialogOpen = false"
|
@hide="isDialogOpen = false"
|
||||||
@before-hide="timesheetStore.getTimesheetOverviews"
|
@before-hide="timesheetStore.getTimesheetOverviews"
|
||||||
>
|
>
|
||||||
|
|
@ -154,7 +165,10 @@
|
||||||
class="q-mx-md"
|
class="q-mx-md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExpenseDialogList mode="approval" />
|
<ExpenseDialogList
|
||||||
|
mode="approval"
|
||||||
|
:key="refreshKey + 1"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!isApproved"
|
v-if="!isApproved"
|
||||||
|
|
@ -162,7 +176,7 @@
|
||||||
hide-expand-icon
|
hide-expand-icon
|
||||||
:dense="!$q.platform.is.mobile"
|
:dense="!$q.platform.is.mobile"
|
||||||
group="expenses"
|
group="expenses"
|
||||||
@show="onClickExpenseCreate()"
|
@show="onClickNewExpense()"
|
||||||
header-class="bg-accent text-white q-mx-md rounded-5"
|
header-class="bg-accent text-white q-mx-md rounded-5"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|
@ -180,7 +194,12 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ExpenseDialogForm :email="timesheetStore.current_pay_period_overview?.email"/>
|
<ExpenseDialogForm
|
||||||
|
:email="timesheetStore.current_pay_period_overview?.email"
|
||||||
|
:key="refreshKey"
|
||||||
|
mode="approval"
|
||||||
|
@click-save="onClickSaveNewExpense"
|
||||||
|
/>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
<q-separator
|
<q-separator
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
<LoadingOverlay v-model="timesheetStore.is_loading" />
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
dense
|
dense
|
||||||
row-key="email"
|
row-key="email"
|
||||||
|
|
|
||||||
|
|
@ -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,4 +2,5 @@ export interface PayPeriodEvent {
|
||||||
employee_email: string;
|
employee_email: string;
|
||||||
event_type: 'shift' | 'expense' | 'preset';
|
event_type: 'shift' | 'expense' | 'preset';
|
||||||
action: 'create' | 'update' | 'delete';
|
action: 'create' | 'update' | 'delete';
|
||||||
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import TargoInput from 'src/modules/shared/components/targo-input.vue';
|
||||||
|
|
||||||
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,297 +19,284 @@
|
||||||
|
|
||||||
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;
|
||||||
|
mode?: 'normal' | 'approval';
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'clickSave': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
const expenses_store = useExpensesStore();
|
const expenseStore = useExpensesStore();
|
||||||
const auth_store = useAuthStore();
|
const expensesApi = useExpensesApi();
|
||||||
const expenses_api = useExpensesApi();
|
const isNavigatorOpen = ref(false);
|
||||||
const is_navigator_open = ref(false);
|
|
||||||
const rules = useExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
|
|
||||||
const expense_options: ExpenseOption[] = [
|
const expenseOptions: ExpenseOption[] = [
|
||||||
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
|
|
||||||
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
|
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
|
||||||
|
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
|
||||||
{ label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
|
{ label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
|
||||||
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
|
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
|
||||||
]
|
]
|
||||||
const expense_selected = ref<ExpenseOption | undefined>();
|
const expenseSelected = ref<ExpenseOption | undefined>();
|
||||||
const employeeEmail = inject<string>('employeeEmail');
|
|
||||||
|
|
||||||
// ================== computed ===================
|
// ================== computed ===================
|
||||||
|
|
||||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
const period_start_date = computed(() => timesheetStore.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
const period_end_date = computed(() => timesheetStore.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||||
|
const isSaveDisabled = computed(() =>
|
||||||
|
JSON.stringify(expenseStore.current_expense) === JSON.stringify(expenseStore.initial_expense)
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== method =======================
|
// ==================== method =======================
|
||||||
|
|
||||||
const openDatePicker = () => {
|
const openDatePicker = () => {
|
||||||
is_navigator_open.value = true;
|
isNavigatorOpen.value = true;
|
||||||
if (expenses_store.current_expense.date === undefined) {
|
if (expenseStore.current_expense.date === undefined) {
|
||||||
expenses_store.current_expense.date = timesheet_store.pay_period?.period_start ?? '';
|
expenseStore.current_expense.date = timesheetStore.pay_period?.period_start ?? '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDatePicker = (date: string) => {
|
const closeDatePicker = (date: string) => {
|
||||||
is_navigator_open.value = false;
|
isNavigatorOpen.value = false;
|
||||||
expenses_store.current_expense.date = date;
|
expenseStore.current_expense.date = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
if (file.value)
|
const success = await expensesApi.upsertExpense(expenseStore.current_expense, email, file.value);
|
||||||
await expenses_api.upsertExpense(
|
|
||||||
expenses_store.current_expense,
|
|
||||||
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL',
|
|
||||||
file.value
|
|
||||||
);
|
|
||||||
else
|
|
||||||
await expenses_api.upsertExpense(
|
|
||||||
expenses_store.current_expense,
|
|
||||||
email ?? employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL'
|
|
||||||
);
|
|
||||||
|
|
||||||
expenses_store.is_showing_create_form = true;
|
if (success) {
|
||||||
expenses_store.mode = 'create';
|
expenseStore.is_showing_create_form = false;
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
emit('clickSave');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (expense.value)
|
if (expense.value)
|
||||||
expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type);
|
expenseSelected.value = expenseOptions.find(expense_option => expense_option.value === expense.value.type);
|
||||||
else
|
else
|
||||||
expense_selected.value = expense_options[1];
|
expenseSelected.value = expenseOptions[0];
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<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 class="column rounded-5 q-pb-sm">
|
||||||
<div class="col q-px-xs">
|
<div class="row">
|
||||||
<q-input
|
<!-- date selection input -->
|
||||||
v-model="expenses_store.current_expense.date"
|
<div class="row col items-center q-pl-sm">
|
||||||
dense
|
|
||||||
standout
|
|
||||||
readonly
|
|
||||||
stack-label
|
|
||||||
color="primary"
|
|
||||||
input-class="text-weight-medium"
|
|
||||||
input-style="font-size: 1em;"
|
|
||||||
:label="$t('timesheet.expense.date')"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
icon="event"
|
icon="event"
|
||||||
color="accent"
|
color="accent"
|
||||||
class="q-mr-sm"
|
class="col-auto"
|
||||||
@click="openDatePicker"
|
@click="openDatePicker"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="is_navigator_open"
|
v-model="isNavigatorOpen"
|
||||||
transition-show="jump-right"
|
transition-show="jump-right"
|
||||||
transition-hide="jump-right"
|
transition-hide="jump-right"
|
||||||
class="z-top"
|
class="z-top"
|
||||||
>
|
>
|
||||||
<q-date
|
<q-date
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenseStore.current_expense.date"
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
event-color="accent"
|
event-color="accent"
|
||||||
:options="date => date >= period_start_date && date <= period_end_date"
|
:options="date => date >= period_start_date && date <= period_end_date"
|
||||||
@update:model-value="closeDatePicker"
|
@update:model-value="closeDatePicker"
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #label>
|
<TargoInput
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
v-model="expenseStore.current_expense.date"
|
||||||
{{ $t('timesheet.expense.date') }}
|
no-top-padding
|
||||||
</span>
|
:label="$t('timesheet.expense.date')"
|
||||||
</template>
|
background-color="bg-dark"
|
||||||
</q-input>
|
class="col"
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
|
||||||
<div class="col q-px-xs">
|
|
||||||
<q-select
|
|
||||||
v-model="expense_selected"
|
|
||||||
standout
|
|
||||||
dense
|
|
||||||
:options="expense_options"
|
|
||||||
hide-dropdown-icon
|
|
||||||
stack-label
|
|
||||||
label-slot
|
|
||||||
color="primary"
|
|
||||||
:label="$t('timesheet.expense.type')"
|
|
||||||
:menu-offset="[0, 10]"
|
|
||||||
menu-anchor="bottom middle"
|
|
||||||
menu-self="top middle"
|
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5 z-top"
|
|
||||||
options-selected-class="text-weight-bolder text-white bg-accent"
|
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
|
||||||
:rules="[rules.typeRequired]"
|
|
||||||
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
||||||
{{ $t('timesheet.expense.type') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #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(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
|
|
||||||
v-else
|
|
||||||
v-model="expenses_store.current_expense.mileage"
|
|
||||||
standout
|
|
||||||
dense
|
|
||||||
label-slot
|
|
||||||
stack-label
|
|
||||||
suffix="km"
|
|
||||||
type="number"
|
|
||||||
input-class="text-right text-weight-medium"
|
|
||||||
input-style="font-size: 1.3em;"
|
|
||||||
color="primary"
|
|
||||||
lazy-rules="ondemand"
|
|
||||||
:rules="[rules.mileageRequired]"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
||||||
{{ $t('timesheet.expense.mileage') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- employee comment input -->
|
|
||||||
<div class="col q-px-xs">
|
|
||||||
<q-input
|
|
||||||
v-model="expenses_store.current_expense.comment"
|
|
||||||
standout
|
|
||||||
dense
|
|
||||||
stack-label
|
|
||||||
label-slot
|
|
||||||
color="primary"
|
|
||||||
input-class="text-weight-medium"
|
|
||||||
input-style="font-size: 1.3em;"
|
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
|
||||||
lazy-rules="ondemand"
|
|
||||||
:rules="[rules.commentRequired]"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- import attach file section -->
|
|
||||||
<div class="col q-px-xs">
|
|
||||||
<q-file
|
|
||||||
v-model="file"
|
|
||||||
standout
|
|
||||||
dense
|
|
||||||
stack-label
|
|
||||||
label-slot
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon
|
|
||||||
name="attach_file"
|
|
||||||
size="sm"
|
|
||||||
color="accent"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template #label>
|
<!-- expenses type selection -->
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<div class="col">
|
||||||
{{ $t('timesheet.expense.hints.attach_file') }}
|
<q-select
|
||||||
</span>
|
v-model="expenseSelected"
|
||||||
</template>
|
dense
|
||||||
</q-file>
|
borderless
|
||||||
|
color="accent"
|
||||||
|
label-color="white"
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
|
:options="expenseOptions"
|
||||||
|
hide-dropdown-icon
|
||||||
|
lazy-rules
|
||||||
|
no-error-icon
|
||||||
|
hide-bottom-space
|
||||||
|
options-selected-class="text-white text-bold bg-accent"
|
||||||
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
|
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
||||||
|
popup-content-class="text-uppercase text-weight-medium rounded-5 shadow-12 z-top"
|
||||||
|
popup-content-style="border: 1px solid var(--q-primary);"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
:menu-offset="[0, 5]"
|
||||||
|
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||||
|
:rules="[rules.typeRequired]"
|
||||||
|
@update:model-value="option => expenseStore.current_expense.type = option.value"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||||
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||||
|
>
|
||||||
|
{{ $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">
|
||||||
|
<TargoInput
|
||||||
|
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenseStore.current_expense?.type ?? 'EXPENSES')"
|
||||||
|
v-model.number="expenseStore.current_expense.amount"
|
||||||
|
no-top-padding
|
||||||
|
background-color="bg-dark"
|
||||||
|
type="number"
|
||||||
|
input-class="text-right"
|
||||||
|
append-content=" $"
|
||||||
|
:label="$t('timesheet.expense.amount')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TargoInput
|
||||||
|
v-else
|
||||||
|
v-model.number="expenseStore.current_expense.mileage"
|
||||||
|
no-top-padding
|
||||||
|
background-color="bg-dark"
|
||||||
|
type="number"
|
||||||
|
input-class="text-right"
|
||||||
|
append-content=" km"
|
||||||
|
:label="$t('timesheet.expense.mileage')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-pt-md">
|
||||||
|
<!-- employee comment input -->
|
||||||
|
<div class="col">
|
||||||
|
<TargoInput
|
||||||
|
v-model="expenseStore.current_expense.comment"
|
||||||
|
no-top-padding
|
||||||
|
background-color="bg-dark"
|
||||||
|
:max-length="COMMENT_MAX_LENGTH"
|
||||||
|
:label="$t('timesheet.expense.employee_comment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="mode === 'approval'"
|
||||||
|
class="col"
|
||||||
|
>
|
||||||
|
<TargoInput
|
||||||
|
v-model="expenseStore.current_expense.supervisor_comment"
|
||||||
|
no-top-padding
|
||||||
|
background-color="bg-dark"
|
||||||
|
:max-length="COMMENT_MAX_LENGTH"
|
||||||
|
:label="$t('timesheet.expense.supervisor_comment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- import attach file section -->
|
||||||
|
<div class="col-3 q-px-sm">
|
||||||
|
<q-file
|
||||||
|
v-model="file"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
color="accent"
|
||||||
|
label-color="white"
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="q-px-md rounded-5 inset-shadow"
|
||||||
|
:class="$q.dark.isActive ? 'bg-primary' : 'bg-dark'"
|
||||||
|
:style="`border: 1px solid var(${$q.dark.isActive ? '--q-secondary' : '--q-primary'});`"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon
|
||||||
|
name="attach_file"
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
class="text-weight-medium text-uppercase q-px-sm no-pointer-events"
|
||||||
|
:class="$q.dark.isActive ? 'bg-secondary' : 'bg-primary'"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col row full-width items-center">
|
<div class="col row full-width items-center">
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
:disable="expenses_store.is_save_disabled"
|
:disable="isSaveDisabled"
|
||||||
:color="expenses_store.is_save_disabled ? 'grey-5' : 'accent'"
|
:color="isSaveDisabled ? 'grey-5' : 'accent'"
|
||||||
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
:icon="expenseStore.mode === 'update' ? 'save' : 'upload'"
|
||||||
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
:label="expenseStore.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
class="q-px-sm "
|
class="q-px-sm "
|
||||||
:class="expenses_store.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
:class="expenseStore.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
||||||
type="submit"
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style
|
||||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
scoped
|
||||||
border: transparent;
|
lang="css"
|
||||||
}
|
>
|
||||||
|
:deep(.q-field--dense.q-field--float .q-field__label) {
|
||||||
|
transform: translate(-17px, -60%) scale(0.75) !important;
|
||||||
|
border-radius: 10px 10px 10px 0px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -4,10 +4,9 @@
|
||||||
>
|
>
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, toRaw } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { date, Notify } from 'quasar';
|
import { date, Notify } from 'quasar';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
|
|
@ -23,10 +22,10 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const expenses_api = useExpensesApi();
|
const expensesApi = useExpensesApi();
|
||||||
const expenses_store = useExpensesStore();
|
const expenseStore = useExpensesStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheetStore = useTimesheetStore();
|
||||||
const is_showing_update_form = ref(false);
|
const isShowingUpdateForm = ref(false);
|
||||||
|
|
||||||
// ========== computed ===================================
|
// ========== computed ===================================
|
||||||
|
|
||||||
|
|
@ -37,30 +36,30 @@
|
||||||
// ===================== methods =========================
|
// ===================== methods =========================
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
await expenses_api.deleteExpenseById(expense.value.id);
|
await expensesApi.deleteExpenseById(expense.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickExpenseUpdate = () => {
|
const onClickExpenseUpdate = () => {
|
||||||
if (expense.value.is_approved) return;
|
if (expense.value.is_approved) return;
|
||||||
|
|
||||||
expenses_store.mode = 'update';
|
expenseStore.mode = 'update';
|
||||||
expenses_store.current_expense = expense.value;
|
expenseStore.current_expense = structuredClone(toRaw(expense.value));
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense.value);
|
expenseStore.initial_expense = structuredClone(toRaw(expense.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickApproval = async () => {
|
const onClickApproval = async () => {
|
||||||
expenses_store.current_expense = unwrapAndClone(expense.value);
|
expenseStore.current_expense = structuredClone(toRaw(expense.value));
|
||||||
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
|
expenseStore.current_expense.is_approved = !expenseStore.current_expense.is_approved;
|
||||||
|
|
||||||
const success = await expenses_store.upsertExpense(
|
const success = await expenseStore.upsertExpense(
|
||||||
expenses_store.current_expense,
|
expenseStore.current_expense,
|
||||||
timesheet_store.current_pay_period_overview?.email
|
timesheetStore.current_pay_period_overview?.email
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
expense.value.is_approved = !expense.value.is_approved;
|
expense.value.is_approved = !expense.value.is_approved;
|
||||||
} else {
|
} else {
|
||||||
expenses_store.current_expense.is_approved = !expenses_store.current_expense.is_approved;
|
expenseStore.current_expense.is_approved = !expenseStore.current_expense.is_approved;
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: t('timesheet.errors.UPDATE_ERROR'),
|
message: t('timesheet.errors.UPDATE_ERROR'),
|
||||||
color: "negative"
|
color: "negative"
|
||||||
|
|
@ -68,16 +67,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getEmployeeEmail = () => {
|
||||||
|
if (mode === 'approval')
|
||||||
|
return timesheetStore.current_pay_period_overview?.email;
|
||||||
|
}
|
||||||
|
|
||||||
const onClickAttachment = async () => {
|
const onClickAttachment = async () => {
|
||||||
expenses_store.isShowingAttachmentDialog = true;
|
expenseStore.isShowingAttachmentDialog = true;
|
||||||
await expenses_store.getAttachmentURL(expense.value.attachment_key);
|
await expenseStore.getAttachmentURL(expense.value.attachment_key);
|
||||||
console.log('image url: ', expenses_store.attachmentURL);
|
console.log('image url: ', expenseStore.attachmentURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideUpdateForm = () => {
|
||||||
|
isShowingUpdateForm.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-model="is_showing_update_form"
|
v-model="isShowingUpdateForm"
|
||||||
hide-expand-icon
|
hide-expand-icon
|
||||||
dense
|
dense
|
||||||
group="expenses"
|
group="expenses"
|
||||||
|
|
@ -106,7 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<div class="col column">
|
<div class="col-auto column q-pr-md">
|
||||||
<span
|
<span
|
||||||
class="text-weight-bolder"
|
class="text-weight-bolder"
|
||||||
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
|
|
@ -122,81 +130,74 @@
|
||||||
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
:class="expense.is_approved ? ' bg-accent text-white' : ''"
|
||||||
>
|
>
|
||||||
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
||||||
month: 'short', day: 'numeric', weekday:
|
month: 'long', day: 'numeric', weekday: 'long'
|
||||||
'long'
|
|
||||||
}) }}
|
}) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
<q-separator vertical spaced class="q-my-xs"/>
|
||||||
<div class="col row items-center justify-start">
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
:disable="expense.attachment_name === undefined"
|
|
||||||
:color="attachmentButtonColor"
|
|
||||||
:text-color="expense.is_approved ? 'accent' : 'white'"
|
|
||||||
class="col-auto q-px-sm q-mr-sm"
|
|
||||||
icon="attach_file"
|
|
||||||
@click.stop="onClickAttachment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-item-label class="col">
|
<!-- comments section -->
|
||||||
<span v-if="expense.attachment_name">
|
<div class="col column">
|
||||||
{{ expense.attachment_name }}
|
<div class="col row items-center">
|
||||||
|
<span
|
||||||
|
class="col-auto text-weight-medium text-accent text-uppercase q-pr-md"
|
||||||
|
style="font-size: 1.2em;"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }} :
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<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,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ExpenseDialogForm v-model="expense" />
|
<ExpenseDialogForm
|
||||||
|
v-model="expense"
|
||||||
|
:email="getEmployeeEmail()"
|
||||||
|
:mode="mode"
|
||||||
|
@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>
|
||||||
|
|
@ -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);
|
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
return 'SUCCESS';
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'INVALID_EXPENSE';
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
||||||
const success = await expenses_store.deleteExpenseById(expense_id);
|
const success = await expenses_store.deleteExpenseById(expense_id);
|
||||||
if (success) {
|
|
||||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
if (success)
|
||||||
}
|
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -163,13 +164,6 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
try {
|
try {
|
||||||
const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
|
const timesheet_ids = timesheets.value.map(timesheet => timesheet.timesheet_id);
|
||||||
|
|
||||||
// Backend returns the amount of shifts and expenses successfully updated, could be useful for error handling???
|
|
||||||
// const shift_expense_count = timesheets.value.reduce((timesheets_sum, timesheet) => {
|
|
||||||
// return timesheets_sum + timesheet.days.reduce((day_sum, day) => {
|
|
||||||
// return day_sum + day.shifts.length + day.expenses.length
|
|
||||||
// }, 0);
|
|
||||||
// }, 0);
|
|
||||||
|
|
||||||
const response = await timesheetApprovalService.updateTimesheetsApprovalStatus(email, timesheet_ids, approval_status);
|
const response = await timesheetApprovalService.updateTimesheetsApprovalStatus(email, timesheet_ids, approval_status);
|
||||||
return response.success;
|
return response.success;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -227,19 +221,20 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
if (pay_period_observer.value === undefined) {
|
if (pay_period_observer.value === undefined) {
|
||||||
pay_period_observer.value = timesheetApprovalService.subscribeToPayPeriodObservable();
|
pay_period_observer.value = timesheetApprovalService.subscribeToPayPeriodObservable();
|
||||||
pay_period_observer.value.onmessage = async (event: MessageEvent<string>) => {
|
pay_period_observer.value.onmessage = async (event: MessageEvent<string>) => {
|
||||||
// find employee that modified their timesheets
|
|
||||||
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
|
||||||
|
if (!isBetweenDateStrings(pay_period_event.date, pay_period.value!.period_start, pay_period.value!.period_end))
|
||||||
|
return;
|
||||||
|
|
||||||
const overview = pay_period_overviews.value.find(overview => overview.email === pay_period_event.employee_email);
|
const overview = pay_period_overviews.value.find(overview => overview.email === pay_period_event.employee_email);
|
||||||
const employee_name = overview?.employee_first_name + ' ' + overview?.employee_last_name;
|
const employee_name = overview?.employee_first_name + ' ' + overview?.employee_last_name;
|
||||||
|
|
||||||
// update overviews
|
|
||||||
await getTimesheetOverviews();
|
await getTimesheetOverviews();
|
||||||
|
|
||||||
// if user is looking at details of employee that generated event, update
|
|
||||||
if (selected_employee_name.value === employee_name)
|
if (selected_employee_name.value === employee_name)
|
||||||
await getTimesheetsByOptionalEmployeeEmail(pay_period_event.employee_email);
|
await getTimesheetsByOptionalEmployeeEmail(pay_period_event.employee_email);
|
||||||
|
|
||||||
// create visual feedback of notification and update
|
|
||||||
Notify.create({
|
Notify.create({
|
||||||
message: `${employee_name} ${t('timesheet_approvals.event.' + pay_period_event.action)} ${t('timesheet_approvals.event.' + pay_period_event.event_type)}`,
|
message: `${employee_name} ${t('timesheet_approvals.event.' + pay_period_event.action)} ${t('timesheet_approvals.event.' + pay_period_event.event_type)}`,
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,19 @@ export const getHoursMinutesBetweenTwoHHmm = (startTime: string, endTime: string
|
||||||
minutes: Number(endMinutes) - Number(startMinutes),
|
minutes: Number(endMinutes) - Number(startMinutes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isBetweenDateStrings = (evDate: string, start: string, end: string, inclusive: boolean = true) => {
|
||||||
|
const eventDate = date.extractDate(evDate, 'YYYY-MM-DD');
|
||||||
|
const startDate = date.extractDate(start, 'YYYY-MM-DD');
|
||||||
|
const endDate = date.extractDate(end, 'YYYY-MM-DD');
|
||||||
|
|
||||||
|
return date.isBetweenDates(
|
||||||
|
eventDate,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
{
|
||||||
|
inclusiveFrom: inclusive,
|
||||||
|
inclusiveTo: inclusive,
|
||||||
|
onlyDate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user