7409-workerBalance #451

Merged
carlossa merged 9 commits from 7409-workerBalance into dev 2024-06-26 09:40:37 +00:00
202 changed files with 8192 additions and 6676 deletions
Showing only changes of commit 84378187be - Show all commits

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 # Changelog
All notable changes to this project will be documented in this file. 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] ## [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] ## [2418.01]
## [2416.01] - 2024-04-18 ## [2416.01] - 2024-04-18

4
Jenkinsfile vendored
View File

@ -94,7 +94,7 @@ pipeline {
sh 'quasar build' sh 'quasar build'
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
dockerBuild() dockerBuild()
} }
@ -106,7 +106,7 @@ pipeline {
steps { steps {
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
withKubeConfig([ withKubeConfig([
serverUrl: "$KUBERNETES_API", serverUrl: "$KUBERNETES_API",

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,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.24.1", "version": "24.26.2",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -1,21 +1,47 @@
import { getCurrentInstance } from 'vue'; import { getCurrentInstance } from 'vue';
const filterAvailableInput = element => element.classList.contains('q-field__native') && !element.disabled const filterAvailableInput = (element) => {
const filterAvailableText = element => element.__vueParentComponent.type.name === 'QInput' && element.__vueParentComponent?.attrs?.class !== 'vn-input-date'; 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 { export default {
mounted: function () { mounted: function () {
const vm = getCurrentInstance(); const vm = getCurrentInstance();
if (vm.type.name === 'QForm') if (vm.type.name === 'QForm') {
if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) { if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) {
// AUTOFOCUS // AUTOFOCUS
const elementsArray = Array.from(this.$el.elements); 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) { if (firstInputElement) {
firstInputElement.focus(); 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; 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 { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useState } from 'src/composables/useState'; 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 SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue'; import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const state = useState(); const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute();
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -26,7 +30,7 @@ const $props = defineProps({
}, },
model: { model: {
type: String, type: String,
default: '', default: null,
}, },
filter: { filter: {
type: Object, type: Object,
@ -74,33 +78,77 @@ const $props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
goTo: {
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
);
const componentIsRendered = ref(false); 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 () => { onMounted(async () => {
originalData.value = $props.formInitialData; originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {}));
nextTick(() => {
componentIsRendered.value = true; nextTick(() => (componentIsRendered.value = true));
});
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData); state.set(modelValue, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData) {
await fetch(); if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch();
} else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
// 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) { if ($props.observeFormChanges) {
setTimeout(() => { watch(
startFormWatcher(); () => formData.value,
}, 100); (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) => { onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges) if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({ quasar.dialog({
@ -116,97 +164,58 @@ onBeforeRouteLeave((to, from, next) => {
onUnmounted(() => { onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas. // 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) { if (hasChanges.value) return state.set(modelValue, originalData.value);
state.set($props.model, originalData.value); if ($props.clearStoreOnUnmount) state.unset(modelValue);
return;
}
if ($props.clearStoreOnUnmount) state.unset($props.model);
}); });
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() { async function fetch() {
try { try {
let { data } = await axios.get($props.url, { let { data } = await axios.get($props.url, {
params: { filter: JSON.stringify($props.filter) }, params: { filter: JSON.stringify($props.filter) },
}); });
if (Array.isArray(data)) data = data[0] ?? {}; if (Array.isArray(data)) data = data[0] ?? {};
state.set($props.model, data); updateAndEmit('onFetch', data);
originalData.value = data && JSON.parse(JSON.stringify(data)); } catch (e) {
state.set(modelValue, {});
emit('onFetch', state.get($props.model));
} catch (error) {
state.set($props.model, {});
originalData.value = {}; originalData.value = {};
} }
} }
async function save() { async function save() {
if ($props.observeFormChanges && !hasChanges.value) { if ($props.observeFormChanges && !hasChanges.value)
notify('globals.noChanges', 'negative'); return notify('globals.noChanges', 'negative');
return;
}
isLoading.value = true;
isLoading.value = true;
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; 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; let response;
if ($props.saveFn) response = await $props.saveFn(body); if ($props.saveFn) response = await $props.saveFn(body);
else else response = await axios[method](url, body);
response = await axios[$props.urlCreate ? 'post' : 'patch'](
$props.urlCreate || $props.urlUpdate || $props.url,
body
);
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');
emit('onDataSaved', formData.value, response?.data); updateAndEmit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notify('errors.writeRequest', 'negative'); notify('errors.writeRequest', 'negative');
} finally {
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
} }
isLoading.value = false; }
async function saveAndGo() {
await save();
push({ path: $props.goTo });
} }
function reset() { function reset() {
state.set($props.model, originalData.value); updateAndEmit('onFetch', originalData.value);
originalData.value = JSON.parse(JSON.stringify(originalData.value));
emit('onFetch', state.get($props.model));
if ($props.observeFormChanges) { if ($props.observeFormChanges) {
hasChanges.value = false; hasChanges.value = false;
isResetting.value = true; isResetting.value = true;
@ -228,17 +237,15 @@ function filter(value, update, filterOptions) {
); );
} }
watch(formUrl, async () => { function updateAndEmit(evt, val, res) {
originalData.value = null; state.set(modelValue, val);
reset(); originalData.value = val && JSON.parse(JSON.stringify(val));
fetch(); if (!$props.url) arrayData.store.data = val;
});
defineExpose({ emit(evt, state.get(modelValue), res);
save, }
isLoading,
hasChanges, defineExpose({ save, isLoading, hasChanges });
});
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
@ -275,10 +282,42 @@ defineExpose({
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.reset.label)" :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 <QBtn
:label="tMobile(defaultButtons.save.label)" v-else
:color="defaultButtons.save.color" :label="tMobile('globals.save')"
:icon="defaultButtons.save.icon" color="primary"
icon="save"
@click="save" @click="save"
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.save.label)" :title="t(defaultButtons.save.label)"

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, computed } from 'vue'; import { onMounted, computed, ref } from 'vue';
import { Dark, Quasar } from 'quasar'; import { Dark, Quasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -10,13 +10,12 @@ import { localeEquivalence } from 'src/i18n/index';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
import { useClipboard } from 'src/composables/useClipboard';
import { ref } from 'vue';
const { copyText } = useClipboard(); const { copyText } = useClipboard();
const userLocale = computed({ const userLocale = computed({
get() { get() {
@ -91,6 +90,15 @@ function logout() {
function copyUserToken() { function copyUserToken() {
copyText(session.getToken(), { label: 'components.userPanel.copyToken' }); copyText(session.getToken(), { label: 'components.userPanel.copyToken' });
} }
function localUserData() {
state.setUser(user.value);
}
function saveUserData(param, value) {
axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData();
}
</script> </script>
<template> <template>
@ -180,6 +188,7 @@ function copyUserToken() {
option-value="id" option-value="id"
input-debounce="0" input-debounce="0"
hide-selected hide-selected
@update:model-value="localUserData"
/> />
<VnSelect <VnSelect
:label="t('components.userPanel.localBank')" :label="t('components.userPanel.localBank')"
@ -189,6 +198,7 @@ function copyUserToken() {
option-value="id" option-value="id"
input-debounce="0" input-debounce="0"
hide-selected hide-selected
@update:model-value="localUserData"
> >
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">
@ -210,6 +220,7 @@ function copyUserToken() {
option-label="code" option-label="code"
option-value="id" option-value="id"
input-debounce="0" input-debounce="0"
@update:model-value="localUserData"
/> />
<VnSelect <VnSelect
:label="t('components.userPanel.userWarehouse')" :label="t('components.userPanel.userWarehouse')"
@ -219,6 +230,7 @@ function copyUserToken() {
option-label="name" option-label="name"
option-value="id" option-value="id"
input-debounce="0" input-debounce="0"
@update:model-value="(v) => saveUserData('warehouseFk', v)"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
@ -232,6 +244,7 @@ function copyUserToken() {
style="flex: 0" style="flex: 0"
dense dense
input-debounce="0" input-debounce="0"
@update:model-value="(v) => saveUserData('companyFk', v)"
/> />
</VnRow> </VnRow>
</div> </div>

View File

@ -35,6 +35,10 @@ const $props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
showLabel: {
type: Boolean,
default: null,
},
}); });
const defaultComponents = { const defaultComponents = {
@ -43,12 +47,18 @@ const defaultComponents = {
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
}, },
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
number: { number: {
component: markRaw(VnInput), component: markRaw(VnInput),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
}, },
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
date: { date: {
component: markRaw(VnInputDate), component: markRaw(VnInputDate),
@ -57,20 +67,27 @@ const defaultComponents = {
disable: !$props.isEditable, disable: !$props.isEditable,
style: 'min-width: 125px', style: 'min-width: 125px',
}, },
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
checkbox: { checkbox: {
component: markRaw(QCheckbox), component: markRaw(QCheckbox),
attrs: (prop) => { attrs: (prop) => {
return { const defaultAttrs = {
disable: !$props.isEditable, disable: !$props.isEditable,
'true-value': 1,
'false-value': 0,
'model-value': Boolean(prop), 'model-value': Boolean(prop),
class: 'no-padding', class: 'no-padding',
}; };
if (typeof prop == 'number') {
defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0;
}
return defaultAttrs;
}, },
forceAttrs: { forceAttrs: {
label: null, label: $props.showLabel && $props.column.label,
}, },
}, },
select: { select: {
@ -78,6 +95,9 @@ const defaultComponents = {
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
}, },
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
icon: { icon: {
component: markRaw(QIcon), component: markRaw(QIcon),
@ -114,18 +134,19 @@ const col = computed(() => {
const components = computed(() => $props.components ?? defaultComponents); const components = computed(() => $props.components ?? defaultComponents);
</script> </script>
<template> <template>
<div class="row no-wrap fit">
<VnComponent <VnComponent
v-if="col.before" v-if="col.before"
:prop="col.before" :prop="col.before"
:components="components" :components="components"
:value="$props.row" :value="model"
v-model="model" v-model="model"
/> />
<VnComponent <VnComponent
v-if="col.component" v-if="col.component"
:prop="col" :prop="col"
:components="components" :components="components"
:value="$props.row" :value="model"
v-model="model" v-model="model"
/> />
<span :title="value" v-else>{{ value }}</span> <span :title="value" v-else>{{ value }}</span>
@ -133,7 +154,8 @@ const components = computed(() => $props.components ?? defaultComponents);
v-if="col.after" v-if="col.after"
:prop="col.after" :prop="col.after"
:components="components" :components="components"
:value="$props.row" :value="model"
v-model="model" v-model="model"
/> />
</div>
</template> </template>

View File

@ -22,9 +22,13 @@ const $props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
searchUrl: {
type: String,
default: 'params',
},
}); });
const model = defineModel(); const model = defineModel();
const arrayData = useArrayData($props.dataKey); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter); const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter }; const updateEvent = { 'update:modelValue': addFilter };
@ -99,16 +103,16 @@ const components = {
}; };
async function addFilter(value) { async function addFilter(value) {
value ??= undefined;
if (value && typeof value === 'object') value = model.value; if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value; value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name; let field = columnFilter.value?.name ?? $props.column.name;
let params = { [field]: value };
if (columnFilter.value?.inWhere) { if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field; if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere(params); return await arrayData.addFilterWhere({ [field]: value });
} }
await arrayData.addFilter({ params }); await arrayData.addFilter({ params: { [field]: value } });
} }
function alignRow() { function alignRow() {

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue'; import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue'; import FormModelPopup from 'components/FormModelPopup.vue';
import VnPaginate from 'components/ui/VnPaginate.vue'; import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
@ -47,16 +47,30 @@ const $props = defineProps({
type: String, type: String,
default: 'flex-one', default: 'flex-one',
}, },
searchUrl: {
type: String,
default: 'table',
},
isEditable: {
type: Boolean,
default: false,
},
useModel: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const route = useRoute();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const mode = ref('card'); const mode = ref('card');
const selected = ref([]); const selected = ref([]);
const params = ref({}); const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const VnPaginateRef = ref({}); const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const CrudModelRef = ref({});
const showForm = ref(false); const showForm = ref(false);
const splittedColumns = ref({ columns: [] }); const splittedColumns = ref({ columns: [] });
const tableModes = [ const tableModes = [
@ -69,13 +83,13 @@ const tableModes = [
icon: 'grid_view', icon: 'grid_view',
title: t('grid view'), title: t('grid view'),
value: 'card', value: 'card',
disable: () => console.log('called'),
}, },
]; ];
onMounted(() => { onMounted(() => {
mode.value = quasar.platform.is.mobile ? 'card' : $props.defaultMode; mode.value = quasar.platform.is.mobile ? 'card' : $props.defaultMode;
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]);
}); });
watch( watch(
@ -84,6 +98,21 @@ watch(
{ immediate: true } { immediate: true }
); );
watch(
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
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;
params.value = { ...params.value, ...watchedParams };
}
function splitColumns(columns) { function splitColumns(columns) {
splittedColumns.value = { splittedColumns.value = {
columns: [], columns: [],
@ -98,6 +127,8 @@ function splitColumns(columns) {
if (col.isTitle) splittedColumns.value.title = col; if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col); if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.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); splittedColumns.value.columns.push(col);
} }
// Status column // Status column
@ -130,12 +161,12 @@ function stopEventPropagation(event) {
event.stopPropagation(); event.stopPropagation();
} }
function reload() { function reload(params) {
VnPaginateRef.value.fetch(); CrudModelRef.value.reload(params);
} }
function columnName(col) { function columnName(col) {
const column = Object.assign({}, col, col.columnFilter); const column = { ...col, ...col.columnFilter };
let name = column.name; let name = column.name;
if (column.alias) name = column.alias + '.' + name; if (column.alias) name = column.alias + '.' + name;
return name; return name;
@ -160,6 +191,7 @@ defineExpose({
:search-button="true" :search-button="true"
v-model="params" v-model="params"
:disable-submit-event="true" :disable-submit-event="true"
:search-url="searchUrl"
> >
<template #body> <template #body>
<VnTableFilter <VnTableFilter
@ -168,6 +200,7 @@ defineExpose({
v-for="col of splittedColumns.columns" v-for="col of splittedColumns.columns"
:key="col.id" :key="col.id"
v-model="params[columnName(col)]" v-model="params[columnName(col)]"
:search-url="searchUrl"
/> />
</template> </template>
<slot <slot
@ -178,29 +211,34 @@ defineExpose({
</VnFilterPanel> </VnFilterPanel>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<VnPaginate <!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel
v-bind="$attrs" v-bind="$attrs"
class="q-px-md"
:limit="20" :limit="20"
ref="VnPaginateRef" ref="CrudModelRef"
:search-url="searchUrl"
:disable-infinite-scroll="mode == 'table'" :disable-infinite-scroll="mode == 'table'"
@save-changes="reload"
:has-subtoolbar="isEditable"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
v-bind="$attrs"
class="vnTable" class="vnTable"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"
row-key="id"
selection="multiple"
v-model:selected="selected" v-model:selected="selected"
:grid="mode != 'table'" :grid="mode != 'table'"
table-header-class="bg-header" table-header-class="bg-header"
card-container-class="grid-three" card-container-class="grid-three"
flat flat
:style="mode == 'table' && 'max-height: 92vh'" :style="mode == 'table' && 'max-height: 90vh'"
virtual-scroll virtual-scroll
@virtual-scroll=" @virtual-scroll="
(event) => event.index > rows.length - 2 && VnPaginateRef.paginate() (event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
" "
@row-click="(_, row) => rowClickFunction(row)" @row-click="(_, row) => rowClickFunction(row)"
> >
@ -231,12 +269,17 @@ defineExpose({
/> />
</template> </template>
<template #header-cell="{ col }"> <template #header-cell="{ col }">
<QTh auto-width style="min-width: 100px" v-if="$props.columnSearch"> <QTh
auto-width
style="min-width: 100px"
v-if="$props.columnSearch"
>
<VnTableFilter <VnTableFilter
:column="col" :column="col"
:show-title="true" :show-title="true"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
v-model="params[columnName(col)]" v-model="params[columnName(col)]"
:search-url="searchUrl"
/> />
</QTh> </QTh>
</template> </template>
@ -245,7 +288,10 @@ defineExpose({
</template> </template>
<template #body-cell-tableStatus="{ col, row }"> <template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="`text-${col.align ?? 'left'}`"> <QTd auto-width :class="`text-${col.align ?? 'left'}`">
<VnTableChip :columns="splittedColumns.columnChips" :row="row"> <VnTableChip
:columns="splittedColumns.columnChips"
:row="row"
>
<template #afterChip> <template #afterChip>
<slot name="afterChip" :row="row"></slot> <slot name="afterChip" :row="row"></slot>
</template> </template>
@ -283,7 +329,9 @@ defineExpose({
class="q-px-sm" class="q-px-sm"
flat flat
:class=" :class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text ' btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
" "
@click="btn.action(row)" @click="btn.action(row)"
/> />
@ -346,15 +394,26 @@ defineExpose({
:key="col.name" :key="col.name"
class="fields" class="fields"
> >
<VnLv :label="col.label && `${col.label}:`"> <VnLv
:label="
!col.component &&
col.label &&
`${col.label}:`
"
>
<template #value> <template #value>
<span <span
@click="stopEventPropagation($event)" @click="
stopEventPropagation($event)
"
> >
<VnTableColumn <VnTableColumn
:column="col" :column="col"
:row :row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField" component-prop="columnField"
:show-label="true"
/> />
</span> </span>
</template> </template>
@ -389,7 +448,8 @@ defineExpose({
</template> </template>
</QTable> </QTable>
</template> </template>
</VnPaginate> </CrudModel>
</div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> <QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
@ -398,7 +458,7 @@ defineExpose({
</QPageSticky> </QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup <FormModelPopup
v-bind="{ ...$attrs, ...create }" v-bind="create"
:model="$attrs['data-key'] + 'Create'" :model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)" @on-data-saved="(_, res) => create.onDataSaved(res)"
> >
@ -411,6 +471,7 @@ defineExpose({
:row="{}" :row="{}"
default="input" default="input"
v-model="data[column.name]" v-model="data[column.name]"
:show-label="true"
/> />
<slot name="more-create-dialog" :data="data" /> <slot name="more-create-dialog" :data="data" />
</div> </div>
@ -448,27 +509,6 @@ defineExpose({
background-color: var(--vn-page-color); background-color: var(--vn-page-color);
} }
/* Works on Firefox */
* {
scrollbar-width: thin;
scrollbar-color: grey transparent;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 12px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 20px;
border: 3px solid var(--vn-page-color);
}
.grid-three { .grid-three {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, max-content)); grid-template-columns: repeat(auto-fit, minmax(400px, max-content));

View File

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

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, computed, watchEffect } from 'vue'; import { computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router'; import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
@ -20,6 +20,9 @@ const props = defineProps({
searchUrl: { type: String, default: undefined }, searchUrl: { type: String, default: undefined },
searchbarLabel: { type: String, default: '' }, searchbarLabel: { type: String, default: '' },
searchbarInfo: { 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(); const stateStore = useStateStore();
@ -29,56 +32,40 @@ const url = computed(() => {
return props.customUrl; return props.customUrl;
}); });
const arrayData = useArrayData(props.dataKey, { useArrayData(props.dataKey, {
url: url.value, url: url.value,
filter: props.filter, filter: props.filter,
}); });
onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: 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 });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];
});
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <QDrawer
<Teleport to="#searchbar" v-if="props.searchDataKey"> v-model="stateStore.leftDrawer"
<slot name="searchbar"> show-if-above
<VnSearchbar :width="256"
:data-key="props.searchDataKey" v-if="stateStore.isHeaderMounted()"
: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"> <QScrollArea class="fit">
<component :is="descriptor" /> <component :is="descriptor" />
<QSeparator /> <QSeparator />
<LeftMenu source="card" /> <LeftMenu source="card" />
</QScrollArea> </QScrollArea>
</QDrawer> </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> <RightMenu>
<template #right-panel v-if="props.filterPanel"> <template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" /> <component :is="props.filterPanel" :data-key="props.searchDataKey" />
</template> </template>
</RightMenu> </RightMenu>
</template>
<QPageContainer> <QPageContainer>
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />

View File

@ -1,14 +1,3 @@
<template>
<span v-for="toComponent of componentArray" :key="toComponent.name">
<component
v-if="toComponent?.component"
:is="mix(toComponent).component"
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event"
v-model="model"
/>
</span>
</template>
<script setup> <script setup>
import { computed, defineModel } from 'vue'; import { computed, defineModel } from 'vue';
@ -21,12 +10,10 @@ const $props = defineProps({
components: { components: {
type: Object, type: Object,
default: () => {}, default: () => {},
required: false,
}, },
value: { value: {
type: Object, type: [Object, Number, String],
default: () => {}, default: () => {},
required: false,
}, },
}); });
@ -55,3 +42,19 @@ function toValueAttrs(attrs) {
return typeof attrs == 'function' ? attrs($props.value) : attrs; return typeof attrs == 'function' ? attrs($props.value) : attrs;
} }
</script> </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"
class="fit"
/>
</span>
</template>

View File

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

View File

@ -18,6 +18,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
info: {
type: String,
default: '',
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -85,9 +89,14 @@ const inputRules = [
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="$slots.append && hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled"
@click="value = null" @click="value = null"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
{{ info }}
</QTooltip>
</QIcon>
</template> </template>
</QInput> </QInput>
</div> </div>

View File

@ -2,7 +2,6 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate'; import isValidDate from 'filters/isValidDate';
import VnInput from 'components/common/VnInput.vue';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {

View File

@ -622,23 +622,7 @@ setLogTree();
</QList> </QList>
</div> </div>
</div> </div>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append"> <Teleport to="#right-panel" v-if="stateStore.isHeaderMounted()">
<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> <QList dense>
<QSeparator /> <QSeparator />
<QItem class="q-mt-sm"> <QItem class="q-mt-sm">
@ -701,10 +685,7 @@ setLogTree();
hide-selected hide-selected
> >
<template #option="{ opt, itemProps }"> <template #option="{ opt, itemProps }">
<QItem <QItem v-bind="itemProps" class="q-pa-xs row items-center">
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection class="col-3 items-center"> <QItemSection class="col-3 items-center">
<VnAvatar :worker-id="opt.id" /> <VnAvatar :worker-id="opt.id" />
</QItemSection> </QItemSection>
@ -774,8 +755,7 @@ setLogTree();
/> />
</QItem> </QItem>
</QList> </QList>
</QScrollArea> </Teleport>
</QDrawer>
<QDialog v-model="dateFromDialog"> <QDialog v-model="dateFromDialog">
<QDate <QDate
:years-in-month-view="false" :years-in-month-view="false"

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

@ -180,6 +180,7 @@ watch(modelValue, (newValue) => {
> >
<template v-if="isClearable" #append> <template v-if="isClearable" #append>
<QIcon <QIcon
v-show="value"
name="close" name="close"
@click.stop="value = null" @click.stop="value = null"
class="cursor-pointer" class="cursor-pointer"

View File

@ -1,16 +1,16 @@
<script setup> <script setup>
const $props = defineProps({ defineProps({
url: { type: String, default: null }, url: { type: String, default: null },
text: { type: String, default: null }, text: { type: String, default: null },
icon: { type: String, default: 'open_in_new' }, icon: { type: String, default: 'open_in_new' },
}); });
</script> </script>
<template> <template>
<div class="titleBox"> <div :class="$q.screen.gt.md ? 'q-pb-lg' : 'q-pb-md'">
<div class="header-link"> <div class="header-link">
<a :href="$props.url" :class="$props.url ? 'link' : 'color-vn-text'"> <a :href="url" :class="url ? 'link' : 'color-vn-text'">
{{ $props.text }} {{ text }}
<QIcon v-if="url" :name="$props.icon" /> <QIcon v-if="url" :name="icon" />
</a> </a>
</div> </div>
</div> </div>
@ -19,7 +19,4 @@ const $props = defineProps({
a { a {
font-size: large; font-size: large;
} }
.titleBox {
padding-bottom: 2%;
}
</style> </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 { useArrayData } from 'composables/useArrayData';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -15,21 +16,21 @@ const $props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
module: {
type: String,
required: true,
},
title: { title: {
type: String, type: String,
default: '', default: '',
}, },
subtitle: { subtitle: {
type: Number, type: Number,
default: 0, default: null,
}, },
dataKey: { dataKey: {
type: String, type: String,
default: '', default: null,
},
module: {
type: String,
default: null,
}, },
summary: { summary: {
type: Object, type: Object,
@ -40,21 +41,27 @@ const $props = defineProps({
const state = useState(); const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const arrayData = useArrayData($props.dataKey || $props.module, { let arrayData;
let store;
let entity;
const isLoading = ref(false);
defineExpose({ getData });
onBeforeMount(async () => {
arrayData = useArrayData($props.dataKey, {
url: $props.url, url: $props.url,
filter: $props.filter, filter: $props.filter,
skip: 0, skip: 0,
}); });
const { store } = arrayData; store = arrayData.store;
const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false); // It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== useRoute().meta.moduleName) await getData();
defineExpose({ watch(
getData, () => [$props.url, $props.filter],
}); async () => await getData()
onBeforeMount(async () => { );
await getData();
watch($props, async () => await getData());
}); });
async function getData() { async function getData() {
@ -132,7 +139,7 @@ const emit = defineEmits(['onFetch']);
<QItemLabel header class="ellipsis text-h5" :lines="1"> <QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title"> <div class="title">
<span v-if="$props.title" :title="$props.title"> <span v-if="$props.title" :title="$props.title">
{{ $props.title }} {{ entity[title] ?? $props.title }}
</span> </span>
<slot v-else name="description" :entity="entity"> <slot v-else name="description" :entity="entity">
<span :title="entity.name"> <span :title="entity.name">
@ -235,6 +242,7 @@ const emit = defineEmits(['onFetch']);
width: 256px; width: 256px;
.header { .header {
display: flex; display: flex;
align-items: center;
} }
.icons { .icons {
margin: 0 10px; margin: 0 10px;

View File

@ -28,7 +28,7 @@ const toggleCardCheck = (item) => {
<div class="title text-primary text-weight-bold text-h5"> <div class="title text-primary text-weight-bold text-h5">
{{ $props.title }} {{ $props.title }}
</div> </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 }} {{ t('ID') }}: {{ $props.id }}
</QChip> </QChip>
</div> </div>

View File

@ -147,7 +147,7 @@ const containerClasses = computed(() => {
.q-calendar-month__head--workweek, .q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis { .q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize; text-transform: capitalize;
color: var(---color-font-secondary); color: $color-font-secondary;
font-weight: bold; font-weight: bold;
font-size: 0.8rem; font-size: 0.8rem;
text-align: center; text-align: center;

View File

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

View File

@ -4,12 +4,11 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n(); const { t } = useI18n();
const params = defineModel(); const params = defineModel({ default: {}, required: true, type: Object });
const props = defineProps({ const $props = defineProps({
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
@ -36,7 +35,7 @@ const props = defineProps({
}, },
hiddenTags: { hiddenTags: {
type: Array, type: Array,
default: () => [], default: () => ['filter'],
}, },
customTags: { customTags: {
type: Array, type: Array,
@ -46,82 +45,76 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
searchUrl: {
type: String,
default: 'params',
},
redirect: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']); const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData($props.dataKey, {
exprBuilder: props.exprBuilder, exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl,
navigate: {},
}); });
const route = useRoute(); const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
onMounted(() => { onMounted(() => {
if (params.value) userParams.value = JSON.parse(JSON.stringify(params.value)); emit('init', { params: params.value });
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
params.value = {
...params.value,
...userParams.value,
...userParams.value?.filter?.where,
};
}
emit('init', { params: userParams.value });
}); });
function setUserParams(params) { function setUserParams(watchedParams) {
if (!params) { if (!watchedParams) return;
userParams.value = {};
} else { if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
userParams.value = typeof params == 'string' ? JSON.parse(params) : params; watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
} delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
} }
watch( watch(
() => route.query.params, () => route.query[$props.searchUrl],
(val) => { (val) => setUserParams(val)
setUserParams(val);
}
); );
watch( watch(
() => arrayData.store.userParams, () => arrayData.store.userParams,
(val) => { (val) => setUserParams(val)
setUserParams(val);
}
); );
const isLoading = ref(false); const isLoading = ref(false);
async function search(evt) { async function search(evt) {
if (evt && props.disableSubmitEvent) return; if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const filter = { ...userParams.value, ...params.value }; const filter = { ...params.value };
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
const { params: newParams } = await arrayData.addFilter({ params: filter }); const { params: newParams } = await arrayData.addFilter({ params: params.value });
userParams.value = newParams; params.value = newParams;
if (!props.showAll && !Object.values(filter).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('search'); emit('search');
navigate(store.data, {});
} }
async function reload() { async function reload() {
isLoading.value = true; isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param); const params = Object.values(params.value).filter((param) => param);
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = []; if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('refresh'); emit('refresh');
navigate(store.data, {});
} }
async function clearFilters() { async function clearFilters() {
@ -130,19 +123,19 @@ async function clearFilters() {
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
// Filtrar los params no removibles // Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) => const removableFilters = Object.keys(params.value).filter((param) =>
props.unremovableParams.includes(param) $props.unremovableParams.includes(param)
); );
const newParams = {}; const newParams = {};
// Conservar solo los params que no son removibles // Conservar solo los params que no son removibles
for (const key of removableFilters) { for (const key of removableFilters) {
newParams[key] = userParams.value[key]; newParams[key] = params.value[key];
} }
params.value = {}; params.value = {};
userParams.value = { ...newParams }; // Actualizar los params con los removibles params.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value }); await arrayData.applyFilter({ params: params.value });
if (!props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
} }
@ -152,45 +145,30 @@ async function clearFilters() {
const tagsList = computed(() => { const tagsList = computed(() => {
const tagList = []; const tagList = [];
const params = { for (const key of Object.keys(params.value)) {
...userParams.value, const value = params.value[key];
}; if (value == null || ($props.hiddenTags || []).includes(key)) continue;
const where = params?.filter?.where; tagList.push({ label: key, value });
if (where) {
Object.assign(params, where);
}
delete params.filter;
for (const key of Object.keys(params)) {
const value = params[key];
if (!value || (props.hiddenTags || []).includes(key)) continue;
tagList.push({ key, value });
} }
return tagList; return tagList;
}); });
const tags = computed(() => const tags = computed(() => {
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.key)) return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.key)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
); );
async function remove(key) { async function remove(key) {
delete userParams.value[key];
delete userParams.value.filter.where[key];
params.value[key] = undefined; params.value[key] = undefined;
await arrayData.applyFilter({ params: userParams.value }); search();
emit('remove', key); emit('remove', key);
} }
function formatValue(value) { function formatValue(value) {
if (typeof value === 'boolean') { if (typeof value === 'boolean') return value ? t('Yes') : t('No');
return value ? t('Yes') : t('No'); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
return `"${value}"`; return `"${value}"`;
} }
@ -244,21 +222,21 @@ function formatValue(value) {
<div> <div>
<VnFilterPanelChip <VnFilterPanelChip
v-for="chip of tags" v-for="chip of tags"
:key="chip.key" :key="chip.label"
:removable="!unremovableParams.includes(chip.key)" :removable="!unremovableParams.includes(chip.label)"
@remove="remove(chip.key)" @remove="remove(chip.label)"
> >
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ chip.key }}:</strong> <strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span> <span>"{{ formatValue(chip.value) }}"</span>
</div> </div>
</slot> </slot>
</VnFilterPanelChip> </VnFilterPanelChip>
<slot <slot
v-if="$slots.customTags" v-if="$slots.customTags"
name="customTags" name="customTags"
:params="userParams" :params="params"
:tags="customTags" :tags="customTags"
:format-fn="formatValue" :format-fn="formatValue"
:search-fn="search" :search-fn="search"
@ -268,9 +246,9 @@ function formatValue(value) {
<QSeparator /> <QSeparator />
</QList> </QList>
<QList dense class="list q-gutter-y-sm q-mt-sm"> <QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot> <slot name="body" :params="params" :search-fn="search"></slot>
</QList> </QList>
<template v-if="props.searchButton"> <template v-if="$props.searchButton">
<QItem> <QItem>
<QItemSection class="q-py-sm"> <QItemSection class="q-py-sm">
<QBtn <QBtn
@ -282,7 +260,6 @@ function formatValue(value) {
rounded rounded
:type="disableSubmitEvent ? 'button' : 'submit'" :type="disableSubmitEvent ? 'button' : 'submit'"
unelevated unelevated
@click="search()"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -295,7 +272,6 @@ function formatValue(value) {
color="primary" color="primary"
/> />
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.list { .list {
width: 256px; width: 256px;

View File

@ -20,7 +20,12 @@ const state = useState();
const currentUser = ref(state.getUser()); const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = ref('');
const vnPaginateRef = ref(); const vnPaginateRef = ref();
function handleKeyUp(event) {
if (event.key === 'Enter') {
event.preventDefault();
if (!event.shiftKey) insert();
}
}
async function insert() { async function insert() {
const body = $props.body; const body = $props.body;
Object.assign(body, { text: newNote.value }); Object.assign(body, { text: newNote.value });
@ -48,12 +53,12 @@ async function insert() {
size="lg" size="lg"
autogrow autogrow
autofocus autofocus
@keyup.ctrl.enter.stop="insert" @keyup="handleKeyUp"
clearable clearable
> >
<template #append <template #append>
><QBtn <QBtn
:title="t('Save (ctrl + Enter)')" :title="t('Save (Enter)')"
icon="save" icon="save"
color="primary" color="primary"
flat flat
@ -130,6 +135,6 @@ async function insert() {
es: es:
Add note here...: Añadir nota aquí... Add note here...: Añadir nota aquí...
New note: Nueva nota New note: Nueva nota
Save (ctrl + Enter): Guardar (Ctrl + Intro) Save (Enter): Guardar (Intro)
</i18n> </i18n>

View File

@ -58,14 +58,19 @@ const props = defineProps({
type: Function, type: Function,
default: null, default: null,
}, },
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: { disableInfiniteScroll: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false); const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
rowsPerPage: props.limit, rowsPerPage: props.limit,
@ -81,11 +86,13 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts, keepOpts: props.keepOpts,
searchUrl: props.searchUrl,
}); });
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(async () => {
if (props.autoLoad) fetch(); if (props.autoLoad) await fetch();
mounted.value = true;
}); });
watch( watch(
@ -95,11 +102,22 @@ watch(
} }
); );
watch(
() => store.data,
(data) => emit('onChange', data)
);
watch(
() => props.url,
(url) => fetch({ url })
);
const addFilter = async (filter, params) => { const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params }); await arrayData.addFilter({ filter, params });
}; };
async function fetch() { async function fetch(params) {
useArrayData(props.dataKey, params);
store.filter.skip = 0; store.filter.skip = 0;
store.skip = 0; store.skip = 0;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
@ -107,6 +125,7 @@ async function fetch() {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
return store.data;
} }
async function paginate() { async function paginate() {
@ -138,7 +157,7 @@ function endPagination() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { 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); if (store.data.length === 0 || !props.url) return done(false);

View File

@ -1,13 +1,14 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const state = useStateStore();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -16,17 +17,14 @@ const props = defineProps({
}, },
label: { label: {
type: String, type: String,
required: false,
default: 'Search', default: 'Search',
}, },
info: { info: {
type: String, type: String,
required: false,
default: '', default: '',
}, },
redirect: { redirect: {
type: Boolean, type: Boolean,
required: false,
default: true, default: true,
}, },
url: { url: {
@ -65,12 +63,34 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
makeFetch: {
type: Boolean,
default: true,
},
}); });
const arrayData = useArrayData(props.dataKey, { ...props });
const { store } = arrayData;
const searchText = ref(''); 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(() => { onMounted(() => {
const params = store.userParams; const params = store.userParams;
@ -84,23 +104,18 @@ async function search() {
([key, value]) => value && (props.staticParams || []).includes(key) ([key, value]) => value && (props.staticParams || []).includes(key)
); );
store.skip = 0; store.skip = 0;
if (props.makeFetch)
await arrayData.applyFilter({ await arrayData.applyFilter({
params: { params: {
...Object.fromEntries(staticParams), ...Object.fromEntries(staticParams),
search: searchText.value, search: searchText.value,
}, },
}); });
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
} }
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="state.isHeaderMounted()">
<QForm @submit="search" id="searchbarForm"> <QForm @submit="search" id="searchbarForm">
<VnInput <VnInput
id="searchbar" id="searchbar"
@ -129,6 +144,7 @@ async function search() {
</template> </template>
</VnInput> </VnInput>
</QForm> </QForm>
</Teleport>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -18,7 +18,7 @@ onMounted(() => {
const observer = new MutationObserver( const observer = new MutationObserver(
() => () =>
(hasContent.value = (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 (actions.value) observer.observe(actions.value, opts);
if (data.value) observer.observe(data.value, opts); if (data.value) observer.observe(data.value, opts);

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

@ -6,7 +6,7 @@ import { buildFilter } from 'filters/filterPanel';
const arrayDataStore = useArrayDataStore(); 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 (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) arrayDataStore.set(key); if (!arrayDataStore.get(key)) arrayDataStore.set(key);
@ -23,8 +23,13 @@ export function useArrayData(key, userOptions) {
store.skip = 0; store.skip = 0;
const query = route.query; const query = route.query;
if (query.params) { const searchUrl = store.searchUrl;
store.userParams = JSON.parse(query.params); 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 };
} }
}); });
@ -41,13 +46,15 @@ export function useArrayData(key, userOptions) {
'userParams', 'userParams',
'userFilter', 'userFilter',
'exprBuilder', 'exprBuilder',
'searchUrl',
'navigate',
]; ];
if (typeof userOptions === 'object') { if (typeof userOptions === 'object') {
for (const option in userOptions) { for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === ''; const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue; if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) { if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option]; const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option) store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option]) ? Object.assign(defaultOpts, store[option])
@ -88,8 +95,8 @@ export function useArrayData(key, userOptions) {
Object.assign(params, userParams); Object.assign(params, userParams);
store.isLoading = true;
store.currentFilter = params; store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
signal: canceller.signal, signal: canceller.signal,
params, params,
@ -119,6 +126,10 @@ export function useArrayData(key, userOptions) {
} }
} }
function deleteOption(option) {
delete store[option];
}
function cancelRequest() { function cancelRequest() {
if (canceller) { if (canceller) {
canceller.abort(); canceller.abort();
@ -129,7 +140,7 @@ export function useArrayData(key, userOptions) {
async function applyFilter({ filter, params }) { async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter; if (filter) store.userFilter = filter;
store.filter = {}; store.filter = {};
if (params) store.userParams = Object.assign({}, params); if (params) store.userParams = { ...params };
const response = await fetch({ append: false }); const response = await fetch({ append: false });
return response; return response;
@ -138,7 +149,7 @@ export function useArrayData(key, userOptions) {
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.userFilter = Object.assign(store.userFilter, filter);
let userParams = Object.assign({}, store.userParams, params); let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder); userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams; store.userParams = userParams;
@ -163,9 +174,7 @@ export function useArrayData(key, userOptions) {
delete store.userParams[param]; delete store.userParams[param];
delete params[param]; delete params[param];
if (store.filter?.where) { if (store.filter?.where) {
const key = Object.keys( const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
if (key[0]) delete store.filter.where[key[0]]; if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) { if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where; delete store.filter.where;
@ -190,22 +199,34 @@ export function useArrayData(key, userOptions) {
} }
function updateStateParams() { function updateStateParams() {
const query = {}; const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
if (store.order) query.order = store.order; newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
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 = store.userParams;
if (store.userFilter && Object.keys(store.userFilter).length !== 0) {
if (!query.params) query.params = {};
query.params.filter = store.userFilter;
}
if (query.params) query.params = JSON.stringify(query.params);
router.replace({ if (store.navigate) {
path: route.path, const { customRouteRedirectName, searchText } = store.navigate;
query, if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
}); });
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
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); const totalRows = computed(() => (store.data && store.data.length) || 0);
@ -223,5 +244,6 @@ export function useArrayData(key, userOptions) {
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading, isLoading,
deleteOption,
}; };
} }

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

@ -1,5 +1,6 @@
import { useState } from './useState'; import { useState } from './useState';
import { useRole } from './useRole'; import { useRole } from './useRole';
import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import useNotify from './useNotify'; import useNotify from './useNotify';
@ -88,6 +89,7 @@ export function useSession() {
setSession(data); setSession(data);
await useRole().fetch(); await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
await useTokenConfig().fetch(); await useTokenConfig().fetch();

View File

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

View File

@ -76,7 +76,7 @@ select:-webkit-autofill {
} }
.color-vn-label { .color-vn-label {
color: var(--vn-label); color: var(--vn-label-color);
} }
.color-vn-text { .color-vn-text {
@ -196,3 +196,26 @@ input::-webkit-inner-spin-button {
.q-scrollarea__content { .q-scrollarea__content {
max-width: 100%; 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

Width:  |  Height:  |  Size: 180 KiB

After

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ globals:
date: Date date: Date
dataSaved: Data saved dataSaved: Data saved
dataDeleted: Data deleted dataDeleted: Data deleted
delete: Delete
search: Search search: Search
changes: Changes changes: Changes
dataCreated: Data created dataCreated: Data created
@ -24,6 +25,7 @@ globals:
create: Create create: Create
edit: Edit edit: Edit
save: Save save: Save
saveAndContinue: Save and continue
remove: Remove remove: Remove
reset: Reset reset: Reset
close: Close close: Close
@ -100,11 +102,17 @@ globals:
zonesList: Zones zonesList: Zones
deliveryList: Delivery days deliveryList: Delivery days
upcomingList: Upcoming deliveries upcomingList: Upcoming deliveries
role: Role
alias: Alias
aliasUsers: Users
subRoles: Subroles
inheritedRoles: Inherited Roles
created: Created created: Created
worker: Worker worker: Worker
now: Now now: Now
name: Name name: Name
new: New new: New
comment: Comment
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -388,6 +396,7 @@ entry:
type: Type type: Type
color: Color color: Color
id: ID id: ID
printedStickers: Printed stickers
notes: notes:
observationType: Observation type observationType: Observation type
descriptor: descriptor:
@ -464,6 +473,7 @@ ticket:
agency: Agency agency: Agency
zone: Zone zone: Zone
warehouse: Warehouse warehouse: Warehouse
collection: Collection
route: Route route: Route
invoice: Invoice invoice: Invoice
shipped: Shipped shipped: Shipped
@ -573,6 +583,9 @@ claim:
created: Created created: Created
state: State state: State
pickup: Pick up pickup: Pick up
null: No
agency: Agency
delivery: Delivery
photo: photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}' fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file' noData: 'There are no images/videos, click here or drag and drop the file'
@ -961,7 +974,7 @@ roadmap:
route: route:
pageTitles: pageTitles:
routes: Routes routes: Routes
cmrsList: External CMRs list cmrsList: CMRs list
RouteList: List RouteList: List
routeCreate: New route routeCreate: New route
basicData: Basic Data basicData: Basic Data
@ -1176,6 +1189,7 @@ item:
available: Available available: Available
warehouseText: 'Calculated on the warehouse of { warehouseName }' warehouseText: 'Calculated on the warehouse of { warehouseName }'
itemDiary: Item diary itemDiary: Item diary
producer: Producer
list: list:
id: Identifier id: Identifier
grouping: Grouping grouping: Grouping
@ -1259,6 +1273,13 @@ monitor:
pageTitles: pageTitles:
monitors: Monitors monitors: Monitors
list: List list: List
zone:
pageTitles:
zones: Zones
zonesList: Zones
deliveryList: Delivery days
upcomingList: Upcoming deliveries
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:

View File

@ -17,6 +17,7 @@ globals:
date: Fecha date: Fecha
dataSaved: Datos guardados dataSaved: Datos guardados
dataDeleted: Datos eliminados dataDeleted: Datos eliminados
delete: Eliminar
search: Buscar search: Buscar
changes: Cambios changes: Cambios
dataCreated: Datos creados dataCreated: Datos creados
@ -24,6 +25,7 @@ globals:
create: Crear create: Crear
edit: Modificar edit: Modificar
save: Guardar save: Guardar
saveAndContinue: Guardar y continuar
remove: Eliminar remove: Eliminar
reset: Restaurar reset: Restaurar
close: Cerrar close: Cerrar
@ -100,11 +102,17 @@ globals:
zonesList: Zonas zonesList: Zonas
deliveryList: Días de entrega deliveryList: Días de entrega
upcomingList: Próximos repartos upcomingList: Próximos repartos
role: Role
alias: Alias
aliasUsers: Usuarios
subRoles: Subroles
inheritedRoles: Roles heredados
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
name: Nombre name: Nombre
new: Nuevo new: Nuevo
comment: Comentario
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -386,6 +394,7 @@ entry:
type: Tipo type: Tipo
color: Color color: Color
id: ID id: ID
printedStickers: Etiquetas impresas
notes: notes:
observationType: Tipo de observación observationType: Tipo de observación
descriptor: descriptor:
@ -462,6 +471,7 @@ ticket:
agency: Agencia agency: Agencia
zone: Zona zone: Zona
warehouse: Almacén warehouse: Almacén
collection: Colección
route: Ruta route: Ruta
invoice: Factura invoice: Factura
shipped: Enviado shipped: Enviado
@ -950,7 +960,7 @@ roadmap:
route: route:
pageTitles: pageTitles:
routes: Rutas routes: Rutas
cmrsList: Listado de CMRs externos cmrsList: Listado de CMRs
RouteList: Listado RouteList: Listado
routeCreate: Nueva ruta routeCreate: Nueva ruta
basicData: Datos básicos basicData: Datos básicos
@ -1166,6 +1176,7 @@ item:
available: Disponible available: Disponible
warehouseText: 'Calculado sobre el almacén de { warehouseName }' warehouseText: 'Calculado sobre el almacén de { warehouseName }'
itemDiary: Registro de compra-venta itemDiary: Registro de compra-venta
producer: Productor
list: list:
id: Identificador id: Identificador
grouping: Grouping grouping: Grouping
@ -1247,8 +1258,14 @@ item/itemType:
summary: Resumen summary: Resumen
zone: zone:
pageTitles: pageTitles:
zones: Zona zones: Zonas
zonesList: Zonas list: Zonas
deliveryList: Días de entrega
upcomingList: Próximos repartos
role:
pageTitles:
zones: Zonas
list: Zonas
deliveryList: Días de entrega deliveryList: Días de entrega
upcomingList: Próximos repartos upcomingList: Próximos repartos
monitor: monitor:

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,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 @@
<template>Account list</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>

View File

@ -0,0 +1,49 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const { store } = useArrayData('Alias');
const alias = ref(store.data);
const entityId = computed(() => $props.id || route.params.id);
</script>
<template>
<CardSummary
ref="summary"
:url="`MailAliases/${entityId}`"
@on-fetch="(data) => (alias = data)"
>
<template #header> {{ alias.id }} - {{ alias.alias }} </template>
<template #body>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<router-link
:to="{ name: 'AliasBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" />
</router-link>
</QCardSection>
<VnLv :label="t('mailAlias.id')" :value="alias.id" />
<VnLv :label="t('mailAlias.description')" :value="alias.description" />
</QCard>
</template>
</CardSummary>
</template>

View File

@ -0,0 +1,122 @@
<script setup>
import { useRoute } from 'vue-router';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const { notify } = useNotify();
const route = useRoute();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref(null);
const arrayData = useArrayData('AliasUsers');
const { store } = arrayData;
const data = computed(() => {
const dataCopy = JSON.parse(JSON.stringify(store.data));
return dataCopy.sort((a, b) => a.user?.name.localeCompare(b.user?.name));
});
const filter = {
include: {
relation: 'user',
scope: {
fields: ['id', 'name'],
},
},
};
const urlPath = computed(() => `MailAliases/${route.params.id}/accounts`);
const columns = computed(() => [
{
name: 'alias',
},
{
name: 'action',
},
]);
const deleteAlias = async (row) => {
try {
await axios.delete(`${urlPath.value}/${row.id}`);
notify(t('User removed'), 'positive');
fetchAliases();
} catch (error) {
console.error(error);
}
};
watch(
() => route.params.id,
() => {
store.url = urlPath.value;
store.filter = filter;
fetchAliases();
}
);
const fetchAliases = () => paginateRef.value.fetch();
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<VnPaginate
ref="paginateRef"
data-key="AliasUsers"
:filter="filter"
:url="urlPath"
:limit="0"
auto-load
>
<template #body>
<QTable :rows="data" :columns="columns" hide-header>
<template #body="{ row }">
<QTr>
<QTd>
<span>{{ row.user?.name }}</span>
</QTd>
<QTd style="width: 50px !important">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="
openConfirmationModal(
t('User will be removed from alias'),
t('Are you sure you want to continue?'),
() => deleteAlias(row)
)
"
>
<QTooltip>
{{ t('Delete') }}
</QTooltip>
</QIcon>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
User will be removed from alias: El usuario será borrado del alias
Are you sure you want to continue?: ¿Seguro que quieres continuar?
User removed: Usuario borrado
Delete: Eliminar
</i18n>

View File

@ -0,0 +1,131 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { ref } from 'vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import RoleSummary from './Card/RoleSummary.vue';
import RoleForm from './Card/RoleForm.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import AccountRolesFilter from './AccountRolesFilter.vue';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const roleCreateDialogRef = ref(null);
const exprBuilder = (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? { id: value }
: {
or: [
{ name: { like: `%${value}%` } },
{ nickname: { like: `%${value}%` } },
],
};
case 'name':
case 'description':
return { [param]: { like: `%${value}%` } };
}
};
const openCreateModal = () => roleCreateDialogRef.value.show();
const getApiUrl = () => new URL(window.location).origin;
const navigate = (event, id) => {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/account/role/${id}/summary`);
router.push({ name: 'RoleSummary', params: { id } });
};
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="RolesList"
url="VnRoles"
:label="t('role.searchRoles')"
:info="t('role.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">
<AccountRolesFilter data-key="RolesList" :expr-builder="exprBuilder" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate data-key="RolesList" url="VnRoles">
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
@click="navigate($event, row.id)"
v-for="row of rows"
>
<template #list-items>
<div style="flex-direction: column; width: 100%">
<VnLv :label="t('role.card.name')" :value="row.name">
</VnLv>
<VnLv
:label="t('role.card.description')"
:value="row.description"
>
</VnLv>
</div>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, RoleSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="roleCreateDialogRef"
transition-show="scale"
transition-hide="scale"
>
<RoleForm />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openCreateModal()" />
<QTooltip>
{{ t('role.newRole') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>

View File

@ -0,0 +1,52 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
</script>
<template>
<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(`role.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('role.name')"
v-model="params.name"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem class="q-my-sm">
<QItemSection>
<VnInput
:label="t('role.description')"
v-model="params.description"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -0,0 +1,96 @@
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
const route = useRoute();
const router = useRouter();
const paginateRef = ref(null);
const arrayData = useArrayData('InheritedRoles');
const store = arrayData.store;
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.inherits?.name.localeCompare(b.inherits?.name));
});
const filter = computed(() => ({
where: { role: route.params.id },
include: {
relation: 'inherits',
scope: {
fields: ['id', 'name', 'description'],
},
},
}));
const urlPath = computed(() => 'RoleRoles');
const columns = computed(() => [
{
name: 'name',
},
]);
watch(
() => route.params.id,
() => {
store.url = urlPath.value;
store.filter = filter.value;
fetchSubRoles();
}
);
const fetchSubRoles = () => paginateRef.value.fetch();
const redirectToRoleSummary = (id) =>
router.push({ name: 'RoleSummary', params: { id } });
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<VnPaginate
ref="paginateRef"
data-key="InheritedRoles"
:filter="filter"
:url="urlPath"
:limit="0"
auto-load
>
<template #body>
<QTable :rows="data" :columns="columns" hide-header>
<template #body="{ row }">
<QTr
@click="redirectToRoleSummary(row.inherits?.id)"
class="cursor-pointer"
>
<QTd>
<div class="column">
<span>{{ row.inherits?.name }}</span>
<span class="color-vn-label">{{
row.inherits?.description
}}</span>
</div>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
Role removed. Changes will take a while to fully propagate.: Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente.
Role added! Changes will take a while to fully propagate.: ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente.
El rol va a ser eliminado: Role will be removed
¿Seguro que quieres continuar?: Are you sure you want to continue?
</i18n>

View File

@ -0,0 +1,33 @@
<script setup>
import { useRoute } from 'vue-router';
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';
const route = useRoute();
const { t } = useI18n();
</script>
<template>
<FormModel :url="`VnRoles/${route.params.id}`" model="VnRole" auto-load>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput v-model="data.name" :label="t('role.card.name')" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.description"
:label="t('role.card.description')"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<QCheckbox :label="t('mailAlias.isPublic')" v-model="data.isPublic" />
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,32 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue';
import RoleDescriptor from './RoleDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => routeName.value);
const searchBarDataKeys = {
RoleSummary: 'RoleSummary',
RoleBasicData: 'RoleBasicData',
SubRoles: 'SubRoles',
InheritedRoles: 'InheritedRoles',
RoleLog: 'RoleLog',
};
</script>
<template>
<VnCard
data-key="Role"
:descriptor="RoleDescriptor"
:search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName"
:search-redirect="!!customRouteRedirectName"
:searchbar-label="t('role.searchRoles')"
:searchbar-info="t('role.searchInfo')"
/>
</template>

View File

@ -0,0 +1,92 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import { useQuasar } from 'quasar';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const quasar = useQuasar();
const router = useRouter();
const { notify } = useNotify();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id));
const filter = {
where: { id: entityId },
};
const removeRole = () => {
quasar
.dialog({
title: 'Are you sure you want to delete it?',
message: 'Delete department',
ok: {
push: true,
color: 'primary',
},
cancel: true,
})
.onOk(async () => {
try {
await axios.post(
`/Departments/${entityId.value}/removeChild`,
entityId.value
);
router.push({ name: 'WorkerDepartment' });
notify('department.departmentRemoved', 'positive');
} catch (err) {
console.error('Error removing department');
}
});
};
</script>
<template>
<CardDescriptor
ref="descriptor"
:url="`VnRoles`"
:filter="filter"
module="Role"
@on-fetch="setData"
data-key="accountData"
:title="data.title"
:subtitle="data.subtitle"
>
<template #menu>
<QItem v-ripple clickable @click="removeRole()">
<QItemSection>{{ t('Delete') }}</QItemSection>
</QItem>
</template>
<template #body="{ entity }">
<VnLv :label="t('role.card.description')" :value="entity.description" />
</template>
</CardDescriptor>
</template>
<style scoped>
.q-item__label {
margin-top: 0;
}
</style>
<i18n>
en:
accountRate: Claming rate
es:
accountRate: Ratio de reclamación
</i18n>

View File

@ -0,0 +1,44 @@
<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';
const router = useRouter();
const { t } = useI18n();
</script>
<template>
<FormModelPopup
:title="t('Create role')"
ref="formModelPopupRef"
url-create="VnRoles"
model="VnRole"
:form-initial-data="{}"
@on-data-saved="
(_, { id }) => router.push({ name: 'RoleBasicData', params: { id } })
"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput v-model="data.name" :label="t('role.card.name')" />
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInput
v-model="data.description"
:label="t('role.card.description')"
/>
</div>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create role: Crear role
</i18n>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Role" url="/RoleLogs"></VnLog>
</template>

View File

@ -0,0 +1,52 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const { store } = useArrayData('Role');
const role = ref(store.data);
const entityId = computed(() => $props.id || route.params.id);
const filter = {
where: { id: entityId },
};
</script>
<template>
<CardSummary
ref="summary"
:url="`VnRoles`"
:filter="filter"
@on-fetch="(data) => (role = data)"
>
<template #header> {{ role.id }} - {{ role.name }} </template>
<template #body>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a
class="header header-link"
:href="`#/VnUser/${entityId}/basic-data`"
>
{{ t('globals.pageTitles.basicData') }}
<QIcon name="open_in_new" />
</a>
</QCardSection>
<VnLv :label="t('role.card.id')" :value="role.id" />
<VnLv :label="t('role.card.name')" :value="role.name" />
<VnLv :label="t('role.card.description')" :value="role.description" />
</QCard>
</template>
</CardSummary>
</template>

View File

@ -0,0 +1,51 @@
<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormPopup from 'components/FormPopup.vue';
const emit = defineEmits(['onSubmitCreateSubrole']);
const { t } = useI18n();
const route = useRoute();
const subRoleFormData = reactive({
inheritsFrom: null,
role: route.params.id,
});
const rolesOptions = ref([]);
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (rolesOptions = data)"
/>
<FormPopup
model="ZoneWarehouse"
@on-submit="emit('onSubmitCreateSubrole', subRoleFormData)"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelect
:label="t('account.card.role')"
v-model="subRoleFormData.inheritsFrom"
:options="rolesOptions"
option-value="id"
option-label="name"
hide-selected
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>

View File

@ -0,0 +1,162 @@
<script setup>
import { useRoute, useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import SubRoleCreateForm from './SubRoleCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const paginateRef = ref(null);
const createSubRoleDialogRef = ref(null);
const arrayData = useArrayData('SubRoles');
const store = arrayData.store;
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.inherits?.name.localeCompare(b.inherits?.name));
});
const filter = computed(() => ({
where: { role: route.params.id },
include: {
relation: 'inherits',
scope: {
fields: ['id', 'name', 'description'],
},
},
}));
const urlPath = computed(() => `RoleInherits`);
const columns = computed(() => [
{
name: 'name',
},
{
name: 'action',
},
]);
const deleteSubRole = async (row) => {
try {
await axios.delete(`${urlPath.value}/${row.id}`);
fetchSubRoles();
notify(
t('Role removed. Changes will take a while to fully propagate.'),
'positive'
);
} catch (error) {
console.error(error);
}
};
const createSubRole = async (subRoleFormData) => {
try {
await axios.post(urlPath.value, subRoleFormData);
notify(
t('Role added! Changes will take a while to fully propagate.'),
'positive'
);
fetchSubRoles();
} catch (error) {
console.error(error);
}
};
watch(
() => route.params.id,
() => {
store.url = urlPath.value;
store.filter = filter.value;
fetchSubRoles();
}
);
const fetchSubRoles = () => paginateRef.value.fetch();
const openCreateSubRoleForm = () => createSubRoleDialogRef.value.show();
const redirectToRoleSummary = (id) =>
router.push({ name: 'RoleSummary', params: { id } });
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<VnPaginate
ref="paginateRef"
data-key="SubRoles"
:filter="filter"
:url="urlPath"
auto-load
>
<template #body="{ rows }">
<QTable :rows="data" :columns="columns" hide-header>
<template #body="{ row, rowIndex }">
<QTr
@click="redirectToRoleSummary(row.inherits?.id)"
class="cursor-pointer"
>
<QTd>
<div class="column">
<span>{{ row.inherits?.name }}</span>
<span class="color-vn-label">{{
row.inherits?.description
}}</span>
</div>
</QTd>
<QTd style="width: 50px !important">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click.stop.prevent="
openConfirmationModal(
t('El rol va a ser eliminado'),
t('¿Seguro que quieres continuar?'),
() => deleteSubRole(row, rows, rowIndex)
)
"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
</div>
<QDialog ref="createSubRoleDialogRef">
<SubRoleCreateForm @on-submit-create-subrole="createSubRole" />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateSubRoleForm()">
<QTooltip>{{ t('warehouses.add') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
Role removed. Changes will take a while to fully propagate.: Rol eliminado. Los cambios tardaran un tiempo en propagarse completamente.
Role added! Changes will take a while to fully propagate.: ¡Rol añadido! Los cambios tardaran un tiempo en propagarse completamente.
El rol va a ser eliminado: Role will be removed
¿Seguro que quieres continuar?: Are you sure you want to continue?
</i18n>

View File

@ -0,0 +1,109 @@
account:
pageTitles:
users: Users
list: Users
roles: Roles
alias: Mail aliasses
accounts: Accounts
ldap: LDAP
samba: Samba
acls: ACLs
connections: Connections
inheritedRoles: Inherited Roles
subRoles: Sub Roles
newRole: New role
privileges: Privileges
mailAlias: Mail Alias
mailForwarding: Mail Forwarding
aliasUsers: Users
card:
name: Name
nickname: User
role: Rol
email: Email
alias: Alias
lang: Language
actions:
setPassword: Set password
disableAccount:
name: Disable account
title: La cuenta será deshabilitada
subtitle: ¿Seguro que quieres continuar?
disableUser: Disable user
sync: Sync
delete: Delete
search: Search user
role:
pageTitles:
inheritedRoles: Inherited Roles
subRoles: Sub Roles
card:
description: Description
id: Id
name: Name
newRole: New role
searchRoles: Search role
searchInfo: Search role by id or name
name: Name
description: Description
id: Id
mailAlias:
pageTitles:
aliasUsers: Users
search: Search mail alias
searchInfo: Search alias by id or name
alias: Alias
description: Description
id: Id
newAlias: New alias
name: Name
isPublic: Public
ldap:
enableSync: Enable synchronization
server: Server
rdn: RDN
userDN: User DN
filter: Filter
groupDN: Group DN
testConnection: Test connection
success: LDAP connection established!
password: Password
samba:
enableSync: Enable synchronization
domainController: Domain controller
domainAD: AD domain
userAD: AD user
groupDN: Group DN
passwordAD: AD password
domainPart: User DN (without domain part)
verifyCertificate: Verify certificate
testConnection: Test connection
success: Samba connection established!
accounts:
homedir: Homedir base
shell: Shell
idBase: User and role base id
min: Min
max: Max
warn: Warn
inact: Inact
syncAll: Synchronize all
syncRoles: Synchronize roles
connections:
refresh: Refresh
username: Username
created: Created
killSession: Kill session
acls:
role: Role
accessType: Access type
permissions: Permission
search: Search acls
searchInfo: Search acls by model name
tooltip: Use * to match all properties
aclFilter:
principalId: Role
model: Model
property: Property
accessType: Access type
permission: Permission

View File

@ -0,0 +1,120 @@
account:
pageTitles:
users: Usuarios
list: Usuarios
roles: Roles
alias: Alias de correo
accounts: Cuentas
ldap: LDAP
samba: Samba
acls: ACLs
connections: Conexiones
inheritedRoles: Roles heredados
newRole: Nuevo rol
subRoles: Subroles
privileges: Privilegios
mailAlias: Alias de correo
mailForwarding: Reenvío de correo
aliasUsers: Usuarios
card:
nickname: Usuario
name: Nombre
role: Rol
email: Mail
alias: Alias
lang: dioma
actions:
setPassword: Establecer contraseña
disableAccount:
name: Deshabilitar cuenta
title: La cuenta será deshabilitada
subtitle: ¿Seguro que quieres continuar?
disableUser:
name: Desactivar usuario
title: El usuario será deshabilitado
subtitle: ¿Seguro que quieres continuar?
sync:
name: Sincronizar
title: El usuario será sincronizado
subtitle: ¿Seguro que quieres continuar?
delete:
name: Eliminar
title: El usuario será eliminado
subtitle: ¿Seguro que quieres continuar?
search: Buscar usuario
role:
pageTitles:
inheritedRoles: Roles heredados
subRoles: Subroles
newRole: Nuevo rol
card:
description: Descripción
id: Id
name: Nombre
newRole: Nuevo rol
searchRoles: Buscar roles
searchInfo: Buscar rol por id o nombre
name: Nombre
description: Descripción
id: Id
mailAlias:
pageTitles:
aliasUsers: Usuarios
search: Buscar alias de correo
searchInfo: Buscar alias por id o nombre
alias: Alias
description: Descripción
id: Id
newAlias: Nuevo alias
name: Nombre
isPublic: Público
ldap:
password: Contraseña
enableSync: Habilitar sincronización
server: Servidor
rdn: RDN
userDN: DN usuarios
filter: Filtro
groupDN: DN grupos
testConnection: Probar conexión
success: ¡Conexión con LDAP establecida!
samba:
enableSync: Habilitar sincronización
domainController: Controlador de dominio
domainAD: Dominio AD
groupDN: DN grupos
userAD: Usuario AD
passwordAD: Contraseña AD
domainPart: DN usuarios (sin la parte del dominio)
verifyCertificate: Verificar certificado
testConnection: Probar conexión
success: ¡Conexión con Samba establecida!
accounts:
homedir: Directorio base para carpetas de usuario
shell: Intérprete de línea de comandos
idBase: Id base usuarios y roles
min: Min
max: Max
warn: Warn
inact: Inact
syncAll: Sincronizar todo
syncRoles: Sincronizar roles
connections:
refresh: Actualizar
username: Nombre de usuario
created: Creado
killSession: Matar sesión
acls:
role: Rol
accessType: Tipo de acceso
permissions: Permiso
search: Buscar acls
searchInfo: Buscar acls por nombre
tooltip: Usa * para marcar todas las propiedades
aclFilter:
principalId: Rol
model: Modelo
property: Propiedad
accessType: Tipo de acceso
permission: Permiso

View File

@ -2,13 +2,11 @@
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore();
function navigate(id) { function navigate(id) {
router.push({ path: `/agency/${id}` }); router.push({ path: `/agency/${id}` });
} }
@ -22,16 +20,12 @@ function exprBuilder(param, value) {
} }
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
:info="t('You can search by name')" :info="t('You can search by name')"
:label="t('Search agency')" :label="t('Search agency')"
data-key="AgencyList" data-key="AgencyList"
url="Agencies" url="Agencies"
/> />
</Teleport>
</template>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<div class="vn-card-list"> <div class="vn-card-list">
<VnPaginate <VnPaginate

View File

@ -158,8 +158,7 @@ const statesFilter = {
map-options map-options
use-input use-input
:input-debounce="0" :input-debounce="0"
> />
</QSelect>
</VnRow> </VnRow>
</template> </template>
</FormModel> </FormModel>

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
@ -12,8 +11,8 @@ import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorP
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -37,35 +36,16 @@ function navigate(event, id) {
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="ClaimList" data-key="ClaimList"
:label="t('Search claim')" :label="t('Search claim')"
:info="t('You can search by claim id or customer name')" :info="t('You can search by claim id or customer name')"
/> />
</Teleport> <RightMenu>
<Teleport to="#actions-append"> <template #right-panel>
<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">
<ClaimFilter data-key="ClaimList" /> <ClaimFilter data-key="ClaimList" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<div class="vn-card-list"> <div class="vn-card-list">
<VnPaginate <VnPaginate

View File

@ -234,7 +234,7 @@ const showBalancePdf = (balance) => {
<template #body-cell-employee="{ row }"> <template #body-cell-employee="{ row }">
<QTd auto-width @click.stop> <QTd auto-width @click.stop>
<QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn> <QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn>
<WorkerDescriptorProxy :id="row.clientFk" /> <WorkerDescriptorProxy :id="row.workerFk" />
</QTd> </QTd>
</template> </template>

View File

@ -1,114 +1,433 @@
<script setup> <script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore'; import VnTable from 'components/VnTable/VnTable.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import CustomerSummary from './Card/CustomerSummary.vue'; import CustomerSummary from './Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const router = useRouter();
function navigate(id) { const postcodesOptions = ref([]);
router.push({ path: `/customer/${id}` }); const tableRef = ref();
}
const redirectToCreateView = () => { const columns = computed(() => [
router.push({ name: 'CustomerCreate' }); {
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
component: 'select',
name: 'search',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
isTitle: true,
create: true,
},
{
align: 'left',
name: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
isTitle: true,
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
},
create: true,
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
cardVisible: true,
columnFilter: {
component: 'number',
},
columnField: {
component: null,
after: {
component: markRaw(VnLinkPhone),
attrs: (prop) => {
return {
'phone-number': prop,
}; };
},
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
cardVisible: true,
columnFilter: {
component: 'number',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
columnFilter: {
component: 'select',
inWhere: true,
alias: 'c',
attrs: {
url: 'Countries',
},
},
format: (row, dashIfEmpty) => dashIfEmpty(row.country),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
component: 'select',
attrs: {
url: 'Provinces',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.province),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
cardVisible: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: ({ created }) => toDate(created),
component: 'date',
columnFilter: {
alias: 'c',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
create: true,
component: 'select',
attrs: {
url: 'BusinessTypes',
optionLabel: 'description',
optionValue: 'code',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.businessType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
columnFilter: {
component: 'select',
attrs: {
url: 'PayMethods',
},
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.payMethod),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'vat',
url: 'SageTaxTypes',
},
alias: 'sti',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTaxType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'transaction',
url: 'SageTransactionTypes',
},
alias: 'stt',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTransactionType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
chip: {
color: null,
condition: (value) => !value,
icon: 'vn:disabled',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:frozen',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Client ticket list'),
icon: 'vn:ticket',
action: redirectToCreateView,
isPrimary: true,
},
{
title: t('Client ticket list'),
icon: 'preview',
action: (row) => viewSummary(row.id, CustomerSummary),
},
],
},
]);
const { viewSummary } = useSummaryDialog();
const redirectToCreateView = (row) => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: row.id,
}),
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <VnTable
<Teleport to="#searchbar"> ref="tableRef"
<VnSearchbar data-key="CustomerExtendedList"
:info="t('You can search by customer id or name')" url="Clients/extendedListFilter"
:label="t('Search customer')" :create="{
data-key="CustomerList" urlCreate: 'Clients/createWithUser',
/> title: 'Create client',
</Teleport> onDataSaved: ({ id }) => tableRef.redirect(id),
<Teleport to="#actions-append"> formInitialData: {
<div class="row q-gutter-x-sm"> active: true,
<QBtn isEqualizated: false,
@click="stateStore.toggleRightDrawer()" },
dense }"
flat
icon="menu"
round
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<QScrollArea class="fit text-grey-8">
<CustomerFilter data-key="CustomerList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
auto-load
data-key="CustomerList"
order="id DESC" order="id DESC"
url="/Clients/filter" :columns="columns"
default-mode="table"
redirect="customer"
auto-load
> >
<template #body="{ rows }"> <template #more-create-dialog="{ data }">
<CardList <VnLocation
:id="row.id" :roles-allowed-to-create="['deliveryAssistant']"
:key="row.id" :options="postcodesOptions"
:title="row.name" v-model="data.location"
@click="navigate(row.id)" @update:model-value="(location) => handleLocation(data, location)"
v-for="row of rows"
>
<template #list-items>
<VnLv :label="t('customer.list.email')" :value="row.email" />
<VnLv :value="row.phone">
<template #label>
{{ t('customer.list.phone') }}
<VnLinkPhone :phone-number="row.phone" />
</template>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
outline
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, CustomerSummary)"
color="primary"
style="margin-top: 15px"
/> />
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput :label="t('Email')" clearable type="email" v-model="data.email">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template> </template>
</CardList> </QInput>
</template> </template>
</VnPaginate> </VnTable>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn @click="redirectToCreateView()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New client') }}
</QTooltip>
</QPageSticky>
</QPage>
</template> </template>
<i18n> <i18n>
es: es:
Search customer: Buscar cliente Web user: Usuario Web
You can search by customer id or name: Puedes buscar por id o nombre del cliente
New client: Nuevo cliente
</i18n> </i18n>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -1,10 +1,7 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QBtn, QCheckbox, useQuasar } from 'quasar'; import { QBtn, QCheckbox, useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toDate, dateRange } from 'filters/index'; import { toCurrency, toDate, dateRange } from 'filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue'; import CustomerNotificationsFilter from './CustomerDefaulterFilter.vue';
@ -15,7 +12,7 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
import axios from 'axios'; import axios from 'axios';
const stateStore = useStateStore(); import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
@ -266,30 +263,11 @@ function exprBuilder(param, value) {
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <RightMenu>
<Teleport to="#actions-append"> <template #right-panel>
<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">
<CustomerNotificationsFilter data-key="CustomerDefaulter" /> <CustomerNotificationsFilter data-key="CustomerDefaulter" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<VnSubToolbar> <VnSubToolbar>
<template #st-data> <template #st-data>
<CustomerBalanceDueTotal :amount="balanceDueTotal" /> <CustomerBalanceDueTotal :amount="balanceDueTotal" />

View File

@ -1,428 +0,0 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import CustomerSummary from '../Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
const router = useRouter();
const postcodesOptions = ref([]);
const tableRef = ref();
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
component: 'select',
name: 'search',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
isTitle: true,
create: true,
},
{
align: 'left',
name: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
isTitle: true,
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
},
create: true,
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
cardVisible: true,
after: {
component: VnLinkPhone,
props: (prop) => ({
'phone-number': prop.phone,
}),
},
component: 'number',
columnField: {
component: null,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
cardVisible: true,
columnFilter: {
component: 'number',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
columnFilter: {
component: 'select',
inWhere: true,
alias: 'c',
attrs: {
url: 'Countries',
},
},
format: (row, dashIfEmpty) => dashIfEmpty(row.country),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
component: 'select',
attrs: {
url: 'Provinces',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.province),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
cardVisible: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: ({ created }) => toDate(created),
component: 'date',
columnFilter: {
alias: 'c',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
create: true,
component: 'select',
attrs: {
url: 'BusinessTypes',
optionLabel: 'description',
optionValue: 'code',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.businessType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
columnFilter: {
component: 'select',
attrs: {
url: 'PayMethods',
},
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.payMethod),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'vat',
url: 'SageTaxTypes',
},
alias: 'sti',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTaxType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'transaction',
url: 'SageTransactionTypes',
},
alias: 'stt',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTransactionType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
chip: {
color: null,
condition: (value) => !value,
icon: 'vn:disabled',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
created: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:frozen',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Client ticket list'),
icon: 'vn:ticket',
action: redirectToCreateView,
isPrimary: true,
},
{
title: t('Client ticket list'),
icon: 'preview',
action: (row) => viewSummary(row.id, CustomerSummary),
},
],
},
]);
const { viewSummary } = useSummaryDialog();
const redirectToCreateView = (row) => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: row.id,
}),
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script>
<template>
<VnTable
ref="tableRef"
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
url-create="Clients/createWithUser"
:create="{
urlCreate: 'Clients/createWithUser',
title: 'Create client',
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
isEqualizated: false,
},
}"
order="id DESC"
:columns="columns"
default-mode="table"
redirect="customer"
auto-load
>
<template #more-create-dialog="{ data }">
<QInput :label="t('Email')" clearable type="email" v-model="data.email">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</QInput>
<QInput v-model="data.userName" :label="t('Web user')" />
<VnLocation
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -5,11 +5,10 @@ import { QBtn } from 'quasar';
import CustomerNotificationsFilter from './CustomerNotificationsFilter.vue'; import CustomerNotificationsFilter from './CustomerNotificationsFilter.vue';
import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CustomerNotificationsCampaignConsumption from './CustomerNotificationsCampaignConsumption.vue'; import CustomerNotificationsCampaignConsumption from './CustomerNotificationsCampaignConsumption.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const selected = ref([]); const selected = ref([]);
const selectedCustomerId = ref(0); const selectedCustomerId = ref(0);
@ -81,29 +80,11 @@ const selectCustomerId = (id) => {
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <RightMenu>
<Teleport to="#actions-append"> <template #right-panel>
<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">
<CustomerNotificationsFilter data-key="CustomerNotifications" /> <CustomerNotificationsFilter data-key="CustomerNotifications" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<VnSubToolbar class="justify-end"> <VnSubToolbar class="justify-end">
<template #st-data> <template #st-data>
<CustomerNotificationsCampaignConsumption <CustomerNotificationsCampaignConsumption

View File

@ -3,15 +3,14 @@ import axios from 'axios';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue';
import { toDate, toCurrency } from 'filters/index'; import { toDate, toCurrency } from 'filters/index';
import CustomerPaymentsFilter from './CustomerPaymentsFilter.vue'; import CustomerPaymentsFilter from './CustomerPaymentsFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData('CustomerTransactions'); const arrayData = useArrayData('CustomerTransactions');
@ -93,28 +92,11 @@ function stateColor(row) {
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <RightMenu>
<Teleport to="#actions-append"> <template #right-panel>
<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">
<CustomerPaymentsFilter data-key="CustomerTransactions" /> <CustomerPaymentsFilter data-key="CustomerTransactions" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<QPage class="column items-center q-pa-md customer-payments"> <QPage class="column items-center q-pa-md customer-payments">
<div class="vn-card-list"> <div class="vn-card-list">
<QToolbar class="q-pa-none justify-end"> <QToolbar class="q-pa-none justify-end">

View File

@ -2,7 +2,6 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
@ -23,7 +22,6 @@ const $props = defineProps({
}, },
}); });
const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();

View File

@ -82,6 +82,16 @@ const tableColumnComponents = computed(() => ({
}, },
event: getInputEvents, event: getInputEvents,
}, },
printedStickers: {
component: VnInput,
props: {
type: 'number',
min: 0,
class: 'input-number',
dense: true,
},
event: getInputEvents,
},
weight: { weight: {
component: VnInput, component: VnInput,
props: { props: {
@ -147,7 +157,7 @@ const entriesTableColumns = computed(() => {
return [ return [
{ {
label: t('entry.summary.item'), label: t('entry.summary.item'),
field: 'id', field: 'itemFk',
name: 'item', name: 'item',
align: 'left', align: 'left',
}, },
@ -169,6 +179,12 @@ const entriesTableColumns = computed(() => {
name: 'stickers', name: 'stickers',
align: 'left', align: 'left',
}, },
{
label: t('entry.buys.printedStickers'),
field: 'printedStickers',
name: 'printedStickers',
align: 'left',
},
{ {
label: t('entry.summary.weight'), label: t('entry.summary.weight'),
field: 'weight', field: 'weight',
@ -216,7 +232,6 @@ const entriesTableColumns = computed(() => {
}); });
const copyOriginalRowsData = (rows) => { const copyOriginalRowsData = (rows) => {
// el objetivo de esto es guardar los valores iniciales de todas las rows para evitar guardar cambios si la data no cambió al disparar los eventos
originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); originalRowDataCopy.value = JSON.parse(JSON.stringify(rows));
}; };
@ -386,19 +401,16 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name === 'item'" v-if="col.name === 'item'"
:id="props.row.id" :id="props.row.item.id"
/> />
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr no-hover> <QTr no-hover class="full-width infoRow" style="column-span: all">
<QTd /> <QTd />
<QTd> <QTd cols>
<span>{{ props.row.item.itemType.code }}</span> <span>{{ props.row.item.itemType.code }}</span>
</QTd> </QTd>
<QTd>
<span>{{ props.row.item.id }}</span>
</QTd>
<QTd> <QTd>
<span>{{ props.row.item.size }}</span> <span>{{ props.row.item.size }}</span>
</QTd> </QTd>
@ -413,10 +425,6 @@ const lockIconType = (groupingMode, mode) => {
<FetchedTags :item="props.row.item" :max-length="5" /> <FetchedTags :item="props.row.item" :max-length="5" />
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" class="vn-table-separation-row" />
</QTr>
</template> </template>
<template #item="props"> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
@ -466,11 +474,13 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.separation-row { .q-table--horizontal-separator tbody tr:nth-child(odd) > td {
background-color: var(--vn-section-color) !important; border-bottom-width: 0px;
border-top-width: 2px;
border-color: var(--vn-text-color);
} }
.grid-style-transition { .infoRow > td {
transition: transform 0.28s, background-color 0.28s; color: var(--vn-label-color);
} }
</style> </style>

View File

@ -174,7 +174,7 @@ const redirectToBuysView = () => {
@on-fetch="(data) => (packagingsOptions = data)" @on-fetch="(data) => (packagingsOptions = data)"
auto-load auto-load
/> />
<QForm> <QForm @submit="onSubmit()">
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div> <div>
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">

View File

@ -34,7 +34,7 @@ const entryFilter = {
{ {
relation: 'travel', relation: 'travel',
scope: { scope: {
fields: ['id', 'landed', 'agencyModeFk', 'warehouseOutFk'], fields: ['id', 'landed', 'shipped', 'agencyModeFk', 'warehouseOutFk'],
include: [ include: [
{ {
relation: 'agency', relation: 'agency',
@ -125,10 +125,8 @@ watch;
:label="t('entry.descriptor.agency')" :label="t('entry.descriptor.agency')"
:value="entity.travel?.agency?.name" :value="entity.travel?.agency?.name"
/> />
<VnLv <VnLv :label="t('shipped')" :value="toDate(entity.travel?.shipped)" />
:label="t('entry.descriptor.landed')" <VnLv :label="t('landed')" :value="toDate(entity.travel?.landed)" />
:value="toDate(entity.travel?.landed)"
/>
<VnLv <VnLv
:label="t('entry.descriptor.warehouseOut')" :label="t('entry.descriptor.warehouseOut')"
:value="entity.travel?.warehouseOut?.name" :value="entity.travel?.warehouseOut?.name"

View File

@ -221,10 +221,7 @@ const fetchEntryBuys = async () => {
:value="entry.travel.agency.name" :value="entry.travel.agency.name"
/> />
<VnLv <VnLv :label="t('shipped')" :value="toDate(entry.travel.shipped)" />
:label="t('entry.summary.travelShipped')"
:value="toDate(entry.travel.shipped)"
/>
<VnLv <VnLv
:label="t('entry.summary.travelWarehouseOut')" :label="t('entry.summary.travelWarehouseOut')"
@ -236,10 +233,7 @@ const fetchEntryBuys = async () => {
v-model="entry.travel.isDelivered" v-model="entry.travel.isDelivered"
:disable="true" :disable="true"
/> />
<VnLv <VnLv :label="t('landed')" :value="toDate(entry.travel.landed)" />
:label="t('entry.summary.travelLanded')"
:value="toDate(entry.travel.landed)"
/>
<VnLv <VnLv
:label="t('entry.summary.travelWarehouseIn')" :label="t('entry.summary.travelWarehouseIn')"

View File

@ -19,6 +19,7 @@ import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
@ -646,14 +647,12 @@ onUnmounted(() => (stateStore.rightDrawer = false));
@on-config-saved="visibleColumns = ['picture', ...$event]" @on-config-saved="visibleColumns = ['picture', ...$event]"
/> />
</template> </template>
<QSpace />
<div id="st-actions"></div>
</VnSubToolbar> </VnSubToolbar>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <RightMenu>
<QScrollArea class="fit text-grey-8"> <template #right-panel>
<EntryLatestBuysFilter data-key="EntryLatestBuys" /> <EntryLatestBuysFilter data-key="EntryLatestBuys" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable
:rows="rows" :rows="rows"

View File

@ -11,6 +11,7 @@ import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index'; import { toDate } from 'src/filters/index';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -29,23 +30,18 @@ onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
}); });
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="EntryList" data-key="EntryList"
url="Entries/filter" url="Entries/filter"
:label="t('Search entries')" :label="t('Search entries')"
:info="t('You can search by entry reference')" :info="t('You can search by entry reference')"
/> />
</Teleport> <RightMenu>
</template> <template #right-panel>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<EntryFilter data-key="EntryList" /> <EntryFilter data-key="EntryList" />
</QScrollArea> </template>
</QDrawer> </RightMenu>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<div class="vn-card-list"> <div class="vn-card-list">
<VnPaginate <VnPaginate
@ -82,10 +78,7 @@ onMounted(async () => {
</QIcon> </QIcon>
</template> </template>
<template #list-items> <template #list-items>
<VnLv <VnLv :label="t('landed')" :value="toDate(row.landed)" />
:label="t('entry.list.landed')"
:value="toDate(row.landed)"
/>
<VnLv <VnLv
:label="t('entry.list.booked')" :label="t('entry.list.booked')"
:value="!!row.isBooked" :value="!!row.isBooked"

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