#6772 - Reload data section when change Id #192

Merged
jsegarra merged 34 commits from 6772_reload_sections into dev 2024-08-27 15:17:45 +00:00
406 changed files with 23710 additions and 11684 deletions
Showing only changes of commit 4df52d2692 - Show all commits
.eslintrc.cjsCHANGELOG.mdJenkinsfilechangelog.shdocker-compose.ymlpackage.json
public
src

View File

@ -1,3 +1,100 @@
# Version 24.24 - 2024-06-11
### Added 🆕
- feat: 6942 hashtag in key : value summary by:jgallego
- feat: #6957: Rename FetchedTags instance tag by:Javier Segarra
- feat: refactor template by:Javier Segarra
- feat: refs #6600 Add option to add comment for photo motivation by:jorgep
- feat: refs #6942 test e2e tobook & toUnbook by:jorgep
- feat: refs #6942 to book summary button & reactive value by:jorgep
- feat: refs #6942 to unbook by:jorgep
- feat: refs #6942 url update by:jorgep
- feat: refs #6942 use correct currency in InvoiceIn components by:jorgep
- feat: refs #6942 vat rate total by:jorgep
- feat: refs #7494 new icons (7494-icons) by:alexm
- feat: refs #7494 new icons by:alexm
- feat: refs #7542 drop space by:jorgep
- feat: refs #7542 empty by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: style by:Javier Segarra
- style: color transparent when is fetive by:Javier Segarra
- style: fix color when is empty by:Javier Segarra
- style: reset poc style (6957_refactorFetechedTags) by:Javier Segarra
- style: reset poc style by:Javier Segarra
- style updates by:Javier Segarra
### Changed 📦
- feat: refactor template by:Javier Segarra
- perf: 6957 add color as new shared variable by:Javier Segarra
- perf: 6957 change fetchedTags color by:Javier Segarra
- perf: remove local tree variable by:Javier Segarra
- refactor: add flat by:alexm
- refactor: refs #6600 replace QInput to VnInput by:jorgep
- refactor: refs #6652 improved defaulter section by:Jon
- refactor: refs #6942 Fix getTotalAmount function to correctly calculate the total amount in InvoiceInDueDay.vue by:jorgep
- refactor: refs #6942 new summary layout by:jorgep
- refactor: refs #6942 store key & actions by:jorgep
- refactor: refs #6942 summary by:jorgep
- refactor: refs #6942 use router hook by:jorgep
- refactor: refs #6942 WIP summary layout by:jorgep
### Fixed 🛠️
- fix: 9-12 by:Javier Segarra
- fix: defaulter icon by:alexm
- fix: refs #5186 validation by:jorgep
- fix: refs #6095 add reFfk null on search by:pablone
- fix: refs #6942 cardDescriptor use store if its popup or different source data by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: refs #6942 drop comments by:jorgep
- fix: refs #6942 drop console by:jorgep
- fix: refs #6942 drop console.log by:jorgep
- fix: refs #6942 e2e test (origin/6942-warmfix-fixFormModel) by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 fix emit on data saved by:jorgep
- fix: refs #6942 fix emit on reset by:jorgep
- fix: refs #6942 fix vncard by:jorgep
- fix: refs #6942 formModel & CardDescriptor by:jorgep
- fix: refs #6942 formModel watch changes & invoiceInCreate by:jorgep
- fix: refs #6942 import by:jorgep
- fix: refs #6942 reloading by:jorgep
- fix: refs #6942 rollback by:jorgep
- fix: refs #6942 selectable expense by:jorgep
- fix: refs #6942 skip e2e tests by:jorgep
- fix: refs #6942 table bottom highlight & drop isBooked field by:jorgep
- fix: refs #6942 tests e2e by:jorgep
- fix: refs #6942 tests & summary table spacing by:jorgep
- fix: refs #6942 unit tests by:jorgep
- fix: refs #6942 vnLocation by:jorgep
- fix: refs #6942 wip: formModel by:jorgep
- fix: refs #7542 use right panel by:jorgep
- fix: searchbar redirect by:alexm
- fix: style by:Javier Segarra
- fix: WorkerCalendarItem by:Javier Segarra
- mini fix by:wbuezas
- refs #6111 clean code fix changes by:carlossa
- refs #6111 fix merge, fix column by:carlossa
- refs #6111 fix qtable, actions, scroll by:carlossa
- refs #6111 fix routeList by:carlossa
- refs #6111 fix sticky by:carlossa
- refs #6111 fix trad remove logs by:carlossa
- refs #6111 fix visibleColumns by:carlossa
- refs #6111 routeList fix by:carlossa
- refs #6332 fix calendar by:carlossa
- refs #6332 fix colors by:carlossa
- refs #6332 fix festive by:carlossa
- refs #6820 fix BasicData Tickets by:carlossa
- refs #6820 fix error front by:carlossa
- refs #6820 fix traduction by:carlossa
- refs #7391 fix textarea by:carlossa
- refs #7396 fix summary by:carlossa
- Search childs fix by:wbuezas
- small fix by:wbuezas
- style: fix color when is empty by:Javier Segarra
# Changelog
All notable changes to this project will be documented in this file.
@ -7,6 +104,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2420.01]
### Added
- (Item) => Se añade la opción de añadir un comentario del motivo de hacer una foto
- (Worker) => Se añade la opción de crear un trabajador ajeno a la empresa
- (Route) => Ahora se muestran todos los cmrs
## [2418.01]
## [2416.01] - 2024-04-18

16
Jenkinsfile vendored
View File

@ -54,7 +54,6 @@ pipeline {
}
environment {
PROJECT_NAME = 'lilium'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
}
stages {
stage('Install') {
@ -95,7 +94,7 @@ pipeline {
sh 'quasar build'
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
dockerBuild()
}
@ -104,15 +103,18 @@ pipeline {
when {
expression { PROTECTED_BRANCH }
}
environment {
DOCKER_HOST = "${env.SWARM_HOST}"
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',
namespace: 'lilium'
]) {
sh 'kubectl set image deployment/lilium-$BRANCH_NAME lilium-$BRANCH_NAME=$REGISTRY/salix-frontend:$VERSION'
}
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
}
}
}

34
changelog.sh Normal file
View File

@ -0,0 +1,34 @@
features_types=(chore feat style)
changes_types=(refactor perf)
fix_types=(fix revert)
file="CHANGELOG.md"
file_tmp="temp_log.txt"
file_current_tmp="temp_current_log.txt"
setType(){
echo "### $1" >> $file_tmp
arr=("$@")
echo "" > $file_current_tmp
for i in "${arr[@]}"
do
git log --grep="$i" --oneline --no-merges --format="- %s %d by:%an" master..test >> $file_current_tmp
done
# remove duplicates
sort -o $file_current_tmp -u $file_current_tmp
cat $file_current_tmp >> $file_tmp
echo "" >> $file_tmp
# remove tmp current file
[ -e $file_current_tmp ] && rm $file_current_tmp
}
echo "# Version XX.XX - XXXX-XX-XX" >> $file_tmp
echo "" >> $file_tmp
setType "Added 🆕" "${features_types[@]}"
setType "Changed 📦" "${changes_types[@]}"
setType "Fixed 🛠️" "${fix_types[@]}"
cat $file >> $file_tmp
mv $file_tmp $file

View File

@ -1,17 +1,7 @@
version: '3.7'
services:
main:
image: registry.verdnatura.es/salix-frontend:${BRANCH_NAME:?}
image: registry.verdnatura.es/salix-frontend:${VERSION:?}
build:
context: .
dockerfile: ./Dockerfile
ports:
- 4000
deploy:
replicas: ${FRONT_REPLICAS:?}
placement:
constraints:
- node.role == worker
resources:
limits:
memory: 1G

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "24.22.0",
"version": "24.30.1",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",

BIN
public/no-image-dark.png Normal file

Binary file not shown.

Before

(image error) Size: 6.9 KiB

After

(image error) Size: 6.9 KiB

BIN
public/no-image.png Normal file

Binary file not shown.

Before

(image error) Size: 2.1 KiB

After

(image error) Size: 2.1 KiB

BIN
public/no-user.png Normal file

Binary file not shown.

Before

(image error) Size: 9.9 KiB

After

(image error) Size: 9.9 KiB

View File

@ -1,11 +1,10 @@
import axios from 'axios';
import { Notify } from 'quasar';
import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router';
import { i18n } from './i18n';
import useNotify from 'src/composables/useNotify.js';
const session = useSession();
const { t } = i18n.global;
const { notify } = useNotify();
axios.defaults.baseURL = '/api/';
@ -27,10 +26,7 @@ const onResponse = (response) => {
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
Notify.create({
message: t('globals.dataSaved'),
type: 'positive',
});
notify('globals.dataSaved', 'positive');
}
return response;
@ -59,18 +55,15 @@ const onResponseError = (error) => {
}
if (session.isLoggedIn() && response?.status === 401) {
session.destroy();
session.destroy(false);
const hash = window.location.hash;
const url = hash.slice(1);
Router.push({ path: url });
Router.push(`/login?redirect=${url}`);
} else if (!session.isLoggedIn()) {
return Promise.reject(error);
}
Notify.create({
message: t(message),
type: 'negative',
});
notify(message, 'negative');
return Promise.reject(error);
};

View File

@ -1,21 +1,47 @@
import { getCurrentInstance } from 'vue';
const filterAvailableInput = element => element.classList.contains('q-field__native') && !element.disabled
const filterAvailableText = element => element.__vueParentComponent.type.name === 'QInput' && element.__vueParentComponent?.attrs?.class !== 'vn-input-date';
const filterAvailableInput = (element) => {
return element.classList.contains('q-field__native') && !element.disabled;
};
const filterAvailableText = (element) => {
return (
element.__vueParentComponent.type.name === 'QInput' &&
element.__vueParentComponent?.attrs?.class !== 'vn-input-date'
);
};
export default {
mounted: function () {
const vm = getCurrentInstance();
if (vm.type.name === 'QForm')
if (!['searchbarForm','filterPanelForm'].includes(this.$el?.id)) {
if (vm.type.name === 'QForm') {
if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) {
// AUTOFOCUS
const elementsArray = Array.from(this.$el.elements);
const firstInputElement = elementsArray.filter(filterAvailableInput).find(filterAvailableText);
const availableInputs = elementsArray.filter(filterAvailableInput);
const firstInputElement = availableInputs.find(filterAvailableText);
if (firstInputElement) {
firstInputElement.focus();
}
const that = this;
this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') {
const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;
input.value =
input.value.substring(0, selectionStart) +
'\n' +
input.value.substring(selectionEnd);
selectionStart = selectionEnd = selectionStart + 1;
return;
}
evt.preventDefault();
that.onSubmit();
}
});
}
}
},
};

View File

@ -46,18 +46,6 @@ const onDataSaved = async (formData, requestResponse) => {
@on-fetch="(data) => (taxAreasOptions = data)"
auto-load
/>
<FetchData
url="Tickets"
:filter="{ fields: ['id', 'nickname'], order: 'shipped DESC', limit: 30 }"
@on-fetch="(data) => (ticketsOptions = data)"
auto-load
/>
<FetchData
url="Clients"
:filter="{ fields: ['id', 'name'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FormModelPopup
ref="formModelPopupRef"
:title="t('Create manual invoice')"
@ -80,6 +68,10 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id"
v-model="data.ticketFk"
@update:model-value="data.clientFk = null"
url="Tickets"
:where="{ refFk: null }"
:fields="['id', 'nickname']"
:filter-options="{ order: 'shipped DESC' }"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -101,6 +93,9 @@ const onDataSaved = async (formData, requestResponse) => {
option-value="id"
v-model="data.clientFk"
@update:model-value="data.ticketFk = null"
url="Clients"
:fields="['id', 'name']"
:filter-options="{ order: 'name ASC' }"
/>
<VnInputDate :label="t('Max date')" v-model="data.maxShipped" />
</VnRow>
@ -112,7 +107,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="description"
option-value="code"
v-model="data.serial"
:required="true"
/>
<VnSelect
:label="t('Area')"
@ -121,7 +115,6 @@ const onDataSaved = async (formData, requestResponse) => {
option-label="code"
option-value="code"
v-model="data.taxArea"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">

View File

@ -1,6 +1,7 @@
<script setup>
import axios from 'axios';
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
@ -10,6 +11,7 @@ import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
const { push } = useRouter();
const quasar = useQuasar();
const stateStore = useStateStore();
const { t } = useI18n();
@ -60,6 +62,15 @@ const $props = defineProps({
type: Function,
default: null,
},
goTo: {
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
hasSubToolbar: {
type: Boolean,
default: true,
},
});
const isLoading = ref(false);
@ -68,6 +79,7 @@ const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -82,6 +94,7 @@ defineExpose({
saveChanges,
getChanges,
formData,
vnPaginateRef,
});
async function fetch(data) {
@ -90,19 +103,26 @@ async function fetch(data) {
data.map((d) => (d.$index = $index++));
}
originalData.value = data && JSON.parse(JSON.stringify(data));
formData.value = data && JSON.parse(JSON.stringify(data));
watch(formData, () => (hasChanges.value = true), { deep: true });
resetData(data);
emit('onFetch', data);
return data;
}
function resetData(data) {
if (!data) return;
originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destoy watcher
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
}
// eslint-disable-next-line vue/no-dupe-keys
function filter(value, update, filterOptions) {
update(
() => {
@ -128,6 +148,11 @@ async function onSubmit() {
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
await onSubmit();
push({ path: $props.goTo });
}
async function saveChanges(data) {
if ($props.saveFn) {
$props.saveFn(data, getChanges);
@ -259,8 +284,9 @@ function isEmpty(obj) {
if (obj.length > 0) return false;
}
async function reload() {
vnPaginateRef.value.fetch();
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
}
watch(formUrl, async () => {
@ -272,10 +298,11 @@ watch(formUrl, async () => {
<VnPaginate
:url="url"
:limit="limit"
v-bind="$attrs"
@on-fetch="fetch"
@on-change="resetData"
:skeleton="false"
ref="vnPaginateRef"
v-bind="$attrs"
>
<template #body v-if="formData">
<slot
@ -286,8 +313,11 @@ watch(formUrl, async () => {
></slot>
</template>
</VnPaginate>
<SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<SkeletonTable
v-if="!formData && $attrs.autoLoad"
:columns="$attrs.columns?.length"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn
@ -310,7 +340,40 @@ watch(formUrl, async () => {
:title="t('globals.reset')"
v-if="$props.defaultReset"
/>
<QBtnDropdown
v-if="$props.goTo && $props.defaultSave"
@click="onSubmitAndGo"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"
split
>
<QList>
<QItem
color="primary"
clickable
v-close-popup
@click="onSubmit"
:title="t('globals.save')"
>
<QItemSection>
<QItemLabel>
<QIcon
name="save"
color="white"
class="q-mr-sm"
size="sm"
/>
{{ t('globals.save').toUpperCase() }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
v-else-if="!$props.goTo && $props.defaultSave"
:label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary"
@ -318,7 +381,6 @@ watch(formUrl, async () => {
@click="onSubmit"
:disable="!hasChanges"
:title="t('globals.save')"
v-if="$props.defaultSave"
/>
<slot name="moreAfterActions" />
</QBtnGroup>

View File

@ -155,7 +155,7 @@ const rotateRight = () => {
editor.value.rotate(-90);
};
const onUploadAccept = () => {
const onSubmit = () => {
try {
if (!newPhoto.files && !newPhoto.url) {
notify(t('Select an image'), 'negative');
@ -206,7 +206,7 @@ const makeRequest = async () => {
@on-fetch="(data) => (allowedContentTypes = data.join(', '))"
auto-load
/>
<QForm @submit="onUploadAccept()" class="all-pointer-events">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="q-pa-lg">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />

View File

@ -50,7 +50,7 @@ const onDataSaved = () => {
closeForm();
};
const submitData = async () => {
const onSubmit = async () => {
try {
isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
@ -74,7 +74,7 @@ const closeForm = () => {
</script>
<template>
<QForm @submit="submitData()" class="all-pointer-events">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="q-pa-lg">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />

View File

@ -24,7 +24,7 @@ const $props = defineProps({
default: '',
},
limit: {
type: String,
type: [String, Number],
default: '',
},
params: {

View File

@ -83,7 +83,7 @@ const tableColumns = computed(() => [
},
]);
const fetchResults = async () => {
const onSubmit = async () => {
try {
let filter = itemFilter;
const params = itemFilterParams;
@ -145,7 +145,7 @@ const selectItem = ({ id }) => {
@on-fetch="(data) => (InksOptions = data)"
auto-load
/>
<QForm @submit="fetchResults()" class="all-pointer-events">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="column" style="padding: 32px; z-index: 100">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />

View File

@ -85,7 +85,7 @@ const tableColumns = computed(() => [
},
]);
const fetchResults = async () => {
const onSubmit = async () => {
try {
let filter = travelFilter;
const params = travelFilterParams;
@ -138,7 +138,7 @@ const selectTravel = ({ id }) => {
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
/>
<QForm @submit="fetchResults()" class="all-pointer-events">
<QForm @submit="onSubmit()" class="all-pointer-events">
<QCard class="column" style="padding: 32px; z-index: 100">
<span ref="closeButton" class="close-icon" v-close-popup>
<QIcon name="close" size="sm" />

View File

@ -1,7 +1,7 @@
<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { onBeforeRouteLeave, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState';
@ -11,13 +11,17 @@ import useNotify from 'src/composables/useNotify.js';
import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter();
const quasar = useQuasar();
const state = useState();
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const { notify } = useNotify();
const route = useRoute();
const $props = defineProps({
url: {
@ -26,7 +30,7 @@ const $props = defineProps({
},
model: {
type: String,
default: '',
default: null,
},
filter: {
type: Object,
@ -74,31 +78,80 @@ const $props = defineProps({
type: Function,
default: null,
},
goTo: {
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
reload: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`,
).value;
const componentIsRendered = ref(false);
const arrayData = useArrayData(modelValue);
const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({});
const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'save',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
onMounted(async () => {
originalData.value = $props.formInitialData;
nextTick(() => {
componentIsRendered.value = true;
});
originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {}));
nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData) {
await fetch();
}
state.set(modelValue, $props.formInitialData);
// Si así se desea disparamos el watcher del form después de 100ms, asi darle tiempo de que se haya cargado la data inicial
// para evitar que detecte cambios cuando es data inicial default
if ($props.observeFormChanges) {
setTimeout(() => {
startFormWatcher();
}, 100);
if (!$props.formInitialData) {
if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
}
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 },
);
}
});
if (!$props.url)
watch(
() => arrayData.store.data,
(val) => updateAndEmit('onFetch', val),
);
watch(formUrl, async () => {
originalData.value = null;
reset();
await fetch();
});
onBeforeRouteLeave((to, from, next) => {
@ -116,95 +169,59 @@ onBeforeRouteLeave((to, from, next) => {
onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) {
state.set($props.model, originalData.value);
return;
}
if ($props.clearStoreOnUnmount) state.unset($props.model);
if (hasChanges.value) return state.set(modelValue, originalData.value);
if ($props.clearStoreOnUnmount) state.unset(modelValue);
});
const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({});
const formData = computed(() => state.get($props.model));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({
save: {
color: 'primary',
icon: 'save',
label: 'globals.save',
},
reset: {
color: 'primary',
icon: 'restart_alt',
label: 'globals.reset',
},
...$props.defaultButtons,
}));
const startFormWatcher = () => {
watch(
() => formData.value,
(val) => {
hasChanges.value = !isResetting.value && val;
isResetting.value = false;
},
{ deep: true }
);
};
async function fetch() {
try {
let { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) },
});
if (Array.isArray(data)) data = data[0] ?? {};
state.set($props.model, data);
originalData.value = data && JSON.parse(JSON.stringify(data));
emit('onFetch', state.get($props.model));
} catch (error) {
state.set($props.model, {});
updateAndEmit('onFetch', data);
} catch (e) {
state.set(modelValue, {});
originalData.value = {};
}
}
async function save() {
if ($props.observeFormChanges && !hasChanges.value) {
notify('globals.noChanges', 'negative');
return;
}
isLoading.value = true;
if ($props.observeFormChanges && !hasChanges.value)
return notify('globals.noChanges', 'negative');
isLoading.value = true;
try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const method = $props.urlCreate ? 'post' : 'patch';
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
let response;
if ($props.saveFn) response = await $props.saveFn(body);
else
response = await axios[$props.urlCreate ? 'post' : 'patch'](
$props.urlCreate || $props.urlUpdate || $props.url,
body
);
else response = await axios[method](url, body);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');
} finally {
isLoading.value = false;
}
isLoading.value = false;
}
async function saveAndGo() {
await save();
push({ path: $props.goTo });
}
function reset() {
state.set($props.model, originalData.value);
originalData.value = JSON.parse(JSON.stringify(originalData.value));
emit('onFetch', state.get($props.model));
updateAndEmit('onFetch', originalData.value);
if ($props.observeFormChanges) {
hasChanges.value = false;
isResetting.value = true;
@ -222,26 +239,30 @@ function filter(value, update, filterOptions) {
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
watch(formUrl, async () => {
originalData.value = null;
reset();
fetch();
});
function updateAndEmit(evt, val, res) {
state.set(modelValue, val);
originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val;
emit(evt, state.get(modelValue), res);
}
defineExpose({
save,
isLoading,
hasChanges,
reset,
fetch,
});
</script>
<template>
<div class="column items-center full-width">
<QForm
v-if="formData"
@submit="save"
@reset="reset"
class="q-pa-md"
@ -249,11 +270,13 @@ defineExpose({
>
<QCard>
<slot
v-if="formData"
name="form"
:data="formData"
:validate="validate"
:filter="filter"
/>
<SkeletonForm v-else/>
</QCard>
</QForm>
</div>
@ -273,10 +296,42 @@ defineExpose({
:disable="!hasChanges"
:title="t(defaultButtons.reset.label)"
/>
<QBtnDropdown
v-if="$props.goTo"
@click="saveAndGo"
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
:disable="!hasChanges"
color="primary"
icon="save"
split
>
<QList>
<QItem
clickable
v-close-popup
@click="save"
:title="t('globals.save')"
>
<QItemSection>
<QItemLabel>
<QIcon
name="save"
color="white"
class="q-mr-sm"
size="sm"
/>
{{ t('globals.save').toUpperCase() }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
:label="tMobile(defaultButtons.save.label)"
:color="defaultButtons.save.color"
:icon="defaultButtons.save.icon"
v-else
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
:disable="!hasChanges"
:title="t(defaultButtons.save.label)"
@ -284,7 +339,7 @@ defineExpose({
</QBtnGroup>
</div>
</Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading
:showing="isLoading"
:label="t('globals.pleaseWait')"

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved']);
const emit = defineEmits(['onDataSaved', 'onDataCanceled']);
defineProps({
title: {
@ -15,26 +15,6 @@ defineProps({
type: String,
default: '',
},
url: {
type: String,
default: '',
},
model: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
urlCreate: {
type: String,
default: null,
},
formInitialData: {
type: Object,
default: () => {},
},
});
const { t } = useI18n();
@ -43,8 +23,8 @@ const formModelRef = ref(null);
const closeButton = ref(null);
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
closeForm();
emit('onDataSaved', formData, requestResponse);
};
const isLoading = computed(() => formModelRef.value?.isLoading);
@ -61,11 +41,9 @@ defineExpose({
<template>
<FormModel
ref="formModelRef"
:form-initial-data="formInitialData"
:observe-form-changes="false"
:default-actions="false"
:url-create="urlCreate"
:model="model"
v-bind="$attrs"
@on-data-saved="onDataSaved"
>
<template #form="{ data, validate }">
@ -84,6 +62,7 @@ defineExpose({
flat
:disabled="isLoading"
:loading="isLoading"
@click="emit('onDataCanceled')"
v-close-popup
/>
<QBtn

View File

@ -74,7 +74,7 @@ const closeForm = () => {
:disabled="isLoading"
:loading="isLoading"
/>
<slot name="customButtons" />
<slot name="custom-buttons" />
</div>
</QCard>
</QForm>

View File

@ -58,6 +58,7 @@ function addChildren(module, route, parent) {
}
const items = ref([]);
function getRoutes() {
if (props.source === 'main') {
const modules = Object.assign([], navigation.getModules().value);
@ -66,9 +67,8 @@ function getRoutes() {
const moduleDef = routes.find(
(route) => toLowerCamel(route.name) === item.module
);
item.children = [];
if (!moduleDef) continue;
item.children = [];
addChildren(item.module, moduleDef, item.children);
}

View File

@ -20,7 +20,13 @@ const itemComputed = computed(() => {
});
</script>
<template>
<QItem active-class="bg-hover" :to="{ name: itemComputed.name }" clickable v-ripple>
<QItem
active-class="bg-vn-hover"
class="min-height"
:to="{ name: itemComputed.name }"
clickable
v-ripple
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />
</QItemSection>
@ -33,3 +39,9 @@ const itemComputed = computed(() => {
</QItemSection>
</QItem>
</template>
<style lang="scss" scoped>
.q-item {
min-height: 5vh;
}
</style>

View File

@ -1,21 +1,19 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore';
import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const { t } = useI18n();
const stateStore = useStateStore();
const quasar = useQuasar();
const state = useState();
const user = state.getUser();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const appName = 'Lilium';
onMounted(() => stateStore.setMounted());
@ -83,11 +81,12 @@ const pinnedModulesRef = ref();
id="user"
>
<QAvatar size="lg">
<QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="primary"
>
</QImg>
<VnImg
:id="user.id"
collection="user"
size="160x160"
:zoom-size="null"
/>
</QAvatar>
<QTooltip bottom>
{{ t('globals.userPanel') }}

View File

@ -2,12 +2,13 @@
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
@ -17,34 +18,66 @@ const $props = defineProps({
default: () => {},
},
});
const { dialogRef } = useDialogPluginComponent();
const quasar = useQuasar();
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
const selectedClient = (client) => {
transferInvoiceParams.selectedClientData = client;
};
const transferInvoice = async () => {
const makeInvoice = async () => {
const hasToInvoiceByAddress =
transferInvoiceParams.selectedClientData.hasToInvoiceByAddress;
const params = {
id: transferInvoiceParams.id,
cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk,
invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk,
newClientFk: transferInvoiceParams.newClientFk,
refFk: transferInvoiceParams.refFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
makeInvoice: checked.value,
};
try {
const { data } = await axios.post(
'InvoiceOuts/transferInvoice',
transferInvoiceParams
);
if (checked.value && hasToInvoiceByAddress) {
const response = await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Bill destination client'),
message: t('transferInvoiceInfo'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
if (!response) {
return;
}
}
const { data } = await axios.post('InvoiceOuts/transferInvoice', params);
notify(t('Transferred invoice'), 'positive');
closeForm();
router.push('InvoiceOutSummary', { id: data.id });
const id = data?.[0];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error transfering invoice', err);
}
@ -52,22 +85,30 @@ const transferInvoice = async () => {
</script>
<template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)"
@on-fetch="
(data) => (
(rectificativeTypeOptions = data),
(transferInvoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)"
@on-fetch="
(data) => (
(siiTypeInvoiceOutsOptions = data),
(transferInvoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4'
)[0].id)
)
"
auto-load
/>
<FetchData
@ -75,80 +116,99 @@ const transferInvoice = async () => {
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<FormPopup
@on-submit="transferInvoice()"
<QDialog ref="dialogRef">
<FormPopup
@on-submit="makeInvoice()"
:title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</VnRow>
</template>
</FormPopup>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('Client')"
:options="clientsOptions"
hide-selected
option-label="name"
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load
>
<template #option="scope">
<QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} - {{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="transferInvoiceParams.invoiceCorrectionTypeFk"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div>
<QCheckbox :label="t('Bill destination client')" v-model="checked" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template>
</FormPopup>
</QDialog>
</template>
<i18n>
en:
checkInfo: New tickets from the destination customer will be generated in the consignee by default.
transferInvoiceInfo: Destination customer is marked to bill in the consignee
confirmTransferInvoice: The destination customer has selected to bill in the consignee, do you want to continue?
es:
Transfer invoice: Transferir factura
Transfer client: Transferir cliente
@ -157,4 +217,7 @@ es:
Class: Clase
Type: Tipo
Transferred invoice: Factura transferida
Bill destination client: Facturar cliente destino
transferInvoiceInfo: Los nuevos tickets del cliente destino, serán generados en el consignatario por defecto.
confirmTransferInvoice: El cliente destino tiene marcado facturar por consignatario, desea continuar?
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, computed } from 'vue';
import { onMounted, computed, ref } from 'vue';
import { Dark, Quasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
@ -10,14 +10,16 @@ import { localeEquivalence } from 'src/i18n/index';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard';
import VnImg from 'src/components/ui/VnImg.vue';
import { useRole } from 'src/composables/useRole';
const state = useState();
const session = useSession();
const router = useRouter();
const { t, locale } = useI18n();
import { useClipboard } from 'src/composables/useClipboard';
import { ref } from 'vue';
const { copyText } = useClipboard();
const userLocale = computed({
get() {
return locale.value;
@ -48,7 +50,6 @@ const darkMode = computed({
});
const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref();
const companiesData = ref();
const accountBankData = ref();
@ -91,6 +92,16 @@ function logout() {
function copyUserToken() {
copyText(session.getToken(), { label: 'components.userPanel.copyToken' });
}
function localUserData() {
state.setUser(user.value);
}
function saveUserData(param, value) {
axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData();
}
const isEmployee = computed(() => useRole().isEmployee());
</script>
<template>
@ -101,12 +112,14 @@ function copyUserToken() {
auto-load
/>
<FetchData
v-if="isEmployee"
url="Companies"
order="name"
@on-fetch="(data) => (companiesData = data)"
auto-load
/>
<FetchData
v-if="isEmployee"
url="Accountings"
order="name"
@on-fetch="(data) => (accountBankData = data)"
@ -141,10 +154,7 @@ function copyUserToken() {
<div class="col column items-center q-mb-sm">
<QAvatar size="80px">
<QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
<VnImg :id="user.id" collection="user" size="160x160" />
</QAvatar>
<div class="text-subtitle1 q-mt-md">
@ -180,6 +190,7 @@ function copyUserToken() {
option-value="id"
input-debounce="0"
hide-selected
@update:model-value="localUserData"
/>
<VnSelect
:label="t('components.userPanel.localBank')"
@ -189,6 +200,7 @@ function copyUserToken() {
option-value="id"
input-debounce="0"
hide-selected
@update:model-value="localUserData"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
@ -210,6 +222,7 @@ function copyUserToken() {
option-label="code"
option-value="id"
input-debounce="0"
@update:model-value="localUserData"
/>
<VnSelect
:label="t('components.userPanel.userWarehouse')"
@ -219,6 +232,7 @@ function copyUserToken() {
option-label="name"
option-value="id"
input-debounce="0"
@update:model-value="(v) => saveUserData('warehouseFk', v)"
/>
</VnRow>
<VnRow>
@ -232,6 +246,7 @@ function copyUserToken() {
style="flex: 0"
dense
input-debounce="0"
@update:model-value="(v) => saveUserData('companyFk', v)"
/>
</VnRow>
</div>

View File

@ -0,0 +1,55 @@
<script setup>
defineProps({
columns: {
type: Array,
required: true,
},
row: {
type: Object,
default: null,
},
});
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
</script>
<template>
<slot name="beforeChip" :row="row"></slot>
<span
v-for="col of columns"
:key="col.name"
@click="stopEventPropagation"
class="cursor-text"
>
<QChip
v-if="col.chip.condition(row[col.name], row)"
:title="col.label"
:class="[
col.chip.color
? col.chip.color(row)
: !col.chip.icon && 'bg-chip-secondary',
col.chip.icon && 'q-px-none',
]"
dense
square
>
<span v-if="!col.chip.icon">{{ row[col.name] }}</span>
<QIcon v-else :name="col.chip.icon" color="primary-light" />
</QChip>
</span>
<slot name="afterChip" :row="row"></slot>
</template>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -0,0 +1,165 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QIcon, QCheckbox } from 'quasar';
import { dashIfEmpty } from 'src/filters';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnComponent from 'components/common/VnComponent.vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
column: {
type: Object,
required: true,
},
row: {
type: Object,
default: () => {},
},
default: {
type: [Object, String],
default: null,
},
componentProp: {
type: String,
default: null,
},
isEditable: {
type: Boolean,
default: true,
},
components: {
type: Object,
default: null,
},
showLabel: {
type: Boolean,
default: null,
},
});
const defaultComponents = {
input: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
number: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
date: {
component: markRaw(VnInputDate),
attrs: {
readonly: true,
disable: !$props.isEditable,
style: 'min-width: 125px',
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: {
component: markRaw(QCheckbox),
attrs: (prop) => {
const defaultAttrs = {
disable: !$props.isEditable,
'model-value': Boolean(prop),
class: 'no-padding fit',
};
if (typeof prop == 'number') {
defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0;
}
return defaultAttrs;
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
select: {
component: markRaw(VnSelect),
attrs: {
disable: !$props.isEditable,
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
icon: {
component: markRaw(QIcon),
},
};
const value = computed(() => {
return $props.column.format
? $props.column.format($props.row, dashIfEmpty)
: dashIfEmpty($props.row[$props.column.name]);
});
const col = computed(() => {
let newColumn = { ...$props.column };
const specific = newColumn[$props.componentProp];
if (specific) {
newColumn = {
...newColumn,
...specific,
...specific.attrs,
...specific.forceAttrs,
};
}
if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) &&
!newColumn.component
)
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
return newColumn;
});
const components = computed(() => $props.components ?? defaultComponents);
</script>
<template>
<div class="row no-wrap">
<VnComponent
v-if="col.before"
:prop="col.before"
:components="components"
:value="model"
v-model="model"
/>
<VnComponent
v-if="col.component"
:prop="col"
:components="components"
:value="model"
v-model="model"
/>
<span :title="value" v-else>{{ value }}</span>
<VnComponent
v-if="col.after"
:prop="col.after"
:components="components"
:value="model"
v-model="model"
/>
</div>
</template>

View File

@ -0,0 +1,150 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QCheckbox } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({
column: {
type: Object,
required: true,
},
showTitle: {
type: Boolean,
default: false,
},
dataKey: {
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
});
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
const enterEvent = {
'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null),
};
const defaultAttrs = {
filled: !$props.showTitle,
class: 'q-px-xs q-pb-xs q-pt-none fit',
dense: true,
};
const forceAttrs = {
label: $props.showTitle ? '' : $props.column.label,
};
const components = {
input: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
number: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
date: {
component: markRaw(VnInputDate),
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
time: {
component: markRaw(VnInputTime),
event: updateEvent,
attrs: {
...defaultAttrs,
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: {
component: markRaw(QCheckbox),
event: updateEvent,
attrs: {
dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true,
},
forceAttrs,
},
select: {
component: markRaw(VnSelect),
event: updateEvent,
attrs: {
class: 'q-px-sm q-pb-xs q-pt-none fit',
dense: true,
filled: !$props.showTitle,
},
forceAttrs,
},
};
async function addFilter(value) {
value ??= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name;
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value });
}
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
);
</script>
<template>
<div v-if="showFilter" class="full-width" :class="alignRow()">
<VnTableColumn
:column="$props.column"
default="input"
v-model="model"
:components="components"
component-prop="columnFilter"
/>
</div>
</template>

View File

@ -0,0 +1,683 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue';
import TableVisibleColumns from 'src/components/VnTable/VnVisibleColumn.vue';
const $props = defineProps({
columns: {
type: Array,
required: true,
},
defaultMode: {
type: String,
default: 'card', // 'table', 'card'
},
columnSearch: {
type: Boolean,
default: true,
},
rightSearch: {
type: Boolean,
default: true,
},
rowClick: {
type: Function,
default: null,
},
redirect: {
type: String,
default: null,
},
create: {
type: Object,
default: null,
},
cardClass: {
type: String,
default: 'flex-one',
},
searchUrl: {
type: String,
default: 'table',
},
isEditable: {
type: Boolean,
default: false,
},
useModel: {
type: Boolean,
default: false,
},
hasSubToolbar: {
type: Boolean,
default: true,
},
disableOption: {
type: Object,
default: () => ({ card: false, table: false }),
},
withoutHeader: {
type: Boolean,
default: false,
},
tableCode: {
type: String,
default: null,
},
table: {
type: Object,
default: () => ({}),
},
});
const { t } = useI18n();
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const DEFAULT_MODE = 'card';
const TABLE_MODE = 'table';
const mode = ref(DEFAULT_MODE);
const selected = ref([]);
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const CrudModelRef = ref({});
const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
const tableModes = [
{
icon: 'view_column',
title: t('table view'),
value: TABLE_MODE,
disable: $props.disableOption?.table,
},
{
icon: 'grid_view',
title: t('grid view'),
value: DEFAULT_MODE,
disable: $props.disableOption?.card,
},
];
onMounted(() => {
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]);
});
watch(
() => $props.columns,
(value) => splitColumns(value),
{ immediate: true }
);
watch(
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
const rowClickFunction = computed(() => {
if ($props.rowClick) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id);
return () => {};
});
const isTableMode = computed(() => mode.value == TABLE_MODE);
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where;
watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter;
delete params.value?.filter;
params.value = { ...params.value, ...watchedParams };
}
function splitColumns(columns) {
splittedColumns.value = {
columns: [],
chips: [],
create: [],
visible: [],
};
for (const col of columns) {
if (col.name == 'tableActions') splittedColumns.value.actions = col;
if (col.chip) splittedColumns.value.chips.push(col);
if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true };
splittedColumns.value.columns.push(col);
}
// Status column
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId
);
if (splittedColumns.value.columnChips.length)
splittedColumns.value.columns.unshift({
align: 'left',
label: t('status'),
name: 'tableStatus',
columnFilter: false,
});
}
}
function redirectFn(id) {
router.push({ path: `/${$props.redirect}/${id}` });
}
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
function reload(params) {
CrudModelRef.value.reload(params);
}
function columnName(col) {
const column = { ...col, ...col.columnFilter };
let name = column.name;
if (column.alias) name = column.alias + '.' + name;
return name;
}
function getColAlign(col) {
return 'text-' + (col.align ?? 'left');
}
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({
reload,
redirect: redirectFn,
selected,
});
</script>
<template>
<QDrawer
v-if="$props.rightSearch"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
show-if-above
>
<QScrollArea class="fit">
<VnFilterPanel
:data-key="$attrs['data-key']"
:search-button="true"
v-model="params"
:disable-submit-event="true"
:search-url="searchUrl"
:redirect="!!redirect"
>
<template #body>
<VnTableFilter
:column="col"
:data-key="$attrs['data-key']"
v-for="col of splittedColumns.columns"
:key="col.id"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</template>
<slot
name="moreFilterPanel"
:params="params"
:columns="splittedColumns.columns"
/>
</VnFilterPanel>
</QScrollArea>
</QDrawer>
<!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel
v-bind="$attrs"
:limit="20"
ref="CrudModelRef"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
@save-changes="reload"
:has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
>
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #body="{ rows }">
<QTable
v-bind="table"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="!isTableMode"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="isTableMode && 'max-height: 90vh'"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
>
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot>
</template>
<template #top-right>
<TableVisibleColumns
v-if="isTableMode"
v-model="splittedColumns.columns"
:table-code="tableCode ?? route.name"
/>
<QBtnToggle
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes"
/>
<QBtn
v-if="$props.rightSearch"
icon="filter_alt"
title="asd"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh v-if="col.visible" auto-width style="min-width: 100px">
<div
class="q-pt-sm q-px-sm ellipsis"
:class="`text-${col?.align ?? 'left'}`"
:style="
$props.columnSearch && col.columnFilter == false
? { 'min-height': 72 + 'px' }
: ''
"
>
{{ col?.label }}
</div>
<VnTableFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip
:columns="splittedColumns.columnChips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="getColAlign(col)"
v-if="col.visible"
>
<slot :name="`column-${col.name}`" :col="col" :row="row">
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
/>
</slot>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
>
<QCardSection
vertical
class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
>
<!-- Chips -->
<QCardSection
v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
>
<VnTableChip
:columns="splittedColumns.chips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QCardSection>
<!-- Title -->
<QCardSection
v-if="splittedColumns.title"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
>
<span
:title="row[splittedColumns.title.name]"
@click="stopEventPropagation($event)"
class="cursor-text"
>
{{ row[splittedColumns.title.name] }}
</span>
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
:class="$props.cardClass"
>
<div
v-for="col of splittedColumns.visible"
:key="col.name"
class="fields"
>
<VnLv
:label="
!col.component && col.label
? `${col.label}:`
: ''
"
>
<template #value>
<span
@click="
stopEventPropagation($event)
"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</slot>
</span>
</template>
</VnLv>
</div>
</QCardSection>
</QCardSection>
<!-- Actions -->
<QCardSection
v-if="colsMap.tableActions"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of splittedColumns.actions
.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QCardSection>
</QCard>
</component>
</template>
</QTable>
</template>
</CrudModel>
</div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip>
{{ create.title }}
</QTooltip>
</QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup
v-bind="create"
:model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)"
>
<template #form-inputs="{ data }">
<div class="grid-create">
<VnTableColumn
v-for="column of splittedColumns.create"
:key="column.name"
:column="column"
:row="{}"
default="input"
v-model="data[column.name]"
:show-label="true"
component-prop="columnCreate"
/>
<slot name="more-create-dialog" :data="data" />
</div>
</template>
</FormModelPopup>
</QDialog>
</template>
<i18n>
en:
status: Status
es:
status: Estados
</i18n>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.bg-header {
background-color: #5d5d5d;
color: var(--vn-text-color);
}
.q-table--dark .q-table__bottom,
.q-table--dark thead,
.q-table--dark tr,
.q-table--dark th,
.q-table--dark td {
border-color: #222222;
}
.q-table__container > div:first-child {
background-color: var(--vn-page-color);
}
.grid-three {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.grid-create {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.flex-one {
display: flex;
flex-flow: row wrap;
div.fields {
width: 100%;
.vn-label-value {
display: flex;
gap: 2%;
}
}
}
.q-table th {
padding: 0;
}
.vnTable {
thead tr th {
position: sticky;
z-index: 2;
}
thead tr:first-child th {
top: 0;
}
.q-table__top {
top: 0;
}
tbody {
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}
}
.sticky {
position: sticky;
right: 0;
}
td.sticky {
background-color: var(--q-dark);
z-index: 1;
}
}
.vn-label-value {
display: flex;
flex-direction: row;
color: var(--vn-text-color);
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: text;
user-select: all;
}
}
.cardEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-two {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
margin: 0 auto;
overflow: scroll;
white-space: wrap;
width: 100%;
}
.w-80 {
width: 80%;
}
.w-20 {
width: 20%;
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -0,0 +1,181 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, computed, onMounted } from 'vue';
import { useState } from 'src/composables/useState';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const columns = defineModel({ type: Object, default: [] });
const $props = defineProps({
tableCode: {
type: String,
default: '',
},
});
const { notify } = useNotify();
const { t } = useI18n();
const state = useState();
const user = state.getUser();
const popupProxyRef = ref();
const initialUserConfigViewData = ref();
const localColumns = ref([]);
const areAllChecksMarked = computed(() => {
return localColumns.value.every((col) => col.visible);
});
function setUserConfigViewData(data, isLocal) {
if (!data) return;
// Importante: El name de las columnas de la tabla debe conincidir con el name de las variables que devuelve la view config
if (!isLocal) localColumns.value = [];
for (let column of columns.value) {
const { label, name } = column;
column.visible = data[name] ?? true;
if (!isLocal) localColumns.value.push({ name, label, visible: column.visible });
}
}
function toggleMarkAll(val) {
localColumns.value.forEach((col) => (col.visible = val));
}
async function getConfig(url, filter) {
const response = await axios.get(url, {
params: { filter: filter },
});
return response.data && response.data.length > 0 ? response.data[0] : null;
}
async function fetchViewConfigData() {
try {
const defaultFilter = {
where: { tableCode: $props.tableCode },
};
const userConfig = await getConfig('UserConfigViews', {
where: {
...defaultFilter.where,
...{ userFk: user.id },
},
});
if (userConfig) {
initialUserConfigViewData.value = userConfig;
setUserConfigViewData(userConfig.configuration);
return;
}
const defaultConfig = await getConfig('DefaultViewConfigs', defaultFilter);
if (defaultConfig) {
setUserConfigViewData(defaultConfig.columns);
return;
}
} catch (err) {
console.err('Error fetching config view data', err);
}
}
async function saveConfig() {
const configuration = {};
for (const { name, visible } of localColumns.value)
configuration[name] = visible ?? true;
setUserConfigViewData(configuration, true);
if (!$props.tableCode) return popupProxyRef.value.hide();
try {
const params = {};
// Si existe una view config del usuario hacemos un update si no la creamos
if (initialUserConfigViewData.value) {
params.updates = [
{
data: {
configuration,
},
where: {
id: initialUserConfigViewData.value.id,
},
},
];
} else {
params.creates = [
{
userFk: user.value.id,
tableCode: $props.tableCode,
tableConfig: $props.tableCode,
configuration,
},
];
}
const response = await axios.post('UserConfigViews/crud', params);
if (response.data && response.data[0]) {
initialUserConfigViewData.value = response.data[0];
}
notify('globals.dataSaved', 'positive');
popupProxyRef.value.hide();
} catch (err) {
console.error('Error saving user view config', err);
notify('errors.writeRequest', 'negative');
}
}
onMounted(async () => {
setUserConfigViewData({});
await fetchViewConfigData();
});
</script>
<template>
<QBtn icon="vn:visible_columns" class="bg-vn-section-color q-mr-md" dense>
<QPopupProxy ref="popupProxyRef">
<QCard class="column q-pa-md">
<QIcon name="info" size="sm" class="info-icon">
<QTooltip>{{ t('Check the columns you want to see') }}</QTooltip>
</QIcon>
<span class="text-body1 q-mb-sm">{{ t('Visible columns') }}</span>
<QCheckbox
:label="t('Tick all')"
:model-value="areAllChecksMarked"
@update:model-value="toggleMarkAll($event)"
class="q-mb-sm"
/>
<div v-if="columns.length > 0" class="checks-layout">
<QCheckbox
v-for="col in localColumns"
:key="col.name"
:label="col.label"
v-model="col.visible"
/>
</div>
<QBtn
class="full-width q-mt-md"
color="primary"
@click="saveConfig()"
:label="t('globals.save')"
/>
</QCard>
</QPopupProxy>
<QTooltip>{{ t('Visible columns') }}</QTooltip>
</QBtn>
</template>
<style lang="scss" scoped>
.info-icon {
position: absolute;
top: 20px;
right: 20px;
}
.checks-layout {
display: grid;
grid-template-columns: repeat(3, 200px);
}
</style>
<i18n>
es:
Check the columns you want to see: Marca las columnas que quieres ver
Visible columns: Columnas visibles
Tick all: Marcar todas
</i18n>

View File

@ -9,19 +9,18 @@ const rightPanel = ref(null);
onMounted(() => {
rightPanel.value = document.querySelector('#right-panel');
if (rightPanel.value.childNodes.length) hasContent.value = true;
if (!rightPanel.value) return;
// Check if there's content to display
const observer = new MutationObserver(() => {
hasContent.value = rightPanel.value.childNodes.length;
});
if (rightPanel.value)
observer.observe(rightPanel.value, {
subtree: true,
childList: true,
attributes: true,
});
observer.observe(rightPanel.value, {
subtree: true,
childList: true,
attributes: true,
});
if (!slots['right-panel'] && !hasContent.value) stateStore.rightDrawer = false;
});
@ -30,7 +29,7 @@ const { t } = useI18n();
const stateStore = useStateStore();
</script>
<template>
<Teleport to="#actions-append">
<Teleport to="#actions-append" v-if="stateStore.isHeaderMounted()">
<div class="row q-gutter-x-sm">
<QBtn
v-if="hasContent || $slots['right-panel']"

View File

@ -52,7 +52,7 @@ const toggleMarkAll = (val) => {
const getConfig = async (url, filter) => {
const response = await axios.get(url, {
params: { filter: filter },
params: { filter: JSON.stringify(filter) },
});
return response.data && response.data.length > 0 ? response.data[0] : null;
};
@ -60,7 +60,7 @@ const getConfig = async (url, filter) => {
const fetchViewConfigData = async () => {
try {
const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.id },
where: { tableCode: $props.tableCode, userFk: user.value.id },
};
const userConfig = await getConfig('UserConfigViews', userConfigFilter);
@ -74,8 +74,14 @@ const fetchViewConfigData = async () => {
const defaultConfig = await getConfig('DefaultViewConfigs', defaultConfigFilter);
if (defaultConfig) {
// Si el backend devuelve una configuración por defecto la usamos
setUserConfigViewData(defaultConfig.columns);
return;
} else {
// Si no hay configuración por defecto mostramos todas las columnas
const defaultColumns = {};
$props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns);
}
} catch (err) {
console.err('Error fetching config view data', err);

View File

@ -1,5 +1,5 @@
<script setup>
import { onBeforeMount, computed, watchEffect } from 'vue';
import { onBeforeMount, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
@ -20,6 +20,9 @@ const props = defineProps({
searchUrl: { type: String, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
searchCustomRouteRedirect: { type: String, default: undefined },
searchRedirect: { type: Boolean, default: true },
searchMakeFetch: { type: Boolean, default: true },
});
const stateStore = useStateStore();
@ -36,49 +39,47 @@ const arrayData = useArrayData(props.dataKey, {
onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false });
await arrayData.fetch({ append: false, updateRouter: false });
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${route.params.id}`;
await arrayData.fetch({ append: false });
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false, updateRouter: false });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];
});
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar" v-if="props.searchDataKey">
<slot name="searchbar">
<VnSearchbar
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
/>
</slot>
</Teleport>
<slot v-else name="searchbar" />
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<RightMenu>
<template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" />
</template>
</RightMenu>
</template>
<QDrawer
v-model="stateStore.leftDrawer"
show-if-above
:width="256"
v-if="stateStore.isHeaderMounted()"
>
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<slot name="searchbar" v-if="props.searchDataKey">
<VnSearchbar
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
:custom-route-redirect-name="searchCustomRouteRedirect"
:redirect="searchRedirect"
/>
</slot>
<slot v-else name="searchbar" />
<RightMenu>
<template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" />
</template>
</RightMenu>
<QPageContainer>
<QPage>
<VnSubToolbar />

View File

@ -0,0 +1,59 @@
<script setup>
import { computed, defineModel } from 'vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
prop: {
type: Object,
required: true,
},
components: {
type: Object,
default: () => {},
},
value: {
type: [Object, Number, String, Boolean],
default: () => {},
},
});
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
});
function mix(toComponent) {
const { component, attrs, event } = toComponent;
const customComponent = $props.components[component];
return {
component: customComponent?.component ?? component,
attrs: {
...toValueAttrs(attrs),
...toValueAttrs(customComponent?.attrs),
...toComponent,
...toValueAttrs(customComponent?.forceAttrs),
},
event: event ?? customComponent?.event,
};
}
function toValueAttrs(attrs) {
if (!attrs) return;
return typeof attrs == 'function' ? attrs($props.value) : attrs;
}
</script>
<template>
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column flex-center fit"
>
<component
v-if="toComponent?.component"
:is="mix(toComponent).component"
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
/>
</span>
</template>

View File

@ -78,6 +78,7 @@ async function save() {
const body = mapperDms(dms.value);
const response = await axios.post(getUrl(), body[0], body[1]);
emit('onDataSaved', body[1].params, response);
return response;
}
function defaultData() {

View File

@ -35,7 +35,7 @@ const $props = defineProps({
downloadModel: {
type: String,
required: false,
default: null,
default: undefined,
},
defaultDmsCode: {
type: String,

View File

@ -2,7 +2,12 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']);
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
]);
const $props = defineProps({
modelValue: {
@ -13,6 +18,14 @@ const $props = defineProps({
type: Boolean,
default: false,
},
info: {
type: String,
default: '',
},
clearable: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
@ -37,14 +50,6 @@ const styleAttrs = computed(() => {
: {};
});
const onEnterPress = () => {
emit('keyup.enter');
};
const handleValue = (val = null) => {
value.value = val;
};
const focus = () => {
vnInputRef.value.focus();
};
@ -73,7 +78,7 @@ const inputRules = [
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: $attrs.required }"
@keyup.enter="onEnterPress()"
@keyup.enter="emit('keyup.enter')"
:clearable="false"
:rules="inputRules"
:lazy-rules="true"
@ -82,15 +87,24 @@ const inputRules = [
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
</template>
<template #append v-if="!!$attrs.disabled">
<slot name="append" v-if="$slots.append" />
<template #append>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon
name="close"
size="xs"
v-if="hover && value"
@click="handleValue(null)"
v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="
() => {
value = null;
emit('remove');
}
"
></QIcon>
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
{{ info }}
</QTooltip>
</QIcon>
</template>
</QInput>
</div>

View File

@ -1,80 +1,31 @@
<script setup>
import { computed, ref } from 'vue';
import isValidDate from 'filters/isValidDate';
import { onMounted, watch, computed, ref } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
const props = defineProps({
modelValue: {
type: String,
default: null,
},
readonly: {
type: Boolean,
default: false,
},
const model = defineModel({ type: String });
const $props = defineProps({
isOutlined: {
type: Boolean,
default: false,
},
emitDateFormat: {
type: Boolean,
default: false,
},
});
const hover = ref(false);
const emit = defineEmits(['update:modelValue']);
const joinDateAndTime = (date, time) => {
if (!date) {
return null;
}
if (!time) {
return new Date(date).toISOString();
}
const [year, month, day] = date.split('/');
return new Date(`${year}-${month}-${day}T${time}`).toISOString();
};
const time = computed(() => (props.modelValue ? props.modelValue.split('T')?.[1] : null));
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit(
'update:modelValue',
props.emitDateFormat ? new Date(value) : joinDateAndTime(value, time.value)
);
},
});
const isPopupOpen = ref(false);
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const onDateUpdate = (date) => {
value.value = date;
isPopupOpen.value = false;
};
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const padDate = (value) => value.toString().padStart(2, '0');
const formatDate = (dateString) => {
const date = new Date(dateString || '');
return `${date.getFullYear()}/${padDate(date.getMonth() + 1)}/${padDate(
date.getDate()
)}`;
};
const displayDate = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
return new Date(dateString).toLocaleDateString([], {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
};
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
});
const styleAttrs = computed(() => {
return props.isOutlined
return $props.isOutlined
? {
dense: true,
outlined: true,
@ -82,40 +33,101 @@ const styleAttrs = computed(() => {
}
: {};
});
const formattedDate = computed({
get() {
if (!model.value) return model.value;
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
let newDate;
if (value) {
// parse input
if (value.includes('/') && value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ'
);
}
let ymd = value.split('-').map((e) => parseInt(e));
newDate = new Date(ymd[0], ymd[1] - 1, ymd[2]);
if (model.value) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds()
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value
);
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true }
);
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
v-model="formattedDate"
class="vn-input-date"
readonly
:model-value="displayDate(value)"
:mask="mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
:clearable="false"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="hover && value"
@click="onDateUpdate(null)"
></QIcon>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
v-model="isPopupOpen"
cover
transition-show="scale"
transition-hide="scale"
:no-parent-event="props.readonly"
>
<QDate
:today-btn="true"
:model-value="formatDate(value)"
@update:model-value="onDateUpdate"
/>
</QPopupProxy>
</QIcon>
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
model = null;
isPopupOpen = false;
"
/>
<QIcon name="event" class="cursor-pointer" />
</template>
<QMenu
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
>
<QDate
v-model="popupDate"
:today-btn="true"
@update:model-value="
(date) => {
formattedDate = date;
isPopupOpen = false;
}
"
/>
</QMenu>
</QInput>
</div>
</template>

View File

@ -1,14 +1,11 @@
<script setup>
import { computed, ref } from 'vue';
import { watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate';
import { date } from 'quasar';
const model = defineModel({ type: String });
const props = defineProps({
modelValue: {
type: String,
default: null,
},
readonly: {
timeOnly: {
type: Boolean,
default: false,
},
@ -18,41 +15,11 @@ const props = defineProps({
},
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const value = computed({
get() {
return props.modelValue;
},
set(value) {
const [hours, minutes] = value.split(':');
const date = new Date(props.modelValue);
date.setHours(Number.parseInt(hours) || 0, Number.parseInt(minutes) || 0, 0, 0);
emit('update:modelValue', value ? date.toISOString() : null);
},
});
const isPopupOpen = ref(false);
const onDateUpdate = (date) => {
internalValue.value = date;
};
const save = () => {
value.value = internalValue.value;
};
const formatTime = (dateString) => {
if (!dateString || !isValidDate(dateString)) {
return '';
}
const date = new Date(dateString || '');
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
};
const internalValue = ref(formatTime(value));
const dateFormat = 'HH:mm';
const isPopupOpen = ref();
const hover = ref();
const styleAttrs = computed(() => {
return props.isOutlined
@ -63,52 +30,87 @@ const styleAttrs = computed(() => {
}
: {};
});
const formattedTime = computed({
get() {
if (!model.value || model.value?.length <= 5) return model.value;
return dateToTime(model.value);
},
set(value) {
if (value == model.value) return;
let time = value;
if (time) {
if (time?.length > 5) time = dateToTime(time);
if (!props.timeOnly) {
const hours = time.split(':');
const date = new Date();
date.setHours(hours[0], hours[1], 0);
time = date.toISOString();
}
}
model.value = time;
},
});
function dateToTime(newDate) {
return date.formatDate(new Date(newDate), dateFormat);
}
watch(
() => model.value,
(val) => (formattedTime.value = val),
{ immediate: true }
);
</script>
<template>
<QInput
class="vn-input-time"
readonly
:model-value="formatTime(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
@click="isPopupOpen = true"
>
<template #append>
<QIcon name="schedule" class="cursor-pointer">
<QPopupProxy
v-model="isPopupOpen"
cover
transition-show="scale"
transition-hide="scale"
:no-parent-event="props.readonly"
>
<QTime
:format24h="false"
:model-value="formatTime(value)"
@update:model-value="onDateUpdate"
>
<div class="row items-center justify-end q-gutter-sm">
<QBtn
:label="t('Cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
label="Ok"
color="primary"
flat
@click="save"
v-close-popup
/>
</div>
</QTime>
</QPopupProxy>
</QIcon>
</template>
</QInput>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
class="vn-input-time"
mask="##:##"
placeholder="--:--"
v-model="formattedTime"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
style="min-width: 100px"
:rules="$attrs.required ? [requiredFieldRule] : null"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
model = null;
isPopupOpen = false;
"
/>
<QIcon name="Schedule" class="cursor-pointer" />
</template>
<QMenu
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
>
<QTime
:format24h="false"
v-model="formattedTime"
mask="HH:mm"
landscape
now-btn
/>
</QMenu>
</QInput>
</div>
</template>
<style lang="scss">
.vn-input-time.q-field--standard.q-field--readonly .q-field__control:before {
border-bottom-style: solid;
@ -118,8 +120,3 @@ const styleAttrs = computed(() => {
border-style: solid;
}
</style>
<i18n>
es:
Cancel: Cancelar
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { ref, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
@ -49,6 +49,7 @@ const filter = {
'changedModelId',
'changedModelValue',
'description',
'summaryId',
],
include: [
{
@ -376,6 +377,10 @@ async function clearFilter() {
}
setLogTree();
onUnmounted(() => {
stateStore.rightDrawer = false;
});
</script>
<template>
<FetchData
@ -455,12 +460,12 @@ setLogTree();
:style="{
backgroundColor: useColor(modelLog.model),
}"
:title="modelLog.model"
:title="`${modelLog.model} #${modelLog.id}`"
>
{{ t(modelLog.modelI18n) }}
</QChip>
<span class="model-id" v-if="modelLog.id"
>#{{ modelLog.id }}</span
<span class="model-id" v-if="modelLog.summaryId"
>#{{ modelLog.summaryId }}</span
>
<span class="model-value" :title="modelLog.showValue">
{{ modelLog.showValue }}
@ -622,160 +627,140 @@ setLogTree();
</QList>
</div>
</div>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click.stop="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<QScrollArea class="fit text-grey-8">
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()">
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem>
<VnSelect
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
option-label="locale"
option-value="value"
:options="actions"
@update:model-value="selectFilter('action')"
hide-selected
/>
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
>
<template #label="{ label }">
{{ t(`Users.${label}`) }}
</template>
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers && userRadio !== null">
<VnSelect
class="full-width"
:label="t('globals.entity')"
v-model="selectedFilters.changedModel"
option-label="locale"
option-value="value"
:options="actions"
@update:model-value="selectFilter('action')"
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
option-value="id"
:options="workers"
@update:model-value="selectFilter('userSelect')"
hide-selected
/>
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
>
<template #label="{ label }">
{{ t(`Users.${label}`) }}
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers && userRadio !== null">
<VnSelect
class="full-width"
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
option-value="id"
:options="workers"
@update:model-value="selectFilter('userSelect')"
hide-selected
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QInput
:label="t('globals.changes')"
v-model="changeInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="selectFilter('change')"
@focusout="selectFilter('change')"
@clear="selectFilter('change')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip max-width="250px">{{
t('tooltips.changes')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'"
v-for="(checkboxOption, index) in checkboxOptions"
:key="index"
</VnSelect>
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QInput
:label="t('globals.changes')"
v-model="changeInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="selectFilter('change')"
@focusout="selectFilter('change')"
@clear="selectFilter('change')"
>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('to')"
@click="dateToDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'from')"
v-model="dateTo"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</QScrollArea>
</QDrawer>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip max-width="250px">{{
t('tooltips.changes')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'"
v-for="(checkboxOption, index) in checkboxOptions"
:key="index"
>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('to')"
@click="dateToDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'from')"
v-model="dateTo"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</Teleport>
<QDialog v-model="dateFromDialog">
<QDate
:years-in-month-view="false"

View File

@ -0,0 +1,23 @@
<script setup>
defineProps({
title: { type: String, default: null },
content: { type: [String, Number], default: null },
});
</script>
<template>
<QPopupProxy>
<QCard>
<slot name="title">
<div
class="header q-px-sm q-py-xs q-ma-none text-white text-bold bg-primary"
v-text="title"
/>
</slot>
<slot name="content">
<QCardSection class="change-detail q-pa-sm">
{{ content }}
</QCardSection>
</slot>
</QCard>
</QPopupProxy>
</template>

View File

@ -0,0 +1,97 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
const { t } = useI18n();
const $props = defineProps({
progress: {
type: Number, //Progress value (1.0 > x > 0.0)
required: true,
},
showDialog: {
type: Boolean,
required: true,
},
cancelled: {
type: Boolean,
required: false,
default: false,
},
});
const emit = defineEmits(['cancel', 'close']);
const dialogRef = ref(null);
const _showDialog = computed({
get: () => $props.showDialog,
set: (value) => {
if (value) dialogRef.value.show();
},
});
const _progress = computed(() => $props.progress);
const progressLabel = computed(() => `${Math.round($props.progress * 100)}%`);
const cancel = () => {
dialogRef.value.hide();
emit('cancel');
};
</script>
<template>
<QDialog ref="dialogRef" v-model="_showDialog" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row">
<span class="text-h6">{{ t('Progress') }}</span>
<QSpace />
<QBtn icon="close" flat round dense @click="emit('close')" />
</QCardSection>
<QCardSection>
<div class="column">
<span>{{ t('Total progress') }}:</span>
<QLinearProgress
size="30px"
:value="_progress"
color="primary"
stripe
class="q-mt-sm q-mb-md"
>
<div class="absolute-full flex flex-center">
<QBadge
v-if="cancelled"
text-color="white"
color="negative"
:label="t('Cancelled')"
/>
<span v-else class="text-white text-subtitle1">
{{ progressLabel }}
</span>
</div>
</QLinearProgress>
<slot />
</div>
</QCardSection>
<QCardActions align="right">
<QBtn
v-if="!cancelled && progress < 1"
type="button"
flat
class="text-primary"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
Progress: Progreso
Total progress: Progreso total
Cancelled: Cancelado
</i18n>

View File

@ -0,0 +1,6 @@
<script setup>
const model = defineModel({ type: Boolean, required: true });
</script>
<template>
<QRadio v-model="model" v-bind="$attrs" dense :dark="true" class="q-mr-sm" />
</template>

View File

@ -1,6 +1,5 @@
<script setup>
import { ref, toRefs, computed, watch } from 'vue';
import { onMounted } from 'vue';
import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
@ -16,11 +15,15 @@ const $props = defineProps({
},
optionLabel: {
type: [String],
default: '',
default: 'name',
},
optionValue: {
type: String,
default: '',
default: 'id',
},
optionFilter: {
type: String,
default: null,
},
url: {
type: String,
@ -54,12 +57,20 @@ const $props = defineProps({
type: [Number, String],
default: '30',
},
focusOnMount: {
type: Boolean,
default: false,
},
useLike: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
const requiredFieldRule = (val) => val ?? t('globals.fieldRequired');
const { optionLabel, optionValue, options, modelValue } = toRefs($props);
const { optionLabel, optionValue, optionFilter, options, modelValue } = toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
@ -107,12 +118,17 @@ async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return;
const { fields, sortBy, limit } = $props;
let key = optionLabel.value;
let key = optionFilter.value ?? optionLabel.value;
if (new RegExp(/\d/g).test(val)) key = optionValue.value;
const where = { [key]: { like: `%${val}%` } };
return dataRef.value.fetch({ fields, where, order: sortBy, limit });
const defaultWhere = $props.useLike
? { [key]: { like: `%${val}%` } }
: { [key]: val };
const where = { ...defaultWhere, ...$props.where };
const fetchOptions = { where, order: sortBy, limit };
if (fields) fetchOptions.fields = fields;
return dataRef.value.fetch(fetchOptions);
}
async function filterHandler(val, update) {
@ -142,6 +158,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
});
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script>
<template>
@ -174,6 +194,7 @@ watch(modelValue, (newValue) => {
>
<template v-if="isClearable" #append>
<QIcon
v-show="value"
name="close"
@click.stop="value = null"
class="cursor-pointer"

View File

@ -184,6 +184,7 @@ en:
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }'
productNotAvailable: 'Verdnatura communicates: Your order {ticketFk} with reception date on {landed}. {notAvailables} not available. Sorry for the inconvenience.'
en: English
es: Spanish
fr: French
@ -203,6 +204,7 @@ es:
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
productNotAvailable: 'Verdnatura le comunica: Pedido {ticketFk} con fecha de recepción {landed}. {notAvailables} no disponible/s. Disculpe las molestias.'
en: Inglés
es: Español
fr: Francés
@ -222,6 +224,7 @@ fr:
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
productNotAvailable: 'Verdnatura communique : Votre commande {ticketFk} avec date de réception le {landed}. {notAvailables} non disponible. Nous sommes désolés pour les inconvénients.'
en: Anglais
es: Espagnol
fr: Français
@ -240,6 +243,7 @@ pt:
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
productNotAvailable: 'Verdnatura comunica: Seu pedido {ticketFk} com data de recepção em {landed}. {notAvailables} não disponível/eis. Desculpe pelo transtorno.'
en: Inglês
es: Espanhol
fr: Francês

View File

@ -1,16 +1,16 @@
<script setup>
const $props = defineProps({
defineProps({
url: { type: String, default: null },
text: { type: String, default: null },
icon: { type: String, default: 'open_in_new' },
});
</script>
<template>
<div class="titleBox">
<div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'">
<div class="header-link">
<a :href="$props.url" :class="$props.url ? 'link' : 'color-vn-text'">
{{ $props.text }}
<QIcon v-if="url" :name="$props.icon" />
<a :href="url" :class="url ? 'link' : 'color-vn-text'">
{{ text }}
<QIcon v-if="url" :name="icon" />
</a>
</div>
</div>
@ -19,7 +19,4 @@ const $props = defineProps({
a {
font-size: large;
}
.titleBox {
padding-bottom: 2%;
}
</style>

View File

@ -0,0 +1,37 @@
<script setup>
import { computed } from 'vue';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
const props = defineProps({
wdays: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:wdays']);
const weekdayStore = useWeekdayStore();
const selectedWDays = computed({
get: () => props.wdays,
set: (value) => emit('update:wdays', value),
});
const toggleDay = (index) => (selectedWDays.value[index] = !selectedWDays.value[index]);
</script>
<template>
<div class="q-gutter-x-sm" style="width: max-content">
<QBtn
v-for="(weekday, index) in weekdayStore.getLocalesMap"
:key="index"
:label="weekday.localeChar"
rounded
style="max-width: 36px"
:color="selectedWDays[weekday.index] ? 'primary' : ''"
@click="toggleDay(weekday.index)"
/>
</div>
</template>

View File

@ -5,6 +5,7 @@ import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
const $props = defineProps({
url: {
@ -15,21 +16,21 @@ const $props = defineProps({
type: Object,
default: null,
},
module: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
subtitle: {
type: Number,
default: 0,
default: null,
},
dataKey: {
type: String,
default: '',
default: null,
},
module: {
type: String,
default: null,
},
summary: {
type: Object,
@ -38,23 +39,37 @@ const $props = defineProps({
});
const state = useState();
const route = useRoute();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const arrayData = useArrayData($props.dataKey || $props.module, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
let arrayData;
let store;
let entity;
const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
defineExpose({ getData });
defineExpose({
getData,
});
onBeforeMount(async () => {
await getData();
watch($props, async () => await getData());
arrayData = useArrayData($props.dataKey, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
store = arrayData.store;
entity = computed(() => {
const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {};
if (data) emit('onFetch', data);
return data;
});
// It enables to load data only once if the module is the same as the dataKey
if (!isSameDataKey.value || !route.params.id) await getData();
watch(
() => [$props.url, $props.filter],
async () => {
if (!isSameDataKey.value) await getData();
}
);
});
async function getData() {
@ -69,14 +84,50 @@ async function getData() {
isLoading.value = false;
}
}
function getValueFromPath(path) {
if (!path) return;
const keys = path.toString().split('.');
let current = entity.value;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}
const emit = defineEmits(['onFetch']);
const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect
);
</script>
<template>
<div class="descriptor">
<template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action" />
<slot name="header-extra-action"
><QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn></slot
>
<QBtn
@click.stop="viewSummary(entity.id, $props.summary)"
round
@ -108,20 +159,20 @@ const emit = defineEmits(['onFetch']);
</QBtn>
</RouterLink>
<QBtn
v-if="$slots.menu"
color="white"
dense
flat
icon="more_vert"
round
size="md"
:class="{ invisible: !$slots.menu }"
>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu>
<QMenu ref="menuRef">
<QList>
<slot name="menu" :entity="entity" />
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList>
</QMenu>
</QBtn>
@ -131,8 +182,8 @@ const emit = defineEmits(['onFetch']);
<QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title">
<span v-if="$props.title" :title="$props.title">
{{ $props.title }}
<span v-if="$props.title" :title="getValueFromPath(title)">
{{ getValueFromPath(title) ?? $props.title }}
</span>
<slot v-else name="description" :entity="entity">
<span :title="entity.name">
@ -143,7 +194,7 @@ const emit = defineEmits(['onFetch']);
</QItemLabel>
<QItem dense>
<QItemLabel class="subtitle" caption>
#{{ $props.subtitle ?? entity.id }}
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
</QItem>
</QList>
@ -235,6 +286,7 @@ const emit = defineEmits(['onFetch']);
width: 256px;
.header {
display: flex;
align-items: center;
}
.icons {
margin: 0 10px;

View File

@ -28,7 +28,7 @@ const toggleCardCheck = (item) => {
<div class="title text-primary text-weight-bold text-h5">
{{ $props.title }}
</div>
<QChip class="q-chip-color" outline size="sm">
<QChip v-if="$props.id" class="q-chip-color" outline size="sm">
{{ t('ID') }}: {{ $props.id }}
</QChip>
</div>

View File

@ -22,11 +22,15 @@ const props = defineProps({
type: String,
default: '',
},
moduleName: {
type: String,
default: null,
},
});
const emit = defineEmits(['onFetch']);
const route = useRoute();
const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
const arrayData = useArrayData(props.dataKey, {
url: props.url,
filter: props.filter,
skip: 0,
@ -56,11 +60,20 @@ async function fetch() {
}
const showRedirectToSummaryIcon = computed(() => {
const routeExists = route.matched.some(
(route) => route.name === `${route.meta.moduleName}Summary`
);
return !isSummary.value && route.meta.moduleName && routeExists;
const exist = existSummary(route.matched);
return !isSummary.value && route.meta.moduleName && exist;
});
function existSummary(routes) {
const hasSummary = routes.some((r) => r.name === `${route.meta.moduleName}Summary`);
if (hasSummary) return hasSummary;
for (const current of routes) {
if (current.path != '/' && current.children) {
const exist = existSummary(current.children);
if (exist) return exist;
}
}
}
</script>
<template>
@ -74,7 +87,7 @@ const showRedirectToSummaryIcon = computed(() => {
v-if="showRedirectToSummaryIcon"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
>
@ -150,9 +163,9 @@ const showRedirectToSummaryIcon = computed(() => {
margin-top: 2px;
.label {
color: var(--vn-label-color);
width: 8em;
width: 9em;
overflow: hidden;
white-space: nowrap;
white-space: wrap;
text-overflow: ellipsis;
margin-right: 10px;
flex-grow: 0;
@ -174,15 +187,10 @@ const showRedirectToSummaryIcon = computed(() => {
color: lighten($primary, 20%);
}
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnLv from 'components/ui/VnLv.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { useSession } from 'composables/useSession';
import toCurrency from '../../../filters/toCurrency';
import { toCurrency } from 'filters/index';
const DEFAULT_PRICE_KG = 0;
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const { t } = useI18n();
defineProps({
@ -20,6 +18,10 @@ defineProps({
type: Object,
required: true,
},
isCatalog: {
type: Boolean,
default: false,
},
});
const dialog = ref(null);
@ -29,15 +31,8 @@ const dialog = ref(null);
<div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6">
<div class="img-wrapper">
<QImg
:src="`/api/Images/catalog/200x200/${item.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="192"
width="192"
class="image"
/>
<div v-if="item.hex" class="item-color-container">
<VnImg :id="item.id" zoom-size="lg" class="image" />
<div v-if="item.hex && isCatalog" class="item-color-container">
<div
class="item-color"
:style="{ backgroundColor: `#${item.hex}` }"
@ -59,8 +54,12 @@ const dialog = ref(null);
</template>
<div class="footer">
<div class="price">
<p>{{ item.available }} {{ t('to') }} {{ item.price }}</p>
<QIcon name="add_circle" class="icon">
<p v-if="isCatalog">
{{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }}
</p>
<slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog">
<OrderCatalogItemDialog

View File

@ -1,5 +1,7 @@
<script setup>
defineProps({
import { computed } from 'vue';
const $props = defineProps({
maxLength: {
type: Number,
required: true,
@ -8,53 +10,40 @@ defineProps({
type: Object,
required: true,
},
tag: {
type: String,
required: false,
default: 'tag',
},
value: {
type: String,
required: false,
default: 'value',
},
});
const tags = computed(() => {
return Object.keys($props.item)
.filter((i) => i.startsWith(`${$props.tag}`))
.reduce((acc, tag) => {
const n = tag.split(`${$props.tag}`)[1];
const key = `${$props.tag}${n}`;
const value = `${$props.value}${n}`;
acc[$props.item[key] ?? key] = $props.item[value] ?? '';
return acc;
}, {});
});
</script>
<template>
<div class="fetchedTags">
<div class="wrap">
<div
v-for="(val, key) in tags"
:key="key"
class="inline-tag"
:class="{ empty: !$props.item.value5 }"
:title="$props.item.tag5 + ': ' + $props.item.value5"
:title="`${key}: ${val}`"
:class="{ empty: !val }"
>
{{ $props.item.value5 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.tag6 }"
:title="$props.item.tag6 + ': ' + $props.item.value6"
>
{{ $props.item.value6 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value7 }"
:title="$props.item.tag7 + ': ' + $props.item.value7"
>
{{ $props.item.value7 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value8 }"
:title="$props.item.tag8 + ': ' + $props.item.value8"
>
{{ $props.item.value8 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value9 }"
:title="$props.item.tag9 + ': ' + $props.item.value9"
>
{{ $props.item.value9 }}
</div>
<div
class="inline-tag"
:class="{ empty: !$props.item.value10 }"
:title="$props.item.tag10 + ': ' + $props.item.value10"
>
{{ $props.item.value10 }}
{{ val }}
</div>
</div>
</div>
@ -72,7 +61,7 @@ defineProps({
.inline-tag {
height: 1rem;
margin: 0.05rem;
color: $secondary;
color: $color-font-secondary;
text-align: center;
font-size: smaller;
padding: 1px;
@ -83,9 +72,8 @@ defineProps({
min-width: 4rem;
max-width: 4rem;
}
.empty {
border: 1px solid $color-spacer-light;
border: 1px solid #2b2b2b;
}
}
</style>

View File

@ -59,12 +59,10 @@ const containerClasses = computed(() => {
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-month__head--weekday {
@ -108,11 +106,10 @@ const containerClasses = computed(() => {
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
background-color: var(--vn-label-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
@ -150,7 +147,7 @@ const containerClasses = computed(() => {
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
color: $color-font-secondary;
font-weight: bold;
font-size: 0.8rem;
text-align: center;

View File

@ -1,20 +1,14 @@
<template>
<div class="q-pa-md">
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md">
<QSkeleton type="QBtn" />
<QSkeleton type="QBtn" />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
</template>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
</template>

View File

@ -1,25 +1,38 @@
<script setup>
defineProps({
columns: {
type: Number,
default: 6,
},
});
</script>
<template>
<div class="q-pa-md w">
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<div class="q-pa-md q-mx-md container">
<div class="row q-gutter-md q-mb-md justify-around no-wrap">
<QSkeleton type="rect" square v-for="n in columns" :key="n" class="column" />
</div>
<div
class="row q-gutter-md q-mb-md justify-around no-wrap"
v-for="n in 5"
:key="n"
>
<QSkeleton
type="QInput"
square
v-for="m in columns"
:key="m"
class="column"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.w {
width: 80vw;
.container {
width: 100%;
overflow-x: hidden;
}
.column {
flex-shrink: 0;
width: 200px;
}
</style>

View File

@ -67,6 +67,7 @@ async function confirm() {
</QCardSection>
<QCardSection class="row items-center">
<span v-html="message"></span>
<slot name="customHTML"></slot>
</QCardSection>
<QCardActions align="right">
<QBtn

View File

@ -4,11 +4,14 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n();
const props = defineProps({
const $props = defineProps({
modelValue: {
type: Object,
default: () => {},
},
dataKey: {
type: String,
required: true,
@ -18,11 +21,6 @@ const props = defineProps({
required: false,
default: false,
},
params: {
type: Object,
required: false,
default: null,
},
showAll: {
type: Boolean,
default: true,
@ -40,134 +38,169 @@ const props = defineProps({
},
hiddenTags: {
type: Array,
default: () => [],
default: () => ['filter'],
},
customTags: {
type: Array,
default: () => [],
},
disableSubmitEvent: {
type: Boolean,
default: false,
},
searchUrl: {
type: String,
default: 'params',
},
redirect: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
defineExpose({ search });
const emit = defineEmits([
'update:modelValue',
'refresh',
'clear',
'search',
'init',
'remove',
]);
const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder,
const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl,
navigate: $props.redirect ? {} : null,
});
const route = useRoute();
const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
onMounted(() => {
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params));
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
userParams.value = $props.modelValue ?? {};
emit('init', { params: userParams.value });
});
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter;
userParams.value = { ...userParams.value, ...watchedParams };
}
watch(
() => route.query.params,
(val) => {
if (!val) {
userParams.value = {};
} else {
const parsedParams = JSON.parse(val);
userParams.value = { ...parsedParams };
}
}
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
watch(
() => arrayData.store.userParams,
(val) => setUserParams(val)
);
watch(
() => $props.modelValue,
(val) => (userParams.value = val ?? {})
);
const isLoading = ref(false);
async function search() {
async function search(evt) {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {};
isLoading.value = true;
const params = { ...userParams.value };
const filter = { ...userParams.value };
store.userParamsChanged = true;
store.filter.skip = 0;
store.skip = 0;
const { params: newParams } = await arrayData.addFilter({ params });
arrayData.reset(['skip', 'filter.skip', 'page']);
const { params: newParams } = await arrayData.addFilter({ params: userParams.value });
userParams.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = [];
if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false;
emit('search');
navigate(store.data, {});
}
async function reload() {
isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param);
store.skip = 0;
store.page = 1;
await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = [];
if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
navigate(store.data, {});
}
async function clearFilters() {
isLoading.value = true;
store.userParamsChanged = true;
store.filter.skip = 0;
store.skip = 0;
arrayData.reset(['skip', 'filter.skip', 'page']);
// Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) =>
props.unremovableParams.includes(param)
$props.unremovableParams.includes(param)
);
const newParams = {};
// Conservar solo los params que no son removibles
for (const key of removableFilters) {
newParams[key] = userParams.value[key];
}
userParams.value = {};
userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value });
if (!props.showAll) {
if (!$props.showAll) {
store.data = [];
}
isLoading.value = false;
emit('clear');
emit('update:modelValue', userParams.value);
}
const tagsList = computed(() =>
Object.entries(userParams.value)
.filter(([key, value]) => value && !(props.hiddenTags || []).includes(key))
.map(([key, value]) => ({
label: key,
value: value,
}))
);
const tagsList = computed(() => {
const tagList = [];
for (const key of Object.keys(userParams.value)) {
const value = userParams.value[key];
if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value });
}
return tagList;
});
const tags = computed(() =>
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.label))
);
const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
});
const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.label))
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
);
async function remove(key) {
userParams.value[key] = null;
await arrayData.applyFilter({ params: userParams.value });
userParams.value[key] = undefined;
search();
emit('remove', key);
emit('update:modelValue', userParams.value);
}
function formatValue(value) {
if (typeof value === 'boolean') {
return value ? t('Yes') : t('No');
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
if (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
return `"${value}"`;
}
</script>
<template>
<QBtn
class="q-mt-lg q-mr-xs q-mb-lg"
round
color="primary"
style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search"
@click="search()"
></QBtn>
<QForm @submit="search" id="filterPanelForm">
<QList dense>
<QItem class="q-mt-xs">
@ -222,7 +255,7 @@ function formatValue(value) {
<slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs">
<strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span>
<span>"{{ formatValue(chip.value) }}"</span>
</div>
</slot>
</VnFilterPanelChip>
@ -241,23 +274,6 @@ function formatValue(value) {
<QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot>
</QList>
<template v-if="props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
:label="t('Search')"
class="full-width"
color="primary"
dense
icon="search"
rounded
type="submit"
unelevated
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QForm>
<QInnerLoading
:label="t('globals.pleaseWait')"
@ -265,7 +281,6 @@ function formatValue(value) {
color="primary"
/>
</template>
<style scoped lang="scss">
.list {
width: 256px;

View File

@ -0,0 +1,78 @@
<script setup>
import { ref, computed } from 'vue';
import { useSession } from 'src/composables/useSession';
const $props = defineProps({
storage: {
type: [String, Number],
default: 'Images',
},
collection: {
type: String,
default: 'catalog',
},
size: {
type: String,
default: '200x200',
},
zoomSize: {
type: String,
required: false,
default: 'lg',
},
id: {
type: Number,
required: true,
},
});
const show = ref(false);
const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`);
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
const url = computed(() => {
const isEmployee = useRole().isEmployee();
return isEmployee
? `/api/${$props.storage}/${$props.collection}/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noImage;
});
const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`;
};
defineExpose({
reload,
});
</script>
<template>
<QImg
:class="{ zoomIn: $props.zoomSize }"
:src="url"
v-bind="$attrs"
@click="show = !show"
spinner-color="primary"
/>
<QDialog v-model="show" v-if="$props.zoomSize">
<QImg
:src="url"
size="full"
class="img_zoom"
v-bind="$attrs"
spinner-color="primary"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.q-img {
&.zoomIn {
cursor: zoom-in;
}
min-width: 50px;
}
.rounded {
border-radius: 50%;
}
.img_zoom {
border-radius: 0%;
}
</style>

View File

@ -2,6 +2,7 @@
import { dashIfEmpty } from 'src/filters';
import { useI18n } from 'vue-i18n';
import { useClipboard } from 'src/composables/useClipboard';
import { computed } from 'vue';
const $props = defineProps({
label: { type: String, default: null },
@ -24,52 +25,67 @@ function copyValueText() {
},
});
}
const val = computed(() => $props.value);
</script>
<style scoped>
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
</style>
<template>
<div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label">
<slot name="label">
<span>{{ $props.label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="$props.value">
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }}
</span>
</slot>
</div>
<div class="info" v-if="$props.info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ $props.info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
<QCheckbox
v-if="typeof value === 'boolean'"
v-model="val"
:label="label"
disable
dense
/>
<template v-else>
<div v-if="label || $slots.label" class="label">
<slot name="label">
<span>{{ label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="value">
{{ dash ? dashIfEmpty(value) : value }}
</span>
</slot>
</div>
<div class="info" v-if="info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="copy && value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.vn-label-value:hover .copy {
visibility: visible;
cursor: pointer;
.vn-label-value {
&:hover .copy {
visibility: visible;
cursor: pointer;
}
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
}
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
:deep(.q-checkbox.disabled) {
opacity: 1 !important;
}
</style>

View File

@ -20,7 +20,12 @@ const state = useState();
const currentUser = ref(state.getUser());
const newNote = ref('');
const vnPaginateRef = ref();
function handleKeyUp(event) {
if (event.key === 'Enter') {
event.preventDefault();
if (!event.shiftKey) insert();
}
}
async function insert() {
const body = $props.body;
Object.assign(body, { text: newNote.value });
@ -48,12 +53,12 @@ async function insert() {
size="lg"
autogrow
autofocus
@keyup.ctrl.enter.stop="insert"
@keyup="handleKeyUp"
clearable
>
<template #append
><QBtn
:title="t('Save (ctrl + Enter)')"
<template #append>
<QBtn
:title="t('Save (Enter)')"
icon="save"
color="primary"
flat
@ -73,6 +78,7 @@ async function insert() {
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
search-url="notes"
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">
@ -130,6 +136,6 @@ async function insert() {
es:
Add note here...: Añadir nota aquí...
New note: Nueva nota
Save (ctrl + Enter): Guardar (Ctrl + Intro)
Save (Enter): Guardar (Intro)
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
@ -28,6 +28,10 @@ const props = defineProps({
type: Object,
default: null,
},
userFilter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
@ -60,14 +64,19 @@ const props = defineProps({
type: Function,
default: null,
},
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onFetch', 'onPaginate']);
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({
sortBy: props.order,
rowsPerPage: props.limit,
@ -77,25 +86,40 @@ const pagination = ref({
const arrayData = useArrayData(props.dataKey, {
url: props.url,
filter: props.filter,
userFilter: props.userFilter,
where: props.where,
limit: props.limit,
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Esta parte del reload la veo mas en arrayData

Esta parte del reload la veo mas en arrayData
order: props.order,
userParams: props.userParams,
exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts,
Outdated
Review

Esta part entonces deuria estar igual pero en FormModel per exemple??
Si es així igual valdria la pena ficar-ho en ArrayData y en una prop poder habilitar-ho o deshabilitar-ho?

Esta part entonces deuria estar igual pero en FormModel per exemple?? Si es així igual valdria la pena ficar-ho en ArrayData y en una prop poder habilitar-ho o deshabilitar-ho?

Mmm...el fallo no está en los formularios, ya que en FormModel no se han reportado errores.
Por otra parte, en que casos querrías deshabitar la funcionalidad de que al cambiar la URL cambie la ventana?

Mmm...el fallo no está en los formularios, ya que en FormModel no se han reportado errores. Por otra parte, en que casos querrías deshabitar la funcionalidad de que al cambiar la URL cambie la ventana?

Mover la funcionalidad a ArrayData
la funcion fecth cambiar a fetch({append:false});
Añadir condición para decidir que casos no se quiere. Por defecto si se quiere
Probar en formModel

Mover la funcionalidad a ArrayData la funcion fecth cambiar a fetch({append:false}); Añadir condición para decidir que casos no se quiere. Por defecto si se quiere Probar en formModel
searchUrl: props.searchUrl,
});
const store = arrayData.store;
onMounted(() => {
if (props.autoLoad) fetch();
onMounted(async () => {
if (props.autoLoad) await fetch();
mounted.value = true;
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Clave seria Key?

Clave seria Key?
});
onBeforeUnmount(() => arrayData.reset());
watch(
() => props.data,
() => {
store.data = props.data;
}
);
watch(
() => store.data,
(data) => emit('onChange', data)
);
watch(
() => props.url,
(url) => fetch({ url })
);
onBeforeRouteUpdate((to, from, next) => {
if (to.params.id !== from.params.id) {
arrayData.reloadRoute(to.params.id);
@ -107,14 +131,15 @@ const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
async function fetch() {
store.filter.skip = 0;
store.skip = 0;
async function fetch(params) {
useArrayData(props.dataKey, params);
arrayData.reset(['filter.skip', 'skip']);
await arrayData.fetch({ append: false });
if (!store.hasMoreData) {
isLoading.value = false;
}
emit('onFetch', store.data);
return store.data;
}
async function paginate() {
@ -146,7 +171,7 @@ function endPagination() {
emit('onPaginate');
}
async function onLoad(index, done) {
if (!store.data) return done();
if (!store.data || !mounted.value) return done();
if (store.data.length === 0 || !props.url) return done(false);
@ -158,7 +183,7 @@ async function onLoad(index, done) {
done(isDone);
}
defineExpose({ fetch, addFilter });
defineExpose({ fetch, addFilter, paginate });
</script>
<template>
@ -207,12 +232,6 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" />
</div>
</QInfiniteScroll>
<div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div>
</template>
<style lang="scss" scoped>

View File

@ -1,13 +1,14 @@
<script setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore';
const quasar = useQuasar();
const { t } = useI18n();
const state = useStateStore();
const props = defineProps({
dataKey: {
@ -16,17 +17,14 @@ const props = defineProps({
},
label: {
type: String,
required: false,
default: 'Search',
},
info: {
type: String,
required: false,
default: '',
},
redirect: {
type: Boolean,
required: false,
default: true,
},
url: {
@ -65,12 +63,34 @@ const props = defineProps({
type: String,
default: '',
},
makeFetch: {
type: Boolean,
default: true,
},
});
const arrayData = useArrayData(props.dataKey, { ...props });
const { store } = arrayData;
const searchText = ref('');
const { navigate } = useRedirect();
let arrayDataProps = { ...props };
if (props.redirect)
arrayDataProps = {
...props,
...{
navigate: {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
},
},
};
let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store;
watch(
() => props.dataKey,
(val) => {
arrayData = useArrayData(val, { ...props });
store = arrayData.store;
}
);
onMounted(() => {
const params = store.userParams;
@ -83,52 +103,48 @@ async function search() {
const staticParams = Object.entries(store.userParams).filter(
([key, value]) => value && (props.staticParams || []).includes(key)
);
store.skip = 0;
await arrayData.applyFilter({
params: {
...Object.fromEntries(staticParams),
search: searchText.value,
},
});
arrayData.reset(['skip', 'page']);
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
if (props.makeFetch)
await arrayData.applyFilter({
params: {
...Object.fromEntries(staticParams),
search: searchText.value,
},
});
}
</script>
<template>
<QForm @submit="search" id="searchbarForm">
<VnInput
id="searchbar"
v-model="searchText"
:placeholder="t(props.label)"
dense
standout
autofocus
>
<template #prepend>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template>
<template #append>
<QIcon
v-if="props.info && $q.screen.gt.xs"
name="info"
class="cursor-info"
>
<QTooltip>{{ t(props.info) }}</QTooltip>
</QIcon>
</template>
</VnInput>
</QForm>
<Teleport to="#searchbar" v-if="state.isHeaderMounted()">
<QForm @submit="search" id="searchbarForm">
<VnInput
id="searchbar"
v-model="searchText"
:placeholder="t(props.label)"
dense
standout
autofocus
>
<template #prepend>
<QIcon
v-if="!quasar.platform.is.mobile"
class="cursor-pointer"
name="search"
@click="search"
/>
</template>
<template #append>
<QIcon
v-if="props.info && $q.screen.gt.xs"
name="info"
class="cursor-info"
>
<QTooltip>{{ t(props.info) }}</QTooltip>
</QIcon>
</template>
</VnInput>
</QForm>
</Teleport>
</template>
<style lang="scss" scoped>

View File

@ -1,6 +1,7 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { onMounted, onBeforeUnmount, ref, nextTick } from 'vue';
import { useStateStore } from 'stores/useStateStore';
const stateStore = useStateStore();
const actions = ref(null);
const data = ref(null);
@ -18,15 +19,13 @@ onMounted(() => {
const observer = new MutationObserver(
() =>
(hasContent.value =
actions.value.childNodes.length + data.value.childNodes.length)
actions.value?.childNodes?.length + data.value?.childNodes?.length)
);
if (actions.value) observer.observe(actions.value, opts);
if (data.value) observer.observe(data.value, opts);
});
onUnmounted(() => {
stateStore.toggleSubToolbar();
});
onBeforeUnmount(() => stateStore.toggleSubToolbar());
</script>
<template>

33
src/composables/useAcl.js Normal file
View File

@ -0,0 +1,33 @@
import axios from 'axios';
import { useState } from './useState';
export function useAcl() {
const state = useState();
async function fetch() {
const { data } = await axios.get('VnUsers/acls');
const acls = {};
data.forEach((acl) => {
acls[acl.model] = acls[acl.model] || {};
acls[acl.model][acl.property] = acls[acl.model][acl.property] || {};
acls[acl.model][acl.property][acl.accessType] = true;
});
state.setAcls(acls);
}
function hasAny(model, prop, accessType) {
const acls = state.getAcls().value[model];
if (acls)
return ['*', prop].some((key) => {
const acl = acls[key];
return acl && (acl['*'] || acl[accessType]);
});
}
return {
fetch,
hasAny,
state,
};
}

View File

@ -1,29 +1,33 @@
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
const arrayDataStore = useArrayDataStore();
export function useArrayData(key, userOptions) {
export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
if (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) arrayDataStore.set(key);
const store = arrayDataStore.get(key);
const route = useRoute();
const router = useRouter();
let canceller = null;
const page = ref(1);
onMounted(() => {
setOptions();
store.skip = 0;
arrayDataStore.reset(['skip']);
const query = route.query;
if (query.params) {
store.userParams = JSON.parse(query.params);
const searchUrl = store.searchUrl;
if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]);
const filter = params?.filter;
delete params.filter;
store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter ?? '{}'), ...store.userFilter };
}
});
@ -56,13 +60,15 @@ export function useArrayData(key, userOptions) {
'userParams',
'userFilter',
'exprBuilder',
'searchUrl',
'navigate',
];
if (typeof userOptions === 'object') {
for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option])
@ -81,7 +87,6 @@ export function useArrayData(key, userOptions) {
const filter = {
order: store.order,
limit: store.limit,
skip: store.skip,
};
let exprFilter;
@ -96,15 +101,19 @@ export function useArrayData(key, userOptions) {
}
Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter);
const params = {
filter: JSON.stringify(store.filter),
};
let where;
if (filter?.where || store.filter?.where)
where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams);
params.filter.skip = store.skip;
store.isLoading = true;
params.filter = JSON.stringify(params.filter);
store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
params,
@ -134,6 +143,14 @@ export function useArrayData(key, userOptions) {
}
}
function deleteOption(option) {
delete store[option];
}
function reset(opts = []) {
if (arrayDataStore.get(key)) arrayDataStore.reset(key, opts);
}
function cancelRequest() {
if (canceller) {
canceller.abort();
@ -144,35 +161,39 @@ export function useArrayData(key, userOptions) {
async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter;
store.filter = {};
if (params) store.userParams = Object.assign({}, params);
if (params) store.userParams = { ...params };
await fetch({ append: false });
const response = await fetch({ append: false });
return response;
}
async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter);
if (filter) store.filter = filter;
let userParams = Object.assign({}, store.userParams, params);
let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams;
store.skip = 0;
store.filter.skip = 0;
page.value = 1;
arrayDataStore.reset(['skip', 'filter.skip', 'page']);
await fetch({ append: false });
return { filter, params };
}
async function addFilterWhere(where) {
const storedFilter = { ...store.filter };
if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } });
}
function sanitizerParams(params, exprBuilder) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
const key = Object.keys(
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
@ -186,8 +207,8 @@ export function useArrayData(key, userOptions) {
async function loadMore() {
if (!store.hasMoreData) return;
store.skip = store.limit * page.value;
page.value += 1;
store.skip = store.limit * store.page;
store.page += 1;
await fetch({ append: true });
}
@ -197,22 +218,34 @@ export function useArrayData(key, userOptions) {
}
function updateStateParams() {
const query = {};
if (store.order) query.order = store.order;
if (store.limit) query.limit = store.limit;
if (store.skip) query.skip = store.skip;
if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams);
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
const url = new URL(window.location.href);
const { hash: currentHash } = url;
const [currentRoute] = currentHash.split('?');
if (store.navigate) {
const { customRouteRedirectName, searchText } = store.navigate;
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const params = new URLSearchParams();
for (const param in query) params.append(param, query[param]);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
url.hash = currentRoute + '?' + params.toString();
window.history.pushState({}, '', url.hash);
if (route.path != to) {
const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query;
destroy();
return router.push(pushUrl);
}
}
router.replace(newUrl);
}
const totalRows = computed(() => (store.data && store.data.length) || 0);
@ -222,6 +255,7 @@ export function useArrayData(key, userOptions) {
fetch,
applyFilter,
addFilter,
addFilterWhere,
refresh,
destroy,
loadMore,
@ -229,6 +263,8 @@ export function useArrayData(key, userOptions) {
totalRows,
updateStateParams,
isLoading,
deleteOption,
reset,
reloadRoute,
};
}

View File

@ -1,25 +0,0 @@
import { useRouter } from 'vue-router';
export default function useRedirect() {
const router = useRouter();
const navigate = (data, { customRouteRedirectName, searchText }) => {
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
data.length === 1
? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`)
: path.replace(/:id.*/, '');
router.push({ path: to });
};
return { navigate };
}

View File

@ -27,8 +27,12 @@ export function useRole() {
return false;
}
function isEmployee() {
return hasAny(['employee']);
}
return {
isEmployee,
fetch,
hasAny,
state,

View File

@ -1,5 +1,6 @@
import { useState } from './useState';
import { useRole } from './useRole';
import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig';
import axios from 'axios';
import useNotify from './useNotify';
@ -57,37 +58,44 @@ export function useSession() {
}
}
}
async function destroy() {
async function destroy(destroyTokens = true) {
const tokens = {
tokenMultimedia: 'Accounts/logout',
token: 'VnUsers/logout',
};
const storage = keepLogin() ? localStorage : sessionStorage;
let destroyTokenPromises = [];
try {
if (destroyTokens) {
const { data: isValidToken } = await axios.get('VnUsers/validateToken');
if (isValidToken)
destroyTokenPromises = Object.entries(tokens).map(([key, url]) =>
destroyToken(url, storage, key)
);
}
} finally {
localStorage.clear();
sessionStorage.clear();
await Promise.allSettled(destroyTokenPromises);
const { setUser } = useState();
for (const [key, url] of Object.entries(tokens)) {
await destroyToken(url, storage, key);
setUser({
id: 0,
name: '',
nickname: '',
lang: '',
darkMode: null,
});
stopRenewer();
}
localStorage.clear();
sessionStorage.clear();
const { setUser } = useState();
setUser({
id: 0,
name: '',
nickname: '',
lang: '',
darkMode: null,
});
stopRenewer();
}
async function login(data) {
setSession(data);
await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch();
await useTokenConfig().fetch();

View File

@ -11,8 +11,11 @@ const user = ref({
companyFk: null,
warehouseFk: null,
});
if (sessionStorage.getItem('user'))
user.value = JSON.parse(sessionStorage.getItem('user'));
const roles = ref([]);
const acls = ref([]);
const tokenConfig = ref({});
const drawer = ref(true);
const headerMounted = ref(false);
@ -25,7 +28,10 @@ export function useState() {
}
function setUser(data) {
user.value = data;
const currentUser = { ...JSON.parse(sessionStorage.getItem('user')), ...data };
sessionStorage.setItem('user', JSON.stringify(currentUser));
user.value = currentUser;
return currentUser;
}
function getRoles() {
@ -37,6 +43,14 @@ export function useState() {
function setRoles(data) {
roles.value = data;
}
function getAcls() {
return computed(() => acls.value);
}
function setAcls(data) {
acls.value = data;
}
function getTokenConfig() {
return computed(() => {
return tokenConfig.value;
@ -64,6 +78,8 @@ export function useState() {
setUser,
getRoles,
setRoles,
getAcls,
setAcls,
getTokenConfig,
setTokenConfig,
set,

View File

@ -5,6 +5,7 @@
body.body--light {
--font-color: black;
--vn-section-color: #e0e0e0;
--vn-section-hover-color: #b9b9b9;
--vn-page-color: #ffffff;
--vn-text-color: var(--font-color);
--vn-label-color: #5f5f5f;
@ -19,6 +20,7 @@ body.body--light {
body.body--dark {
--vn-page-color: #222;
--vn-section-color: #3d3d3d;
--vn-section-hover-color: #747474;
--vn-text-color: white;
--vn-label-color: #a8a8a8;
--vn-accent-color: #424242;
@ -71,12 +73,13 @@ select:-webkit-autofill {
.bg-vn-section-color {
background-color: var(--vn-section-color);
}
.bg-hover {
background-color: #666666;
.bg-vn-hover {
background-color: var(--vn-section-hover-color);
}
.color-vn-label {
color: var(--vn-label);
color: var(--vn-label-color);
}
.color-vn-text {
@ -115,6 +118,13 @@ select:-webkit-autofill {
background-color: var(--vn-accent-color);
}
.text-primary-light {
color: $primary-light !important;
}
.bg-primary-light {
background: $primary-light !important;
}
.fill-icon {
font-variation-settings: 'FILL' 1;
}
@ -145,6 +155,15 @@ select:-webkit-autofill {
color: var(--vn-label-color);
}
.disabled {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.q-chip,
.q-notification__message,
.q-notification__icon {
@ -167,7 +186,12 @@ select:-webkit-autofill {
justify-content: center;
}
/* q-notification row items-stretch q-notification--standard bg-negative text-white */
.q-card,
.q-table,
.q-table__bottom,
.q-drawer {
background-color: var(--vn-section-color);
}
input[type='number'] {
-moz-appearance: textfield;
@ -182,3 +206,26 @@ input::-webkit-inner-spin-button {
.q-scrollarea__content {
max-width: 100%;
}
/* ===== Scrollbar CSS ===== /
/ Firefox */
* {
scrollbar-width: auto;
scrollbar-color: var(--vn-label-color) transparent;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background-color: var(--vn-label-color);
border-radius: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

(image error) Size: 180 KiB

After

(image error) Size: 180 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,418 +1,438 @@
@font-face {
font-family: 'icon';
src: url('fonts/icon.eot?2omjsr');
src: url('fonts/icon.eot?2omjsr#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?2omjsr') format('truetype'),
url('fonts/icon.woff?2omjsr') format('woff'),
url('fonts/icon.svg?2omjsr#icon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
font-family: 'icon';
src: url('fonts/icon.eot?y0x93o');
src: url('fonts/icon.eot?y0x93o#iefix') format('embedded-opentype'),
url('fonts/icon.ttf?y0x93o') format('truetype'),
url('fonts/icon.woff?y0x93o') format('woff'),
url('fonts/icon.svg?y0x93o#icon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^='icon-'],
[class*=' icon-'] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-entry_lastbuys:before {
content: "\e91a";
}
.icon-100:before {
content: '\e926';
content: "\e901";
}
.icon-Client_unpaid:before {
content: '\e925';
}
.icon-Client_unpaid:before {
content: '\e925';
content: "\e98c";
}
.icon-History:before {
content: '\e964';
content: "\e902";
}
.icon-Person:before {
content: '\e984';
content: "\e903";
}
.icon-accessory:before {
content: '\e948';
content: "\e904";
}
.icon-account:before {
content: '\e927';
content: "\e905";
}
.icon-actions:before {
content: '\e928';
content: "\e907";
}
.icon-addperson:before {
content: '\e929';
content: "\e908";
}
.icon-agency:before {
content: '\e92a';
}
.icon-agency:before {
content: '\e92a';
content: "\e92a";
}
.icon-agency-term:before {
content: '\e92b';
content: "\e909";
}
.icon-albaran:before {
content: '\e92c';
}
.icon-albaran:before {
content: '\e92c';
content: "\e92c";
}
.icon-anonymous:before {
content: '\e92d';
content: "\e90b";
}
.icon-apps:before {
content: '\e92e';
content: "\e90c";
}
.icon-artificial:before {
content: '\e92f';
content: "\e90d";
}
.icon-attach:before {
content: '\e930';
content: "\e90e";
}
.icon-barcode:before {
content: '\e932';
content: "\e90f";
}
.icon-basket:before {
content: '\e933';
content: "\e910";
}
.icon-basketadd:before {
content: '\e934';
content: "\e911";
}
.icon-bin:before {
content: '\e935';
content: "\e913";
}
.icon-botanical:before {
content: '\e936';
content: "\e914";
}
.icon-bucket:before {
content: '\e937';
content: "\e915";
}
.icon-buscaman:before {
content: '\e938';
content: "\e916";
}
.icon-buyrequest:before {
content: '\e939';
content: "\e917";
}
.icon-calc_volum:before {
content: '\e93a';
.icon-calc_volum .path1:before {
content: "\e918";
color: rgb(0, 0, 0);
}
.icon-calc_volum .path2:before {
content: "\e919";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path3:before {
content: "\e91c";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path4:before {
content: "\e91d";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path5:before {
content: "\e91e";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path6:before {
content: "\e91f";
margin-left: -1em;
color: rgb(255, 255, 255);
}
.icon-calendar:before {
content: '\e940';
content: "\e920";
}
.icon-catalog:before {
content: '\e941';
content: "\e921";
}
.icon-claims:before {
content: '\e942';
content: "\e922";
}
.icon-client:before {
content: '\e943';
content: "\e923";
}
.icon-clone:before {
content: '\e945';
content: "\e924";
}
.icon-columnadd:before {
content: '\e946';
content: "\e925";
}
.icon-columndelete:before {
content: '\e947';
content: "\e926";
}
.icon-components:before {
content: '\e949';
content: "\e927";
}
.icon-consignatarios:before {
content: '\e94b';
content: "\e928";
}
.icon-control:before {
content: '\e94c';
content: "\e929";
}
.icon-credit:before {
content: '\e94d';
content: "\e92b";
}
.icon-defaulter:before {
content: '\e94e';
content: "\e92d";
}
.icon-deletedTicket:before {
content: '\e94f';
content: "\e92e";
}
.icon-deleteline:before {
content: '\e950';
content: "\e92f";
}
.icon-delivery:before {
content: '\e951';
content: "\e930";
}
.icon-deliveryprices:before {
content: '\e952';
content: "\e932";
}
.icon-details:before {
content: '\e954';
content: "\e933";
}
.icon-dfiscales:before {
content: '\e955';
content: "\e934";
}
.icon-disabled:before {
content: '\e965';
content: "\e935";
}
.icon-doc:before {
content: '\e956';
content: "\e936";
}
.icon-entry:before {
content: '\e958';
content: "\e937";
}
.icon-exit:before {
content: '\e959';
content: "\e938";
}
.icon-eye:before {
content: '\e95a';
content: "\e939";
}
.icon-fixedPrice:before {
content: '\e95b';
content: "\e93a";
}
.icon-flower:before {
content: '\e95c';
content: "\e93b";
}
.icon-frozen:before {
content: '\e95d';
content: "\e93c";
}
.icon-fruit:before {
content: '\e95e';
content: "\e93d";
}
.icon-funeral:before {
content: '\e95f';
content: "\e93e";
}
.icon-grafana:before {
content: '\e931';
}
.icon-grafana:before {
content: '\e931';
content: "\e906";
}
.icon-greenery:before {
content: '\e91e';
content: "\e93f";
}
.icon-greuge:before {
content: '\e960';
content: "\e940";
}
.icon-grid:before {
content: '\e961';
content: "\e941";
}
.icon-handmade:before {
content: '\e94a';
content: "\e942";
}
.icon-handmadeArtificial:before {
content: '\e962';
content: "\e943";
}
.icon-headercol:before {
content: '\e963';
content: "\e945";
}
.icon-info:before {
content: '\e966';
content: "\e946";
}
.icon-inventory:before {
content: '\e967';
content: "\e947";
}
.icon-invoice:before {
content: '\e969';
content: "\e968";
color: #5f5f5f;
}
.icon-invoice-in:before {
content: '\e96a';
content: "\e949";
}
.icon-invoice-in-create:before {
content: '\e96b';
content: "\e94a";
}
.icon-invoice-out:before {
content: '\e96c';
content: "\e94b";
}
.icon-isTooLittle:before {
content: '\e96e';
content: "\e94c";
}
.icon-item:before {
content: '\e96f';
content: "\e94d";
}
.icon-languaje:before {
content: '\e912';
content: "\e970";
}
.icon-lines:before {
content: '\e971';
content: "\e94e";
}
.icon-linesprepaired:before {
content: '\e972';
content: "\e94f";
}
.icon-link-to-corrected:before {
content: '\e900';
content: "\e931";
}
.icon-link-to-correcting:before {
content: '\e906';
content: "\e944";
}
.icon-logout:before {
content: '\e90a';
content: "\e973";
}
.icon-mana:before {
content: '\e974';
content: "\e950";
}
.icon-mandatory:before {
content: '\e975';
content: "\e951";
}
.icon-net:before {
content: '\e976';
content: "\e952";
}
.icon-newalbaran:before {
content: '\e977';
content: "\e954";
}
.icon-niche:before {
content: '\e979';
content: "\e955";
}
.icon-no036:before {
content: '\e97a';
content: "\e956";
}
.icon-noPayMethod:before {
content: '\e97b';
content: "\e958";
}
.icon-notes:before {
content: '\e97c';
content: "\e959";
}
.icon-noweb:before {
content: '\e97e';
content: "\e95a";
}
.icon-onlinepayment:before {
content: '\e97f';
content: "\e95b";
}
.icon-package:before {
content: '\e980';
content: "\e95c";
}
.icon-payment:before {
content: '\e982';
content: "\e95d";
}
.icon-pbx:before {
content: '\e983';
content: "\e95e";
}
.icon-pets:before {
content: '\e985';
content: "\e95f";
}
.icon-photo:before {
content: '\e986';
content: "\e960";
}
.icon-plant:before {
content: '\e987';
content: "\e961";
}
.icon-polizon:before {
content: '\e989';
content: "\e962";
}
.icon-preserved:before {
content: '\e98a';
content: "\e963";
}
.icon-recovery:before {
content: '\e98b';
content: "\e964";
}
.icon-regentry:before {
content: '\e901';
content: "\e965";
}
.icon-reserva:before {
content: '\e902';
content: "\e966";
}
.icon-revision:before {
content: '\e903';
content: "\e967";
}
.icon-risk:before {
content: '\e904';
content: "\e969";
}
.icon-saysimple:before {
content: "\e912";
}
.icon-services:before {
content: '\e905';
content: "\e96a";
}
.icon-settings:before {
content: '\e907';
content: "\e96b";
}
.icon-shipment:before {
content: '\e908';
content: "\e96c";
}
.icon-sign:before {
content: '\e909';
content: "\e90a";
}
.icon-sms:before {
content: '\e90b';
content: "\e96e";
}
.icon-solclaim:before {
content: '\e90c';
content: "\e96f";
}
.icon-solunion:before {
content: '\e90d';
content: "\e971";
}
.icon-splitline:before {
content: '\e90e';
content: "\e972";
}
.icon-splur:before {
content: '\e90f';
content: "\e974";
}
.icon-stowaway:before {
content: '\e910';
content: "\e975";
}
.icon-supplier:before {
content: '\e911';
content: "\e976";
}
.icon-supplierfalse:before {
content: '\e913';
content: "\e977";
}
.icon-tags:before {
content: '\e914';
content: "\e979";
}
.icon-tax:before {
content: '\e915';
content: "\e97a";
}
.icon-thermometer:before {
content: '\e916';
content: "\e97b";
}
.icon-ticket:before {
content: '\e917';
content: "\e97c";
}
.icon-ticketAdd:before {
content: '\e918';
content: "\e97e";
}
.icon-traceability:before {
content: '\e919';
content: "\e97f";
}
.icon-transaction:before {
content: '\e93b';
}
.icon-transaction:before {
content: '\e93b';
content: "\e91b";
}
.icon-treatments:before {
content: '\e91c';
content: "\e980";
}
.icon-trolley:before {
content: '\e91a';
content: "\e900";
}
.icon-troncales:before {
content: '\e91b';
content: "\e982";
}
.icon-unavailable:before {
content: '\e91d';
content: "\e983";
}
.icon-visible_columns:before {
content: "\e984";
}
.icon-volume:before {
content: '\e91f';
content: "\e985";
}
.icon-wand:before {
content: '\e920';
content: "\e986";
}
.icon-web:before {
content: '\e921';
content: "\e987";
}
.icon-wiki:before {
content: '\e922';
content: "\e989";
}
.icon-worker:before {
content: '\e923';
content: "\e98a";
}
.icon-zone:before {
content: '\e924';
content: "\e98b";
}

View File

@ -28,11 +28,11 @@ $color-link: #66bfff;
$color-spacer-light: #a3a3a31f;
$color-spacer: #7979794d;
$border-thin-light: 1px solid $color-spacer-light;
$primary-light: lighten($primary, 35%);
$primary-light: #f5b351;
$dark-shadow-color: black;
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
$spacing-md: 16px;
$color-font-secondary: #777;
.bg-success {
background-color: $positive;
}

View File

@ -0,0 +1,6 @@
import toCurrency from './toCurrency';
export default function (value) {
if (value == null || value === '') return () => '-';
return () => toCurrency(value);
}

View File

@ -1,7 +1,7 @@
export default function dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
const maxHour = new Date();
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];

View File

@ -10,6 +10,7 @@ import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange';
import toHour from './toHour';
import dashOrCurrency from './dashOrCurrency';
export {
toLowerCase,
@ -17,6 +18,7 @@ export {
toDate,
toHour,
toDateString,
dashOrCurrency,
toDateHourMin,
toDateHourMinSec,
toRelativeDate,

View File

@ -8,11 +8,8 @@ export default function (value, fractionSize = 2) {
const options = {
style: 'percent',
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
maximumFractionDigits: fractionSize,
};
return new Intl.NumberFormat(locale, options)
.format(parseFloat(value));
}
return new Intl.NumberFormat(locale, options).format(parseFloat(value));
}

View File

@ -17,6 +17,7 @@ globals:
date: Date
dataSaved: Data saved
dataDeleted: Data deleted
delete: Delete
search: Search
changes: Changes
dataCreated: Data created
@ -24,6 +25,7 @@ globals:
create: Create
edit: Edit
save: Save
saveAndContinue: Save and continue
remove: Remove
reset: Reset
close: Close
@ -88,6 +90,7 @@ globals:
send: Send
code: Code
pageTitles:
logIn: Login
summary: Summary
basicData: Basic data
log: Logs
@ -98,13 +101,151 @@ globals:
modes: Modes
zones: Zones
zonesList: Zones
deliveryList: Delivery days
upcomingList: Upcoming deliveries
deliveryDays: Delivery days
upcomingDeliveries: Upcoming deliveries
role: Role
alias: Alias
aliasUsers: Users
subRoles: Subroles
inheritedRoles: Inherited Roles
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
entries: Entries
buys: Buys
dms: File management
entryCreate: New entry
latestBuys: Latest buys
tickets: Tickets
ticketCreate: New Tickets
boxing: Boxing
sale: Sale
claims: Claims
claimCreate: New claim
lines: Lines
photos: Photos
development: Development
action: Action
invoiceOuts: Invoice out
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
invoiceIns: Invoices In
invoiceInCreate: Create invoice in
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
workers: Workers
workerCreate: New worker
department: Department
pda: PDA
pbx: Private Branch Exchange
calendar: Calendar
timeControl: Time control
locker: Locker
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
roadmap: Roadmap
stops: Stops
routes: Routes
cmrsList: CMRs list
RouteList: List
routeCreate: New route
RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap
autonomous: Autonomous
suppliers: Suppliers
supplier: Supplier
labeler: Labeler
supplierCreate: New supplier
accounts: Accounts
addresses: Addresses
agencyTerm: Agency agreement
travel: Travels
extraCommunity: Extra community
travelCreate: New travel
history: Log
thermographs: Thermograph
items: Items
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
botanical: Botanical
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
itemType: Item type
monitors: Monitors
dashboard: Dashboard
users: Users
createTicket: Create ticket
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
formation: Formation
locations: Locations
warehouses: Warehouses
roles: Roles
connections: Connections
acls: ACLs
mailForwarding: Mail forwarding
mailAlias: Mail alias
privileges: Privileges
created: Created
worker: Worker
now: Now
name: Name
new: New
comment: Comment
observations: Observations
goToModuleIndex: Go to module index
errors:
statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred
@ -123,8 +264,6 @@ login:
loginError: Invalid username or password
fieldRequired: This field is required
twoFactorRequired: Two-factor verification required
pageTitles:
logIn: Login
twoFactor:
code: Code
validate: Validate
@ -139,40 +278,8 @@ verifyEmail:
verifyEmail: Email verification
dashboard:
pageTitles:
dashboard: Dashboard
customer:
pageTitles:
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
log: Log
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
list:
phone: Phone
email: Email
@ -271,8 +378,8 @@ customer:
extendedList:
tableVisibleColumns:
id: Identifier
name: Name
socialName: Social name
name: Comercial name
socialName: Business name
fi: Tax number
salesPersonFk: Salesperson
credit: Credit
@ -302,17 +409,6 @@ customer:
hasCoreVnl: VNL core received
hasSepaVnl: VNL B2B received
entry:
pageTitles:
entries: Entries
list: List
summary: Summary
basicData: Basic data
buys: Buys
notes: Notes
dms: File management
log: Log
entryCreate: New entry
latestBuys: Latest buys
list:
newEntry: New entry
landed: Landed
@ -321,6 +417,18 @@ entry:
booked: Booked
confirmed: Confirmed
ordered: Ordered
tableVisibleColumns:
id: Id
reference: Reference
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
isRaid: Raid
summary:
commission: Commission
currency: Currency
@ -388,6 +496,7 @@ entry:
type: Type
color: Color
id: ID
printedStickers: Printed stickers
notes:
observationType: Observation type
descriptor:
@ -395,33 +504,37 @@ entry:
landed: Landed
warehouseOut: Warehouse Out
latestBuys:
picture: Picture
itemFk: Item ID
packing: Packing
grouping: Grouping
quantity: Quantity
size: Size
tags: Tags
type: Type
intrastat: Intrastat
origin: Origin
weightByPiece: Weight/Piece
isActive: Active
family: Family
entryFk: Entry
buyingValue: Buying value
freightValue: Freight value
comissionValue: Commission value
packageValue: Package value
isIgnored: Is ignored
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
weight: Weight
packagingFk: Package
packingOut: Package out
landing: Landing
tableVisibleColumns:
image: Picture
itemFk: Item ID
packing: Packing
grouping: Grouping
quantity: Quantity
size: Size
tags: Tags
type: Type
intrastat: Intrastat
origin: Origin
weightByPiece: Weight/Piece
isActive: Active
family: Family
entryFk: Entry
buyingValue: Buying value
freightValue: Freight value
comissionValue: Commission value
description: Description
packageValue: Package value
isIgnored: Is ignored
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
weight: Weight
packagingFk: Package
packingOut: Package out
landing: Landing
isExcludedFromAvailable: Es inventory
isRaid: Raid
ticket:
pageTitles:
tickets: Tickets
@ -433,6 +546,18 @@ ticket:
sms: Sms
notes: Notes
sale: Sale
volume: Volume
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
expedition: Expedition
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
services: Service
tracking: Tracking
components: Components
pictures: Pictures
packages: Packages
list:
nickname: Nickname
state: State
@ -464,6 +589,7 @@ ticket:
agency: Agency
zone: Zone
warehouse: Warehouse
collection: Collection
route: Route
invoice: Invoice
shipped: Shipped
@ -505,87 +631,7 @@ ticket:
landed: Landed
warehouse: Warehouse
agency: Agency
claim:
pageTitles:
claims: Claims
list: List
claimCreate: New claim
summary: Summary
basicData: Basic Data
lines: Lines
photos: Photos
development: Development
log: Audit logs
notes: Notes
action: Action
list:
customer: Customer
assignedTo: Assigned
created: Created
state: State
rmaList:
code: Code
records: records
card:
claimId: Claim ID
attendedBy: Attended by
created: Created
state: State
ticketId: Ticket ID
customerSummary: Customer summary
claimedTicket: Claimed ticket
saleTracking: Sale tracking
ticketTracking: Ticket tracking
commercial: Commercial
province: Province
zone: Zone
customerId: client ID
summary:
customer: Customer
assignedTo: Assigned
attendedBy: Attended by
created: Created
state: State
details: Details
item: Item
landed: Landed
quantity: Quantity
claimed: Claimed
price: Price
discount: Discount
total: Total
actions: Actions
responsibility: Responsibility
company: Company
person: Employee/Customer
notes: Notes
photos: Photos
development: Development
reason: Reason
result: Result
responsible: Responsible
worker: Worker
redelivery: Redelivery
changeState: Change state
basicData:
customer: Customer
assignedTo: Assigned
created: Created
state: State
pickup: Pick up
photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here
invoiceOut:
pageTitles:
invoiceOuts: Invoice out
list: List
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
summary: Summary
basicData: Basic Data
list:
ref: Reference
issued: Issued
@ -653,13 +699,6 @@ invoiceOut:
errors:
downloadCsvFailed: CSV download failed
shelving:
pageTitles:
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
summary: Summary
basicData: Basic Data
log: Logs
list:
parking: Parking
priority: Priority
@ -686,17 +725,6 @@ parking:
info: You can search by parking code
label: Search parking...
invoiceIn:
pageTitles:
invoiceIns: Invoices In
list: List
invoiceInCreate: Create invoice in
summary: Summary
basicData: Basic data
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
log: Logs
list:
ref: Reference
supplier: Supplier
@ -747,15 +775,6 @@ invoiceIn:
stems: Stems
country: Country
order:
pageTitles:
order: Orders
orderList: List
orderCreate: New order
summary: Summary
basicData: Basic Data
catalog: Catalog
volume: Volume
lines: Lines
field:
salesPersonFk: Sales Person
clientFk: Client
@ -830,6 +849,7 @@ worker:
calendar: Calendar
timeControl: Time control
locker: Locker
balance: Balance
list:
name: Name
email: Email
@ -861,6 +881,15 @@ worker:
role: Role
sipExtension: Extension
locker: Locker
fiDueDate: Fecha de caducidad del DNI
sex: Sexo
seniority: Antigüedad
fi: DNI/NIE/NIF
birth: Fecha de nacimiento
isFreelance: Autónomo
isSsDiscounted: Bonificación SS
hasMachineryAuthorized: Autorizado para llevar maquinaria
isDisable: Trabajador desactivado
notificationsManager:
activeNotifications: Active notifications
availableNotifications: Available notifications
@ -890,17 +919,25 @@ worker:
payMethods: Pay method
iban: IBAN
bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found
balance:
tableVisibleColumns:
paymentDate: Date
incomeType: Type
debit: Debt
credit: Have
concept: Concept
wagon:
pageTitles:
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
type:
name: Name
submit: Submit
@ -929,22 +966,11 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays
route/roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
route:
pageTitles:
agency: Agency List
routes: Routes
cmrsList: External CMRs list
cmrsList: CMRs list
RouteList: List
routeCreate: New route
basicData: Basic Data
@ -968,29 +994,33 @@ route:
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs
columnLabels:
Id: Id
vehicle: Vehicle
description: Description
isServed: Served
worker: Worker
date: Date
started: Started
actions: Actions
agency: Agency
volume: Volume
finished: Finished
supplier:
pageTitles:
suppliers: Suppliers
supplier: Supplier
list: List
supplierCreate: New supplier
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
log: Log
accounts: Accounts
contacts: Contacts
addresses: Addresses
consumption: Consumption
agencyTerm: Agency agreement
dms: File management
list:
payMethod: Pay method
payDeadline: Pay deadline
payDay: Pay day
account: Account
newSupplier: New supplier
tableVisibleColumns:
id: Id
name: Name
nif: NIF/CIF
nickname: Alias
account: Account
payMethod: Pay Method
payDay: Pay Day
summary:
responsible: Responsible
notes: Notes
@ -1076,15 +1106,16 @@ supplier:
date: Date
reference: Reference
travel:
pageTitles:
travel: Travels
list: List
summary: Summary
extraCommunity: Extra community
travelCreate: New travel
basicData: Basic data
history: Log
thermographs: Thermograph
travelList:
tableVisibleColumns:
id: Id
ref: Reference
agency: Agency
shipped: Shipped
landed: Landed
warehouseIn: Warehouse in
warehouseOut: Warehouse out
totalEntries: Total entries
summary:
confirmed: Confirmed
entryId: Entry Id
@ -1131,24 +1162,6 @@ travel:
travelFileDescription: 'Travel id { travelId }'
file: File
item:
pageTitles:
items: Items
list: List
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
log: Log
botanical: Botanical
shelving: Shelving
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
descriptor:
item: Item
buyer: Buyer
@ -1159,6 +1172,7 @@ item:
available: Available
warehouseText: 'Calculated on the warehouse of { warehouseName }'
itemDiary: Item diary
producer: Producer
list:
id: Identifier
grouping: Grouping
@ -1233,15 +1247,6 @@ item:
minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus'
specie: 'Specie'
item/itemType:
pageTitles:
itemType: Item type
basicData: Basic data
summary: Summary
monitor:
pageTitles:
monitors: Monitors
list: List
components:
topbar: {}
itemsFilterPanel:

View File

@ -17,6 +17,7 @@ globals:
date: Fecha
dataSaved: Datos guardados
dataDeleted: Datos eliminados
delete: Eliminar
search: Buscar
changes: Cambios
dataCreated: Datos creados
@ -24,6 +25,7 @@ globals:
create: Crear
edit: Modificar
save: Guardar
saveAndContinue: Guardar y continuar
remove: Eliminar
reset: Restaurar
close: Cerrar
@ -88,6 +90,7 @@ globals:
send: Enviar
code: Código
pageTitles:
logIn: Inicio de sesión
summary: Resumen
basicData: Datos básicos
log: Historial
@ -98,13 +101,159 @@ globals:
modes: Modos
zones: Zonas
zonesList: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
deliveryDays: Días de entrega
upcomingDeliveries: Próximos repartos
role: Role
alias: Alias
aliasUsers: Usuarios
subRoles: Subroles
inheritedRoles: Roles heredados
customers: Clientes
customerCreate: Nuevo cliente
list: Listado
webPayments: Pagos Web
extendedList: Listado extendido
notifications: Notificaciones
defaulter: Morosos
createCustomer: Crear cliente
fiscalData: Datos fiscales
billingData: Forma de pago
consignees: Consignatarios
notes: Notas
credits: Créditos
greuges: Greuges
balance: Balance
recoveries: Recobros
webAccess: Acceso web
sms: Sms
creditManagement: Gestión de crédito
creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito
others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
entries: Entradas
buys: Compras
dms: Gestión documental
entryCreate: Nueva entrada
latestBuys: Últimas compras
tickets: Tickets
ticketCreate: Nuevo ticket
boxing: Encajado
sale: Lineas del pedido
claims: Reclamaciones
claimCreate: Crear reclamación
lines: Líneas
development: Trazabilidad
photos: Fotos
action: Acción
invoiceOuts: Fact. emitidas
negativeBases: Bases Negativas
globalInvoicing: Facturación global
invoiceOutCreate: Crear fact. emitida
order: Cesta
orderList: Listado
orderCreate: Nueva orden
catalog: Catálogo
volume: Volumen
shelving: Carros
shelvingList: Listado de carros
shelvingCreate: Nuevo carro
invoiceIns: Fact. recibidas
invoiceInCreate: Crear fact. recibida
vat: IVA
labeler: Etiquetas
dueDay: Vencimiento
intrastat: Intrastat
corrective: Rectificativa
workers: Trabajadores
workerCreate: Nuevo trabajador
department: Departamentos
pda: PDA
pbx: Centralita
calendar: Calendario
timeControl: Control de horario
locker: Taquilla
wagons: Vagones
wagonsList: Listado vagones
wagonCreate: Crear tipo
wagonEdit: Editar tipo
typesList: Listado tipos
typeCreate: Crear tipo
typeEdit: Editar tipo
wagonCounter: Contador de carros
roadmap: Troncales
stops: Paradas
routes: Rutas
cmrsList: Listado de CMRs
RouteList: Listado
routeCreate: Nueva ruta
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
autonomous: Autónomos
suppliers: Proveedores
supplier: Proveedor
supplierCreate: Nuevo proveedor
accounts: Cuentas
addresses: Direcciones
agencyTerm: Acuerdo agencia
travel: Envíos
create: Crear
extraCommunity: Extra comunitarios
travelCreate: Nuevo envío
history: Historial
thermographs: Termógrafos
items: Artículos
diary: Histórico
tags: Etiquetas
fixedPrice: Precios fijados
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
itemType: Familia
monitors: Monitores
dashboard: Tablón
users: Usuarios
createTicket: Crear ticket
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
formation: Formación
locations: Ubicaciones
warehouses: Almacenes
roles: Roles
connections: Conexiones
acls: ACLs
mailForwarding: Reenvío de correo
mailAlias: Alias de correo
privileges: Privilegios
observation: Notas
expedition: Expedición
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
created: Fecha creación
worker: Trabajador
now: Ahora
name: Nombre
new: Nuevo
comment: Comentario
observations: Observaciones
goToModuleIndex: Ir al índice del módulo
errors:
statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor
@ -123,8 +272,6 @@ login:
loginError: Nombre de usuario o contraseña incorrectos
fieldRequired: Este campo es obligatorio
twoFactorRequired: Verificación de doble factor requerida
pageTitles:
logIn: Inicio de sesión
twoFactor:
code: Código
validate: Validar
@ -137,41 +284,8 @@ verifyEmail:
verifyEmail: Verificación de correo
dashboard:
pageTitles:
dashboard: Tablón
customer:
pageTitles:
customers: Clientes
customerCreate: Nuevo cliente
list: Listado
webPayments: Pagos Web
extendedList: Listado extendido
notifications: Notificaciones
defaulter: Morosos
createCustomer: Crear cliente
summary: Resumen
basicData: Datos básicos
fiscalData: Datos fiscales
billingData: Forma de pago
consignees: Consignatarios
notes: Notas
credits: Créditos
greuges: Greuges
balance: Balance
recoveries: Recobros
webAccess: Acceso web
log: Historial
sms: Sms
creditManagement: Gestión de crédito
creditContracts: Contratos de crédito
creditOpinion: Opinión de crédito
others: Otros
samples: Plantillas
consumption: Consumo
mandates: Mandatos
contacts: Contactos
webPayment: Pago web
fileManagement: Gestión documental
unpaid: Impago
list:
phone: Teléfono
email: Email
@ -269,7 +383,7 @@ customer:
extendedList:
tableVisibleColumns:
id: Identificador
name: Nombre
name: Nombre Comercial
socialName: Razón social
fi: NIF / CIF
salesPersonFk: Comercial
@ -300,17 +414,6 @@ customer:
hasCoreVnl: Recibido core VNL
hasSepaVnl: Recibido B2B VNL
entry:
pageTitles:
entries: Entradas
list: Listado
summary: Resumen
basicData: Datos básicos
buys: Compras
notes: Notas
dms: Gestión documental
log: Historial
entryCreate: Nueva entrada
latestBuys: Últimas compras
list:
newEntry: Nueva entrada
landed: F. entrega
@ -319,6 +422,18 @@ entry:
booked: Asentado
confirmed: Confirmado
ordered: Pedida
tableVisibleColumns:
id: Id
reference: Referencia
created: Creación
supplierFk: Proveedor
isBooked: Asentado
isConfirmed: Confirmado
isOrdered: Pedida
companyFk: Empresa
travelFk: Envio
isExcludedFromAvailable: Inventario
isRaid: Redada
summary:
commission: Comisión
currency: Moneda
@ -386,6 +501,7 @@ entry:
type: Tipo
color: Color
id: ID
printedStickers: Etiquetas impresas
notes:
observationType: Tipo de observación
descriptor:
@ -393,33 +509,37 @@ entry:
landed: F. entrega
warehouseOut: Alm. salida
latestBuys:
picture: Foto
itemFk: ID Artículo
packing: Packing
grouping: Grouping
quantity: Cantidad
size: Medida
tags: Etiquetas
type: Tipo
intrastat: Intrastat
origin: Origen
weightByPiece: Peso (gramos)/tallo
isActive: Activo
family: Familia
entryFk: Entrada
buyingValue: Coste
freightValue: Porte
comissionValue: Comisión
packageValue: Embalaje
isIgnored: Ignorado
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
weight: Peso
packagingFk: Embalaje
packingOut: Embalaje envíos
landing: Llegada
tableVisibleColumns:
image: Foto
itemFk: Id Artículo
packing: packing
grouping: Grouping
quantity: Cantidad
size: Medida
tags: Etiquetas
type: Tipo
intrastat: Intrastat
origin: Origen
weightByPiece: Peso (gramos)/tallo
isActive: Activo
family: Familia
entryFk: Entrada
buyingValue: Coste
freightValue: Porte
comissionValue: Comisión
description: Descripción
packageValue: Embalaje
isIgnored: Ignorado
price2: Grouping
price3: Packing
minPrice: Min
ektFk: Ekt
weight: Peso
packagingFk: Embalaje
packingOut: Embalaje envíos
landing: Llegada
isExcludedFromAvailable: Es inventario
isRaid: Redada
ticket:
pageTitles:
tickets: Tickets
@ -431,6 +551,18 @@ ticket:
sms: Sms
notes: Notas
sale: Lineas del pedido
volume: Volumen
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
expedition: Expedición
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
list:
nickname: Alias
state: Estado
@ -462,6 +594,7 @@ ticket:
agency: Agencia
zone: Zona
warehouse: Almacén
collection: Colección
route: Ruta
invoice: Factura
shipped: Enviado
@ -503,90 +636,7 @@ ticket:
landed: F. entrega
warehouse: Almacén
agency: Agencia
claim:
pageTitles:
claims: Reclamaciones
list: Listado
claimCreate: Crear reclamación
summary: Resumen
basicData: Datos básicos
lines: Líneas
development: Trazabilidad
photos: Fotos
log: Historial
notes: Notas
action: Acción
list:
customer: Cliente
assignedTo: Asignada a
created: Creada
state: Estado
rmaList:
code: Código
records: registros
card:
claimId: ID reclamación
attendedBy: Atendida por
created: Creada
state: Estado
ticketId: ID ticket
customerSummary: Resumen del cliente
claimedTicket: Ticket reclamado
saleTracking: Líneas preparadas
ticketTracking: Estados del ticket
commercial: Comercial
province: Provincia
zone: Zona
customerId: ID del cliente
summary:
customer: Cliente
assignedTo: Asignada a
attendedBy: Atendida por
created: Creada
state: Estado
details: Detalles
item: Artículo
landed: Entregado
quantity: Cantidad
claimed: Reclamado
price: Precio
discount: Descuento
total: Total
actions: Acciones
responsibility: Responsabilidad
company: Empresa
person: Comercial/Cliente
notes: Observaciones
photos: Fotos
development: Trazabilidad
reason: Motivo
result: Consecuencias
responsible: Responsable
worker: Trabajador
redelivery: Devolución
changeState: Cambiar estado
basicData:
customer: Cliente
assignedTo: Asignada a
created: Creada
state: Estado
pickup: Recogida
null: No
agency: Agencia
delivery: Reparto
photo:
fileDescription: 'Reclamacion ID {claimId} del cliente {clientName} id {clientId}'
noData: No hay imágenes/videos haz click aquí o arrastra y suelta el archivo
dragDrop: Arrástralo y sueltalo aquí
invoiceOut:
pageTitles:
invoiceOuts: Fact. emitidas
list: Listado
negativeBases: Bases Negativas
globalInvoicing: Facturación global
invoiceOutCreate: Crear fact. emitida
summary: Resumen
basicData: Datos básicos
list:
ref: Referencia
issued: Fecha emisión
@ -654,15 +704,6 @@ invoiceOut:
errors:
downloadCsvFailed: Error al descargar CSV
order:
pageTitles:
order: Cesta
orderList: Listado
orderCreate: Nueva orden
summary: Resumen
basicData: Datos básicos
catalog: Catálogo
volume: Volumen
lines: Líneas
field:
salesPersonFk: Comercial
clientFk: Cliente
@ -704,13 +745,6 @@ order:
price: Precio
amount: Monto
shelving:
pageTitles:
shelving: Carros
shelvingList: Listado de carros
shelvingCreate: Nuevo carro
summary: Resumen
basicData: Datos básicos
log: Historial
list:
parking: Parking
priority: Prioridad
@ -736,17 +770,6 @@ parking:
info: Puedes buscar por código de parking
label: Buscar parking...
invoiceIn:
pageTitles:
invoiceIns: Fact. recibidas
list: Listado
invoiceInCreate: Crear fact. recibida
summary: Resumen
basicData: Datos básicos
vat: IVA
dueDay: Vencimiento
intrastat: Intrastat
corrective: Rectificativa
log: Historial
list:
ref: Referencia
supplier: Proveedor
@ -828,6 +851,7 @@ worker:
calendar: Calendario
timeControl: Control de horario
locker: Taquilla
balance: Balance
list:
name: Nombre
email: Email
@ -888,17 +912,25 @@ worker:
payMethods: Método de pago
iban: IBAN
bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: No se ha encontrado la imagen
balance:
tableVisibleColumns:
paymentDate: Fecha
incomeType: Tipo
debit: Debe
credit: Haber
concept: Concepto
wagon:
pageTitles:
wagons: Vagones
wagonsList: Listado vagones
wagonCreate: Crear tipo
wagonEdit: Editar tipo
typesList: Listado tipos
typeCreate: Crear tipo
typeEdit: Editar tipo
wagonCounter: Contador de carros
type:
name: Nombre
submit: Guardar
@ -927,36 +959,12 @@ wagon:
minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es '
uncompleteTrays: Hay bandejas sin completar
route/roadmap:
pageTitles:
roadmap: Troncales
summary: Resumen
basicData: Datos básicos
stops: Paradas
roadmap:
pageTitles:
roadmap: Troncales
summary: Resumen
basicData: Datos básicos
stops: Paradas
route:
pageTitles:
routes: Rutas
cmrsList: Listado de CMRs externos
RouteList: Listado
routeCreate: Nueva ruta
basicData: Datos básicos
summary: Resumen
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
tickets: Tickets
log: Historial
autonomous: Autónomos
cmr:
list:
results: resultados
cmrFk: Id CMR
hasCmrDms: Adjuntado en gestdoc
hasCmrDms: Gestdoc
'true':
'false': 'No'
ticketFk: Id ticket
@ -966,29 +974,33 @@ route:
shipped: Fecha preparación
viewCmr: Ver CMR
downloadCmrs: Descargar CMRs
columnLabels:
Id: Id
vehicle: Vehículo
description: Descripción
isServed: Servida
worker: Trabajador
date: Fecha
started: Iniciada
actions: Acciones
agency: Agencia
volume: Volumen
finished: Finalizada
supplier:
pageTitles:
suppliers: Proveedores
supplier: Proveedor
list: Listado
supplierCreate: Nuevo proveedor
summary: Resumen
basicData: Datos básicos
fiscalData: Datos fiscales
billingData: Forma de pago
log: Historial
accounts: Cuentas
contacts: Contactos
addresses: Direcciones
consumption: Consumo
agencyTerm: Acuerdo agencia
dms: Gestión documental
list:
payMethod: Método de pago
payDeadline: Plazo de pago
payDay: Día de pago
account: Cuenta
newSupplier: Nuevo proveedor
tableVisibleColumns:
id: Id
name: Nombre
nif: NIF/CIF
nickname: Alias
account: Cuenta
payMethod: Método de pago
payDay: Dia de pago
summary:
responsible: Responsable
notes: Notas
@ -1074,16 +1086,16 @@ supplier:
date: Fecha
reference: Referencia
travel:
pageTitles:
travel: Envíos
list: Listado
create: Crear
summary: Resumen
extraCommunity: Extra comunitarios
travelCreate: Nuevo envío
basicData: Datos básicos
history: Historial
thermographs: Termógrafos
travelList:
tableVisibleColumns:
id: Id
ref: Referencia
agency: Agencia
shipped: Enviado
landed: Llegada
warehouseIn: Almacén de salida
warehouseOut: Almacén de entrada
totalEntries: Total de entradas
summary:
confirmed: Confirmado
entryId: Id entrada
@ -1130,24 +1142,6 @@ travel:
travelFileDescription: 'Id envío { travelId }'
file: Fichero
item:
pageTitles:
items: Artículos
list: Listado
diary: Histórico
tags: Etiquetas
fixedPrice: Precios fijados
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
basicData: 'Datos básicos'
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
log: Historial
shelving: Carros
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
descriptor:
item: Artículo
buyer: Comprador
@ -1158,6 +1152,7 @@ item:
available: Disponible
warehouseText: 'Calculado sobre el almacén de { warehouseName }'
itemDiary: Registro de compra-venta
producer: Productor
list:
id: Identificador
grouping: Grouping
@ -1232,21 +1227,6 @@ item:
achieved: 'Conseguido'
concept: 'Concepto'
state: 'Estado'
item/itemType:
pageTitles:
itemType: Familia
basicData: Datos básicos
summary: Resumen
zone:
pageTitles:
zones: Zona
zonesList: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
monitor:
pageTitles:
monitors: Monitores
list: Listado
components:
topbar: {}
itemsFilterPanel:

View File

@ -0,0 +1,104 @@
<script setup>
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const { notify } = useNotify();
const onSynchronizeAll = async () => {
try {
notify(t('Synchronizing in the background'), 'positive');
await axios.patch(`Accounts/syncAll`);
} catch (error) {
console.error('Error synchronizing all accounts', error);
}
};
const onSynchronizeRoles = async () => {
try {
await axios.patch(`RoleInherits/sync`);
notify(t('Roles synchronized!'), 'positive');
} catch (error) {
console.error('Error synchronizing roles', error);
}
};
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
:url="`AccountConfigs/${1}`"
:url-update="`AccountConfigs/${1}`"
model="AccountAccounts"
auto-load
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('accounts.syncAll')"
@click="onSynchronizeAll()"
/>
<QBtn
color="primary"
:label="t('accounts.syncRoles')"
@click="onSynchronizeRoles()"
/>
</template>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<VnInput :label="t('accounts.homedir')" v-model="data.homedir" />
<VnInput :label="t('accounts.shell')" v-model="data.shell" />
<VnInput
:label="t('accounts.idBase')"
v-model="data.idBase"
type="number"
min="0"
/>
<VnRow>
<VnInput
:label="t('accounts.min')"
v-model="data.min"
type="number"
min="0"
/>
<VnInput
:label="t('accounts.max')"
v-model="data.max"
type="number"
min="0"
/>
</VnRow>
<VnRow>
<VnInput
:label="t('accounts.warn')"
v-model="data.warn"
type="number"
min="0"
/>
<VnInput
:label="t('accounts.inact')"
v-model="data.inact"
type="number"
min="0"
/>
</VnRow>
</div>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
Roles synchronized!: ¡Roles sincronizados!
Synchronizing in the background: Sincronizando en segundo plano
</i18n>

View File

@ -0,0 +1,151 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import FetchData from 'components/FetchData.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AclFilter from './Acls/AclFilter.vue';
import AclFormView from './Acls/AclFormView.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useStateStore } from 'stores/useStateStore';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
defineProps({
id: {
type: Number,
default: 0,
},
});
const { notify } = useNotify();
const { t } = useI18n();
const stateStore = useStateStore();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref();
const formDialog = ref(false);
const rolesOptions = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'search':
return { model: { like: `%${value}%` } };
default:
return { [param]: value };
}
};
const deleteAcl = async (id) => {
try {
await axios.delete(`ACLs/${id}`);
paginateRef.value.fetch();
notify('ACL removed', 'positive');
} catch (error) {
console.error('Error deleting Acl: ', error);
}
};
function showFormDialog(data) {
formDialog.value = {
show: true,
formInitialData: { ...data },
};
}
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="AccountAcls"
url="ACLs"
:expr-builder="exprBuilder"
:label="t('acls.search')"
:info="t('acls.searchInfo')"
/>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<AclFilter data-key="AccountAcls" />
</QScrollArea>
</QDrawer>
<QPage class="flex justify-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
ref="paginateRef"
data-key="AccountAcls"
url="ACLs"
:expr-builder="exprBuilder"
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:id="row.id"
:key="row.id"
:title="`${row.model}.${row.property}`"
@click="showFormDialog(row)"
>
<template #list-items>
<VnLv :label="t('acls.role')" :value="row.principalId" />
<VnLv :label="t('acls.accessType')" :value="row.accessType" />
<VnLv
:label="t('acls.permissions')"
:value="row.permission"
/>
</template>
<template #actions>
<QBtn
:label="t('globals.delete')"
@click.stop="
openConfirmationModal(
t('ACL will be removed'),
t('Are you sure you want to continue?'),
() => deleteAcl(row.id)
)
"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
v-model="formDialog.show"
transition-show="scale"
transition-hide="scale"
>
<AclFormView
:form-initial-data="formDialog.formInitialData"
@on-data-change="paginateRef.fetch()"
:roles-options="rolesOptions"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="showFormDialog()">
<QTooltip class="text-no-wrap">{{ t('New ACL') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
New ACL: Nuevo ACL
ACL removed: ACL eliminado
ACL will be removed: El ACL será eliminado
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -0,0 +1,105 @@
<script setup>
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AliasSummary from './Alias/Card/AliasSummary.vue';
import AliasCreateForm from './Alias/AliasCreateForm.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useStateStore } from 'stores/useStateStore';
defineProps({
id: {
type: Number,
default: 0,
},
});
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const router = useRouter();
const stateStore = useStateStore();
const aliasCreateDialogRef = ref(null);
const exprBuilder = (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? { id: value }
: { alias: { like: `%${value}%` } };
}
};
const navigate = (id) => router.push({ name: 'AliasSummary', params: { id } });
const openCreateModal = () => aliasCreateDialogRef.value.show();
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="AccountAliasList"
url="MailAliases"
:expr-builder="exprBuilder"
:label="t('mailAlias.search')"
:info="t('mailAlias.searchInfo')"
/>
</Teleport>
</template>
<QPage class="flex justify-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
ref="paginateRef"
data-key="AccountAliasList"
url="MailAliases"
:expr-builder="exprBuilder"
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:id="row.id"
:key="row.id"
:title="row.alias"
@click="navigate(row.id)"
>
<template #list-items>
<VnLv :label="t('mailAlias.alias')" :value="row.alias">
</VnLv>
<VnLv
:label="t('mailAlias.description')"
:value="row.description"
>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, AliasSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="aliasCreateDialogRef"
transition-show="scale"
transition-hide="scale"
>
<AliasCreateForm />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateModal()">
<QTooltip class="text-no-wrap">{{ t('mailAlias.newAlias') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>

View File

@ -0,0 +1,112 @@
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'composables/useVnConfirm';
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref(null);
const filter = {
fields: ['id', 'created', 'userId'],
include: {
relation: 'user',
scope: {
fields: ['username'],
},
},
order: 'created DESC',
};
const urlPath = 'AccessTokens';
const refresh = () => paginateRef.value.fetch();
const navigate = (id) => router.push({ name: 'AccountSummary', params: { id } });
const killSession = async (id) => {
try {
await axios.delete(`${urlPath}/${id}`);
paginateRef.value.fetch();
notify(t('Session killed'), 'positive');
} catch (error) {
console.error('Error killing session', error);
}
};
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
:data-key="urlPath"
ref="paginateRef"
:filter="filter"
:url="urlPath"
order="created DESC"
auto-load
>
<template #body="{ rows }">
<CardList
:key="row.id"
:title="row.user?.username"
@click="navigate(row.userId)"
v-for="row of rows"
>
<template #list-items>
<div style="flex-direction: column; width: 100%">
<VnLv
:label="t('connections.username')"
:value="row.user?.username"
>
</VnLv>
<VnLv
:label="t('connections.created')"
:value="toDateTimeFormat(row.created)"
>
</VnLv>
</div>
</template>
<template #actions>
<QBtn
class="q-mt-xs"
:label="t('connections.killSession')"
@click.stop="
openConfirmationModal(
t('Session will be killed'),
t('Are you sure you want to continue?'),
() => killSession(row.id)
)
"
outline
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="refresh" color="primary" @click="refresh()">
<QTooltip>{{ t('connections.refresh') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
Session killed: Sesión matada
Session will be killed: Se va a matar la sesión
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -0,0 +1,81 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const router = useRouter();
const newAccountForm = reactive({
active: true,
});
const rolesOptions = ref([]);
const redirectToAccountBasicData = (_, { id }) => {
router.push({ name: 'AccountBasicData', params: { id } });
};
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<FormModelPopup
:title="t('account.card.newUser')"
url-create="VnUsers"
model="users"
:form-initial-data="newAccountForm"
@on-data-saved="redirectToAccountBasicData"
>
<template #form-inputs="{ data, validate }">
<div class="column q-gutter-sm">
<VnInput
v-model="data.name"
:label="t('account.create.name')"
:rules="validate('VnUser.name')"
/>
<VnInput
v-model="data.nickname"
:label="t('account.create.nickname')"
:rules="validate('VnUser.nickname')"
/>
<VnInput
v-model="data.email"
:label="t('account.create.email')"
type="email"
:rules="validate('VnUser.email')"
/>
<VnSelect
:label="t('account.create.role')"
v-model="data.roleFk"
:options="rolesOptions"
option-value="id"
option-label="name"
map-options
hide-selected
:rules="validate('VnUser.roleFk')"
/>
<VnInput
v-model="data.password"
:label="t('account.create.password')"
type="password"
:rules="validate('VnUser.password')"
/>
<QCheckbox
:label="t('account.create.active')"
v-model="data.active"
:toggle-indeterminate="false"
:rules="validate('VnUser.active')"
/>
</div>
</template>
</FormModelPopup>
</template>

View File

@ -0,0 +1,87 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
exprBuilder: {
type: Function,
default: null,
},
});
const rolesOptions = ref([]);
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
:redirect="false"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`account.card.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('account.card.name')"
v-model="params.name"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('account.card.alias')"
v-model="params.nickname"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('account.card.role')"
v-model="params.roleFk"
@update:model-value="searchFn()"
:options="rolesOptions"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -0,0 +1,171 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const { notify } = useNotify();
const arrayData = useArrayData('AccountLdap');
const URL_UPDATE = `LdapConfigs/${1}`;
const URL_CREATE = `LdapConfigs`;
const DEFAULT_DATA = {
id: 1,
hasData: false,
groupDn: null,
password: null,
rdn: null,
server: null,
userDn: null,
};
const initialData = ref({
...DEFAULT_DATA,
});
const hasData = computed({
get: () => initialData.value.hasData,
set: (val) => {
initialData.value.hasData = val;
if (!val) formCustomFn.value = deleteMailForward;
else formCustomFn.value = null;
},
});
const initialDataLoaded = ref(false);
const formUrlCreate = ref(null);
const formUrlUpdate = ref(null);
const formCustomFn = ref(null);
const onTestConection = async () => {
try {
await axios.get(`LdapConfigs/test`);
notify(t('LDAP connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
};
const getInitialLdapConfig = async () => {
try {
initialDataLoaded.value = false;
const { data } = await axios.get(URL_UPDATE);
initialData.value = data;
hasData.value = true;
return data;
} catch (error) {
hasData.value = false;
arrayData.destroy();
console.error('Error fetching initial LDAP config', error);
return null;
} finally {
// Si asignamos un valor a urlUpdate, debemos asignar null a urlCreate y viceversa, ya puede causar problemas en formModel
if (hasData.value) {
formUrlUpdate.value = URL_UPDATE;
formUrlCreate.value = null;
} else {
formUrlUpdate.value = null;
formUrlCreate.value = URL_CREATE;
}
initialDataLoaded.value = true;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA };
hasData.value = false;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
onMounted(async () => await getInitialLdapConfig());
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
:key="initialDataLoaded"
model="AccountLdap"
:form-initial-data="initialData"
:url-create="formUrlCreate"
:url-update="formUrlUpdate"
:save-fn="formCustomFn"
auto-load
@on-data-saved="getInitialLdapConfig()"
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('ldap.testConnection')"
@click="onTestConection()"
>
<QTooltip>
{{ t('ldap.testConnection') }}
</QTooltip>
</QBtn>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md">
<div class="col">
<QCheckbox
:label="t('ldap.enableSync')"
v-model="data.hasData"
@update:model-value="($event) => (hasData = $event)"
:toggle-indeterminate="false"
/>
</div>
</VnRow>
<template v-if="hasData">
<VnInput
:label="t('ldap.server')"
clearable
v-model="data.server"
:required="true"
:rules="validate('LdapConfig.server')"
/>
<VnInput
:label="t('ldap.rdn')"
clearable
v-model="data.rdn"
:required="true"
:rules="validate('LdapConfig.rdn')"
/>
<VnInput
:label="t('ldap.password')"
clearable
type="password"
v-model="data.password"
:required="true"
:rules="validate('LdapConfig.password')"
/>
<VnInput :label="t('ldap.userDN')" clearable v-model="data.userDn" />
<VnInput
:label="t('ldap.groupDN')"
clearable
v-model="data.groupDn"
/>
</template>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
LDAP connection established!: ¡Conexión con LDAP establecida!
</i18n>

View File

@ -0,0 +1,144 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { computed, ref } from 'vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import AccountSummary from './Card/AccountSummary.vue';
import AccountFilter from './AccountFilter.vue';
import AccountCreate from './AccountCreate.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useStateStore } from 'stores/useStateStore';
import { useRole } from 'src/composables/useRole';
import { QDialog } from 'quasar';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const accountCreateDialogRef = ref(null);
const showNewUserBtn = computed(() => useRole().hasAny(['itManagement']));
const filter = {
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const exprBuilder = (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? { id: value }
: {
or: [
{ name: { like: `%${value}%` } },
{ nickname: { like: `%${value}%` } },
],
};
case 'name':
case 'nickname':
return { [param]: { like: `%${value}%` } };
case 'roleFk':
return { [param]: value };
}
};
const getApiUrl = () => new URL(window.location).origin;
const navigate = (event, id) => {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/account/${id}/summary`);
router.push({ path: `/account/${id}` });
};
const openCreateModal = () => accountCreateDialogRef.value.show();
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="AccountList"
url="VnUsers/preview"
:expr-builder="exprBuilder"
:label="t('account.search')"
:info="t('account.searchInfo')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<AccountFilter data-key="AccountList" :expr-builder="exprBuilder" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
:filter="filter"
data-key="AccountList"
url="VnUsers/preview"
auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:id="row.id"
:key="row.id"
:title="row.nickname"
@click="navigate($event, row.id)"
>
<template #list-items>
<VnLv :label="t('account.card.name')" :value="row.nickname">
</VnLv>
<VnLv
:label="t('account.card.nickname')"
:value="row.username"
>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, AccountSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="accountCreateDialogRef"
transition-hide="scale"
transition-show="scale"
>
<AccountCreate />
</QDialog>
<QPageSticky :offset="[20, 20]" v-if="showNewUserBtn">
<QBtn @click="openCreateModal" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('account.card.newUser') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<LeftMenu />
</QScrollArea>
</QDrawer>
<QPageContainer>
<RouterView></RouterView>
</QPageContainer>
</template>

View File

@ -0,0 +1,187 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const { notify } = useNotify();
const arrayData = useArrayData('AccountSamba');
const formModel = ref(null);
const URL_UPDATE = `SambaConfigs/${1}`;
const URL_CREATE = `SambaConfigs`;
const DEFAULT_DATA = {
id: 1,
hasData: false,
adDomain: null,
adController: null,
adUser: null,
adPassword: null,
userDn: null,
verifyCert: false,
};
const initialData = ref({
...DEFAULT_DATA,
});
const hasData = computed({
get: () => initialData.value.hasData,
set: (val) => {
initialData.value.hasData = val;
if (!val) formCustomFn.value = deleteMailForward;
else formCustomFn.value = null;
},
});
const initialDataLoaded = ref(false);
const formUrlCreate = ref(null);
const formUrlUpdate = ref(null);
const formCustomFn = ref(null);
const onTestConection = async () => {
try {
await axios.get(`SambaConfigs/test`);
notify(t('Samba connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
};
const getInitialSambaConfig = async () => {
try {
initialDataLoaded.value = false;
const { data } = await axios.get(URL_UPDATE);
initialData.value = data;
hasData.value = true;
return data;
} catch (error) {
hasData.value = false;
arrayData.destroy();
console.error('Error fetching initial Samba config', error);
return null;
} finally {
if (hasData.value) {
formUrlUpdate.value = URL_UPDATE;
formUrlCreate.value = null;
} else {
formUrlUpdate.value = null;
formUrlCreate.value = URL_CREATE;
}
initialDataLoaded.value = true;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA };
hasData.value = false;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
onMounted(async () => await getInitialSambaConfig());
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
ref="formModel"
:key="initialDataLoaded"
model="AccountSamba"
:form-initial-data="initialData"
:url-create="formUrlCreate"
:url-update="formUrlUpdate"
:save-fn="formCustomFn"
auto-load
@on-data-saved="getInitialSambaConfig()"
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('samba.testConnection')"
:disable="formModel.hasChanges"
@click="onTestConection()"
>
<QTooltip>
{{ t('samba.testConnection') }}
</QTooltip>
</QBtn>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md">
<div class="col">
<QCheckbox
:label="t('samba.enableSync')"
v-model="data.hasData"
@update:model-value="($event) => (hasData = $event)"
:toggle-indeterminate="false"
/>
</div>
</VnRow>
<template v-if="hasData">
<VnInput
:label="t('samba.domainAD')"
clearable
v-model="data.adDomain"
:required="true"
:rules="validate('SambaConfigs.server')"
/>
<VnInput
:label="t('samba.domainController')"
clearable
v-model="data.adController"
:required="true"
:rules="validate('SambaConfigs.adController')"
/>
<VnInput
:label="t('samba.userAD')"
clearable
v-model="data.adUser"
:rules="validate('SambaConfigs.adUser')"
/>
<VnInput
:label="t('samba.passwordAD')"
clearable
type="password"
v-model="data.adPassword"
/>
<VnInput
:label="t('samba.domainPart')"
clearable
v-model="data.userDn"
:required="true"
:rules="validate('SambaConfigs.userDn')"
/>
<QCheckbox
:label="t('samba.verifyCertificate')"
v-model="data.verifyCert"
:rules="validate('SambaConfigs.groupDn')"
:toggle-indeterminate="false"
/>
</template>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
Samba connection established!: ¡Conexión con LDAP establecida!
</i18n>

View File

@ -0,0 +1,128 @@
<script setup>
import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useValidator } from 'src/composables/useValidator';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const { t } = useI18n();
const validationsStore = useValidator();
const { models } = validationsStore;
const validations = ref([]);
const accessTypes = [{ name: '*' }, { name: 'READ' }, { name: 'WRITE' }];
const permissions = [{ name: 'ALLOW' }, { name: 'DENY' }];
const rolesOptions = ref([]);
onBeforeMount(() => {
for (let model in models) validations.value.push({ name: model });
});
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`acls.aclFilter.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('acls.aclFilter.principalId')"
v-model="params.principalId"
@update:model-value="searchFn()"
:options="rolesOptions"
option-value="name"
option-label="name"
use-input
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('acls.aclFilter.model')"
v-model="params.model"
@update:model-value="searchFn()"
:options="validations"
option-value="name"
option-label="name"
use-input
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnInput
:label="t('acls.aclFilter.property')"
v-model="params.property"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('acls.aclFilter.accessType')"
v-model="params.accessType"
@update:model-value="searchFn()"
:options="accessTypes"
option-value="name"
option-label="name"
use-input
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
:label="t('acls.aclFilter.permission')"
v-model="params.permission"
@update:model-value="searchFn()"
:options="permissions"
option-value="name"
option-label="name"
use-input
dense
outlined
rounded
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -0,0 +1,126 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, onBeforeMount, onMounted } from 'vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import { useValidator } from 'src/composables/useValidator';
import { useArrayData } from 'src/composables/useArrayData';
const emit = defineEmits(['onDataChange']);
const { t } = useI18n();
const validationsStore = useValidator();
const { models } = validationsStore;
const arrayData = useArrayData('aclCreate');
const { store } = arrayData;
const accessTypes = [{ name: '*' }, { name: 'READ' }, { name: 'WRITE' }];
const permissions = [{ name: 'ALLOW' }, { name: 'DENY' }];
const validations = ref([]);
const url = ref();
const urlCreate = ref('ACLs');
const urlUpdate = ref();
const action = ref('New');
const $props = defineProps({
formInitialData: {
type: Object,
default: () => {
return {
property: '*',
principalType: 'ROLE',
accessType: 'READ',
permission: 'ALLOW',
};
},
},
rolesOptions: {
type: Array,
required: true,
},
});
onBeforeMount(() => {
for (let model in models) validations.value.push({ name: model });
});
onMounted(() => {
store.data = $props.formInitialData;
if ($props.formInitialData.id) {
urlCreate.value = null;
urlUpdate.value = 'ACLs';
action.value = 'Edit';
}
});
</script>
<template>
<FormModelPopup
v-if="urlCreate || urlUpdate"
:title="t(`${action} ACL`)"
:url="url"
:url-update="urlUpdate"
:url-create="urlCreate"
:form-initial-data="formInitialData"
auto-load
model="aclCreate"
@on-data-saved="emit('onDataChange')"
@on-data-canceled="emit('onDataChange')"
>
<template #form-inputs="{ data }">
<div class="column q-gutter-y-md">
<VnSelect
:label="t('acls.aclFilter.principalId')"
v-model="data.principalId"
:options="$props.rolesOptions"
option-value="name"
option-label="name"
use-input
rounded
/>
<VnSelect
:label="t('acls.aclFilter.model')"
v-model="data.model"
:options="validations"
option-value="name"
option-label="name"
use-input
rounded
/>
<VnInput
:label="t('acls.aclFilter.property')"
v-model="data.property"
lazy-rules
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('acls.tooltip') }}</QTooltip>
</QIcon>
</template></VnInput
>
<VnSelect
:label="t('acls.aclFilter.accessType')"
v-model="data.accessType"
:options="accessTypes"
option-value="name"
option-label="name"
use-input
rounded
/>
<VnSelect
:label="t('acls.aclFilter.permission')"
v-model="data.permission"
:options="permissions"
option-value="name"
option-label="name"
use-input
rounded
/>
</div>
</template>
</FormModelPopup>
</template>

View File

@ -0,0 +1,57 @@
<script setup>
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useArrayData } from 'src/composables/useArrayData';
const router = useRouter();
const { t } = useI18n();
const arrayData = useArrayData('AliasCreate');
const { store } = arrayData;
const defaultInitialData = {
alias: null,
description: null,
};
const onDataSaved = ({ id }) => {
router.push({ name: 'AliasBasicData', params: { id } });
store.data = null;
};
</script>
<template>
<FormModelPopup
:title="t('Create alias')"
ref="formModelPopupRef"
url-create="MailAliases"
model="AliasCreate"
:form-initial-data="defaultInitialData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput v-model="data.alias" :label="t('mailAlias.name')" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.description"
:label="t('mailAlias.description')"
/>
</div>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create alias: Crear alias
</i18n>

View File

@ -0,0 +1,20 @@
<script setup>
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
</script>
<template>
<FormModel model="Alias">
<template #form="{ data }">
<div class="column q-gutter-y-md">
<VnInput v-model="data.alias" :label="t('mailAlias.name')" />
<VnInput v-model="data.description" :label="t('mailAlias.description')" />
<QCheckbox :label="t('mailAlias.isPublic')" v-model="data.isPublic" />
</div>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,33 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import VnCard from 'components/common/VnCard.vue';
import AliasDescriptor from './AliasDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => {
return routeName.value;
});
const searchBarDataKeys = {
AliasBasicData: 'AliasBasicData',
AliasUsers: 'AliasUsers',
};
</script>
<template>
<VnCard
data-key="Alias"
base-url="MailAliases"
:descriptor="AliasDescriptor"
:search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName"
:search-redirect="!!customRouteRedirectName"
:searchbar-label="t('mailAlias.search')"
:searchbar-info="t('mailAlias.searchInfo')"
/>
</template>

View File

@ -0,0 +1,88 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const { t } = useI18n();
const route = useRoute();
const quasar = useQuasar();
const router = useRouter();
const { notify } = useNotify();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.alias, entity.id));
const removeAlias = () => {
quasar
.dialog({
title: t('Alias will be removed'),
message: t('Are you sure you want to continue?'),
ok: {
push: true,
color: 'primary',
},
cancel: true,
})
.onOk(async () => {
try {
await axios.delete(`MailAliases/${entityId.value}`);
notify(t('Alias removed'), 'positive');
router.push({ name: 'AccountAlias' });
} catch (err) {
console.error('Error removing alias');
}
});
};
</script>
<template>
<CardDescriptor
ref="descriptor"
:url="`MailAliases/${entityId}`"
module="Alias"
@on-fetch="setData"
data-key="aliasData"
:title="data.title"
:subtitle="data.subtitle"
>
<template #menu>
<QItem v-ripple clickable @click="removeAlias()">
<QItemSection>{{ t('Delete') }}</QItemSection>
</QItem>
</template>
<template #body="{ entity }">
<VnLv :label="t('mailAlias.description')" :value="entity.description" />
</template>
</CardDescriptor>
</template>
<i18n>
en:
accountRate: Claming rate
es:
accountRate: Ratio de reclamación
Delete: Eliminar
Alias will be removed: El alias será eliminado
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Alias removed: Alias eliminado
</i18n>

Some files were not shown because too many files have changed in this diff Show More