Account view refactor
This commit is contained in:
parent
47c61a7b35
commit
55e0c68576
|
@ -59,7 +59,7 @@
|
||||||
"salix": "cd ../salix && gulp back",
|
"salix": "cd ../salix && gulp back",
|
||||||
"db": "cd ../salix && gulp docker",
|
"db": "cd ../salix && gulp docker",
|
||||||
"cy:open": "npm run db && cypress open",
|
"cy:open": "npm run db && cypress open",
|
||||||
"test:e2e": "npm run db && cypress run",
|
"test:e2e": "npm run db && cypress run --headed --config video=false",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"build": "rm -rf dist/ ; quasar build",
|
"build": "rm -rf dist/ ; quasar build",
|
||||||
"clean": "rm -rf dist/",
|
"clean": "rm -rf dist/",
|
||||||
|
|
|
@ -23,7 +23,7 @@ module.exports = configure(function (ctx) {
|
||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
|
||||||
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
|
boot: ['i18n', 'axios', 'vnDate', 'app'],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
|
||||||
css: ['app.scss', 'width.scss', 'responsive.scss'],
|
css: ['app.scss', 'width.scss', 'responsive.scss'],
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
export default async ({ app }) => {
|
|
||||||
/*
|
|
||||||
window.addEventListener('error',
|
|
||||||
e => onWindowError(e));
|
|
||||||
window.addEventListener('unhandledrejection',
|
|
||||||
e => onWindowRejection(e));
|
|
||||||
|
|
||||||
,onWindowError(event) {
|
|
||||||
errorHandler(event.error);
|
|
||||||
}
|
|
||||||
,onWindowRejection(event) {
|
|
||||||
errorHandler(event.reason);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
app.config.errorHandler = (err, vm, info) => {
|
|
||||||
errorHandler(err, vm)
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorHandler (err, vm) {
|
|
||||||
let message
|
|
||||||
let tMessage
|
|
||||||
let res = err.response
|
|
||||||
|
|
||||||
// XXX: Compatibility with old JSON service
|
|
||||||
if (err.name === 'JsonException') {
|
|
||||||
res = {
|
|
||||||
status: err.statusCode,
|
|
||||||
data: { error: { message: err.message } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
const status = res.status
|
|
||||||
|
|
||||||
if (status >= 400 && status < 500) {
|
|
||||||
switch (status) {
|
|
||||||
case 401:
|
|
||||||
tMessage = 'loginFailed'
|
|
||||||
break
|
|
||||||
case 403:
|
|
||||||
tMessage = 'authenticationRequired'
|
|
||||||
vm.$router.push('/login')
|
|
||||||
break
|
|
||||||
case 404:
|
|
||||||
tMessage = 'notFound'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
message = res.data.error.message
|
|
||||||
}
|
|
||||||
} else if (status >= 500) {
|
|
||||||
tMessage = 'internalServerError'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tMessage = 'somethingWentWrong'
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tMessage) {
|
|
||||||
message = vm.$t(tMessage)
|
|
||||||
}
|
|
||||||
vm.$q.notify({
|
|
||||||
message,
|
|
||||||
type: 'negative'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,231 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, onMounted, computed, Teleport, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { useAppStore } from 'stores/app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetch
|
||||||
|
formInitialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
autoLoad: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showBottomActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
saveFn: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
separationBetweenInputs: {
|
||||||
|
type: String,
|
||||||
|
default: 'xs'
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
urlUpdate: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
urlCreate: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
observeFormChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
description:
|
||||||
|
'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)'
|
||||||
|
},
|
||||||
|
mapper: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['onDataSaved', 'onDataFetched']);
|
||||||
|
const api = inject('api');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { notify } = useNotify();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const formData = ref({});
|
||||||
|
const formModelRef = ref(null);
|
||||||
|
const hasChanges = ref(!props.observeFormChanges);
|
||||||
|
const isResetting = ref(false);
|
||||||
|
const originalData = ref(null);
|
||||||
|
|
||||||
|
const separationBetweenInputs = computed(() => {
|
||||||
|
return `q-gutter-y-${props.separationBetweenInputs}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmitSuccess = () => {
|
||||||
|
emit('onDataSaved');
|
||||||
|
notify(t('dataSaved'), 'positive');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!props.formInitialData) {
|
||||||
|
if (props.autoLoad && props.url) await fetch();
|
||||||
|
originalData.value = { ...formData.value };
|
||||||
|
} else {
|
||||||
|
formData.value = { ...props.formInitialData };
|
||||||
|
originalData.value = { ...props.formInitialData };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.observeFormChanges) {
|
||||||
|
watch(
|
||||||
|
() => formData.value,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
if (!oldVal) return;
|
||||||
|
hasChanges.value =
|
||||||
|
!isResetting.value &&
|
||||||
|
JSON.stringify(newVal) !==
|
||||||
|
JSON.stringify(originalData.value);
|
||||||
|
isResetting.value = false;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
try {
|
||||||
|
let { data } = await api.get(props.url, {
|
||||||
|
params: { filter: JSON.stringify(props.filter) }
|
||||||
|
});
|
||||||
|
if (Array.isArray(data)) data = data[0] ?? {};
|
||||||
|
formData.value = { ...data };
|
||||||
|
emit('onDataFetched', formData.value);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async evt => {
|
||||||
|
const isFormValid = await formModelRef.value.validate();
|
||||||
|
if (isFormValid) await save(evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (props.observeFormChanges && !hasChanges.value)
|
||||||
|
return notify('noChanges', 'negative');
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const body = props.mapper
|
||||||
|
? props.mapper(formData.value, originalData.value)
|
||||||
|
: formData.value;
|
||||||
|
const method = props.urlCreate ? 'post' : 'patch';
|
||||||
|
const url = props.urlCreate || props.urlUpdate || props.url;
|
||||||
|
|
||||||
|
await Promise.resolve(
|
||||||
|
props.saveFn ? props.saveFn(body) : api[method](url, body)
|
||||||
|
);
|
||||||
|
|
||||||
|
onSubmitSuccess();
|
||||||
|
hasChanges.value = false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
formData,
|
||||||
|
submitForm
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<QCard class="form-container" v-bind="$attrs">
|
||||||
|
<QForm ref="formModelRef" class="form" :class="separationBetweenInputs">
|
||||||
|
<span v-if="title" class="text-h6 text-bold">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<slot name="form" :data="formData" />
|
||||||
|
<slot name="extraForm" :data="formData" />
|
||||||
|
<component
|
||||||
|
v-if="isHeaderMounted"
|
||||||
|
:is="showBottomActions ? 'div' : Teleport"
|
||||||
|
to="#actions"
|
||||||
|
class="flex row justify-end q-gutter-x-sm"
|
||||||
|
:class="{ 'q-mt-md': showBottomActions }"
|
||||||
|
>
|
||||||
|
<QBtn
|
||||||
|
v-if="defaultActions && showBottomActions"
|
||||||
|
:label="t('cancel')"
|
||||||
|
:icon="showBottomActions ? undefined : 'check'"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('cancel') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<QBtn
|
||||||
|
v-if="defaultActions"
|
||||||
|
:label="t('save')"
|
||||||
|
:icon="showBottomActions ? undefined : 'check'"
|
||||||
|
rounded
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
:loading="isLoading"
|
||||||
|
:disabled="!showBottomActions && !hasChanges"
|
||||||
|
@click="submitForm()"
|
||||||
|
data-cy="formModelDefaultSaveButton"
|
||||||
|
>
|
||||||
|
<QTooltip>{{ t('save') }}</QTooltip>
|
||||||
|
</QBtn>
|
||||||
|
<slot name="actions" :data="formData" />
|
||||||
|
</component>
|
||||||
|
</QForm>
|
||||||
|
</QCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 544px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,15 +1,20 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, useAttrs, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRequired } from 'src/composables/useRequired';
|
||||||
|
|
||||||
|
const $attrs = useAttrs();
|
||||||
|
const { isRequired, requiredFieldRule } = useRequired($attrs);
|
||||||
|
const { t } = useI18n();
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'update:modelValue',
|
'update:modelValue',
|
||||||
'update:options',
|
'update:options',
|
||||||
'keyup.enter',
|
'keyup.enter',
|
||||||
'remove'
|
'remove',
|
||||||
|
'blur'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const $props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: null
|
default: null
|
||||||
|
@ -25,24 +30,43 @@ const props = defineProps({
|
||||||
clearable: {
|
clearable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
emptyToNull: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
insertable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
maxlength: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
uppercase: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
|
|
||||||
const vnInputRef = ref(null);
|
const vnInputRef = ref(null);
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.modelValue;
|
return $props.modelValue;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if ($props.emptyToNull && value === '') value = null;
|
||||||
emit('update:modelValue', value);
|
emit('update:modelValue', value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const hover = ref(false);
|
const hover = ref(false);
|
||||||
const styleAttrs = computed(() => {
|
const styleAttrs = computed(() => {
|
||||||
return props.isOutlined
|
return $props.isOutlined
|
||||||
? { dense: true, outlined: true, rounded: true }
|
? {
|
||||||
|
dense: true,
|
||||||
|
outlined: true,
|
||||||
|
rounded: true
|
||||||
|
}
|
||||||
: {};
|
: {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,66 +75,119 @@ const focus = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
focus
|
focus,
|
||||||
|
vnInputRef
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputRules = [
|
const mixinRules = [
|
||||||
|
requiredFieldRule,
|
||||||
|
...($attrs.rules ?? []),
|
||||||
val => {
|
val => {
|
||||||
|
const { maxlength } = vnInputRef.value;
|
||||||
|
if (maxlength && +val.length > maxlength)
|
||||||
|
return t(`maxLength`, { value: maxlength });
|
||||||
const { min, max } = vnInputRef.value.$attrs;
|
const { min, max } = vnInputRef.value.$attrs;
|
||||||
if (min >= 0) {
|
if (!min) return null;
|
||||||
|
if (min >= 0)
|
||||||
if (Math.floor(val) < min) return t('inputMin', { value: min });
|
if (Math.floor(val) < min) return t('inputMin', { value: min });
|
||||||
}
|
if (!max) return null;
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
if (Math.floor(val) > max) return t('inputMax', { value: max });
|
if (Math.floor(val) > max) return t('inputMax', { value: max });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleKeydown = e => {
|
||||||
|
if (e.key === 'Backspace') return;
|
||||||
|
|
||||||
|
if ($props.insertable && e.key.match(/[0-9]/)) {
|
||||||
|
handleInsertMode(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInsertMode = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = e.target;
|
||||||
|
const cursorPos = input.selectionStart;
|
||||||
|
const { maxlength } = vnInputRef.value;
|
||||||
|
let currentValue = value.value;
|
||||||
|
if (!currentValue) currentValue = e.key;
|
||||||
|
const newValue = e.key;
|
||||||
|
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
|
||||||
|
value.value =
|
||||||
|
currentValue.substring(0, cursorPos) +
|
||||||
|
newValue +
|
||||||
|
currentValue.substring(cursorPos + 1);
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUppercase = () => {
|
||||||
|
value.value = value.value?.toUpperCase() || '';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div @mouseover="hover = true" @mouseleave="hover = false">
|
||||||
:rules="$attrs.required ? [requiredFieldRule] : null"
|
|
||||||
@mouseover="hover = true"
|
|
||||||
@mouseleave="hover = false"
|
|
||||||
>
|
|
||||||
<QInput
|
<QInput
|
||||||
ref="vnInputRef"
|
ref="vnInputRef"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
v-bind="{ ...$attrs, ...styleAttrs }"
|
v-bind="{ ...$attrs, ...styleAttrs }"
|
||||||
:type="$attrs.type"
|
:type="$attrs.type"
|
||||||
:class="{ required: $attrs.required }"
|
:class="{ required: isRequired }"
|
||||||
|
@keyup.enter="emit('keyup.enter')"
|
||||||
|
@blur="emit('blur')"
|
||||||
|
@keydown="handleKeydown"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
:rules="inputRules"
|
:rules="mixinRules"
|
||||||
:lazy-rules="true"
|
:lazy-rules="true"
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
@keyup.enter="emit('keyup.enter')"
|
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
|
||||||
>
|
>
|
||||||
<template
|
<template #prepend v-if="$slots.prepend">
|
||||||
v-if="$slots.prepend"
|
|
||||||
#prepend
|
|
||||||
>
|
|
||||||
<slot name="prepend" />
|
<slot name="prepend" />
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<slot
|
|
||||||
v-if="$slots.append && !$attrs.disabled"
|
|
||||||
name="append"
|
|
||||||
/>
|
|
||||||
<QIcon
|
<QIcon
|
||||||
v-if="hover && value && !$attrs.disabled && props.clearable"
|
|
||||||
name="close"
|
name="close"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
:style="{
|
||||||
|
visibility:
|
||||||
|
hover &&
|
||||||
|
value &&
|
||||||
|
!$attrs.disabled &&
|
||||||
|
!$attrs.readonly &&
|
||||||
|
$props.clearable
|
||||||
|
? 'visible'
|
||||||
|
: 'hidden'
|
||||||
|
}"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
value = null;
|
value = null;
|
||||||
|
vnInputRef.focus();
|
||||||
emit('remove');
|
emit('remove');
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
></QIcon>
|
||||||
|
|
||||||
<QIcon
|
<QIcon
|
||||||
v-if="info"
|
name="match_case"
|
||||||
name="info"
|
size="xs"
|
||||||
|
v-if="
|
||||||
|
!$attrs.disabled && !$attrs.readonly && $props.uppercase
|
||||||
|
"
|
||||||
|
@click="handleUppercase"
|
||||||
|
class="uppercase-icon"
|
||||||
>
|
>
|
||||||
|
<QTooltip>
|
||||||
|
{{ t('Convert to uppercase') }}
|
||||||
|
</QTooltip>
|
||||||
|
</QIcon>
|
||||||
|
|
||||||
|
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
|
||||||
|
<QIcon v-if="info" name="info">
|
||||||
<QTooltip max-width="350px">
|
<QTooltip max-width="350px">
|
||||||
{{ info }}
|
{{ info }}
|
||||||
</QTooltip>
|
</QTooltip>
|
||||||
|
@ -120,20 +197,44 @@ const inputRules = [
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.uppercase-icon {
|
||||||
|
transition:
|
||||||
|
color 0.3s,
|
||||||
|
transform 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uppercase-icon:hover {
|
||||||
|
color: #ed9937;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
en-US:
|
en-US:
|
||||||
inputMin: Must be more than {value}
|
inputMin: Must be more than {value}
|
||||||
|
maxLength: The value exceeds {value} characters
|
||||||
inputMax: Must be less than {value}
|
inputMax: Must be less than {value}
|
||||||
|
Convert to uppercase: Convert to uppercase
|
||||||
es-ES:
|
es-ES:
|
||||||
inputMin: Debe ser mayor a {value}
|
inputMin: Debe ser mayor a {value}
|
||||||
|
maxLength: El valor excede los {value} carácteres
|
||||||
inputMax: Debe ser menor a {value}
|
inputMax: Debe ser menor a {value}
|
||||||
|
Convert to uppercase: Convertir a mayúsculas
|
||||||
ca-ES:
|
ca-ES:
|
||||||
inputMin: Ha de ser més gran que {value}
|
inputMin: Ha de ser més gran que {value}
|
||||||
inputMax: Ha de ser menys que {value}
|
maxLength: El valor excedeix els {value} caràcters
|
||||||
|
inputMax: Ha de ser menor que {value}
|
||||||
|
Convert to uppercase: Convertir a majúscules
|
||||||
fr-FR:
|
fr-FR:
|
||||||
inputMin: Doit être supérieur à {value}
|
inputMin: Doit être supérieur à {value}
|
||||||
inputMax: Doit être supérieur à {value}
|
maxLength: La valeur dépasse {value} caractères
|
||||||
|
inputMax: Doit être inférieur à {value}
|
||||||
|
Convert to uppercase: Convertir en majuscules
|
||||||
pt-PT:
|
pt-PT:
|
||||||
inputMin: Deve ser maior que {value}
|
inputMin: Deve ser maior que {value}
|
||||||
inputMax: Deve ser maior que {value}
|
maxLength: O valor excede {value} caracteres
|
||||||
|
inputMax: Deve ser menor que {value}
|
||||||
|
Convert to uppercase: Converter para maiúsculas
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|
|
@ -67,7 +67,7 @@ const $props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const requiredFieldRule = val => val ?? t('globals.fieldRequired');
|
const requiredFieldRule = val => val ?? t('fieldRequired');
|
||||||
|
|
||||||
const { optionLabel, optionValue, options } = toRefs($props);
|
const { optionLabel, optionValue, options } = toRefs($props);
|
||||||
const myOptions = ref([]);
|
const myOptions = ref([]);
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
export function useRequired($attrs) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const isRequired =
|
||||||
|
typeof $attrs['required'] === 'boolean'
|
||||||
|
? $attrs['required']
|
||||||
|
: Object.keys($attrs).includes('required');
|
||||||
|
const requiredFieldRule = val =>
|
||||||
|
isRequired ? !!val || t('fieldRequired') : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRequired,
|
||||||
|
requiredFieldRule
|
||||||
|
};
|
||||||
|
}
|
|
@ -134,6 +134,8 @@ export default {
|
||||||
introduceSearchTerm: 'Introdueix un terme de cerca',
|
introduceSearchTerm: 'Introdueix un terme de cerca',
|
||||||
noOrdersFound: `No s'han trobat comandes`,
|
noOrdersFound: `No s'han trobat comandes`,
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Aquest camp és obligatori',
|
||||||
|
noChanges: 'No s’han fet canvis',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'No es pot bloquejar la memòria cau',
|
'Cant lock cache': 'No es pot bloquejar la memòria cau',
|
||||||
'Bad file format': 'Format de fitxer no reconegut',
|
'Bad file format': 'Format de fitxer no reconegut',
|
||||||
|
|
|
@ -167,6 +167,8 @@ export default {
|
||||||
introduceSearchTerm: 'Enter a search term',
|
introduceSearchTerm: 'Enter a search term',
|
||||||
noOrdersFound: 'No orders found',
|
noOrdersFound: 'No orders found',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
fieldRequired: 'Field required',
|
||||||
|
noChanges: 'No changes',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'The cache could not be blocked',
|
'Cant lock cache': 'The cache could not be blocked',
|
||||||
'Bad file format': 'Unrecognized file format',
|
'Bad file format': 'Unrecognized file format',
|
||||||
|
|
|
@ -166,6 +166,8 @@ export default {
|
||||||
introduceSearchTerm: 'Introduce un término de búsqueda',
|
introduceSearchTerm: 'Introduce un término de búsqueda',
|
||||||
noOrdersFound: 'No se encontrado pedidos',
|
noOrdersFound: 'No se encontrado pedidos',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Campo requerido',
|
||||||
|
noChanges: 'No se han hecho cambios',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'La caché no pudo ser bloqueada',
|
'Cant lock cache': 'La caché no pudo ser bloqueada',
|
||||||
'Bad file format': 'Formato de archivo no reconocido',
|
'Bad file format': 'Formato de archivo no reconocido',
|
||||||
|
|
|
@ -134,6 +134,8 @@ export default {
|
||||||
introduceSearchTerm: 'Entrez un terme de recherche',
|
introduceSearchTerm: 'Entrez un terme de recherche',
|
||||||
noOrdersFound: 'Aucune commande trouvée',
|
noOrdersFound: 'Aucune commande trouvée',
|
||||||
send: 'Envoyer',
|
send: 'Envoyer',
|
||||||
|
fieldRequired: 'Champ obligatoire',
|
||||||
|
noChanges: 'Aucun changement',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': "Le cache n'a pas pu être verrouillé",
|
'Cant lock cache': "Le cache n'a pas pu être verrouillé",
|
||||||
'Bad file format': 'Format de fichier non reconnu',
|
'Bad file format': 'Format de fichier non reconnu',
|
||||||
|
|
|
@ -133,6 +133,8 @@ export default {
|
||||||
introduceSearchTerm: 'Digite um termo de pesquisa',
|
introduceSearchTerm: 'Digite um termo de pesquisa',
|
||||||
noOrdersFound: 'Nenhum pedido encontrado',
|
noOrdersFound: 'Nenhum pedido encontrado',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Campo obrigatório',
|
||||||
|
noChanges: 'Sem alterações',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'O cache não pôde ser bloqueado',
|
'Cant lock cache': 'O cache não pôde ser bloqueado',
|
||||||
'Bad file format': 'Formato de arquivo inválido',
|
'Bad file format': 'Formato de arquivo inválido',
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { ref, inject, onMounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import VnInput from 'src/components/common/VnInput.vue';
|
import VnInput from 'src/components/common/VnInput.vue';
|
||||||
import VnSelect from 'src/components/common/VnSelect.vue';
|
import VnSelect from 'src/components/common/VnSelect.vue';
|
||||||
import VnForm from 'src/components/common/VnForm.vue';
|
|
||||||
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
|
import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue';
|
||||||
|
import FormModel from 'src/components/common/FormModel.vue';
|
||||||
|
import useNotify from 'src/composables/useNotify.js';
|
||||||
|
|
||||||
import { useUserStore } from 'stores/user';
|
import { useUserStore } from 'stores/user';
|
||||||
import { useAppStore } from 'stores/app';
|
import { useAppStore } from 'stores/app';
|
||||||
|
@ -12,54 +13,76 @@ import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const jApi = inject('jApi');
|
const api = inject('api');
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { isHeaderMounted } = storeToRefs(appStore);
|
const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
const { user } = storeToRefs(userStore);
|
const { user } = storeToRefs(userStore);
|
||||||
|
const { notify } = useNotify();
|
||||||
|
|
||||||
const vnFormRef = ref(null);
|
const vnFormRef = ref(null);
|
||||||
const vnFormRef2 = ref(null);
|
|
||||||
const changePasswordFormDialog = ref(null);
|
const changePasswordFormDialog = ref(null);
|
||||||
const showChangePasswordForm = ref(false);
|
const showChangePasswordForm = ref(false);
|
||||||
const langOptions = ref([]);
|
const langOptions = ref([]);
|
||||||
const pks = computed(() => ({ id: userStore?.user?.id }));
|
const formInitialData = ref({});
|
||||||
const fetchConfigDataSql = {
|
const showForm = ref(false);
|
||||||
query: `
|
|
||||||
SELECT u.id, u.name, u.email, u.nickname,
|
|
||||||
u.lang, c.isToBeMailed, c.id clientFk
|
|
||||||
FROM account.myUser u
|
|
||||||
LEFT JOIN myClient c
|
|
||||||
ON u.id = c.id`,
|
|
||||||
params: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLanguagesSql = async () => {
|
const fetchLanguages = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await jApi.query(
|
const filter = { fields: ['code', 'name'], where: { isActive: true } };
|
||||||
'SELECT code, name FROM language WHERE isActive'
|
const { data } = await api.get('/languages', {
|
||||||
);
|
params: { filter: JSON.stringify(filter) }
|
||||||
|
});
|
||||||
langOptions.value = data;
|
langOptions.value = data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchFormInitialData = async () => {
|
||||||
|
try {
|
||||||
|
const filter = {
|
||||||
|
where: { id: user?.value?.id },
|
||||||
|
fields: ['id', 'name', 'isToBeMailed'],
|
||||||
|
include: {
|
||||||
|
relation: 'user',
|
||||||
|
scope: {
|
||||||
|
fields: ['nickname', 'lang', 'email']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { data } = await api.get('/Clients', {
|
||||||
|
params: { filter: JSON.stringify(filter) }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user: userData, ...restOfData } = data[0];
|
||||||
|
|
||||||
|
formInitialData.value = {
|
||||||
|
...restOfData,
|
||||||
|
nickname: userData?.nickname,
|
||||||
|
lang: userData?.lang,
|
||||||
|
email: userData?.email
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
showForm.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUserNickname = async nickname => {
|
const updateUserNickname = async nickname => {
|
||||||
try {
|
try {
|
||||||
await vnFormRef.value.submit();
|
await submitAccountData(nickname);
|
||||||
|
await submitNickname(nickname);
|
||||||
user.value.nickname = nickname;
|
user.value.nickname = nickname;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMailData = data => {
|
|
||||||
data.isToBeMailed = Boolean(data.isToBeMailed);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateConfigLang = async lang => {
|
const updateConfigLang = async lang => {
|
||||||
try {
|
try {
|
||||||
await vnFormRef.value.submit();
|
if (!lang) return;
|
||||||
|
await submitAccountData({ lang });
|
||||||
userStore.updateUserLang(lang);
|
userStore.updateUserLang(lang);
|
||||||
const siteLocaleLang = appStore.localeOptions.find(
|
const siteLocaleLang = appStore.localeOptions.find(
|
||||||
locale => locale.value === lang
|
locale => locale.value === lang
|
||||||
|
@ -70,7 +93,32 @@ const updateConfigLang = async lang => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => fetchLanguagesSql());
|
const submitAccountData = async data => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
await api.patch(`/VnUsers/${user?.value?.id}`, params);
|
||||||
|
notify(t('dataSaved'), 'positive');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitIsToBeMailed = async isToBeMailed => {
|
||||||
|
try {
|
||||||
|
const payload = { isToBeMailed };
|
||||||
|
await api.patch(`/Clients/${user.value.id}`, payload);
|
||||||
|
notify(t('dataSaved'), 'positive');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
fetchLanguages();
|
||||||
|
fetchFormInitialData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -92,14 +140,14 @@ onMounted(() => fetchLanguagesSql());
|
||||||
@click="showChangePasswordForm = true"
|
@click="showChangePasswordForm = true"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
<VnForm
|
<FormModel
|
||||||
|
v-if="showForm"
|
||||||
ref="vnFormRef"
|
ref="vnFormRef"
|
||||||
|
:save-fn="submitFormFn"
|
||||||
|
:form-initial-data="formInitialData"
|
||||||
:title="t('personalInformation')"
|
:title="t('personalInformation')"
|
||||||
:fetch-form-data-sql="fetchConfigDataSql"
|
:show-bottom-actions="false"
|
||||||
:pks="pks"
|
:defaultActions="false"
|
||||||
table="myUser"
|
|
||||||
schema="account"
|
|
||||||
:default-actions="false"
|
|
||||||
>
|
>
|
||||||
<template #form="{ data }">
|
<template #form="{ data }">
|
||||||
<VnInput
|
<VnInput
|
||||||
|
@ -111,14 +159,16 @@ onMounted(() => fetchLanguagesSql());
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="data.email"
|
v-model="data.email"
|
||||||
:label="t('email')"
|
:label="t('email')"
|
||||||
@keyup.enter="vnFormRef.submit()"
|
@keyup.enter="submitAccountData({ email: data.email })"
|
||||||
@blur="vnFormRef.submit()"
|
@blur="submitAccountData({ email: data.email })"
|
||||||
/>
|
/>
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="data.nickname"
|
v-model="data.nickname"
|
||||||
:label="t('nickname')"
|
:label="t('nickname')"
|
||||||
@keyup.enter="updateUserNickname(data.nickname)"
|
@keyup.enter="
|
||||||
@blur="updateUserNickname(data.nickname)"
|
updateUserNickname({ nickname: data.nickname })
|
||||||
|
"
|
||||||
|
@blur="updateUserNickname({ nickname: data.nickname })"
|
||||||
data-cy="configViewNickname"
|
data-cy="configViewNickname"
|
||||||
/>
|
/>
|
||||||
<VnSelect
|
<VnSelect
|
||||||
|
@ -130,30 +180,17 @@ onMounted(() => fetchLanguagesSql());
|
||||||
@update:model-value="updateConfigLang(data.lang)"
|
@update:model-value="updateConfigLang(data.lang)"
|
||||||
data-cy="configViewLang"
|
data-cy="configViewLang"
|
||||||
/>
|
/>
|
||||||
|
<QCheckbox
|
||||||
|
v-model="data.isToBeMailed"
|
||||||
|
:label="t('isToBeMailed')"
|
||||||
|
:toggle-indeterminate="false"
|
||||||
|
@update:model-value="
|
||||||
|
submitIsToBeMailed(data.isToBeMailed)
|
||||||
|
"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #extraForm>
|
</FormModel>
|
||||||
<VnForm
|
|
||||||
class="no-form-container q-mt-md"
|
|
||||||
ref="vnFormRef2"
|
|
||||||
:pks="pks"
|
|
||||||
table="myClient"
|
|
||||||
schema="hedera"
|
|
||||||
:fetch-form-data-sql="fetchConfigDataSql"
|
|
||||||
:default-actions="false"
|
|
||||||
@on-data-fetched="$event => formatMailData($event)"
|
|
||||||
>
|
|
||||||
<template #form="{ data }">
|
|
||||||
<QCheckbox
|
|
||||||
v-model="data.isToBeMailed"
|
|
||||||
:label="t('isToBeMailed')"
|
|
||||||
:toggle-indeterminate="false"
|
|
||||||
@update:model-value="vnFormRef2.submit()"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VnForm>
|
|
||||||
</template>
|
|
||||||
</VnForm>
|
|
||||||
</QPage>
|
</QPage>
|
||||||
<QDialog
|
<QDialog
|
||||||
ref="changePasswordFormDialog"
|
ref="changePasswordFormDialog"
|
||||||
|
|
|
@ -38,8 +38,8 @@ export const useUserStore = defineStore('user', () => {
|
||||||
router.push({ name: 'login' });
|
router.push({ name: 'login' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await fetchTokenConfig();
|
|
||||||
await fetchUser();
|
await fetchUser();
|
||||||
|
await fetchTokenConfig();
|
||||||
await supplantInit();
|
await supplantInit();
|
||||||
startInterval();
|
startInterval();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue