refactor(timesheet): More UI/UX adjustments to timesheet approval filters, mostly work on timesheets UI/UX for mobile
This commit is contained in:
parent
fdbc563a0e
commit
4231b51c11
|
|
@ -176,6 +176,10 @@ export default {
|
|||
|
||||
timesheet: {
|
||||
page_header: "Timesheet",
|
||||
week: "week",
|
||||
total_hours: "total hours: ",
|
||||
current_shifts: "shifts worked",
|
||||
apply_preset: "auto-fill",
|
||||
apply_preset_day: "Apply schedule to day",
|
||||
apply_preset_week: "Apply schedule to week",
|
||||
nav_button: {
|
||||
|
|
@ -254,15 +258,6 @@ export default {
|
|||
|
||||
timesheet_approvals: {
|
||||
page_title: "Validation cartes de temps",
|
||||
table: {
|
||||
full_name: "full name",
|
||||
email: "email address",
|
||||
is_approved: "approval",
|
||||
expenses: "expenses",
|
||||
mileage: "mileage",
|
||||
verified: "approved",
|
||||
unverified: "pending",
|
||||
},
|
||||
chart: {
|
||||
hours_worked_title: "hours worked",
|
||||
expenses_title: "expenses accrued",
|
||||
|
|
@ -275,6 +270,15 @@ export default {
|
|||
expenses: "expenses",
|
||||
options: "options",
|
||||
},
|
||||
table: {
|
||||
full_name: "full name",
|
||||
email: "email address",
|
||||
is_approved: "approval",
|
||||
expenses: "expenses",
|
||||
mileage: "mileage",
|
||||
verified: "approved",
|
||||
unverified: "pending",
|
||||
},
|
||||
tooltip: {
|
||||
button_detailed_view: "detailed view",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -177,6 +177,10 @@ export default {
|
|||
|
||||
timesheet: {
|
||||
page_header: "Carte de temps",
|
||||
week: "semaine",
|
||||
total_hours: "heures totales: ",
|
||||
current_shifts: "quarts entrées",
|
||||
apply_preset: "auto-remplir",
|
||||
apply_preset_day: "Appliquer horaire pour la journée",
|
||||
apply_preset_week: "Appliquer horaire pour la semaine",
|
||||
nav_button: {
|
||||
|
|
@ -255,15 +259,6 @@ export default {
|
|||
|
||||
timesheet_approvals: {
|
||||
page_title: "Validation cartes de temps",
|
||||
table: {
|
||||
full_name: "nom complet",
|
||||
email: "courriel",
|
||||
is_approved: "approuvé",
|
||||
expenses: "dépenses",
|
||||
mileage: "kilométrage",
|
||||
verified: "approuvé",
|
||||
unverified: "à vérifier",
|
||||
},
|
||||
chart: {
|
||||
hours_worked_title: "heures travaillées",
|
||||
expenses_title: "dépenses encourues"
|
||||
|
|
@ -276,6 +271,15 @@ export default {
|
|||
expenses: "dépenses",
|
||||
options: "options",
|
||||
},
|
||||
table: {
|
||||
full_name: "nom complet",
|
||||
email: "courriel",
|
||||
is_approved: "approuvé",
|
||||
expenses: "dépenses",
|
||||
mileage: "kilométrage",
|
||||
verified: "approuvé",
|
||||
unverified: "à vérifier",
|
||||
},
|
||||
tooltip: {
|
||||
button_detailed_view: "vue détaillée",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
setup
|
||||
>
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
||||
// import HeaderBarNotification from './main-layout-header-bar-notification.vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</q-btn>
|
||||
</q-toolbar-title>
|
||||
<q-item class="q-pa-none">
|
||||
<HeaderBarNotification />
|
||||
<!-- <HeaderBarNotification /> -->
|
||||
</q-item>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
lang="ts"
|
||||
setup
|
||||
>
|
||||
import { RouterView } from 'vue-router';
|
||||
import HeaderBar from 'src/layouts/components/main-layout-header-bar.vue';
|
||||
import FooterBar from 'src/layouts/components/main-layout-footer-bar.vue';
|
||||
import LeftDrawer from 'src/layouts/components/main-layout-left-drawer.vue';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const user_preferences = ref(ui_store.user_preferences);
|
||||
|
|
@ -23,7 +22,7 @@
|
|||
|
||||
watch(user_preferences, async () => {
|
||||
if (ui_store.user_preferences.id !== -1) {
|
||||
await ui_store.updateUserPreferences(t);
|
||||
await ui_store.updateUserPreferences();
|
||||
return
|
||||
}
|
||||
await ui_store.getUserPreferences();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<div>
|
||||
<q-card
|
||||
flat
|
||||
class="rounded-5 bg-transparent q-pa-none"
|
||||
class="rounded-5 bg-transparent q-pa-none fit"
|
||||
>
|
||||
<MenuTemplate
|
||||
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
||||
|
|
|
|||
|
|
@ -11,18 +11,15 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-md column fit">
|
||||
<div
|
||||
class="col-auto"
|
||||
style="transform: translate(10px, 12px);"
|
||||
>
|
||||
<div class="q-px-md column no-wrap">
|
||||
<div style="transform: translate(10px, 12px);">
|
||||
<span class="text-uppercase text-weight-bold text-accent bg-dark q-px-sm">
|
||||
{{ $t('profile.preferences.display_options') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto justify-center content-center q-mb-sm q-pa-sm rounded-5"
|
||||
class="col-auto justify-center content-center q-mb-sm q-pa-md rounded-5"
|
||||
:class="ui_store.is_mobile_mode ? 'column' : 'row'"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
|
|
@ -32,8 +29,8 @@
|
|||
clickable
|
||||
dense
|
||||
v-ripple
|
||||
class="col rounded-5 q-ma-sm shadow-4"
|
||||
:class="mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : ''"
|
||||
class="col row rounded-5 q-ma-sm shadow-4"
|
||||
:class="(mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : '') + ($q.platform.is.mobile ? ' full-width q-py-xs' : '')"
|
||||
@click="ui_store.user_preferences.is_dark_mode = mode.value"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="column flex-center"
|
||||
class="column flex-center fit"
|
||||
>
|
||||
<MenuHeader
|
||||
:user-first-name="firstName"
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<q-card
|
||||
class="col"
|
||||
:class="$q.screen.lt.sm ? 'full-width' : 'q-ml-sm'"
|
||||
:class="$q.platform.is.mobile ? 'fit' : 'q-ml-sm'"
|
||||
>
|
||||
<q-tab-panels
|
||||
v-model="current_menu"
|
||||
|
|
@ -49,8 +49,8 @@
|
|||
vertical
|
||||
transition-prev="jump-up"
|
||||
transition-next="jump-up"
|
||||
class="rounded-5"
|
||||
style="height: 50vh;"
|
||||
class="rounded-5 q-py-sm"
|
||||
:style="$q.platform.is.mobile ? '' : 'height: 50vh;'"
|
||||
>
|
||||
<slot name="panels"></slot>
|
||||
</q-tab-panels>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const boolean_filters = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="column bg-accent q-pa-sm">
|
||||
<span class="col">test</span>
|
||||
<span class="col">test</span>
|
||||
<span class="col">test</span>
|
||||
<span class="col">test</span>
|
||||
<div class="column text-weight-medium">
|
||||
<q-separator
|
||||
color="accent"
|
||||
size="5px"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<div class="col row">
|
||||
<q-checkbox
|
||||
v-model="boolean_filters"
|
||||
size="lg"
|
||||
val="inactive"
|
||||
color="accent"
|
||||
label="show inactive"
|
||||
class="col"
|
||||
/>
|
||||
<q-checkbox
|
||||
v-model="boolean_filters"
|
||||
size="lg"
|
||||
val="team"
|
||||
color="accent"
|
||||
label="show team only"
|
||||
class="col"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { QSelect, QInput } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const ui_store = useUiStore();
|
||||
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||
const select_ref = ref<QSelect | null>(null);
|
||||
const is_showing_comment_popup = ref(false);
|
||||
const comment_length = computed(() => shift.value.comment?.length ?? 0);
|
||||
|
||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'requestDelete': [void];
|
||||
'onTimeFieldBlur': [void];
|
||||
}>();
|
||||
|
||||
const onBlurShiftTypeSelect = () => {
|
||||
if (shift_type_selected.value === undefined) {
|
||||
shift.value.type = 'REGULAR';
|
||||
shift.value.id = 0;
|
||||
emit('requestDelete');
|
||||
}
|
||||
};
|
||||
|
||||
const getCommentCounterColor = (comment_length: number) => {
|
||||
if (comment_length < 200) return 'primary';
|
||||
if (comment_length < 250) return 'warning';
|
||||
return 'negative';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (ui_store.focus_next_component) {
|
||||
select_ref.value?.focus();
|
||||
select_ref.value?.showPopup();
|
||||
shift_type_selected.value = undefined;
|
||||
ui_store.focus_next_component = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row q-px-xs">
|
||||
<div class="col column">
|
||||
<div class="col row items-center text-uppercase q-px-xs rounded-5">
|
||||
<!-- comment button -->
|
||||
<q-btn
|
||||
v-if="ui_store.is_mobile_mode && !dense"
|
||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||
:text-color="shift.comment ? ((shift.is_approved && isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
||||
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
||||
@click="is_showing_comment_popup = true"
|
||||
>
|
||||
<q-dialog v-model="is_showing_comment_popup">
|
||||
<q-input
|
||||
color="white"
|
||||
v-model="shift.comment"
|
||||
dense
|
||||
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||
autofocus
|
||||
counter
|
||||
bottom-slots
|
||||
stack-label
|
||||
:label="$t('timesheet.shift.fields.header_comment')"
|
||||
:maxlength="COMMENT_LENGTH_MAX"
|
||||
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="edit" />
|
||||
</template>
|
||||
|
||||
<template #counter>
|
||||
<div class="row flex-center">
|
||||
<q-space />
|
||||
<q-knob
|
||||
v-model="comment_length"
|
||||
readonly
|
||||
:max="COMMENT_LENGTH_MAX"
|
||||
size="1.6em"
|
||||
:thickness="0.4"
|
||||
:color="getCommentCounterColor(comment_length)"
|
||||
track-color="grey-4"
|
||||
class="col-auto q-mr-xs"
|
||||
/>
|
||||
<span
|
||||
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(comment_length)"
|
||||
>{{ 280 - comment_length }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-dialog>
|
||||
</q-btn>
|
||||
|
||||
<!-- shift type -->
|
||||
<q-select
|
||||
ref="select"
|
||||
v-model="shift_type_selected"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
options-dense
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:options="SHIFT_OPTIONS"
|
||||
class="col rounded-5 bg-dark"
|
||||
:class="!shift.is_approved && !isTimesheetApproved ? '' : 'inset-shadow'"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
@blur="onBlurShiftTypeSelect"
|
||||
@update:model-value="option => shift.type = option.value"
|
||||
>
|
||||
<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"
|
||||
:color="scope.opt.icon_color"
|
||||
size="sm"
|
||||
class="col-auto"
|
||||
:class="shift.is_approved ? 'q-mx-xs' : 'q-mr-xs'"
|
||||
/>
|
||||
<span
|
||||
style="line-height: 1.2em;"
|
||||
class="col-auto ellipsis"
|
||||
:class="!shift.is_approved ? '' : 'text-white'"
|
||||
>
|
||||
{{ $t(scope.opt.label) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #after>
|
||||
<q-icon
|
||||
v-if="shift.is_approved"
|
||||
:name="shift.is_remote ? 'las la-laptop' : 'las la-building'"
|
||||
size="1.2em"
|
||||
color="white"
|
||||
class="q-mr-sm"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
class="text-uppercase text-weight-bold text-white bg-primary"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<q-toggle
|
||||
v-else
|
||||
v-model="shift.is_remote"
|
||||
:disable="shift.is_approved"
|
||||
dense
|
||||
keep-color
|
||||
size="3em"
|
||||
color="accent"
|
||||
icon="las la-building"
|
||||
checked-icon="las la-laptop"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 10]"
|
||||
class="text-uppercase text-weight-medium text-white bg-accent"
|
||||
>
|
||||
{{ shift.is_remote ? $t('timesheet.shift.types.REMOTE') :
|
||||
$t('timesheet.shift.types.OFFICE') }}
|
||||
</q-tooltip>
|
||||
</q-toggle>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col row items-start text-uppercase rounded-5 q-pa-xs">
|
||||
<!-- punch in field -->
|
||||
<div class="col q-pr-xs">
|
||||
<q-input
|
||||
v-model="shift.start_time"
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
type="time"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
label-slot
|
||||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
class="rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (!shift.is_approved && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
@blur="emit('onTimeFieldBlur')"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.in') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- punch out field -->
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
standout
|
||||
dense
|
||||
:borderless="(shift.is_approved && isTimesheetApproved)"
|
||||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
type="time"
|
||||
label-slot
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
class="rounded-5 bg-dark"
|
||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (shift.is_approved ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : (isTimesheetApproved ? 'inset-shadow' : ''))"
|
||||
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||
@blur="emit('onTimeFieldBlur')"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder"
|
||||
:class="shift.is_approved ? ' q-ml-md' : ''"
|
||||
style="font-size: 0.95em;"
|
||||
>{{ $t('shared.misc.out') }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
v-if="!shift.is_approved"
|
||||
flat
|
||||
dense
|
||||
color="negative"
|
||||
icon="las la-trash"
|
||||
size="lg"
|
||||
class="full-height"
|
||||
@click="$emit('requestDelete')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-separator
|
||||
v-if="hasShiftAfter"
|
||||
spaced
|
||||
class="q-mx-md col-12"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field--error) {
|
||||
background-color: var(--q-negative) !important;
|
||||
}
|
||||
|
||||
:deep(.q-field--error .q-field__bottom) {
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding-top: 0;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,12 +3,12 @@
|
|||
lang="ts"
|
||||
>
|
||||
/* eslint-disable*/
|
||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, nextTick } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { QSelect, QInput } from 'quasar';
|
||||
import { Shift, type ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
||||
import { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useShiftRules, SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
const { t } = useI18n();
|
||||
|
|
@ -22,15 +22,12 @@
|
|||
const start_time_ref = useTemplateRef<QInput>('start_time');
|
||||
const end_time_ref = useTemplateRef<QInput>('end_time');
|
||||
|
||||
const { dayShifts = [], dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
dayShifts: Shift[];
|
||||
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||
dense?: boolean;
|
||||
hasShiftAfter?: boolean;
|
||||
isTimesheetApproved?: boolean;
|
||||
}>();
|
||||
|
||||
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'), t('timesheet.errors.SHIFT_OVERLAP_SHORT'), dayShifts);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'saveComment': [comment: string, shift_id: number];
|
||||
'requestDelete': [void];
|
||||
|
|
@ -251,7 +248,6 @@
|
|||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
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 && !isTimesheetApproved ? '' : 'cursor-not-allowed inset-shadow')"
|
||||
|
|
@ -279,10 +275,8 @@
|
|||
:readonly="(shift.is_approved && isTimesheetApproved)"
|
||||
type="time"
|
||||
label-slot
|
||||
lazy-rules
|
||||
no-error-icon
|
||||
hide-bottom-space
|
||||
:rules="[shift_rules.isTimeRequiredRule, shift_rules.isShiftOverlapRule]"
|
||||
:label-color="!shift.is_approved ? 'accent' : 'white'"
|
||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'text-white cursor-not-allowed q-px-sm' : '')"
|
||||
input-style="font-size: 1.2em;"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
lang="ts"
|
||||
>
|
||||
import ShiftListDayRow from 'src/modules/timesheets/components/shift-list-day-row.vue';
|
||||
import ShiftListDayRowMobile from 'src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useShiftApi } from 'src/modules/timesheets/composables/use-shift-api';
|
||||
|
|
@ -69,15 +70,29 @@
|
|||
</q-btn>
|
||||
</transition>
|
||||
|
||||
<ShiftListDayRow
|
||||
<div
|
||||
v-for="shift, shift_index in day.shifts"
|
||||
:key="shift_index"
|
||||
class="col"
|
||||
>
|
||||
<ShiftListDayRowMobile
|
||||
v-if="$q.platform.is.mobile"
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:day-shifts="day.shifts"
|
||||
:is-timesheet-approved="approved"
|
||||
:dense="dense"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
/>
|
||||
|
||||
<ShiftListDayRow
|
||||
v-else
|
||||
v-model:shift="day.shifts[shift_index]!"
|
||||
:is-timesheet-approved="approved"
|
||||
:dense="dense"
|
||||
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||
@request-delete="deleteCurrentShift(shift)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -47,20 +47,24 @@
|
|||
return day.shifts.every(shift => shift.is_approved === true);
|
||||
}
|
||||
|
||||
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: {x?: number, y?: number}) => {
|
||||
const handleSwipe = async (direction: 'left' | 'up' | 'down' | 'right' | undefined, distance: { x?: number, y?: number }) => {
|
||||
mobile_animation_direction.value = direction === 'left' ? 'fadeInRight' : 'fadeInLeft';
|
||||
if (distance.x && Math.abs(distance.x) > 10 ) {
|
||||
await timesheet_api.getTimesheetsBySwiping( direction === 'left' ? 1 : -1 )
|
||||
if (distance.x && Math.abs(distance.x) > 10) {
|
||||
await timesheet_api.getTimesheetsBySwiping(direction === 'left' ? 1 : -1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col column fit relative-position" v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? {x: 0, y: 0})">
|
||||
<div
|
||||
class="col column fit relative-position"
|
||||
:style="$q.platform.is.mobile ? 'margin-bottom: 40px' : ''"
|
||||
v-touch-swipe="value => handleSwipe(value.direction, value.distance ?? { x: 0, y: 0 })"
|
||||
>
|
||||
<q-scroll-area
|
||||
ref="timesheet_page"
|
||||
:horizontal-offset="[0, 3]"
|
||||
class="absolute-full hide-scrollbar q-mt-sm"
|
||||
class="absolute-full hide-scrollbar"
|
||||
:thumb-style="{ opacity: '0' }"
|
||||
:bar-style="{ opacity: '0' }"
|
||||
style="min-height: 50vh;"
|
||||
|
|
@ -123,21 +127,20 @@
|
|||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day.date"
|
||||
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
||||
class="col-auto row q-ma-sm"
|
||||
:style="`animation-delay: ${day_index / 15}s;`"
|
||||
>
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && ($q.screen.width < $q.screen.height)"
|
||||
class="col column full-width"
|
||||
class="col column full-width q-px-md q-py-sm"
|
||||
>
|
||||
<q-card
|
||||
class="rounded-10"
|
||||
class="rounded-5 shadow-12"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
||||
>
|
||||
|
||||
<q-card-section
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
||||
class="text-weight-bolder text-uppercase text-h6 q-py-sm text-center relative-position"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent text-white' : 'bg-primary text-white'"
|
||||
style="line-height: 1em;"
|
||||
>
|
||||
|
|
@ -145,6 +148,15 @@
|
|||
weekday: 'long', day: 'numeric', month:
|
||||
'long'
|
||||
}) }}</span>
|
||||
|
||||
<q-icon
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
name="verified"
|
||||
size="3em"
|
||||
color="white"
|
||||
class="absolute-top-left z-top"
|
||||
style="top: -0.2em; left: 0px;"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
|
|
@ -166,26 +178,14 @@
|
|||
<q-btn
|
||||
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||
square
|
||||
dense
|
||||
size="xl"
|
||||
color="accent"
|
||||
icon="more_time"
|
||||
class="full-width"
|
||||
style="border-radius: 0 0 5px 5px;"
|
||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
<q-badge
|
||||
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||
floating
|
||||
class="transparent q-pa-none rounded-50"
|
||||
style="transform: translate(15px, -5px);"
|
||||
>
|
||||
<q-icon
|
||||
name="verified"
|
||||
size="5em"
|
||||
color="white"
|
||||
/>
|
||||
</q-badge>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
|
|
@ -194,13 +194,6 @@
|
|||
class="col row full-width"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||
>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInDown"
|
||||
leave-active-class="animated fadeOutUp"
|
||||
>
|
||||
</transition>
|
||||
<!-- List of shifts -->
|
||||
<div
|
||||
class="col row bg-dark"
|
||||
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
||||
|
|
@ -234,7 +227,7 @@
|
|||
/>
|
||||
<q-btn
|
||||
v-else
|
||||
:dense="!ui_store.is_mobile_mode"
|
||||
:dense="!$q.platform.is.mobile"
|
||||
icon="more_time"
|
||||
size="lg"
|
||||
color="accent"
|
||||
|
|
@ -250,10 +243,11 @@
|
|||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
|
||||
<q-page-sticky
|
||||
position="bottom-right"
|
||||
:offset="[0, -35]"
|
||||
:offset="$q.screen.width > $q.screen.height ? [15, 15] : [15, 65]"
|
||||
class="z-top"
|
||||
>
|
||||
<transition
|
||||
|
|
@ -272,7 +266,6 @@
|
|||
/>
|
||||
</transition>
|
||||
</q-page-sticky>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
<LoadingOverlay v-model="timesheet_store.is_loading" />
|
||||
|
||||
<div
|
||||
class="col-auto row items-center full-width"
|
||||
class="col-auto row items-center full-width q-px-lg"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'justify-between' : 'q-mt-md q-px-md'"
|
||||
>
|
||||
<!-- navigation btn -->
|
||||
|
|
@ -63,20 +63,7 @@
|
|||
@click="expenses_store.open"
|
||||
/>
|
||||
|
||||
<q-space v-if="!$q.platform.is.mobile" />
|
||||
|
||||
<!-- desktop save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="mode === 'normal' && !is_timesheets_approved && !$q.platform.is.mobile"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-mr-md'"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
<q-space v-if="$q.screen.width > $q.screen.height" />
|
||||
|
||||
<!-- desktop expenses button -->
|
||||
<q-btn
|
||||
|
|
@ -88,22 +75,108 @@
|
|||
:label="$t('timesheet.expense.open_btn')"
|
||||
@click="expenses_store.open"
|
||||
/>
|
||||
|
||||
<!-- desktop save timesheet changes button -->
|
||||
<q-btn
|
||||
v-if="mode === 'normal' && !is_timesheets_approved && $q.screen.width > $q.screen.height"
|
||||
push
|
||||
rounded
|
||||
:disable="timesheet_store.is_loading || has_shift_errors"
|
||||
:color="timesheet_store.is_loading || has_shift_errors ? 'grey-5' : 'accent'"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
:class="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? 'full-width' : 'q-ml-md'"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TimesheetErrorWidget class="col-auto"/>
|
||||
<TimesheetErrorWidget class="col-auto" />
|
||||
|
||||
<!-- mobile weekly overview widget -->
|
||||
<div
|
||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
class="col-auto row items-start q-px-sm q-pt-sm full-width"
|
||||
>
|
||||
<!-- per timesheet -->
|
||||
<div
|
||||
v-for="timesheet, timesheet_index in timesheet_store.timesheets"
|
||||
:key="timesheet_index"
|
||||
class="col column flex-center q-pa-sm"
|
||||
>
|
||||
<!-- container -->
|
||||
<div
|
||||
class="rounded-5 relative-position q-px-sm q-pt-sm q-pb-xs full-width shadow-4"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
<!-- label for week number -->
|
||||
<div
|
||||
class="self-start text-uppercase text-weight-bolder text-accent bg-secondary absolute-top-left q-px-xs"
|
||||
style="font-size: 0.8em; top: -7px; left: 10px; line-height: 1em;"
|
||||
>{{ $t('timesheet.week') + ` ${timesheet_index + 1}` }}</div>
|
||||
|
||||
<!-- hours worked in the week -->
|
||||
<div class="col-auto row">
|
||||
<span class="text-weight-bolder text-uppercase text-accent text-caption q-mr-sm">{{
|
||||
$t('timesheet.total_hours') }}</span>
|
||||
<span>{{
|
||||
(timesheet.weekly_hours.regular +
|
||||
timesheet.weekly_hours.evening +
|
||||
timesheet.weekly_hours.emergency +
|
||||
timesheet.weekly_hours.overtime).toFixed(2)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- label for current shifts preview -->
|
||||
<div
|
||||
class="col-auto full-width text-center text-weight-medium text-caption text-uppercase q-mt-xs"
|
||||
style="font-size: 0.65em; line-height: 1.2em;"
|
||||
> {{ $t('timesheet.current_shifts') }}</div>
|
||||
|
||||
<!-- preview of current number of shifts -->
|
||||
<div
|
||||
class="col row flex-center"
|
||||
style="height: 20px;"
|
||||
>
|
||||
<div
|
||||
v-for="day, day_index in timesheet.days"
|
||||
:key="day_index"
|
||||
class="col row flex-center"
|
||||
>
|
||||
<q-badge :color="day.shifts.length > 0 ? 'accent' : 'blue-grey-4'">
|
||||
<span class="text-weight-bolder">{{ day.shifts.length > 0 ? day.shifts.length : ''
|
||||
}}</span>
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- button to apply weekly schedule preset -->
|
||||
<div class="col-auto flex-center row q-pt-xs full-width">
|
||||
<q-btn
|
||||
v-if="timesheet.days.every(day => day.shifts.length < 1)"
|
||||
push
|
||||
dense
|
||||
color="accent"
|
||||
:label="$t('timesheet.apply_preset')"
|
||||
class="full-width"
|
||||
@click="timesheet_api.applyPreset(timesheet.timesheet_id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ShiftList />
|
||||
|
||||
<q-btn
|
||||
v-if="mode === 'approval' || $q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
push
|
||||
rounded
|
||||
v-if="$q.platform.is.mobile && $q.screen.width < $q.screen.height"
|
||||
square
|
||||
:disable="timesheet_store.is_loading"
|
||||
size="lg"
|
||||
color="accent"
|
||||
icon="upload"
|
||||
:label="$t('shared.label.save')"
|
||||
class="col-auto"
|
||||
:class="$q.platform.is.mobile && $q.screen.width < $q.screen.height ? 'full-width q-mt-sm' : 'q-mr-md'"
|
||||
class="col-auto absolute-bottom shadow-up-10 z-top"
|
||||
style="height: 50px;"
|
||||
@click="shift_api.saveShiftChanges"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { date, patterns, type ValidationRule } from "quasar";
|
||||
import { date } from "quasar";
|
||||
import type { SchedulePresetShift } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import type { Shift, ShiftOption } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
|
|
@ -26,16 +26,6 @@ export const isShiftOverlap = (shifts: Shift[] | SchedulePresetShift[]): boolean
|
|||
return false;
|
||||
};
|
||||
|
||||
export const useShiftRules = (time_required_error: string, overlap_error_string: string, day_shifts: Shift[]) => {
|
||||
const isTimeRequiredRule: ValidationRule<string> = (time_string: string) => (!!time_string && patterns.testPattern.time(time_string)) || time_required_error;
|
||||
const isShiftOverlapRule: ValidationRule<string> = (_time_string: string) => !isShiftOverlap(day_shifts) || overlap_error_string;
|
||||
|
||||
return {
|
||||
isTimeRequiredRule,
|
||||
isShiftOverlapRule
|
||||
};
|
||||
};
|
||||
|
||||
export const SHIFT_OPTIONS: ShiftOption[] = [
|
||||
{ label: 'timesheet.shift.types.REGULAR', value: 'REGULAR', icon: 'wb_sunny', icon_color: 'accent' },
|
||||
{ label: 'timesheet.shift.types.EVENING', value: 'EVENING', icon: 'bedtime', icon_color: 'orange-5' },
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ import { onMounted } from 'vue';
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="bg-secondary row items-center justify-center">
|
||||
<q-page class="bg-secondary row items-center justify-center fit">
|
||||
<MenuEmployee
|
||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||
class="col-xs-12 col-md-10 col-lg-7 col-xl-5"
|
||||
:class="$q.platform.is.mobile ? 'self-stretch' : ''"
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -16,8 +16,8 @@
|
|||
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
||||
const is_showing_filters = ref(false);
|
||||
|
||||
|
|
@ -58,6 +58,10 @@ const timesheet_store = useTimesheetStore();
|
|||
|
||||
<q-space />
|
||||
|
||||
<div
|
||||
class="col-auto row no-wrap items-start"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||
>
|
||||
<q-btn-toggle
|
||||
v-model="timesheet_store.is_approval_grid_mode"
|
||||
push
|
||||
|
|
@ -66,7 +70,7 @@ const timesheet_store = useTimesheetStore();
|
|||
text-color="accent"
|
||||
toggle-color="accent"
|
||||
class="col-auto"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-md'"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-sm' : 'q-mr-sm'"
|
||||
:options="[
|
||||
{ icon: 'grid_view', value: true },
|
||||
{ icon: 'view_list', value: false },
|
||||
|
|
@ -74,20 +78,6 @@ const timesheet_store = useTimesheetStore();
|
|||
style="height: 40px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="col-auto row no-wrap items-start"
|
||||
:class="$q.platform.is.mobile ? 'q-mb-md' : ''"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
icon="filter_alt"
|
||||
color="white"
|
||||
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
|
||||
class="col q-mr-sm self-stretch bg-accent"
|
||||
style="border-radius: 10px 10px 0 0;"
|
||||
@click="is_showing_filters = !is_showing_filters"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
push
|
||||
rounded
|
||||
|
|
@ -95,6 +85,7 @@ const timesheet_store = useTimesheetStore();
|
|||
color="accent"
|
||||
:label="$q.screen.lt.md ? '' : $t('shared.label.download')"
|
||||
class="col-auto q-mr-sm"
|
||||
style="height: 40px;"
|
||||
@click="timesheet_store.is_report_dialog_open = true"
|
||||
/>
|
||||
|
||||
|
|
@ -102,22 +93,31 @@ const timesheet_store = useTimesheetStore();
|
|||
v-model:search="timesheet_store.search_filter"
|
||||
class="col-auto q-mb-xs"
|
||||
/>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
icon="filter_alt"
|
||||
color="white"
|
||||
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
|
||||
class="col q-ml-sm self-stretch bg-accent"
|
||||
style="border-radius: 5px 5px 0 0;"
|
||||
@click="is_showing_filters = !is_showing_filters"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-slide-transition>
|
||||
<q-slide-transition class="col-auto">
|
||||
<OverviewListFilters
|
||||
v-if="is_showing_filters"
|
||||
class="q-mx-lg"
|
||||
class="q-mx-lg col-auto"
|
||||
/>
|
||||
</q-slide-transition>
|
||||
|
||||
<q-separator
|
||||
color="accent"
|
||||
size="4px"
|
||||
size="5px"
|
||||
class="q-mx-lg"
|
||||
/>
|
||||
|
||||
<OverviewReport />
|
||||
|
||||
<OverviewList class="col" />
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<template>
|
||||
<q-page
|
||||
padding
|
||||
class="column q-pa-md bg-secondary items-center"
|
||||
class="column bg-secondary items-center"
|
||||
>
|
||||
<PageHeaderTemplate
|
||||
:title="'timesheet.page_header'"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { defineStore } from 'pinia';
|
||||
import { Notify, LocalStorage, useQuasar, Dark } from 'quasar';
|
||||
import { computed, ref } from 'vue';
|
||||
import { LocalStorage, useQuasar, Dark } from 'quasar';
|
||||
import { Preferences } from 'src/modules/profile/models/preferences.models';
|
||||
import { ProfileService } from 'src/modules/profile/services/profile-service';
|
||||
import { useI18n, type ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
|
|
@ -44,7 +44,7 @@ export const useUiStore = defineStore('ui', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const updateUserPreferences = async (t: ComposerTranslation) => {
|
||||
const updateUserPreferences = async () => {
|
||||
try {
|
||||
if (user_preferences.value.id === -1) return;
|
||||
|
||||
|
|
@ -53,13 +53,11 @@ export const useUiStore = defineStore('ui', () => {
|
|||
Object.assign(user_preferences.value, response.data);
|
||||
LocalStorage.setItem('user_preferences', response.data);
|
||||
setPreferences();
|
||||
Notify.create({ message: t('profile.preferences.update_successful'), color: 'accent' });
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not update user preferences: ', error);
|
||||
}
|
||||
Notify.create({ message: t('profile.preferences.update_failed'), color: 'negative' })
|
||||
};
|
||||
|
||||
const setPreferences = () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user