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:
Nicolas 2026-03-11 12:30:08 -04:00
commit 5bb44ad662
15 changed files with 548 additions and 370 deletions

View File

@ -66,10 +66,13 @@ export default defineConfig((ctx) => {
// polyfillModulePreload: true, // polyfillModulePreload: true,
// distDir // distDir
extendViteConf: (_config) => ({ extendViteConf: (config) => ({
optimizeDeps: { optimizeDeps: {
exclude: ['tesseract.js'] exclude: ['tesseract.js']
} },
define: {
__VUE_PROD_DEVTOOLS__: config.mode !== 'production'
},
}), }),
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},

View File

@ -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>

View 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>

View File

@ -2,20 +2,21 @@
setup setup
lang="ts" lang="ts"
> >
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue'; import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue'; import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue'; import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue'; import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue'; import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue'; import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api'; import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api'; import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { Expense } from 'src/modules/timesheets/models/expense.models'; import { Expense } from 'src/modules/timesheets/models/expense.models';
import { date } from 'quasar';
// ========== state ======================================== // ========== state ========================================
@ -25,6 +26,7 @@
const timesheetApprovalApi = useTimesheetApprovalApi(); const timesheetApprovalApi = useTimesheetApprovalApi();
const shiftApi = useShiftApi(); const shiftApi = useShiftApi();
const isDialogOpen = ref(false); const isDialogOpen = ref(false);
const refreshKey = ref(0);
// ========== computed ======================================== // ========== computed ========================================
@ -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,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

View File

@ -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"

View File

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

View File

@ -2,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;
} }

View File

@ -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, 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 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>

View File

@ -4,10 +4,9 @@
> >
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue'; import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import { computed, ref } from 'vue'; import { computed, ref, toRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { date, Notify } from 'quasar'; import { date, Notify } from 'quasar';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useExpensesStore } from 'src/stores/expense-store'; import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api'; import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
@ -23,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,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>

View File

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

View File

@ -11,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>

View File

@ -2,13 +2,13 @@
import { useExpensesStore } from "src/stores/expense-store"; import { useExpensesStore } from "src/stores/expense-store";
import { useTimesheetStore } from "src/stores/timesheet-store"; import { useTimesheetStore } from "src/stores/timesheet-store";
import { Expense } from "src/modules/timesheets/models/expense.models"; import { Expense } from "src/modules/timesheets/models/expense.models";
import { date } from "quasar"; import { date, Notify } from "quasar";
export const useExpensesApi = () => { export const useExpensesApi = () => {
const expenses_store = useExpensesStore(); const expenses_store = useExpensesStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const upsertExpense = async (expense: Expense, employee_email: string, file?: File): Promise<string> => { const upsertExpense = async (expense: Expense, employee_email?: string, file?: File): Promise<boolean> => {
if (file) { if (file) {
const attachmentKey = await expenses_store.uploadAttachment(file); const attachmentKey = await expenses_store.uploadAttachment(file);
@ -19,23 +19,24 @@ export const useExpensesApi = () => {
expense.attachment_name = file.name; expense.attachment_name = file.name;
} }
} }
console.log('employee email provided for expense: ', employee_email)
const success = await expenses_store.upsertExpense(expense, employee_email);
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 {

View File

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

View File

@ -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',

View File

@ -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
});
} }