refactor(timesheet): add at least some updates to approved shift look, maybe more???

This commit is contained in:
Nicolas Drolet 2025-11-14 17:00:27 -05:00
parent da93753515
commit b307f33ab0
13 changed files with 482 additions and 412 deletions

View File

@ -105,7 +105,7 @@ export default defineConfig((ctx) => {
notify: { notify: {
color: 'primary', color: 'primary',
}, },
dark: false, dark: 'auto',
}, },
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set

View File

@ -37,7 +37,7 @@
label-color="accent" label-color="accent"
class="rounded-5 inset-shadow bg-blue-grey-1" class="rounded-5 inset-shadow bg-blue-grey-1"
label-slot label-slot
input-class="text-weight-medium text-h6" input-class="text-weight-medium text-h6 text-primary"
> >
<template #label> <template #label>
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span> <span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>

View File

@ -20,7 +20,7 @@
dense dense
:stack-label="!isEditing" :stack-label="!isEditing"
autogrow autogrow
filled :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
debounce="500" debounce="500"
label-color="accent" label-color="accent"
class="q-ma-xs text-uppercase" class="q-ma-xs text-uppercase"

View File

@ -16,7 +16,7 @@
v-model="model" v-model="model"
dense dense
:stack-label="!isEditing" :stack-label="!isEditing"
filled :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
label-color="accent" label-color="accent"
class="q-ma-xs text-h6 text-uppercase" class="q-ma-xs text-h6 text-uppercase"
popup-content-class="text-weight-medium text-h6" popup-content-class="text-weight-medium text-h6"

View File

@ -11,13 +11,20 @@
}>(); }>();
const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', }; const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', };
</script> </script>
<template> <template>
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4"> <div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
<span class="col">{{ $t(title) }}</span> <span class="col">{{ $t(title) }}</span>
<transition
enter-active-class="animated fadeInDown"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<div <div
:key="startDate"
v-if="startDate.length > 0" v-if="startDate.length > 0"
class="col row flex-center full-width q-py-none q-my-none" class="col row flex-center full-width q-py-none q-my-none"
> >
@ -31,5 +38,6 @@
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }} {{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
</div> </div>
</div> </div>
</transition>
</div> </div>
</template> </template>

View File

@ -2,15 +2,20 @@
setup setup
lang="ts" lang="ts"
> >
import { date } from 'quasar';
import { computed, inject, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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 { useExpenseRules } from 'src/modules/timesheets/utils/expense.util'; import { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { Expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models'; import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
interface ExpenseOption {
label: string;
value: ExpenseType;
icon: string;
}
const { t } = useI18n(); const { t } = useI18n();
@ -28,25 +33,33 @@
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? ''); const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? ''); const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
// const expense_options: { label: string, value: ExpenseType, icon: string }[] = EXPENSE_TYPE.map(expense_type => {
// return { label: t(`timesheet.expense.types.${expense_type}`), value: expense_type, icon: getExpenseIcon(expense_type) };
// });
const expense_options: ExpenseOption[] = [
{label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM')},
{label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES')},
{label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE')},
{label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL')},
]
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
const openDatePicker = () => { const openDatePicker = () => {
is_navigator_open.value = true; is_navigator_open.value = true;
if (timesheet_store.pay_period !== undefined) { if (timesheet_store.pay_period !== undefined) {
expenses_store.current_expense.date = timesheet_store.pay_period.period_start; expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
} }
console.log('current pay period start date: ', period_start_date.value);
console.log('current pay period end date: ', period_end_date.value);
};
const cancelUpdateMode = () => {
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
expenses_store.initial_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
expenses_store.mode = 'create';
}; };
const requestExpenseCreationOrUpdate = async () => { const requestExpenseCreationOrUpdate = async () => {
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? ''); if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? ''); else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
}; };
defineEmits<{
'onClickUpdateCancel': [void];
}>();
</script> </script>
<template> <template>
@ -55,25 +68,30 @@
:key="expenses_store.current_expense.id" :key="expenses_store.current_expense.id"
flat flat
@submit.prevent="requestExpenseCreationOrUpdate" @submit.prevent="requestExpenseCreationOrUpdate"
class="full-width"
> >
<div <div
class="text-uppercase text-weight-medium q-pt-sm q-px-lg q-ma-sm" class="text-uppercase text-weight-medium q-pt-sm q-ma-sm"
:class="expenses_store.mode === 'create' ? '' : 'invisible'" :class="expenses_store.mode === 'create' ? 'q-px-lg' : 'invisible'"
> >
{{ $t('timesheet.expense.add_expense') }} {{ $t('timesheet.expense.add_expense') }}
</div> </div>
<div class="row justify-between items-start rounded-5 q-px-lg q-pb-sm"> <div
class="row justify-between items-start rounded-5 q-pb-sm"
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
>
<!-- date selection input --> <!-- date selection input -->
<q-input <q-input
v-model="expenses_store.current_expense.date" v-model="expenses_store.current_expense.date"
dense dense
type="date"
outlined outlined
readonly readonly
stack-label stack-label
color="primary" color="primary"
class="col q-px-xs" class="col-auto q-px-xs"
input-class="text-weight-medium" input-class="text-weight-medium"
input-style="font-size: 1.2em;" input-style="font-size: 1em;"
:label="$t('timesheet.expense.date')" :label="$t('timesheet.expense.date')"
> >
<template #prepend> <template #prepend>
@ -110,12 +128,12 @@
<!-- expenses type selection --> <!-- expenses type selection -->
<q-select <q-select
v-model="expenses_store.current_expense.type" v-model="expense_selected"
:options="EXPENSE_TYPE"
standout="bg-blue-grey-9" standout="bg-blue-grey-9"
dense dense
emit-value :options="expense_options"
hide-dropdown-icon hide-dropdown-icon
stack-label
label-slot label-slot
class="col q-px-xs" class="col q-px-xs"
color="primary" color="primary"
@ -126,7 +144,7 @@
popup-content-class="text-uppercase text-weight-bold text-center rounded-5" popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
:rules="[rules.typeRequired]" :rules="[rules.typeRequired]"
:option-label="label => $t(`timesheet.expense.types.${label}`)" @update:model-value="option => expenses_store.current_expense.type = option.value"
> >
<template #label> <template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption"> <span class="text-weight-bold text-accent text-uppercase text-caption">
@ -136,13 +154,18 @@
<template #selected-item="scope"> <template #selected-item="scope">
<div <div
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width" class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'" :class="ui_store.is_mobile_mode ? 'full-height' : ''"
:tabindex="scope.tabindex" :tabindex="scope.tabindex"
> >
<q-icon
:name="scope.opt.icon"
size="xs"
class="col-auto q-mx-xs"
/>
<span <span
style="line-height: 0.9em;" style="line-height: 1em;"
class="col-auto ellipsis" class="col-auto ellipsis text-uppercase"
>{{ scope.opt.label }}</span> >{{ scope.opt.label }}</span>
</div> </div>
</template> </template>
@ -153,16 +176,18 @@
<q-input <q-input
key="amount" key="amount"
v-model.number="expenses_store.current_expense.amount" v-model.number="expenses_store.current_expense.amount"
filled standout="bg-blue-grey-9"
input-class="text-right"
dense dense
stack-label
color="primary"
class="col q-px-xs"
label-slot label-slot
stack-label
suffix="$" suffix="$"
color="primary"
class="col-auto q-px-xs"
input-class="text-right text-weight-bold"
:input-style="'font-size: 1.2em;'"
lazy-rules="ondemand" lazy-rules="ondemand"
:rules="[rules.amountRequired]" :rules="[rules.amountRequired]"
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
> >
<template #label> <template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption"> <span class="text-weight-bold text-accent text-uppercase text-caption">
@ -177,7 +202,7 @@
<q-input <q-input
key="mileage" key="mileage"
v-model.number="expenses_store.current_expense.mileage" v-model.number="expenses_store.current_expense.mileage"
filled standout="bg-blue-grey-9"
input-class="text-right" input-class="text-right"
dense dense
stack-label stack-label
@ -200,7 +225,7 @@
<!-- employee comment input --> <!-- employee comment input -->
<q-input <q-input
v-model="expenses_store.current_expense.comment" v-model="expenses_store.current_expense.comment"
filled standout="bg-blue-grey-9"
dense dense
stack-label stack-label
label-slot label-slot
@ -222,8 +247,8 @@
<!-- import attach file section --> <!-- import attach file section -->
<q-file <q-file
v-model="files" v-model="files"
standout="bg-blue-grey-9"
dense dense
filled
use-chips use-chips
multiple multiple
stack-label stack-label
@ -257,7 +282,7 @@
icon="clear" icon="clear"
color="negative" color="negative"
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''" :label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
@click="cancelUpdateMode" @click="$emit('onClickUpdateCancel')"
/> />
<q-btn <q-btn
@ -265,7 +290,8 @@
color="accent" color="accent"
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'" :icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
:label="$q.screen.gt.sm ? (expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')) : ''" :label="$q.screen.gt.sm ? (expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')) : ''"
class="q-px-sm q-mb-sm q-mx-lg" class="q-px-sm "
:class="expenses_store.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
type="submit" type="submit"
/> />
</div> </div>

View File

@ -27,7 +27,6 @@
const refresh_key = ref(1); const refresh_key = ref(1);
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : ''); const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
const background_style = computed(() => deepEqual(expense, expenses_store.current_expense) ? 'border: 3px solid var(--q-accent);' : '');
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '') const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')) const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
const is_showing_update_form = ref(false); const is_showing_update_form = ref(false);
@ -64,7 +63,6 @@
:clickable="horizontal" :clickable="horizontal"
class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark" class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
:class="background_class + approved_class" :class="background_class + approved_class"
:style="background_style"
@click="onExpenseClicked" @click="onExpenseClicked"
> >
<div class="row full-width items-center"> <div class="row full-width items-center">
@ -72,21 +70,9 @@
<q-item-section avatar> <q-item-section avatar>
<q-icon <q-icon
:name="getExpenseIcon(expense.type)" :name="getExpenseIcon(expense.type)"
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')" :color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
size="lg" size="lg"
>
<q-badge
v-if="expense.type === 'ON_CALL'"
floating
class="q-pa-none rounded-50 bg-white z-top"
>
<q-icon
name="shield"
size="xs"
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')"
/> />
</q-badge>
</q-icon>
</q-item-section> </q-item-section>
<!-- amount or mileage section --> <!-- amount or mileage section -->
@ -110,7 +96,7 @@
class="text-uppercase text-weight-light" class="text-uppercase text-weight-light"
:class="approved_class" :class="approved_class"
> >
{{ $d(new Date(expense.date), { month: 'short', day: 'numeric', weekday: 'long' }) }} {{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), { month: 'short', day: 'numeric', weekday: 'long' }) }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
@ -185,13 +171,20 @@
</q-item-section> </q-item-section>
<q-item-section :side="$q.screen.gt.sm"> <q-item-section :side="$q.screen.gt.sm">
<q-icon
v-if="expense.is_approved"
name="verified"
color="white"
size="lg"
/>
<q-btn <q-btn
v-else
flat flat
size="lg" size="lg"
:icon="expense.is_approved ? 'verified' : 'close'" icon="close"
:color="expense.is_approved ? 'white' : 'negative'" color="negative"
class="q-pa-none z-top" class="q-pa-none z-top"
:class="expense.is_approved ? 'no-pointer' : ''"
@click.stop="requestExpenseDeletion" @click.stop="requestExpenseDeletion"
/> />
</q-item-section> </q-item-section>
@ -201,7 +194,7 @@
@hide="expenses_store.is_hiding_create_form = false" @hide="expenses_store.is_hiding_create_form = false"
:duration="200" :duration="200"
> >
<ExpenseDialogForm v-if="is_showing_update_form && expenses_store.is_hiding_create_form" /> <ExpenseDialogForm v-if="is_showing_update_form && expenses_store.is_hiding_create_form" @on-click-update-cancel="onUpdateClicked"/>
</q-slide-transition> </q-slide-transition>
</q-item> </q-item>
</template> </template>

View File

@ -3,8 +3,7 @@
lang="ts" lang="ts"
> >
/* eslint-disable*/ /* eslint-disable*/
import { onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef } from 'vue'; import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QSelect } from 'quasar'; import { QSelect } from 'quasar';
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models'; import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
@ -22,27 +21,26 @@
const COMMENT_LENGTH_MAX = 280; const COMMENT_LENGTH_MAX = 280;
const SHIFT_OPTIONS: ShiftOption[] = [ const SHIFT_OPTIONS: ShiftOption[] = [
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' }, { label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' }, { label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' }, { label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' }, { label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' }, { label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' }, { label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
]; ];
const shift = defineModel<Shift>('shift', { required: true }); const shift = defineModel<Shift>('shift', { required: true });
const { dense = false, outlined = false } = defineProps<{ const { dense = false, hasShiftAfter = false } = defineProps<{
dense?: boolean; dense?: boolean;
outlined?: boolean; hasShiftAfter?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'saveComment': [comment: string, shift_id: number]; 'saveComment': [comment: string, shift_id: number];
'requestDelete': [void]; 'requestDelete': [void];
}>(); }>();
const is_showing_time_picker = ref(false);
const select_ref = useTemplateRef<QSelect>('select'); const select_ref = useTemplateRef<QSelect>('select');
const initial_shift = ref<Shift>(unwrapAndClone(toRaw(shift.value)))
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type)); const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
@ -95,8 +93,11 @@
> >
<q-icon name="delete" /> <q-icon name="delete" />
</template> </template>
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'"> <div :class="ui_store.is_mobile_mode ? 'column' : 'row'" >
<div class="row items-center text-uppercase rounded-5 bg-transparent q-mb-xs" :class="ui_store.is_mobile_mode ? 'col' : 'col-4'"> <div
class="row items-center text-uppercase rounded-5 bg-transparent"
:class="ui_store.is_mobile_mode ? 'col q-mb-xs' : 'col-4'"
>
<!-- mobile comment button --> <!-- mobile comment button -->
<q-btn <q-btn
v-if="ui_store.is_mobile_mode && !dense" v-if="ui_store.is_mobile_mode && !dense"
@ -154,8 +155,9 @@
<q-select <q-select
ref="select" ref="select"
v-model="shift_type_selected" v-model="shift_type_selected"
standout="bg-blue-grey-9" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense dense
:borderless="shift.is_approved"
:readonly="shift.is_approved" :readonly="shift.is_approved"
:options-dense="!ui_store.is_mobile_mode" :options-dense="!ui_store.is_mobile_mode"
hide-dropdown-icon hide-dropdown-icon
@ -164,6 +166,8 @@
menu-self="top middle" menu-self="top middle"
:options="SHIFT_OPTIONS" :options="SHIFT_OPTIONS"
class="col rounded-5 q-mx-xs bg-dark" class="col rounded-5 q-mx-xs bg-dark"
:class="shift.is_approved ? 'inset-shadow' : ''"
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5" popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-accent)" popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect" @blur="onBlurShiftTypeSelect"
@ -184,6 +188,7 @@
<span <span
style="line-height: 0.9em;" style="line-height: 0.9em;"
class="col-auto ellipsis" class="col-auto ellipsis"
:class="shift.is_approved ? 'text-white' : ''"
>{{ scope.opt.label }}</span> >{{ scope.opt.label }}</span>
</div> </div>
</template> </template>
@ -195,15 +200,17 @@
<q-input <q-input
v-model="shift.start_time" v-model="shift.start_time"
dense dense
:borderless="shift.is_approved"
:readonly="shift.is_approved" :readonly="shift.is_approved"
type="time" type="time"
:standout="$q.dark.isActive ? 'bg-blue-grey-9' : 'bg-blue-grey-1 text-white'" :standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
label-slot label-slot
label-color="accent" :label-color="shift.is_approved ? 'white' : 'accent'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed text-white' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark" class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
> >
<template #label> <template #label>
<span <span
@ -216,16 +223,18 @@
<!-- punch out field --> <!-- punch out field -->
<q-input <q-input
v-model="shift.end_time" v-model="shift.end_time"
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
dense dense
:borderless="shift.is_approved"
:readonly="shift.is_approved" :readonly="shift.is_approved"
type="time" type="time"
standout="bg-blue-grey-9"
label-slot label-slot
label-color="accent" :label-color="shift.is_approved ? 'white' : 'accent'"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')" :input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed text-white' : '')"
input-style="font-size: 1.2em;" input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark" class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')" :class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
:style="shift.is_approved ? 'background-color: #0002 !important;' : ''"
> >
<template #label> <template #label>
<span <span
@ -317,4 +326,6 @@
</div> </div>
</div> </div>
</q-slide-item> </q-slide-item>
<q-separator v-if="hasShiftAfter && ui_store.is_mobile_mode" spaced color="accent" class="q-mx-md"/>
</template> </template>

View File

@ -38,6 +38,7 @@
v-model:shift="day.shifts[shift_index]!" v-model:shift="day.shifts[shift_index]!"
:outlined="outlined" :outlined="outlined"
:dense="dense" :dense="dense"
:has-shift-after="shift_index < day.shifts.length - 1"
@request-delete="deleteCurrentShift(shift)" @request-delete="deleteCurrentShift(shift)"
/> />
</div> </div>

View File

@ -2,6 +2,7 @@
setup setup
lang="ts" lang="ts"
> >
import { computed } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { useUiStore } from 'src/stores/ui-store'; import { useUiStore } from 'src/stores/ui-store';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
@ -15,6 +16,8 @@
const ui_store = useUiStore(); const ui_store = useUiStore();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const animation_style = computed(() => ui_store.is_mobile_mode ? 'fadeInLeft' : 'fadeInDown' );
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => { const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
ui_store.focus_next_component = true; ui_store.focus_next_component = true;
const new_shift = new Shift; const new_shift = new Shift;
@ -47,11 +50,16 @@
v-for="timesheet, timesheet_index in timesheet_store.timesheets" v-for="timesheet, timesheet_index in timesheet_store.timesheets"
:key="timesheet.timesheet_id" :key="timesheet.timesheet_id"
class="col column" class="col column"
>
<transition-group
appear
:enter-active-class="`animated ${animation_style}`"
> >
<div <div
v-for="day, day_index in timesheet.days" v-for="day, day_index in timesheet.days"
:key="day.date" :key="day.date"
class="col-auto row rounded-10 q-ma-sm shadow-10" class="col-auto row rounded-10 q-ma-sm shadow-10"
:style="`animation-delay: ${day_index / 15}s;`"
> >
<div <div
v-if="ui_store.is_mobile_mode" v-if="ui_store.is_mobile_mode"
@ -59,7 +67,7 @@
> >
<q-card <q-card
class="rounded-10 bg-dark" class="rounded-10 bg-dark"
:style="ui_store.is_mobile_mode ? (getDayApproval(day) ? 'border: 3px solid var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''" :style="ui_store.is_mobile_mode ? (getDayApproval(day) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
> >
<q-card-section <q-card-section
@ -79,6 +87,7 @@
> >
<ShiftListDay <ShiftListDay
outlined outlined
:animation-delay-multiplier="day_index"
:approved="getDayApproval(day)" :approved="getDayApproval(day)"
:day="day" :day="day"
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)" @delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
@ -117,6 +126,8 @@
class="col row full-width" class="col row full-width"
:class="getDayApproval(day) ? 'rounded-10 bg-accent' : ''" :class="getDayApproval(day) ? 'rounded-10 bg-accent' : ''"
> >
<!-- List of shifts -->
<div <div
class="col row bg-dark" class="col row bg-dark"
:class="getDayApproval(day) ? 'bg-transparent' : ''" :class="getDayApproval(day) ? 'bg-transparent' : ''"
@ -129,7 +140,7 @@
class="col-auto" class="col-auto"
/> />
<!-- List of shifts -->
<ShiftListDay <ShiftListDay
:day="day" :day="day"
class="col" class="col"
@ -137,6 +148,7 @@
/> />
</div> </div>
<div class="col-auto self-stretch"> <div class="col-auto self-stretch">
<q-icon <q-icon
v-if="getDayApproval(day)" v-if="getDayApproval(day)"
@ -160,6 +172,7 @@
</div> </div>
</div> </div>
</div> </div>
</transition-group>
</div> </div>
</div> </div>
</template> </template>

View File

@ -36,7 +36,7 @@
> >
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass"> <q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
<q-spinner-radio <q-spinner-radio
color="primary" color="accent"
size="20vh" size="20vh"
/> />
</q-card> </q-card>

View File

@ -24,3 +24,20 @@ export const useExpenseRules = (t: (_key: string) => string) => {
commentRequired, commentRequired,
}; };
}; };
export const convertToMonetaryAmount = (amount: number | string): number => {
if (typeof amount === 'number') return Number(amount.toFixed(2));
if (typeof amount === 'string') {
try {
const single_decimal_amount = amount.replace(/\.(?=.*\.)/g, '');
const numbers_only_decimal = single_decimal_amount.replace(/[^0-9.]/g, '');
return Number(numbers_only_decimal);
} catch(error) {
console.error(error);
}
}
return 0;
};

View File

@ -25,6 +25,7 @@ export const useExpensesStore = defineStore('expenses', () => {
const close = () => { const close = () => {
is_open.value = false; is_open.value = false;
is_hiding_create_form.value = false;
}; };
const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => { const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => {